File: BrokeredServiceConnection.cs
Web Access
Project: src\src\Workspaces\Remote\Core\Microsoft.CodeAnalysis.Remote.Workspaces.csproj (Microsoft.CodeAnalysis.Remote.Workspaces)
// 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.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.ServiceHub.Framework;
using Microsoft.VisualStudio.Threading;
using Microsoft.Win32.SafeHandles;
using StreamJsonRpc;
using StreamJsonRpc.Protocol;
 
namespace Microsoft.CodeAnalysis.Remote;
 
internal sealed class BrokeredServiceConnection<TService> : RemoteServiceConnection<TService>
    where TService : class
{
    private readonly struct Rental : IDisposable
    {
#pragma warning disable ISB002 // Avoid storing rentals in fields
        private readonly ServiceBrokerClient.Rental<TService> _proxyRental;
#pragma warning restore
 
        public readonly TService Service;
 
        public Rental(ServiceBrokerClient.Rental<TService> proxyRental, TService service)
        {
            _proxyRental = proxyRental;
            Service = service;
        }
 
        public void Dispose()
            => _proxyRental.Dispose();
    }
 
    private readonly IErrorReportingService? _errorReportingService;
    private readonly IRemoteHostClientShutdownCancellationService? _shutdownCancellationService;
    private readonly SolutionAssetStorage _solutionAssetStorage;
 
    private readonly ServiceDescriptor _serviceDescriptor;
    private readonly ServiceBrokerClient _serviceBrokerClient;
    private readonly RemoteServiceCallbackDispatcher.Handle _callbackHandle;
    private readonly IRemoteServiceCallbackDispatcher? _callbackDispatcher;
    private readonly Process? _remoteProcess;
    private readonly SafeProcessHandle? _remoteProcessHandle;
 
    public BrokeredServiceConnection(
        ServiceDescriptor serviceDescriptor,
        object? callbackTarget,
        IRemoteServiceCallbackDispatcher? callbackDispatcher,
        ServiceBrokerClient serviceBrokerClient,
        SolutionAssetStorage solutionAssetStorage,
        IErrorReportingService? errorReportingService,
        IRemoteHostClientShutdownCancellationService? shutdownCancellationService,
        Process? remoteProcess)
    {
        Contract.ThrowIfFalse((callbackDispatcher == null) == (serviceDescriptor.ClientInterface == null));
 
        _serviceDescriptor = serviceDescriptor;
        _serviceBrokerClient = serviceBrokerClient;
        _solutionAssetStorage = solutionAssetStorage;
        _errorReportingService = errorReportingService;
        _shutdownCancellationService = shutdownCancellationService;
        _callbackDispatcher = callbackDispatcher;
        _callbackHandle = callbackDispatcher?.CreateHandle(callbackTarget) ?? default;
        _remoteProcess = remoteProcess;
        _remoteProcessHandle = _remoteProcess?.SafeHandle;
    }
 
    public override void Dispose()
    {
        _callbackHandle.Dispose();
    }
 
    private async ValueTask<Rental> RentServiceAsync(CancellationToken cancellationToken)
    {
        // Make sure we are on the thread pool to avoid UI thread dependencies if external code uses ConfigureAwait(true)
        await TaskScheduler.Default;
 
        var options = new ServiceActivationOptions
        {
            ClientRpcTarget = _callbackDispatcher
        };
 
        var proxyRental = await _serviceBrokerClient.GetProxyAsync<TService>(_serviceDescriptor, options, cancellationToken).ConfigureAwait(false);
        var service = proxyRental.Proxy;
        Contract.ThrowIfNull(service);
        return new Rental(proxyRental, service);
    }
 
    // no solution, no callback
 
    public override async ValueTask<bool> TryInvokeAsync(Func<TService, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(Func<TService, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // no solution, callback
 
    public override async ValueTask<bool> TryInvokeAsync(Func<TService, RemoteServiceCallbackId, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(Func<TService, RemoteServiceCallbackId, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // solution, no callback
 
    public override async ValueTask<bool> TryInvokeAsync(SolutionCompilationState compilationState, Func<TService, Checksum, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, scope.SolutionChecksum, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(SolutionCompilationState compilationState, Func<TService, Checksum, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, scope.SolutionChecksum, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // project, no callback
 
    public override async ValueTask<bool> TryInvokeAsync(SolutionCompilationState compilationState, ProjectId projectId, Func<TService, Checksum, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, projectId, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, scope.SolutionChecksum, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(SolutionCompilationState compilationState, ProjectId projectId, Func<TService, Checksum, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, projectId, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, scope.SolutionChecksum, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // solution, callback
 
    public override async ValueTask<bool> TryInvokeAsync(SolutionCompilationState compilationState, Func<TService, Checksum, RemoteServiceCallbackId, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, scope.SolutionChecksum, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
 
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(SolutionCompilationState compilationState, Func<TService, Checksum, RemoteServiceCallbackId, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, scope.SolutionChecksum, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // project, callback
 
    public override async ValueTask<bool> TryInvokeAsync(SolutionCompilationState compilationState, ProjectId projectId, Func<TService, Checksum, RemoteServiceCallbackId, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, projectId, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, scope.SolutionChecksum, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
 
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    public override async ValueTask<Optional<TResult>> TryInvokeAsync<TResult>(SolutionCompilationState compilationState, ProjectId projectId, Func<TService, Checksum, RemoteServiceCallbackId, CancellationToken, ValueTask<TResult>> invocation, CancellationToken cancellationToken)
    {
        Contract.ThrowIfFalse(_callbackDispatcher is not null);
 
        try
        {
            using var scope = await _solutionAssetStorage.StoreAssetsAsync(compilationState, projectId, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            return await invocation(rental.Service, scope.SolutionChecksum, _callbackHandle.Id, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return default;
        }
    }
 
    // multi-solution, no callback
 
    public override async ValueTask<bool> TryInvokeAsync(SolutionCompilationState compilationState1, SolutionCompilationState compilationState2, Func<TService, Checksum, Checksum, CancellationToken, ValueTask> invocation, CancellationToken cancellationToken)
    {
        try
        {
            using var scope1 = await _solutionAssetStorage.StoreAssetsAsync(compilationState1, cancellationToken).ConfigureAwait(false);
            using var scope2 = await _solutionAssetStorage.StoreAssetsAsync(compilationState2, cancellationToken).ConfigureAwait(false);
            using var rental = await RentServiceAsync(cancellationToken).ConfigureAwait(false);
            await invocation(rental.Service, scope1.SolutionChecksum, scope2.SolutionChecksum, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken))
        {
            OnUnexpectedException(exception, cancellationToken);
            return false;
        }
    }
 
    // Exceptions
 
    private bool ReportUnexpectedException(Exception exception, CancellationToken cancellationToken)
    {
        if (exception is OperationCanceledException)
        {
            // It's a bug for a service to throw OCE based on a different cancellation token than it has received in the call.
            // The server side filter will report NFW in such scenario, so that the underlying issue can be fixed.
            // Do not treat this as a critical failure of the service for now and only fail in debug build.
            Debug.Assert(cancellationToken.IsCancellationRequested);
 
            return false;
        }
 
        // Do not report telemetry when the host is shutting down or the remote service threw an IO exception:
        if (IsHostShuttingDown || IsRemoteIOException(exception))
        {
            return true;
        }
 
        return FatalError.ReportAndCatch(exception);
    }
 
    private bool IsHostShuttingDown
        => _shutdownCancellationService?.ShutdownToken.IsCancellationRequested == true;
 
    // TODO: we need https://github.com/microsoft/vs-streamjsonrpc/issues/468 to be implemented in order to check for IOException subtypes.
    private static bool IsRemoteIOException(Exception exception)
        => exception is RemoteInvocationException { ErrorData: CommonErrorData { TypeName: "System.IO.IOException" } };
 
    private void OnUnexpectedException(Exception exception, CancellationToken cancellationToken)
    {
        // If the cancellation token passed to the remote call is not linked with the host shutdown cancellation token,
        // various non-cancellation exceptions may occur during the remote call.
        // Throw cancellation exception if the cancellation token is signaled.
        // If it is not then show info to the user that the service is not available dure to shutdown.
        cancellationToken.ThrowIfCancellationRequested();
 
        if (_errorReportingService == null)
        {
            return;
        }
 
        // Show the error on the client. See https://github.com/dotnet/roslyn/issues/40476 for error classification details.
        // Based on the exception type and the state of the system we report one of the following:
        // - "Feature xyz is currently unavailable due to an intermittent error. Please try again later. Error message: '{1}'" (RemoteInvocationException: IOException)
        // - "Feature xyz is currently unavailable due to an internal error [Details]" (exception is RemoteInvocationException, MessagePackSerializationException, ConnectionLostException)
        // - "Feature xyz is currently unavailable since Visual Studio is shutting down" (connection exceptions during shutdown cancellation when cancellationToken is not signalled)
 
        // We expect all RPC calls to complete and not drop the connection.
        // ConnectionLostException indicates a bug that is likely thrown because the remote process crashed.
        // Currently, ConnectionLostException is also throw when the result of the RPC method fails to serialize 
        // (see https://github.com/microsoft/vs-streamjsonrpc/issues/549)
 
        string message;
        Exception? internalException = null;
        var featureName = _serviceDescriptor.GetFeatureDisplayName();
 
        if (IsRemoteIOException(exception))
        {
            message = string.Format(RemoteWorkspacesResources.Feature_0_is_currently_unavailable_due_to_an_intermittent_error, featureName, exception.Message);
        }
        else if (IsHostShuttingDown)
        {
            message = string.Format(RemoteWorkspacesResources.Feature_0_is_currently_unavailable_host_shutting_down, featureName, _errorReportingService.HostDisplayName);
        }
        else
        {
            message = string.Format(RemoteWorkspacesResources.Feature_0_is_currently_unavailable_due_to_an_internal_error, featureName);
            internalException = exception;
        }
 
        try
        {
            // Process.ExitCode throws "Process was not started by this object, so requested information cannot be determined.",
            // Use Win32 API directly.
            if (_remoteProcess?.HasExited == true && NativeMethods.GetExitCodeProcess(_remoteProcessHandle!, out var exitCode))
            {
                message += $" Exit code {exitCode}";
            }
        }
        catch
        {
        }
 
        _errorReportingService.ShowFeatureNotAvailableErrorInfo(message, TelemetryFeatureName.GetRemoteFeatureName(_serviceDescriptor.ComponentName, _serviceDescriptor.SimpleName), internalException);
    }
}
 
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool GetExitCodeProcess(SafeProcessHandle processHandle, out int exitCode);
}