File: Remote\IRemoteKeepAliveService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Remote;
 
internal interface IRemoteKeepAliveService
{
    /// <summary>
    /// Keeps alive this solution in the OOP process until the cancellation token is triggered.  Used so that long
    /// running features (like 'inline rename' or 'lightbulbs') we can call into oop several times, with the same
    /// snapshot, knowing that things will stay hydrated and alive on the OOP side.  Importantly, by keeping the
    /// same <see cref="Solution"/> snapshot alive on the OOP side, computed attached values (like <see
    /// cref="Compilation"/>s) will stay alive as well.
    /// </summary>
    ValueTask KeepAliveAsync(Checksum solutionChecksum, CancellationToken cancellationToken);
}
 
internal sealed class RemoteKeepAliveSession : IDisposable
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
 
    private RemoteKeepAliveSession(
        SolutionCompilationState compilationState,
        RemoteHostClient? client)
    {
        if (client is null)
            return;
 
        // Now kick off the keep-alive work.  We don't wait on this as this will stick on the OOP side until
        // the cancellation token triggers.
        _ = client.TryInvokeAsync<IRemoteKeepAliveService>(
            compilationState,
            (service, solutionInfo, cancellationToken) => service.KeepAliveAsync(solutionInfo, cancellationToken),
            _cancellationTokenSource.Token).AsTask();
    }
 
    private RemoteKeepAliveSession(SolutionCompilationState compilationState, IAsynchronousOperationListener listener)
    {
        var cancellationToken = _cancellationTokenSource.Token;
        var token = listener.BeginAsyncOperation(nameof(RemoteKeepAliveSession));
 
        var task = CreateClientAndKeepAliveAsync();
        task.CompletesAsyncOperation(token);
 
        return;
 
        async Task CreateClientAndKeepAliveAsync()
        {
            var client = await RemoteHostClient.TryGetClientAsync(compilationState.Services, cancellationToken).ConfigureAwait(false);
            if (client is null)
                return;
 
            // Now kick off the keep-alive work.  We don't wait on this as this will stick on the OOP side until
            // the cancellation token triggers.
            _ = client.TryInvokeAsync<IRemoteKeepAliveService>(
                compilationState,
                (service, solutionInfo, cancellationToken) => service.KeepAliveAsync(solutionInfo, cancellationToken),
                cancellationToken).AsTask();
        }
    }
 
    ~RemoteKeepAliveSession()
    {
        if (Environment.HasShutdownStarted)
            return;
 
        Contract.Fail("Should have been disposed!");
    }
 
    public void Dispose()
    {
        GC.SuppressFinalize(this);
        _cancellationTokenSource.Cancel();
        _cancellationTokenSource.Dispose();
    }
 
    /// <summary>
    /// Creates a session between the host and OOP, effectively pinning this <paramref name="solution"/> until <see
    /// cref="IDisposable.Dispose"/> is called on it.  By pinning the solution we ensure that all calls to OOP for
    /// the same solution during the life of this session do not need to resync the solution.  Nor do they then need
    /// to rebuild any compilations they've already built due to the solution going away and then coming back.
    /// </summary>
    /// <remarks>
    /// The <paramref name="listener"/> is not strictly necessary for this type.  This class functions just as an
    /// optimization to hold onto data so it isn't resync'ed or recomputed.  However, we still want to let the
    /// system know when unobserved async work is kicked off in case we have any tooling that keep track of this for
    /// any reason (for example for tracking down problems in testing scenarios).
    /// </remarks>
    /// <remarks>
    /// This synchronous entrypoint should be used only in contexts where using the async <see
    /// cref="CreateAsync(Solution, CancellationToken)"/> is not possible (for example, in a constructor).
    /// </remarks>
    public static RemoteKeepAliveSession Create(Solution solution, IAsynchronousOperationListener listener)
        => new(solution.CompilationState, listener);
 
    /// <summary>
    /// Creates a session between the host and OOP, effectively pinning this <paramref name="solution"/> until <see
    /// cref="IDisposable.Dispose"/> is called on it.  By pinning the solution we ensure that all calls to OOP for
    /// the same solution during the life of this session do not need to resync the solution.  Nor do they then need
    /// to rebuild any compilations they've already built due to the solution going away and then coming back.
    /// </summary>
    public static Task<RemoteKeepAliveSession> CreateAsync(Solution solution, CancellationToken cancellationToken)
        => CreateAsync(solution.CompilationState, cancellationToken);
 
    /// <inheritdoc cref="CreateAsync(Solution, CancellationToken)"/>
    public static async Task<RemoteKeepAliveSession> CreateAsync(
        SolutionCompilationState compilationState, CancellationToken cancellationToken)
    {
        var client = await RemoteHostClient.TryGetClientAsync(compilationState.Services, cancellationToken).ConfigureAwait(false);
        return new RemoteKeepAliveSession(compilationState, client);
    }
}