提交 03a9b277 编写于 作者: H Heejae Chang

add ability to recycle OOP when requested.

this ability is needed to so user extension dlls can be re-loaded without re-launching VS.
上级 a722c76c
......@@ -77,6 +77,8 @@ protected override void OnConnected()
protected override void OnDisconnected()
{
// we are asked to disconnect. unsubscribe and dispose to disconnect
_rpc.Disconnected -= OnRpcDisconnected;
_rpc.Dispose();
}
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
......@@ -97,6 +98,8 @@ public void Enable()
public void Disable()
{
RemoteHostClient client = null;
lock (_gate)
{
if (_instanceTask == null)
......@@ -120,13 +123,18 @@ public void Disable()
instanceTask.Wait(_shutdownCancellationTokenSource.Token);
// result can be null if service hub failed to launch
instanceTask.Result?.Shutdown();
client = instanceTask.Result;
}
catch (OperationCanceledException)
{
// _instance wasn't finished running yet.
}
}
// shut it down outside of lock so that
// we don't call into different component while
// holding onto a lock
client?.Shutdown();
}
public Task<RemoteHostClient> GetRemoteHostClientAsync(CancellationToken cancellationToken)
......@@ -226,10 +234,49 @@ private void OnConnectionChanged(object sender, bool connected)
FatalError.ReportWithoutCrash(new Exception("Connection to remote host closed"));
// use info bar to show warning to users
_workspace.Services.GetService<IErrorReportingService>().ShowGlobalErrorInfo(ServicesVSResources.Unfortunately_a_process_used_by_Visual_Studio_has_encountered_an_unrecoverable_error_We_recommend_saving_your_work_and_then_closing_and_restarting_Visual_Studio,
var infoBarUIs = new List<ErrorReportingUI>();
infoBarUIs.Add(
new ErrorReportingUI(ServicesVSResources.Learn_more, ErrorReportingUI.UIKind.HyperLink, () =>
BrowserHelper.StartBrowser(new Uri(OOPKilledMoreInfoLink)), closeAfterAction: false));
var allowRestarting = _workspace.Options.GetOption(RemoteHostOptions.RestartRemoteHostAllowed);
if (allowRestarting)
{
infoBarUIs.Add(
new ErrorReportingUI("Restart OOP", ErrorReportingUI.UIKind.Button, async () =>
await RequestNewRemoteHostAsync(CancellationToken.None).ConfigureAwait(false), closeAfterAction: true));
}
_workspace.Services.GetService<IErrorReportingService>().ShowGlobalErrorInfo(
ServicesVSResources.Unfortunately_a_process_used_by_Visual_Studio_has_encountered_an_unrecoverable_error_We_recommend_saving_your_work_and_then_closing_and_restarting_Visual_Studio,
infoBarUIs.ToArray());
}
}
public async Task RequestNewRemoteHostAsync(CancellationToken cancellationToken)
{
var instance = await GetRemoteHostClientAsync(cancellationToken).ConfigureAwait(false);
if (instance == null)
{
return;
}
// log that remote host is restarted
Logger.Log(FunctionId.RemoteHostClientService_Restarted, KeyValueLogMessage.NoProperty);
// we are going to kill the existing remote host, connection change is expected
instance.ConnectionChanged -= OnConnectionChanged;
lock (_gate)
{
var token = _shutdownCancellationTokenSource.Token;
_instanceTask = Task.Run(() => EnableAsync(token), token);
}
// shutdown
instance.Shutdown();
}
}
}
......
......@@ -34,7 +34,15 @@ internal static class RemoteHostOptions
// when there are many (over 10+ requests) at the same time. one of reasons of this is we put our service hub process as "Below Normal" priority.
// normally response time is within 10s ms. at most 100ms. if priority is changed to "Normal", most of time 10s ms.
[ExportOption]
public static readonly Option<int> RequestServiceTimeoutInMS = new Option<int>(nameof(InternalFeatureOnOffOptions), nameof(RequestServiceTimeoutInMS), defaultValue: 10 * 60 * 1000);
public static readonly Option<int> RequestServiceTimeoutInMS = new Option<int>(
nameof(InternalFeatureOnOffOptions), nameof(RequestServiceTimeoutInMS), defaultValue: 10 * 60 * 1000,
storageLocations: new LocalUserProfileStorageLocation(InternalFeatureOnOffOptions.LocalRegistryPath + nameof(RequestServiceTimeoutInMS)));
// This options allow users to restart OOP when it is killed by users
[ExportOption]
public static readonly Option<bool> RestartRemoteHostAllowed = new Option<bool>(
nameof(InternalFeatureOnOffOptions), nameof(RestartRemoteHostAllowed), defaultValue: false,
storageLocations: new LocalUserProfileStorageLocation(InternalFeatureOnOffOptions.LocalRegistryPath + nameof(RestartRemoteHostAllowed)));
[ExportOption]
public static readonly Option<bool> RemoteHostTest = new Option<bool>(nameof(InternalFeatureOnOffOptions), nameof(RemoteHostTest), defaultValue: false);
......
......@@ -23,6 +23,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Remote
internal sealed partial class ServiceHubRemoteHostClient : RemoteHostClient
{
private static int s_instanceId = 0;
private readonly HubClient _hubClient;
private readonly JsonRpc _rpc;
private readonly HostGroup _hostGroup;
......@@ -33,8 +35,11 @@ internal sealed partial class ServiceHubRemoteHostClient : RemoteHostClient
{
using (Logger.LogBlock(FunctionId.ServiceHubRemoteHostClient_CreateAsync, cancellationToken))
{
// let each client to have unique id so that we can distinguish different clients when service is restarted
var currentInstanceId = Interlocked.Add(ref s_instanceId, 1);
var primary = new HubClient("ManagedLanguage.IDE.RemoteHostClient");
var current = $"VS ({Process.GetCurrentProcess().Id})";
var current = $"VS ({Process.GetCurrentProcess().Id}) ({currentInstanceId})";
var hostGroup = new HostGroup(current);
var timeout = TimeSpan.FromMilliseconds(workspace.Options.GetOption(RemoteHostOptions.RequestServiceTimeoutInMS));
......@@ -115,6 +120,12 @@ protected override void OnConnected()
protected override void OnDisconnected()
{
// we are asked to disconnect. 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.
_rpc.Disconnected -= OnRpcDisconnected;
_rpc.Dispose();
}
......
......@@ -124,6 +124,34 @@ public async Task TestSessionWithNoSolution()
service.Disable();
}
[Fact, Trait(Traits.Feature, Traits.Features.RemoteHost)]
public async Task TestRequestNewRemoteHost()
{
var service = CreateRemoteHostClientService();
service.Enable();
var completionTask = new TaskCompletionSource<bool>();
var client1 = await service.GetRemoteHostClientAsync(CancellationToken.None);
client1.ConnectionChanged += (s, connected) =>
{
// mark done
completionTask.SetResult(connected);
};
await service.RequestNewRemoteHostAsync(CancellationToken.None);
var result = await completionTask.Task;
Assert.False(result);
var client2 = await service.GetRemoteHostClientAsync(CancellationToken.None);
Assert.NotEqual(client1, client2);
service.Disable();
}
private RemoteHostClientServiceFactory.RemoteHostClientService CreateRemoteHostClientService(
Workspace workspace = null,
IEnumerable<AnalyzerReference> hostAnalyzerReferences = null,
......
......@@ -362,5 +362,6 @@ internal enum FunctionId
SolutionSynchronizationService_GetRemotableData,
AssetService_SynchronizeProjectAssetsAsync,
FileTextLoader_FileLengthThresholdExceeded,
RemoteHostClientService_Restarted,
}
}
......@@ -10,7 +10,10 @@ internal partial class DefaultRemoteHostClientServiceFactory
{
public class RemoteHostClientService : IRemoteHostClientService
{
private readonly AsyncLazy<RemoteHostClient> _lazyInstance;
private readonly Workspace _workspace;
private readonly IRemoteHostClientFactory _remoteHostClientFactory;
private AsyncLazy<RemoteHostClient> _lazyInstance;
public RemoteHostClientService(Workspace workspace)
{
......@@ -21,7 +24,10 @@ public RemoteHostClientService(Workspace workspace)
return;
}
_lazyInstance = new AsyncLazy<RemoteHostClient>(c => remoteHostClientFactory.CreateAsync(workspace, c), cacheResult: true);
_workspace = workspace;
_remoteHostClientFactory = remoteHostClientFactory;
_lazyInstance = CreateNewLazyRemoteHostClient();
}
public Task<RemoteHostClient> GetRemoteHostClientAsync(CancellationToken cancellationToken)
......@@ -33,6 +39,25 @@ public Task<RemoteHostClient> GetRemoteHostClientAsync(CancellationToken cancell
return _lazyInstance.GetValueAsync(cancellationToken);
}
public async Task RequestNewRemoteHostAsync(CancellationToken cancellationToken)
{
var instance = await GetRemoteHostClientAsync(cancellationToken).ConfigureAwait(false);
if (instance == null)
{
return;
}
_lazyInstance = CreateNewLazyRemoteHostClient();
// let people know this remote host client is being disconnected
instance.Shutdown();
}
private AsyncLazy<RemoteHostClient> CreateNewLazyRemoteHostClient()
{
return new AsyncLazy<RemoteHostClient>(c => _remoteHostClientFactory.CreateAsync(_workspace, c), cacheResult: true);
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
......@@ -11,6 +12,26 @@ namespace Microsoft.CodeAnalysis.Remote
/// </summary>
internal interface IRemoteHostClientService : IWorkspaceService
{
/// <summary>
/// Request new remote host.
///
/// this is designed to be not distruptive to existing callers and to support scenarioes where
/// features required to reload user extension dlls without re-launching VS.
///
/// if someone requests new remote host, all new callers for <see cref="GetRemoteHostClientAsync(CancellationToken)"/> will
/// receive a new remote host client that connects to a new remote host.
///
/// existing remoteHostClient will still remain connected to old host and that old host will eventually go away once all existing clients
/// are done with their requests.
///
/// callers can subscribe to <see cref="RemoteHostClient.ConnectionChanged"/> event to see whether client is going away if
/// caller is designed to hold onto a service for a while to react to remote host change.
/// </summary>
Task RequestNewRemoteHostAsync(CancellationToken cancellationToken);
/// <summary>
/// Get <see cref="RemoteHostClient"/> to current RemoteHost
/// </summary>
Task<RemoteHostClient> GetRemoteHostClientAsync(CancellationToken cancellationToken);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册