未验证 提交 65722f04 编写于 作者: T Tomáš Matoušek 提交者: GitHub

More RemoteHostClient refactoring (#43928)

* Remove direct usage of Connection

* Refactor connection pooling

* SolutionChecksumUpdater

* GlobalNotificationRemoteDeliveryService
上级 36f71913
......@@ -31,7 +31,7 @@ public static async Task<RemoteHostClient> CreateAsync(Workspace workspace, bool
{
var inprocServices = new InProcRemoteServices(runCacheCleanup);
var remoteHostStream = await inprocServices.RequestServiceAsync(WellKnownRemoteHostServices.RemoteHostService).ConfigureAwait(false);
var remoteHostStream = await inprocServices.RequestServiceAsync(WellKnownServiceHubServices.RemoteHostService).ConfigureAwait(false);
var current = CreateClientId(Process.GetCurrentProcess().Id.ToString());
var instance = new InProcRemoteHostClient(current, workspace, inprocServices, remoteHostStream);
......@@ -98,7 +98,7 @@ public Task<Stream> RequestServiceAsync(string serviceName)
public override string ClientId { get; }
public override bool IsRemoteHost64Bit => IntPtr.Size == 8;
public override async Task<Connection?> TryCreateConnectionAsync(
protected override async Task<Connection?> TryCreateConnectionAsync(
string serviceName, object? callbackTarget, CancellationToken cancellationToken)
{
// get stream from service hub to communicate service specific information
......@@ -108,10 +108,6 @@ public Task<Stream> RequestServiceAsync(string serviceName)
return new JsonRpcConnection(Workspace, _inprocServices.Logger, callbackTarget, serviceStream);
}
protected override void OnStarted()
{
}
public override void Dispose()
{
// we are asked to disconnect. unsubscribe and dispose to disconnect
......@@ -165,7 +161,7 @@ public InProcRemoteServices(bool runCacheCleanup)
_serviceProvider = new ServiceProvider(runCacheCleanup);
_creatorMap = new Dictionary<string, Func<Stream, IServiceProvider, ServiceBase>>();
RegisterService(WellKnownRemoteHostServices.RemoteHostService, (s, p) => new RemoteHostService(s, p));
RegisterService(WellKnownServiceHubServices.RemoteHostService, (s, p) => new RemoteHostService(s, p));
RegisterService(WellKnownServiceHubServices.CodeAnalysisService, (s, p) => new CodeAnalysisService(s, p));
RegisterService(WellKnownServiceHubServices.RemoteSymbolSearchUpdateEngine, (s, p) => new RemoteSymbolSearchUpdateEngine(s, p));
RegisterService(WellKnownServiceHubServices.RemoteDesignerAttributeService, (s, p) => new RemoteDesignerAttributeService(s, p));
......
// 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.
#nullable enable
using System;
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 HostWorkspaceServices _services;
private readonly CancellationToken _cancellationToken;
public GlobalNotificationRemoteDeliveryService(HostWorkspaceServices services, CancellationToken cancellationToken)
{
_services = services;
_cancellationToken = cancellationToken;
RegisterGlobalOperationNotifications();
}
public void Dispose()
{
UnregisterGlobalOperationNotifications();
}
private void RegisterGlobalOperationNotifications()
{
var globalOperationService = _services.GetService<IGlobalOperationNotificationService>();
if (globalOperationService != null)
{
globalOperationService.Started += OnGlobalOperationStarted;
globalOperationService.Stopped += OnGlobalOperationStopped;
}
}
private void UnregisterGlobalOperationNotifications()
{
var globalOperationService = _services.GetService<IGlobalOperationNotificationService>();
if (globalOperationService != null)
{
globalOperationService.Started -= OnGlobalOperationStarted;
globalOperationService.Stopped -= OnGlobalOperationStopped;
}
}
private void OnGlobalOperationStarted(object sender, EventArgs e)
{
lock (_globalNotificationsGate)
{
_globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
SendStartNotificationAsync, _cancellationToken, TaskContinuationOptions.None, 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.TryRunRemoteAsync(
WellKnownServiceHubServices.CodeAnalysisService,
nameof(IRemoteGlobalNotificationDeliveryService.OnGlobalOperationStarted),
solution: null,
Array.Empty<object>(),
callbackTarget: null,
_cancellationToken).ConfigureAwait(false);
return GlobalNotificationState.Started;
}
private void OnGlobalOperationStopped(object sender, GlobalOperationEventArgs e)
{
lock (_globalNotificationsGate)
{
_globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
previous => SendStoppedNotificationAsync(previous, e), _cancellationToken, TaskContinuationOptions.None, TaskScheduler.Default);
}
}
private async Task<GlobalNotificationState> SendStoppedNotificationAsync(Task<GlobalNotificationState> previousTask, GlobalOperationEventArgs e)
{
// 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.TryRunRemoteAsync(
WellKnownServiceHubServices.CodeAnalysisService,
nameof(IRemoteGlobalNotificationDeliveryService.OnGlobalOperationStopped),
solution: null,
new object[] { e.Operations, e.Cancelled },
callbackTarget: null,
_cancellationToken).ConfigureAwait(false);
// Mark that we're stopped now.
return GlobalNotificationState.NotStarted;
}
}
}
......@@ -8,15 +8,10 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Execution;
using Microsoft.CodeAnalysis.Experiments;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
......@@ -34,6 +29,8 @@ public sealed class RemoteHostClientService : ForegroundThreadAffinitizedObject,
private readonly IAsynchronousOperationListener _listener;
private readonly Workspace _workspace;
private GlobalNotificationRemoteDeliveryService? _globalNotificationDelivery;
private readonly object _gate;
private SolutionChecksumUpdater? _checksumUpdater;
......@@ -91,8 +88,8 @@ public void Enable()
var token = _shutdownCancellationTokenSource.Token;
// create solution checksum updater
_checksumUpdater = new SolutionChecksumUpdater(this, token);
_checksumUpdater = new SolutionChecksumUpdater(Workspace, Listener, token);
_globalNotificationDelivery = new GlobalNotificationRemoteDeliveryService(Workspace.Services, token);
_remoteClientTask = Task.Run(() => EnableAsync(token), token);
}
......@@ -115,12 +112,15 @@ public void Disable()
Contract.ThrowIfNull(_shutdownCancellationTokenSource);
Contract.ThrowIfNull(_checksumUpdater);
Contract.ThrowIfNull(_globalNotificationDelivery);
_shutdownCancellationTokenSource.Cancel();
_checksumUpdater.Shutdown();
_checksumUpdater = null;
_globalNotificationDelivery.Dispose();
try
{
remoteClientTask.Wait(_shutdownCancellationTokenSource.Token);
......@@ -192,7 +192,6 @@ private void SetRemoteHostBitness()
Logger.Log(FunctionId.RemoteHost_Bitness, KeyValueLogMessage.Create(LogType.Trace, m => m["64bit"] = x64));
// set service bitness
WellKnownRemoteHostServices.Set64bit(x64);
WellKnownServiceHubServices.Set64bit(x64);
}
......
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Remote
{
internal partial class RemoteHostClientServiceFactory
{
private class SolutionChecksumUpdater : GlobalOperationAwareIdleProcessor
{
private readonly RemoteHostClientService _service;
private readonly TaskQueue _textChangeQueue;
private readonly SemaphoreSlim _event;
private readonly object _gate;
private CancellationTokenSource _globalOperationCancellationSource;
// hold last async token
private IAsyncToken _lastToken;
public SolutionChecksumUpdater(RemoteHostClientService service, CancellationToken shutdownToken)
: base(service.Listener,
service.Workspace.Services.GetService<IGlobalOperationNotificationService>(),
service.Workspace.Options.GetOption(RemoteHostOptions.SolutionChecksumMonitorBackOffTimeSpanInMS), shutdownToken)
{
_service = service;
_textChangeQueue = new TaskQueue(service.Listener, TaskScheduler.Default);
_event = new SemaphoreSlim(initialCount: 0);
_gate = new object();
// start listening workspace change event
_service.Workspace.WorkspaceChanged += OnWorkspaceChanged;
// create its own cancellation token source
_globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken);
Start();
}
private CancellationToken ShutdownCancellationToken => CancellationToken;
protected override async Task ExecuteAsync()
{
lock (_gate)
{
_lastToken?.Dispose();
_lastToken = null;
}
// wait for global operation to finish
await GlobalOperationTask.ConfigureAwait(false);
// update primary solution in remote host
await SynchronizePrimaryWorkspaceAsync(_globalOperationCancellationSource.Token).ConfigureAwait(false);
}
protected override void PauseOnGlobalOperation()
{
var previousCancellationSource = _globalOperationCancellationSource;
// create new cancellation token source linked with given shutdown cancellation token
_globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ShutdownCancellationToken);
CancelAndDispose(previousCancellationSource);
}
protected override Task WaitAsync(CancellationToken cancellationToken)
=> _event.WaitAsync(cancellationToken);
public override void Shutdown()
{
base.Shutdown();
// stop listening workspace change event
_service.Workspace.WorkspaceChanged -= OnWorkspaceChanged;
CancelAndDispose(_globalOperationCancellationSource);
}
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
if (e.Kind == WorkspaceChangeKind.DocumentChanged)
{
PushTextChanges(e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId));
}
// record that we are busy
UpdateLastAccessTime();
EnqueueChecksumUpdate();
}
private void EnqueueChecksumUpdate()
{
// event will raised sequencially. no concurrency on this handler
if (_event.CurrentCount > 0)
{
return;
}
lock (_gate)
{
_lastToken ??= Listener.BeginAsyncOperation(nameof(SolutionChecksumUpdater));
}
_event.Release();
}
private async Task SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken)
{
var workspace = _service.Workspace;
var solution = workspace.CurrentSolution;
if (solution.BranchId != solution.Workspace.PrimaryBranchId)
{
return;
}
var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
{
return;
}
using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken))
{
var checksum = await solution.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
_ = await client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync),
solution,
new object[] { checksum, solution.WorkspaceVersion },
callbackTarget: null,
cancellationToken).ConfigureAwait(false);
}
}
private static void CancelAndDispose(CancellationTokenSource cancellationSource)
{
// cancel running tasks
cancellationSource.Cancel();
// dispose cancellation token source
cancellationSource.Dispose();
}
private void PushTextChanges(Document oldDocument, Document newDocument)
{
// this pushes text changes to the remote side if it can.
// this is purely perf optimization. whether this pushing text change
// worked or not doesn't affect feature's functionality.
//
// this basically see whether it can cheaply find out text changes
// between 2 snapshots, if it can, it will send out that text changes to
// remote side.
//
// the remote side, once got the text change, will again see whether
// it can use that text change information without any high cost and
// create new snapshot from it.
//
// otherwise, it will do the normal behavior of getting full text from
// VS side. this optimization saves times we need to do full text
// synchronization for typing scenario.
if ((oldDocument.TryGetText(out var oldText) == false) ||
(newDocument.TryGetText(out var newText) == false))
{
// we only support case where text already exist
return;
}
// get text changes
var textChanges = newText.GetTextChanges(oldText);
if (textChanges.Count == 0)
{
// no changes
return;
}
// whole document case
if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length)
{
// no benefit here. pulling from remote host is more efficient
return;
}
// only cancelled when remote host gets shutdown
_textChangeQueue.ScheduleTask(nameof(PushTextChanges), async () =>
{
var client = await RemoteHostClient.TryGetClientAsync(_service.Workspace, CancellationToken).ConfigureAwait(false);
if (client == null)
{
return;
}
var state = await oldDocument.State.GetStateChecksumsAsync(CancellationToken).ConfigureAwait(false);
_ = await client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizeTextAsync),
solution: null,
new object[] { oldDocument.Id, state.Text, textChanges },
callbackTarget: null,
CancellationToken).ConfigureAwait(false);
}, CancellationToken);
}
}
}
}
......@@ -8,112 +8,54 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.ServiceHub.Client;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Remote
{
internal sealed partial class ServiceHubRemoteHostClient
{
private partial class ConnectionManager : IDisposable
{
private readonly Workspace _workspace;
private readonly HubClient _hubClient;
private readonly HostGroup _hostGroup;
private delegate Task<Connection> ConnectionFactory(string serviceName, CancellationToken cancellationToken);
private sealed partial class ConnectionPool : IDisposable
{
private readonly ConnectionFactory _connectionFactory;
private readonly ReaderWriterLockSlim _shutdownLock;
private readonly int _maxPoolConnections;
// keyed to serviceName. each connection is for specific service such as CodeAnalysisService
private readonly ConcurrentDictionary<string, ConcurrentQueue<JsonRpcConnection>> _pools;
// indicate whether pool should be used.
private readonly bool _enableConnectionPool;
private readonly ConcurrentDictionary<string, ConcurrentQueue<Connection>> _pools;
private bool _isDisposed;
public ConnectionManager(
Workspace workspace,
HubClient hubClient,
HostGroup hostGroup,
bool enableConnectionPool,
int maxPoolConnection)
public ConnectionPool(ConnectionFactory connectionFactory, int maxPoolConnection)
{
_workspace = workspace;
_hubClient = hubClient;
_hostGroup = hostGroup;
_connectionFactory = connectionFactory;
_maxPoolConnections = maxPoolConnection;
// initial value 4 is chosen to stop concurrent dictionary creating too many locks.
// and big enough for all our services such as codeanalysis, remotehost, snapshot and etc services
_pools = new ConcurrentDictionary<string, ConcurrentQueue<JsonRpcConnection>>(concurrencyLevel: 4, capacity: 4);
_pools = new ConcurrentDictionary<string, ConcurrentQueue<Connection>>(concurrencyLevel: 4, capacity: 4);
_enableConnectionPool = enableConnectionPool;
_shutdownLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
}
public HostGroup HostGroup => _hostGroup;
public Task<Connection> CreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
{
// pool is not enabled by option
if (!_enableConnectionPool)
{
// RemoteHost is allowed to be restarted by IRemoteHostClientService.RequestNewRemoteHostAsync
// when that happens, existing Connection will keep working until they get disposed.
//
// now question is when someone calls RemoteHostClient.TryGetConnection for the client that got
// shutdown, whether it should gracefully handle that request or fail after shutdown.
// for current expected usage case where new remoteHost is only created when new solution is added,
// we should be fine on failing after shutdown.
//
// but, at some point, if we want to support RemoteHost being restarted at any random point,
// we need to revisit this to support such case by creating new temporary connections.
// for now, I dropped it since it felt over-designing when there is no usage case for that yet.
return CreateNewConnectionAsync(serviceName, callbackTarget, cancellationToken);
}
// when callbackTarget is given, we can't share/pool connection since callbackTarget attaches a state to connection.
// so connection is only valid for that specific callbackTarget. it is up to the caller to keep connection open
// if he wants to reuse same connection
if (callbackTarget != null)
{
return CreateNewConnectionAsync(serviceName, callbackTarget, cancellationToken);
}
return GetConnectionFromPoolAsync(serviceName, cancellationToken);
}
private async Task<Connection> GetConnectionFromPoolAsync(string serviceName, CancellationToken cancellationToken)
public async Task<Connection> GetOrCreateConnectionAsync(string serviceName, CancellationToken cancellationToken)
{
var queue = _pools.GetOrAdd(serviceName, _ => new ConcurrentQueue<JsonRpcConnection>());
var queue = _pools.GetOrAdd(serviceName, _ => new ConcurrentQueue<Connection>());
if (queue.TryDequeue(out var connection))
{
return new PooledConnection(this, serviceName, connection);
}
var newConnection = await CreateNewConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false);
return new PooledConnection(this, serviceName, (JsonRpcConnection)newConnection);
var newConnection = await _connectionFactory(serviceName, cancellationToken).ConfigureAwait(false);
return new PooledConnection(this, serviceName, newConnection);
}
private async Task<Connection> CreateNewConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
{
// get stream from service hub to communicate service specific information
// this is what consumer actually use to communicate information
var serviceStream = await RequestServiceAsync(_workspace, _hubClient, serviceName, _hostGroup, cancellationToken).ConfigureAwait(false);
return new JsonRpcConnection(_workspace, _hubClient.Logger, callbackTarget, serviceStream);
}
private void Free(string serviceName, JsonRpcConnection connection)
private void Free(string serviceName, Connection connection)
{
using (_shutdownLock.DisposableRead())
{
if (!_enableConnectionPool || _isDisposed)
if (_isDisposed)
{
// pool is not being used or
// manager is already shutdown
......@@ -152,8 +94,6 @@ public void Dispose()
_pools.Clear();
}
_hubClient.Dispose();
}
}
}
......
......@@ -7,23 +7,22 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Remote;
namespace Microsoft.VisualStudio.LanguageServices.Remote
{
internal sealed partial class ServiceHubRemoteHostClient
{
private partial class ConnectionManager
private partial class ConnectionPool
{
private class PooledConnection : Connection
{
private readonly ConnectionManager _connectionManager;
private readonly ConnectionPool _pool;
private readonly string _serviceName;
private readonly JsonRpcConnection _connection;
private readonly Connection _connection;
public PooledConnection(ConnectionManager pools, string serviceName, JsonRpcConnection connection)
public PooledConnection(ConnectionPool pool, string serviceName, Connection connection)
{
_connectionManager = pools;
_pool = pool;
_serviceName = serviceName;
_connection = connection;
}
......@@ -39,7 +38,7 @@ public override Task<T> InvokeAsync<T>(string targetName, IReadOnlyList<object>
protected override void DisposeImpl()
{
_connectionManager.Free(_serviceName, _connection);
_pool.Free(_serviceName, _connection);
base.DisposeImpl();
}
}
......
......@@ -27,39 +27,32 @@ namespace Microsoft.VisualStudio.LanguageServices.Remote
{
internal sealed partial class ServiceHubRemoteHostClient : RemoteHostClient, IRemoteHostServiceCallback
{
private enum GlobalNotificationState
{
NotStarted,
Started,
Finished
}
private readonly RemoteEndPoint _endPoint;
private readonly ConnectionManager _connectionManager;
private readonly CancellationTokenSource _shutdownCancellationTokenSource;
private readonly HubClient _hubClient;
private readonly HostGroup _hostGroup;
/// <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 appera 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 ConnectionPool? _connectionPool;
private ServiceHubRemoteHostClient(
Workspace workspace,
TraceSource logger,
ConnectionManager connectionManager,
HubClient hubClient,
HostGroup hostGroup,
Stream stream)
: base(workspace)
{
_shutdownCancellationTokenSource = new CancellationTokenSource();
if (workspace.Options.GetOption(RemoteHostOptions.EnableConnectionPool))
{
int maxPoolConnection = workspace.Options.GetOption(RemoteHostOptions.MaxPoolConnection);
_connectionPool = new ConnectionPool(
connectionFactory: (serviceName, cancellationToken) => CreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken),
maxPoolConnection);
}
_connectionManager = connectionManager;
_hubClient = hubClient;
_hostGroup = hostGroup;
_endPoint = new RemoteEndPoint(stream, logger, incomingCallTarget: this);
_endPoint = new RemoteEndPoint(stream, hubClient.Logger, incomingCallTarget: this);
_endPoint.Disconnected += OnDisconnected;
_endPoint.UnexpectedExceptionThrown += OnUnexpectedExceptionThrown;
_endPoint.StartListening();
......@@ -72,9 +65,6 @@ private void OnUnexpectedExceptionThrown(Exception unexpectedException)
{
using (Logger.LogBlock(FunctionId.ServiceHubRemoteHostClient_CreateAsync, cancellationToken))
{
var enableConnectionPool = workspace.Options.GetOption(RemoteHostOptions.EnableConnectionPool);
var maxConnection = workspace.Options.GetOption(RemoteHostOptions.MaxPoolConnection);
// let each client to have unique id so that we can distinguish different clients when service is restarted
var clientId = CreateClientId(Process.GetCurrentProcess().Id.ToString());
......@@ -84,10 +74,9 @@ private void OnUnexpectedExceptionThrown(Exception unexpectedException)
// use the hub client logger for unexpected exceptions from devenv as well, so we have complete information in the log:
WatsonReporter.InitializeLogger(hubClient.Logger);
var remoteHostStream = await RequestServiceAsync(workspace, hubClient, WellKnownRemoteHostServices.RemoteHostService, hostGroup, cancellationToken).ConfigureAwait(false);
var connectionManager = new ConnectionManager(workspace, hubClient, hostGroup, enableConnectionPool, maxConnection);
var remoteHostStream = await RequestServiceAsync(workspace, hubClient, WellKnownServiceHubServices.RemoteHostService, hostGroup, cancellationToken).ConfigureAwait(false);
var client = new ServiceHubRemoteHostClient(workspace, hubClient.Logger, connectionManager, remoteHostStream);
var client = new ServiceHubRemoteHostClient(workspace, hubClient, hostGroup, remoteHostStream);
var uiCultureLCID = CultureInfo.CurrentUICulture.LCID;
var cultureLCID = CultureInfo.CurrentCulture.LCID;
......@@ -157,136 +146,45 @@ static bool ReportNonFatalWatson(Exception e, CancellationToken cancellationToke
}
}
public override string ClientId => _connectionManager.HostGroup.Id;
public override bool IsRemoteHost64Bit => RemoteHostOptions.IsServiceHubProcess64Bit(Workspace);
public override Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
=> _connectionManager.CreateConnectionAsync(serviceName, callbackTarget, cancellationToken).AsNullable();
protected override void OnStarted()
=> RegisterGlobalOperationNotifications();
public override void Dispose()
{
// cancel all pending async work
_shutdownCancellationTokenSource.Cancel();
// we are asked to stop. unsubscribe and dispose to disconnect.
// there are 2 ways to get disconnected. one is Roslyn decided to disconnect with RemoteHost (ex, cancellation or recycle OOP) and
// the other is external thing disconnecting remote host from us (ex, user killing OOP process).
// the Disconnected event we subscribe is to detect #2 case. and this method is for #1 case. so when we are willingly disconnecting
// we don't need the event, otherwise, Disconnected event will be called twice.
UnregisterGlobalOperationNotifications();
public HostGroup HostGroup => _hostGroup;
_endPoint.Disconnected -= OnDisconnected;
_endPoint.UnexpectedExceptionThrown -= OnUnexpectedExceptionThrown;
_endPoint.Dispose();
_connectionManager.Dispose();
base.Dispose();
}
public HostGroup HostGroup
{
get
{
Debug.Assert(_connectionManager.HostGroup.Id == ClientId);
return _connectionManager.HostGroup;
}
}
#region Global Operation Notifications
private void RegisterGlobalOperationNotifications()
{
var globalOperationService = this.Workspace.Services.GetService<IGlobalOperationNotificationService>();
if (globalOperationService != null)
{
globalOperationService.Started += OnGlobalOperationStarted;
globalOperationService.Stopped += OnGlobalOperationStopped;
}
}
public override string ClientId => _hostGroup.Id;
public override bool IsRemoteHost64Bit => RemoteHostOptions.IsServiceHubProcess64Bit(Workspace);
private void UnregisterGlobalOperationNotifications()
protected override Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
{
var globalOperationService = this.Workspace.Services.GetService<IGlobalOperationNotificationService>();
if (globalOperationService != null)
{
globalOperationService.Started -= OnGlobalOperationStarted;
globalOperationService.Stopped -= OnGlobalOperationStopped;
}
// When callbackTarget is given, we can't share/pool connection since callbackTarget attaches a state to connection.
// so connection is only valid for that specific callbackTarget. it is up to the caller to keep connection open
// if he wants to reuse same connection.
Task localTask;
lock (_globalNotificationsGate)
if (callbackTarget == null && _connectionPool != null)
{
// Unilaterally transition us to the finished state. Once we're finished
// we cannot start or stop anymore.
_globalNotificationsTask = _globalNotificationsTask.ContinueWith(
_ => GlobalNotificationState.Finished, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
localTask = _globalNotificationsTask;
return _connectionPool.GetOrCreateConnectionAsync(serviceName, cancellationToken).AsNullable();
}
// Have to wait for all the notifications to make it to the OOP side so we keep
// it in a consistent state. Also, if we don't do this, our _rpc object will
// get disposed while we're remoting over the messages to the oop side.
localTask.Wait();
return CreateConnectionAsync(serviceName, callbackTarget, cancellationToken).AsNullable();
}
private void OnGlobalOperationStarted(object sender, EventArgs e)
private async Task<Connection> CreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
{
lock (_globalNotificationsGate)
{
_globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
continuation, _shutdownCancellationTokenSource.Token, TaskContinuationOptions.None, TaskScheduler.Default);
}
async Task<GlobalNotificationState> continuation(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;
}
await _endPoint.InvokeAsync(
nameof(IRemoteHostService.OnGlobalOperationStarted),
new object[] { "" },
_shutdownCancellationTokenSource.Token).ConfigureAwait(false);
return GlobalNotificationState.Started;
}
var serviceStream = await RequestServiceAsync(Workspace, _hubClient, serviceName, _hostGroup, cancellationToken).ConfigureAwait(false);
return new JsonRpcConnection(Workspace, _hubClient.Logger, callbackTarget, serviceStream);
}
private void OnGlobalOperationStopped(object sender, GlobalOperationEventArgs e)
public override void Dispose()
{
lock (_globalNotificationsGate)
{
_globalNotificationsTask = _globalNotificationsTask.SafeContinueWithFromAsync(
continuation, _shutdownCancellationTokenSource.Token, TaskContinuationOptions.None, TaskScheduler.Default);
}
async Task<GlobalNotificationState> continuation(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;
}
_endPoint.Disconnected -= OnDisconnected;
_endPoint.UnexpectedExceptionThrown -= OnUnexpectedExceptionThrown;
_endPoint.Dispose();
await _endPoint.InvokeAsync(
nameof(IRemoteHostService.OnGlobalOperationStopped),
new object[] { e.Operations, e.Cancelled },
_shutdownCancellationTokenSource.Token).ConfigureAwait(false);
_connectionPool?.Dispose();
_hubClient.Dispose();
// Mark that we're stopped now.
return GlobalNotificationState.NotStarted;
}
base.Dispose();
}
#endregion
private void OnDisconnected(JsonRpcDisconnectedEventArgs e)
=> Dispose();
#region Assets
......@@ -328,8 +226,5 @@ public Task<bool> IsExperimentEnabledAsync(string experimentName, CancellationTo
}
#endregion
private void OnDisconnected(JsonRpcDisconnectedEventArgs e)
=> Dispose();
}
}
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Remote
{
internal sealed class SolutionChecksumUpdater : GlobalOperationAwareIdleProcessor
{
private readonly Workspace _workspace;
private readonly TaskQueue _textChangeQueue;
private readonly SemaphoreSlim _event;
private readonly object _gate;
private CancellationTokenSource _globalOperationCancellationSource;
// hold last async token
private IAsyncToken _lastToken;
public SolutionChecksumUpdater(Workspace workspace, IAsynchronousOperationListener listener, CancellationToken shutdownToken)
: base(listener,
workspace.Services.GetService<IGlobalOperationNotificationService>(),
workspace.Options.GetOption(RemoteHostOptions.SolutionChecksumMonitorBackOffTimeSpanInMS), shutdownToken)
{
_workspace = workspace;
_textChangeQueue = new TaskQueue(listener, TaskScheduler.Default);
_event = new SemaphoreSlim(initialCount: 0);
_gate = new object();
// start listening workspace change event
_workspace.WorkspaceChanged += OnWorkspaceChanged;
// create its own cancellation token source
_globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken);
Start();
}
private CancellationToken ShutdownCancellationToken => CancellationToken;
protected override async Task ExecuteAsync()
{
lock (_gate)
{
_lastToken?.Dispose();
_lastToken = null;
}
// wait for global operation to finish
await GlobalOperationTask.ConfigureAwait(false);
// update primary solution in remote host
await SynchronizePrimaryWorkspaceAsync(_globalOperationCancellationSource.Token).ConfigureAwait(false);
}
protected override void PauseOnGlobalOperation()
{
var previousCancellationSource = _globalOperationCancellationSource;
// create new cancellation token source linked with given shutdown cancellation token
_globalOperationCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ShutdownCancellationToken);
CancelAndDispose(previousCancellationSource);
}
protected override Task WaitAsync(CancellationToken cancellationToken)
=> _event.WaitAsync(cancellationToken);
public override void Shutdown()
{
base.Shutdown();
// stop listening workspace change event
_workspace.WorkspaceChanged -= OnWorkspaceChanged;
CancelAndDispose(_globalOperationCancellationSource);
}
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
if (e.Kind == WorkspaceChangeKind.DocumentChanged)
{
PushTextChanges(e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId));
}
// record that we are busy
UpdateLastAccessTime();
EnqueueChecksumUpdate();
}
private void EnqueueChecksumUpdate()
{
// event will raised sequencially. no concurrency on this handler
if (_event.CurrentCount > 0)
{
return;
}
lock (_gate)
{
_lastToken ??= Listener.BeginAsyncOperation(nameof(SolutionChecksumUpdater));
}
_event.Release();
}
private async Task SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken)
{
var solution = _workspace.CurrentSolution;
if (solution.BranchId != _workspace.PrimaryBranchId)
{
return;
}
var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
{
return;
}
using (Logger.LogBlock(FunctionId.SolutionChecksumUpdater_SynchronizePrimaryWorkspace, cancellationToken))
{
var checksum = await solution.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
_ = await client.TryRunRemoteAsync(
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync),
solution,
new object[] { checksum, solution.WorkspaceVersion },
callbackTarget: null,
cancellationToken).ConfigureAwait(false);
}
}
private static void CancelAndDispose(CancellationTokenSource cancellationSource)
{
// cancel running tasks
cancellationSource.Cancel();
// dispose cancellation token source
cancellationSource.Dispose();
}
private void PushTextChanges(Document oldDocument, Document newDocument)
{
// this pushes text changes to the remote side if it can.
// this is purely perf optimization. whether this pushing text change
// worked or not doesn't affect feature's functionality.
//
// this basically see whether it can cheaply find out text changes
// between 2 snapshots, if it can, it will send out that text changes to
// remote side.
//
// the remote side, once got the text change, will again see whether
// it can use that text change information without any high cost and
// create new snapshot from it.
//
// otherwise, it will do the normal behavior of getting full text from
// VS side. this optimization saves times we need to do full text
// synchronization for typing scenario.
if ((oldDocument.TryGetText(out var oldText) == false) ||
(newDocument.TryGetText(out var newText) == false))
{
// we only support case where text already exist
return;
}
// get text changes
var textChanges = newText.GetTextChanges(oldText);
if (textChanges.Count == 0)
{
// no changes
return;
}
// whole document case
if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length)
{
// no benefit here. pulling from remote host is more efficient
return;
}
// only cancelled when remote host gets shutdown
_textChangeQueue.ScheduleTask(nameof(PushTextChanges), async () =>
{
var client = await RemoteHostClient.TryGetClientAsync(_workspace, CancellationToken).ConfigureAwait(false);
if (client == null)
{
return;
}
var state = await oldDocument.State.GetStateChecksumsAsync(CancellationToken).ConfigureAwait(false);
_ = await client.TryRunRemoteAsync(
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizeTextAsync),
solution: null,
new object[] { oldDocument.Id, state.Text, textChanges },
callbackTarget: null,
CancellationToken).ConfigureAwait(false);
}, CancellationToken);
}
}
}
......@@ -118,7 +118,7 @@ private async Task<List<VSPublishSymbolParams>> GetVsSearchResultsAsync(Solution
private async Task UpdatePrimaryWorkspace(InProcRemoteHostClient client, Solution solution)
{
Assert.True(await client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync),
solution,
new object[] { await solution.State.GetChecksumAsync(CancellationToken.None), _solutionVersion++ },
......
......@@ -115,7 +115,7 @@ public async Task TestRemoteHostTextSynchronize()
// sync
_ = await client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizeTextAsync),
solution: null,
new object[] { oldDocument.Id, oldState.Text, newText.GetTextChanges(oldText) },
......@@ -481,7 +481,7 @@ private static (Project, Document) GetProjectAndDocument(Solution solution, stri
private async Task UpdatePrimaryWorkspace(InProcRemoteHostClient client, Solution solution)
{
Assert.True(await client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SynchronizePrimaryWorkspaceAsync),
solution,
new object[] { await solution.State.GetChecksumAsync(CancellationToken.None), _solutionVersion++ },
......
......@@ -146,15 +146,15 @@ public async Task TestCancellationOnSessionWithSolution()
{
var solution = workspace.CurrentSolution;
var solutionChecksum = await solution.State.GetChecksumAsync(CancellationToken.None);
var service = solution.Workspace.Services.GetService<IRemotableDataService>();
var source = new CancellationTokenSource();
using var connection = new InvokeThrowsCancellationConnection(source);
var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => SessionWithSolution.CreateAsync(connection, solution, source.Token));
using var session = new KeepAliveSession(new InvokeThrowsCancellationConnection(source), service);
var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => SessionWithSolution.CreateAsync(session, solution, source.Token));
Assert.Equal(exception.CancellationToken, source.Token);
// make sure things that should have been cleaned up are cleaned up
var service = (RemotableDataServiceFactory.Service)solution.Workspace.Services.GetService<IRemotableDataService>();
Assert.Null(await service.TestOnly_GetRemotableDataAsync(solutionChecksum, CancellationToken.None).ConfigureAwait(false));
Assert.Null(await ((RemotableDataServiceFactory.Service)service).TestOnly_GetRemotableDataAsync(solutionChecksum, CancellationToken.None).ConfigureAwait(false));
}
}
......
......@@ -39,8 +39,8 @@ public Task<Optional<T>> TryRunRemoteAsync<T>(string targetName, Solution? solut
return null;
}
var connection = await _client.TryCreateConnectionAsync(_serviceName, callbackTarget, cancellationToken).ConfigureAwait(false);
if (connection == null)
var keepAliveSession = await _client.TryCreateKeepAliveSessionAsync(_serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false);
if (keepAliveSession == null)
{
return null;
}
......@@ -49,13 +49,13 @@ public Task<Optional<T>> TryRunRemoteAsync<T>(string targetName, Solution? solut
try
{
// transfer ownership of the connection to the session object:
session = await SessionWithSolution.CreateAsync(connection, solution, cancellationToken).ConfigureAwait(false);
session = await SessionWithSolution.CreateAsync(keepAliveSession, solution, cancellationToken).ConfigureAwait(false);
}
finally
{
if (session == null)
{
connection.Dispose();
keepAliveSession.Dispose();
}
}
......@@ -74,12 +74,12 @@ internal Session(SessionWithSolution inner)
public Task InvokeAsync(string targetName, IReadOnlyList<object> arguments, CancellationToken cancellationToken)
{
return _inner.Connection.InvokeAsync(targetName, arguments, cancellationToken);
return _inner.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken);
}
public Task<T> InvokeAsync<T>(string targetName, IReadOnlyList<object> arguments, CancellationToken cancellationToken)
{
return _inner.Connection.InvokeAsync<T>(targetName, arguments, cancellationToken);
return _inner.KeepAliveSession.RunRemoteAsync<T>(targetName, solution: null, arguments, cancellationToken);
}
public void Dispose()
......
......@@ -70,7 +70,7 @@ public static void SetLoggers(IGlobalOptionService optionService, IThreadingCont
var functionIds = GetFunctionIds(options).ToList();
threadingContext.JoinableTaskFactory.Run(() => client.TryRunRemoteAsync(
WellKnownRemoteHostServices.RemoteHostService,
WellKnownServiceHubServices.RemoteHostService,
nameof(IRemoteHostService.SetLoggingFunctionIds),
solution: null,
new object[] { loggerTypes, functionIds },
......
......@@ -5,8 +5,6 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Execution;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
namespace Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api
......@@ -26,21 +24,7 @@ public static async Task<Optional<T>> TryRunRemoteAsync<T>(Workspace workspace,
serviceName += "64";
}
using var connection = await client.TryCreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false);
if (connection == null)
{
return default;
}
var remoteDataService = workspace.Services.GetRequiredService<IRemotableDataService>();
using var scope = await remoteDataService.CreatePinnedRemotableDataScopeAsync(solution, cancellationToken).ConfigureAwait(false);
using var _ = ArrayBuilder<object>.GetInstance(arguments.Count + 1, out var argumentsBuilder);
argumentsBuilder.Add(scope.SolutionInfo);
argumentsBuilder.AddRange(arguments);
return await connection.InvokeAsync<T>(targetName, argumentsBuilder, cancellationToken).ConfigureAwait(false);
return await client.TryRunRemoteAsync<T>(serviceName, targetName, solution, arguments, callbackTarget: null, cancellationToken).ConfigureAwait(false);
}
}
}
......@@ -27,8 +27,8 @@ public async Task<UnitTestingKeepAliveSessionWrapper> TryCreateUnitTestingKeepAl
public async Task<UnitTestingSessionWithSolutionWrapper> TryCreateUnitingSessionWithSolutionWrapperAsync(string serviceName, Solution solution, CancellationToken cancellationToken)
{
var connection = await UnderlyingObject.TryCreateConnectionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false);
if (connection == null)
var keepAliveSession = await UnderlyingObject.TryCreateKeepAliveSessionAsync(serviceName, callbackTarget: null, cancellationToken).ConfigureAwait(false);
if (keepAliveSession == null)
{
return default;
}
......@@ -37,13 +37,13 @@ public async Task<UnitTestingSessionWithSolutionWrapper> TryCreateUnitingSession
try
{
// transfer ownership of the connection to the session object:
session = await SessionWithSolution.CreateAsync(connection, solution, cancellationToken).ConfigureAwait(false);
session = await SessionWithSolution.CreateAsync(keepAliveSession, solution, cancellationToken).ConfigureAwait(false);
}
finally
{
if (session == null)
{
connection.Dispose();
keepAliveSession.Dispose();
}
}
......
......@@ -20,10 +20,10 @@ public UnitTestingSessionWithSolutionWrapper(SessionWithSolution underlyingObjec
=> UnderlyingObject = underlyingObject;
public Task InvokeAsync(string targetName, IReadOnlyList<object> arguments, CancellationToken cancellationToken)
=> UnderlyingObject?.Connection.InvokeAsync(targetName, arguments, cancellationToken) ?? Task.CompletedTask;
=> UnderlyingObject?.KeepAliveSession.RunRemoteAsync(targetName, solution: null, arguments, cancellationToken) ?? Task.CompletedTask;
public Task<T> InvokeAsync<T>(string targetName, IReadOnlyList<object> arguments, CancellationToken cancellationToken)
=> UnderlyingObject?.Connection.InvokeAsync<T>(targetName, arguments, cancellationToken);
=> UnderlyingObject?.KeepAliveSession.RunRemoteAsync<T>(targetName, solution: null, arguments, cancellationToken);
public void Dispose()
=> UnderlyingObject?.Dispose();
......
......@@ -2,13 +2,16 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Collections.Generic;
namespace Microsoft.CodeAnalysis.Remote
{
internal static class WellKnownRemoteHostServices
internal interface IRemoteGlobalNotificationDeliveryService
{
public static void Set64bit(bool x64)
=> RemoteHostService = "roslynRemoteHost" + (x64 ? "64" : "");
void OnGlobalOperationStarted();
public static string RemoteHostService { get; private set; } = "roslynRemoteHost";
void OnGlobalOperationStopped(IReadOnlyList<string> operations, bool cancelled);
}
}
......@@ -20,13 +20,6 @@ internal interface IRemoteHostService
/// </summary>
void SetLoggingFunctionIds(List<string> loggerTypes, List<string> functionIds, CancellationToken cancellationToken);
/// <remarks>
/// JsonRPC seems to have a problem with empty parameter lists. So passing a dummy parameter
/// just to make it work properly.
/// </remarks>
void OnGlobalOperationStarted(string unused);
void OnGlobalOperationStopped(IReadOnlyList<string> operations, bool cancelled);
/// <summary>
/// Synchronize data to OOP proactively without anyone asking for it to make most of operation
/// faster
......
......@@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.Execution;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Host;
#if DEBUG
using System.Diagnostics;
......@@ -24,7 +25,7 @@ namespace Microsoft.CodeAnalysis.Remote
///
/// user can create a connection to communicate with the server (remote host) through this client
/// </summary>
internal abstract partial class RemoteHostClient : IDisposable
internal abstract class RemoteHostClient : IDisposable
{
public readonly Workspace Workspace;
public event EventHandler<bool>? StatusChanged;
......@@ -52,16 +53,12 @@ protected RemoteHostClient(Workspace workspace)
/// Creating session could fail if remote host is not available. one of example will be user killing
/// remote host.
/// </summary>
public abstract Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken);
protected abstract Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken);
public abstract bool IsRemoteHost64Bit { get; }
protected abstract void OnStarted();
protected void Started()
{
OnStarted();
OnStatusChanged(started: true);
}
......@@ -85,8 +82,11 @@ public static string CreateClientId(string prefix)
}
public static Task<RemoteHostClient?> TryGetClientAsync(Workspace workspace, CancellationToken cancellationToken)
=> TryGetClientAsync(workspace.Services, cancellationToken);
public static Task<RemoteHostClient?> TryGetClientAsync(HostWorkspaceServices services, CancellationToken cancellationToken)
{
var service = workspace.Services.GetService<IRemoteHostClientService>();
var service = services.GetService<IRemoteHostClientService>();
if (service == null)
{
return SpecializedTasks.Null<RemoteHostClient>();
......@@ -196,13 +196,8 @@ public NoOpClient(Workspace workspace)
public override string ClientId => nameof(NoOpClient);
public override bool IsRemoteHost64Bit => false;
public override Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
protected override Task<Connection?> TryCreateConnectionAsync(string serviceName, object? callbackTarget, CancellationToken cancellationToken)
=> SpecializedTasks.Null<Connection>();
protected override void OnStarted()
{
// do nothing
}
}
/// <summary>
......
......@@ -15,12 +15,13 @@ namespace Microsoft.CodeAnalysis.Remote
[Obsolete("Only used by Razor and LUT", error: false)]
internal sealed class SessionWithSolution : IDisposable
{
public readonly RemoteHostClient.Connection Connection;
internal readonly KeepAliveSession KeepAliveSession;
private readonly PinnedRemotableDataScope _scope;
public static async Task<SessionWithSolution> CreateAsync(RemoteHostClient.Connection connection, Solution solution, CancellationToken cancellationToken)
public static async Task<SessionWithSolution> CreateAsync(KeepAliveSession keepAliveSession, Solution solution, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(connection);
Contract.ThrowIfNull(keepAliveSession);
Contract.ThrowIfNull(solution);
var service = solution.Workspace.Services.GetRequiredService<IRemotableDataService>();
......@@ -31,13 +32,14 @@ public static async Task<SessionWithSolution> CreateAsync(RemoteHostClient.Conne
{
// set connection state for this session.
// we might remove this in future. see https://github.com/dotnet/roslyn/issues/24836
await connection.InvokeAsync(
await keepAliveSession.RunRemoteAsync(
WellKnownServiceHubServices.ServiceHubServiceBase_Initialize,
solution: null,
new object[] { scope.SolutionInfo },
cancellationToken).ConfigureAwait(false);
// transfer ownership of connection and scope to the session object:
session = new SessionWithSolution(connection, scope);
session = new SessionWithSolution(keepAliveSession, scope);
}
finally
{
......@@ -50,16 +52,16 @@ public static async Task<SessionWithSolution> CreateAsync(RemoteHostClient.Conne
return session;
}
private SessionWithSolution(RemoteHostClient.Connection connection, PinnedRemotableDataScope scope)
private SessionWithSolution(KeepAliveSession keepAliveSession, PinnedRemotableDataScope scope)
{
Connection = connection;
KeepAliveSession = keepAliveSession;
_scope = scope;
}
public void Dispose()
{
_scope.Dispose();
Connection.Dispose();
KeepAliveSession.Dispose();
}
}
}
......@@ -12,6 +12,7 @@ public static void Set64bit(bool x64)
{
var bit = x64 ? "64" : "";
RemoteHostService = "roslynRemoteHost" + bit;
CodeAnalysisService = NamePrefix + "CodeAnalysis" + bit;
RemoteDesignerAttributeService = NamePrefix + "RemoteDesignerAttributeService" + bit;
RemoteProjectTelemetryService = NamePrefix + "RemoteProjectTelemetryService" + bit;
......@@ -20,6 +21,7 @@ public static void Set64bit(bool x64)
LanguageServer = NamePrefix + "LanguageServer" + bit;
}
public static string RemoteHostService { get; private set; } = NamePrefix + "RemoteHost";
public static string CodeAnalysisService { get; private set; } = NamePrefix + "CodeAnalysis";
public static string RemoteSymbolSearchUpdateEngine { get; private set; } = NamePrefix + "RemoteSymbolSearchUpdateEngine";
public static string RemoteDesignerAttributeService { get; private set; } = NamePrefix + "RemoteDesignerAttributeService";
......
// 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.
#nullable enable
using System.Collections.Generic;
using System.Threading;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote.Services;
namespace Microsoft.CodeAnalysis.Remote
{
internal partial class CodeAnalysisService : ServiceBase, IRemoteGlobalNotificationDeliveryService
{
/// <summary>
/// Remote API.
/// </summary>
public void OnGlobalOperationStarted()
{
RunService(() =>
{
var globalOperationNotificationService = GetGlobalOperationNotificationService();
globalOperationNotificationService?.OnStarted();
}, CancellationToken.None);
}
/// <summary>
/// Remote API.
/// </summary>
public void OnGlobalOperationStopped(IReadOnlyList<string> operations, bool cancelled)
{
RunService(() =>
{
var globalOperationNotificationService = GetGlobalOperationNotificationService();
globalOperationNotificationService?.OnStopped(operations, cancelled);
}, CancellationToken.None);
}
private RemoteGlobalOperationNotificationService? GetGlobalOperationNotificationService()
=> SolutionService.PrimaryWorkspace.Services.GetService<IGlobalOperationNotificationService>() as RemoteGlobalOperationNotificationService;
}
}
......@@ -20,7 +20,6 @@
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote.Diagnostics;
using Microsoft.CodeAnalysis.Remote.Services;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Telemetry;
......@@ -140,30 +139,6 @@ Task<bool> IAssetSource.IsExperimentEnabledAsync(string experimentName, Cancella
}, cancellationToken);
}
/// <summary>
/// Remote API.
/// </summary>
public void OnGlobalOperationStarted(string unused)
{
RunService(() =>
{
var globalOperationNotificationService = GetGlobalOperationNotificationService();
globalOperationNotificationService?.OnStarted();
}, CancellationToken.None);
}
/// <summary>
/// Remote API.
/// </summary>
public void OnGlobalOperationStopped(IReadOnlyList<string> operations, bool cancelled)
{
RunService(() =>
{
var globalOperationNotificationService = GetGlobalOperationNotificationService();
globalOperationNotificationService?.OnStopped(operations, cancelled);
}, CancellationToken.None);
}
/// <summary>
/// Remote API.
/// </summary>
......@@ -270,12 +245,6 @@ private static bool ExpectedCultureIssue(Exception ex)
return ex is ArgumentOutOfRangeException || ex is CultureNotFoundException;
}
private RemoteGlobalOperationNotificationService? GetGlobalOperationNotificationService()
{
var notificationService = SolutionService.PrimaryWorkspace.Services.GetService<IGlobalOperationNotificationService>() as RemoteGlobalOperationNotificationService;
return notificationService;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr AddDllDirectory(string directory);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册