From 1059c22726ecebf0b86b0a2b240bc1eb06e33b8e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 11 May 2021 12:55:08 -0700 Subject: [PATCH 1/2] Force services to wait for intialization before using the language server --- .../Extensions/Api/EditorContextService.cs | 5 +- .../Api/EditorExtensionServiceProvider.cs | 8 +- .../Extensions/Api/EditorUIService.cs | 5 +- .../Extensions/Api/LanguageServerService.cs | 5 +- .../Hosting/EditorServicesServerFactory.cs | 1 + .../Server/PsesLanguageServer.cs | 8 +- .../Server/PsesServiceCollectionExtensions.cs | 8 +- .../Server/SafeLanguageServer.cs | 195 ++++++++++++++++++ .../Services/Analysis/AnalysisService.cs | 5 +- .../EditorOperationsService.cs | 5 +- .../PowerShellContext/ExtensionService.cs | 5 +- .../PowerShellContextService.cs | 7 +- .../Session/Host/PromptHandlers.cs | 9 +- .../Host/ProtocolPSHostUserInterface.cs | 5 +- .../Handlers/ConfigurationHandler.cs | 5 +- 15 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 src/PowerShellEditorServices/Server/SafeLanguageServer.cs diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs index 2b880c805..128eff690 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Extensions.Services @@ -82,10 +83,10 @@ public interface IEditorContextService internal class EditorContextService : IEditorContextService { - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; internal EditorContextService( - ILanguageServerFacade languageServer) + ISafeLanguageServer languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs index 661797f1d..689e1e381 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -41,12 +42,13 @@ public class EditorExtensionServiceProvider internal EditorExtensionServiceProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - LanguageServer = new LanguageServerService(_serviceProvider.GetService()); + var languageServer = _serviceProvider.GetService(); + LanguageServer = new LanguageServerService(languageServer); //DocumentSymbols = new DocumentSymbolService(_serviceProvider.GetService()); ExtensionCommands = new ExtensionCommandService(_serviceProvider.GetService()); Workspace = new WorkspaceService(_serviceProvider.GetService()); - EditorContext = new EditorContextService(_serviceProvider.GetService()); - EditorUI = new EditorUIService(_serviceProvider.GetService()); + EditorContext = new EditorContextService(languageServer); + EditorUI = new EditorUIService(languageServer); } /// diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs index 0ab4fe497..7d07f8eb9 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -101,9 +102,9 @@ internal class EditorUIService : IEditorUIService { private static string[] s_choiceResponseLabelSeparators = new[] { ", " }; - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; - public EditorUIService(ILanguageServerFacade languageServer) + public EditorUIService(ISafeLanguageServer languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs index 466e6b106..30042f4aa 100644 --- a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using MediatR; +using Microsoft.PowerShell.EditorServices.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System.Threading; using System.Threading.Tasks; @@ -64,9 +65,9 @@ public interface ILanguageServerService internal class LanguageServerService : ILanguageServerService { - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; - internal LanguageServerService(ILanguageServerFacade languageServer) + internal LanguageServerService(ISafeLanguageServer languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index b2b96d4d1..d1aebd588 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -123,6 +123,7 @@ public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Strea .AddSingleton(provider => null) .AddPsesLanguageServices(hostStartupInfo) // For a Temp session, there is no LanguageServer so just set it to null + // TODO: Why are we doing this twice? .AddSingleton( typeof(ILanguageServerFacade), _ => null) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 74427fa85..82fb2b789 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -93,7 +93,7 @@ public async Task StartAsync() .WithHandler() .OnInitialize( // TODO: Either fix or ignore "method lacks 'await'" warning. - async (languageServer, request, cancellationToken) => + (languageServer, request, cancellationToken) => { var serviceProvider = languageServer.Services; var workspaceService = serviceProvider.GetService(); @@ -113,6 +113,12 @@ public async Task StartAsync() break; } } + + // Allow services to send requests and notifications now + var safeLanguageServer = (SafeLanguageServer)serviceProvider.GetService(); + safeLanguageServer.SetReady(); + + return Task.CompletedTask; }); }).ConfigureAwait(false); diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 0ca546453..3d22bd5b0 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -17,14 +17,16 @@ public static IServiceCollection AddPsesLanguageServices( this IServiceCollection collection, HostStartupInfo hostStartupInfo) { - return collection.AddSingleton() + return collection + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton( (provider) => PowerShellContextService.Create( provider.GetService(), - provider.GetService(), + provider.GetService(), hostStartupInfo)) .AddSingleton() .AddSingleton() @@ -34,7 +36,7 @@ public static IServiceCollection AddPsesLanguageServices( { var extensionService = new ExtensionService( provider.GetService(), - provider.GetService()); + provider.GetService()); extensionService.InitializeAsync( serviceProvider: provider, editorOperations: provider.GetService()) diff --git a/src/PowerShellEditorServices/Server/SafeLanguageServer.cs b/src/PowerShellEditorServices/Server/SafeLanguageServer.cs new file mode 100644 index 000000000..0f5124a0c --- /dev/null +++ b/src/PowerShellEditorServices/Server/SafeLanguageServer.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; + +namespace Microsoft.PowerShell.EditorServices.Server +{ + internal interface ISafeLanguageServer : IResponseRouter + { + ITextDocumentLanguageServer TextDocument { get; } + + IClientLanguageServer Client { get; } + + IGeneralLanguageServer General { get; } + + IWindowLanguageServer Window { get; } + + IWorkspaceLanguageServer Workspace { get; } + } + + internal class SafeLanguageServer : ISafeLanguageServer + { + private readonly ILanguageServerFacade _languageServer; + + private readonly AsyncLatch _serverReady; + + public ITextDocumentLanguageServer TextDocument + { + get + { + _serverReady.Wait(); + return _languageServer.TextDocument; + } + } + + public IClientLanguageServer Client + { + get + { + _serverReady.Wait(); + return _languageServer.Client; + } + } + + public IGeneralLanguageServer General + { + get + { + _serverReady.Wait(); + return _languageServer.General; + } + } + + public IWindowLanguageServer Window + { + get + { + _serverReady.Wait(); + return _languageServer.Window; + } + } + + public IWorkspaceLanguageServer Workspace + { + get + { + _serverReady.Wait(); + return _languageServer.Workspace; + } + } + + public void SetReady() + { + _serverReady.Open(); + } + + public SafeLanguageServer(ILanguageServerFacade languageServer) + { + _languageServer = languageServer; + _serverReady = new AsyncLatch(); + } + + public void SendNotification(string method) + { + _serverReady.Wait(); + _languageServer.SendNotification(method); + } + + public void SendNotification(string method, T @params) + { + _serverReady.Wait(); + _languageServer.SendNotification(method, @params); + } + + public void SendNotification(IRequest request) + { + _serverReady.Wait(); + _languageServer.SendNotification(request); + } + + public IResponseRouterReturns SendRequest(string method, T @params) + { + _serverReady.Wait(); + return _languageServer.SendRequest(method, @params); + } + + public IResponseRouterReturns SendRequest(string method) + { + _serverReady.Wait(); + return _languageServer.SendRequest(method); + } + + public async Task SendRequest(IRequest request, CancellationToken cancellationToken) + { + await _serverReady.WaitAsync(); + return await _languageServer.SendRequest(request, cancellationToken); + } + + public bool TryGetRequest(long id, out string method, out TaskCompletionSource pendingTask) + { + if (!_serverReady.TryWait()) + { + method = default; + pendingTask = default; + return false; + } + + return _languageServer.TryGetRequest(id, out method, out pendingTask); + } + + private class AsyncLatch + { + private readonly ManualResetEvent _resetEvent; + + private readonly Task _awaitLatchOpened; + + private volatile bool _isOpen; + + public AsyncLatch() + { + _resetEvent = new ManualResetEvent(/* start in blocking state */ initialState: false); + _awaitLatchOpened = CreateLatchOpenedAwaiterTask(_resetEvent); + _isOpen = false; + } + + public void Wait() => _resetEvent.WaitOne(); + + public Task WaitAsync() => _awaitLatchOpened; + + public bool TryWait() => _isOpen; + + public void Open() + { + // Unblocks the reset event + _resetEvent.Set(); + _isOpen = true; + } + + private static Task CreateLatchOpenedAwaiterTask(WaitHandle handle) + { + var tcs = new TaskCompletionSource(); + + // In a dedicated waiter thread, wait for the reset event and then set the task completion source + // to turn the reset event wait into a task. + // From https://stackoverflow.com/a/18766131. + RegisteredWaitHandle registration = ThreadPool.RegisterWaitForSingleObject(handle, (state, timedOut) => + { + ((TaskCompletionSource)state).TrySetResult(result: null); + }, tcs, Timeout.Infinite, executeOnlyOnce: true); + + // Register an action to unregister the registration when the reset event task has completed. + EnsureWaitHandleUnregistered(tcs.Task, registration); + + return tcs.Task; + } + + private static async Task EnsureWaitHandleUnregistered(Task task, RegisteredWaitHandle handle) + { + try + { + await task; + } + finally + { + handle.Unregister(waitObject: null); + } + } + } + } +} diff --git a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs index 11c604ccc..731870bee 100644 --- a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services.Analysis; using Microsoft.PowerShell.EditorServices.Services.Configuration; using Microsoft.PowerShell.EditorServices.Services.TextDocument; @@ -82,7 +83,7 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) private readonly ILogger _logger; - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; private readonly ConfigurationService _configurationService; @@ -107,7 +108,7 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) /// The workspace service for file handling within a workspace. public AnalysisService( ILoggerFactory loggerFactory, - ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, ConfigurationService configurationService, WorkspaceService workspaceService) { diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs index 1c1775562..e373fb1d0 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs @@ -3,6 +3,7 @@ using Microsoft.PowerShell.EditorServices.Extensions; using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -17,12 +18,12 @@ internal class EditorOperationsService : IEditorOperations private readonly WorkspaceService _workspaceService; private readonly PowerShellContextService _powerShellContextService; - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; public EditorOperationsService( WorkspaceService workspaceService, PowerShellContextService powerShellContextService, - ILanguageServerFacade languageServer) + ISafeLanguageServer languageServer) { _workspaceService = workspaceService; _powerShellContextService = powerShellContextService; diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs index db9d6ced9..3364ffee6 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs @@ -6,6 +6,7 @@ using System.Management.Automation; using System.Threading.Tasks; using Microsoft.PowerShell.EditorServices.Extensions; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -23,7 +24,7 @@ internal sealed class ExtensionService private readonly Dictionary editorCommands = new Dictionary(); - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; #endregion @@ -55,7 +56,7 @@ internal sealed class ExtensionService /// PowerShellContext for loading and executing extension code. /// /// A PowerShellContext used to execute extension code. - internal ExtensionService(PowerShellContextService powerShellContext, ILanguageServerFacade languageServer) + internal ExtensionService(PowerShellContextService powerShellContext, ISafeLanguageServer languageServer) { this.PowerShellContext = powerShellContext; _languageServer = languageServer; diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 6d50baa0b..4e207f38c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -24,6 +24,7 @@ namespace Microsoft.PowerShell.EditorServices.Services { using System.Management.Automation; + using Microsoft.PowerShell.EditorServices.Server; /// /// Manages the lifetime and usage of a PowerShell session. @@ -67,7 +68,7 @@ static PowerShellContextService() private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); private readonly SessionStateLock sessionStateLock = new SessionStateLock(); - private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; private readonly bool isPSReadLineEnabled; private readonly ILogger logger; @@ -171,7 +172,7 @@ public RunspaceDetails CurrentRunspace /// public PowerShellContextService( ILogger logger, - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, bool isPSReadLineEnabled) { _languageServer = languageServer; @@ -185,7 +186,7 @@ public PowerShellContextService( [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Checked by Validate call")] public static PowerShellContextService Create( ILoggerFactory factory, - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, HostStartupInfo hostStartupInfo) { Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo); diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs index 4bd29ba29..daa729bcd 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs @@ -7,17 +7,18 @@ using System.Security; using Microsoft.Extensions.Logging; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Microsoft.PowerShell.EditorServices.Server; namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler { - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; private readonly IHostInput _hostInput; private TaskCompletionSource _readLineTask; public ProtocolChoicePromptHandler( - ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, IHostInput hostInput, IHostOutput hostOutput, ILogger logger) @@ -94,12 +95,12 @@ private void HandlePromptResponse( internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler { - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; private readonly IHostInput hostInput; private TaskCompletionSource readLineTask; public ProtocolInputPromptHandler( - ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, IHostInput hostInput, IHostOutput hostOutput, ILogger logger) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs index 89203e5c1..e6c11a2fb 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using System; using System.Threading; @@ -13,7 +14,7 @@ internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface { #region Private Fields - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; #endregion @@ -25,7 +26,7 @@ internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface /// /// public ProtocolPSHostUserInterface( - ILanguageServerFacade languageServer, + ISafeLanguageServer languageServer, PowerShellContextService powerShellContext, ILogger logger) : base ( diff --git a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs index b21294338..9be724002 100644 --- a/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/ConfigurationHandler.cs @@ -9,6 +9,7 @@ using MediatR; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Logging; +using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Configuration; using Newtonsoft.Json.Linq; @@ -26,7 +27,7 @@ internal class PsesConfigurationHandler : DidChangeConfigurationHandlerBase private readonly WorkspaceService _workspaceService; private readonly ConfigurationService _configurationService; private readonly PowerShellContextService _powerShellContextService; - private readonly ILanguageServerFacade _languageServer; + private readonly ISafeLanguageServer _languageServer; private bool _profilesLoaded; private bool _consoleReplStarted; private bool _cwdSet; @@ -37,7 +38,7 @@ public PsesConfigurationHandler( AnalysisService analysisService, ConfigurationService configurationService, PowerShellContextService powerShellContextService, - ILanguageServerFacade languageServer) + ISafeLanguageServer languageServer) { _logger = factory.CreateLogger(); _workspaceService = workspaceService; From 1d3e9da884bdd91ea3d33f88035b4a2a3e1d5824 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 12 May 2021 13:59:52 -0700 Subject: [PATCH 2/2] Implement a notification queue so sends don't block --- .../Server/SafeLanguageServer.cs | 60 +++++++++++++++---- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/src/PowerShellEditorServices/Server/SafeLanguageServer.cs b/src/PowerShellEditorServices/Server/SafeLanguageServer.cs index 0f5124a0c..897969630 100644 --- a/src/PowerShellEditorServices/Server/SafeLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/SafeLanguageServer.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -10,6 +12,10 @@ namespace Microsoft.PowerShell.EditorServices.Server { + /// + /// An LSP server that ensures that client/server initialization has occurred + /// before sending or receiving any other notifications or requests. + /// internal interface ISafeLanguageServer : IResponseRouter { ITextDocumentLanguageServer TextDocument { get; } @@ -23,12 +29,25 @@ internal interface ISafeLanguageServer : IResponseRouter IWorkspaceLanguageServer Workspace { get; } } + /// + /// An implementation around Omnisharp's LSP server to ensure + /// messages are not sent before initialization has completed. + /// internal class SafeLanguageServer : ISafeLanguageServer { private readonly ILanguageServerFacade _languageServer; private readonly AsyncLatch _serverReady; + private readonly ConcurrentQueue _notificationQueue; + + public SafeLanguageServer(ILanguageServerFacade languageServer) + { + _languageServer = languageServer; + _serverReady = new AsyncLatch(); + _notificationQueue = new ConcurrentQueue(); + } + public ITextDocumentLanguageServer TextDocument { get @@ -77,29 +96,44 @@ public IWorkspaceLanguageServer Workspace public void SetReady() { _serverReady.Open(); - } - public SafeLanguageServer(ILanguageServerFacade languageServer) - { - _languageServer = languageServer; - _serverReady = new AsyncLatch(); + // Send any pending notifications now + while (_notificationQueue.TryDequeue(out Action notifcationAction)) + { + notifcationAction(); + } } public void SendNotification(string method) { - _serverReady.Wait(); + if (!_serverReady.IsReady) + { + _notificationQueue.Enqueue(() => _languageServer.SendNotification(method)); + return; + } + _languageServer.SendNotification(method); } public void SendNotification(string method, T @params) { - _serverReady.Wait(); + if (!_serverReady.IsReady) + { + _notificationQueue.Enqueue(() => _languageServer.SendNotification(method, @params)); + return; + } + _languageServer.SendNotification(method, @params); } public void SendNotification(IRequest request) { - _serverReady.Wait(); + if (!_serverReady.IsReady) + { + _notificationQueue.Enqueue(() => _languageServer.SendNotification(request)); + return; + } + _languageServer.SendNotification(request); } @@ -123,7 +157,7 @@ public async Task SendRequest(IRequest request, public bool TryGetRequest(long id, out string method, out TaskCompletionSource pendingTask) { - if (!_serverReady.TryWait()) + if (!_serverReady.IsReady) { method = default; pendingTask = default; @@ -133,6 +167,10 @@ public bool TryGetRequest(long id, out string method, out TaskCompletionSource + /// Implements a latch (a monotonic manual reset event that starts in the blocking state) + /// that can be waited on synchronously or asynchronously without wasting thread resources. + /// private class AsyncLatch { private readonly ManualResetEvent _resetEvent; @@ -148,12 +186,12 @@ public AsyncLatch() _isOpen = false; } + public bool IsReady => _isOpen; + public void Wait() => _resetEvent.WaitOne(); public Task WaitAsync() => _awaitLatchOpened; - public bool TryWait() => _isOpen; - public void Open() { // Unblocks the reset event