File: Remote\GlobalNotificationRemoteDeliveryService.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Remote
{
    /// <summary>
    /// Delivers global notifications to remote services.
    /// </summary>
    internal sealed class GlobalNotificationRemoteDeliveryService : IDisposable
    {
        private enum GlobalNotificationState
        {
            NotStarted,
            Started
        }
 
        /// <summary>
        /// Lock for the <see cref="_globalNotificationsTask"/> task chain.  Each time we hear 
        /// about a global operation starting or stopping (i.e. a build) we will '.ContinueWith'
        /// this task chain with a new notification to the OOP side.  This way all the messages
        /// are properly serialized and appear in the right order (i.e. we don't hear about a 
        /// stop prior to hearing about the relevant start).
        /// </summary>
        private readonly object _globalNotificationsGate = new object();
        private Task<GlobalNotificationState> _globalNotificationsTask = Task.FromResult(GlobalNotificationState.NotStarted);
 
        private readonly SolutionServices _services;
        private readonly CancellationToken _cancellationToken;
 
        public GlobalNotificationRemoteDeliveryService(SolutionServices services, CancellationToken cancellationToken)
        {
            _services = services;
            _cancellationToken = cancellationToken;
 
            RegisterGlobalOperationNotifications();
        }
 
        public void Dispose()
        {
            UnregisterGlobalOperationNotifications();
        }
 
        private void RegisterGlobalOperationNotifications()
        {
            // We are in the VS layer, so getting the IGlobalOperationNotificationService must succeed.
            var globalOperationService = _services.ExportProvider.GetExports<IGlobalOperationNotificationService>().Single().Value;
            globalOperationService.Started += OnGlobalOperationStarted;
            globalOperationService.Stopped += OnGlobalOperationStopped;
        }
 
        private void UnregisterGlobalOperationNotifications()
        {
            var globalOperationService = _services.ExportProvider.GetExports<IGlobalOperationNotificationService>().Single().Value;
            globalOperationService.Started -= OnGlobalOperationStarted;
            globalOperationService.Stopped -= OnGlobalOperationStopped;
        }
 
        private void OnGlobalOperationStarted(object? sender, EventArgs e)
        {
            lock (_globalNotificationsGate)
            {
                // Pass TaskContinuationOptions.OnlyOnRanToCompletion to avoid delivering further notifications once the task gets canceled or fails.
                // The cancellation happens only when VS is shutting down. The task might fail if communication with OOP fails. 
                // Once that happens there is not point in sending more notifications to the remote service.
 
                _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
                    SendStartNotificationAsync, _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
            }
        }
 
        private async Task<GlobalNotificationState> SendStartNotificationAsync(Task<GlobalNotificationState> previousTask)
        {
            // Can only transition from NotStarted->Started.  If we hear about
            // anything else, do nothing.
            if (previousTask.Result != GlobalNotificationState.NotStarted)
            {
                return previousTask.Result;
            }
 
            var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false);
            if (client == null)
            {
                return previousTask.Result;
            }
 
            _ = await client.TryInvokeAsync<IRemoteGlobalNotificationDeliveryService>(
                (service, cancellationToken) => service.OnGlobalOperationStartedAsync(cancellationToken),
                _cancellationToken).ConfigureAwait(false);
 
            return GlobalNotificationState.Started;
        }
 
        private void OnGlobalOperationStopped(object? sender, EventArgs e)
        {
            lock (_globalNotificationsGate)
            {
                // Pass TaskContinuationOptions.OnlyOnRanToCompletion to avoid delivering further notifications once the task gets canceled or fails.
                // The cancellation happens only when VS is shutting down. The task might fail if communication with OOP fails. 
                // Once that happens there is not point in sending more notifications to the remote service.
 
                _globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
                    previous => SendStoppedNotificationAsync(previous), _cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
            }
        }
 
        private async Task<GlobalNotificationState> SendStoppedNotificationAsync(Task<GlobalNotificationState> previousTask)
        {
            // Can only transition from Started->NotStarted.  If we hear about
            // anything else, do nothing.
            if (previousTask.Result != GlobalNotificationState.Started)
            {
                return previousTask.Result;
            }
 
            var client = await RemoteHostClient.TryGetClientAsync(_services, _cancellationToken).ConfigureAwait(false);
            if (client == null)
            {
                return previousTask.Result;
            }
 
            _ = await client.TryInvokeAsync<IRemoteGlobalNotificationDeliveryService>(
                (service, cancellationToken) => service.OnGlobalOperationStoppedAsync(cancellationToken),
                _cancellationToken).ConfigureAwait(false);
 
            // Mark that we're stopped now.
            return GlobalNotificationState.NotStarted;
        }
    }
}