diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index b37b6ee65..5861b9420 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -150,12 +150,12 @@ function Start-EditorServicesHost { } if ($DebugServiceOnly.IsPresent) { - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false); + $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true); } elseif($Stdio.IsPresent) { $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); } else { $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true); + $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false); } return $editorServicesHost diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs index fd14dd787..68c004c0c 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs @@ -21,6 +21,7 @@ using Microsoft.PowerShell.EditorServices.Server; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using Serilog; namespace Microsoft.PowerShell.EditorServices.Hosting @@ -284,14 +285,33 @@ public void StartLanguageService( /// /// The config that contains information on the communication protocol that will be used. /// The profiles that will be loaded in the session. - /// Determines if we will reuse the session that we have. + /// Determines if we will make a new session typically used for temporary console debugging. public void StartDebugService( EditorServiceTransportConfig config, ProfilePaths profilePaths, - bool useExistingSession) + bool useTempSession) { _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); + IServiceProvider serviceProvider = null; + if (useTempSession) + { + serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .AddSerilog() + .SetMinimumLevel(LogLevel.Trace)) + .AddSingleton(provider => null) + .AddPsesLanguageServices( + profilePaths, + _featureFlags, + _enableConsoleRepl, + _internalHost, + _hostDetails, + _additionalModules) + .BuildServiceProvider(); + } + switch (config.TransportType) { case EditorServiceTransportType.NamedPipe: @@ -312,7 +332,7 @@ public void StartDebugService( .ContinueWith(async task => { _logger.LogInformation("Starting debug server"); - await _debugServer.StartAsync(_languageServer.LanguageServer.Services); + await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); _logger.LogInformation( $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); }); @@ -325,25 +345,11 @@ public void StartDebugService( Console.OpenStandardInput(), Console.OpenStandardOutput()); + _logger.LogInformation("Starting debug server"); Task.Run(async () => { - _logger.LogInformation("Starting debug server"); - - IServiceProvider serviceProvider = useExistingSession - ? _languageServer.LanguageServer.Services - : new ServiceCollection().AddSingleton( - (provider) => PowerShellContextService.Create( - _factory, - provider.GetService(), - profilePaths, - _featureFlags, - _enableConsoleRepl, - _internalHost, - _hostDetails, - _additionalModules)) - .BuildServiceProvider(); - - await _debugServer.StartAsync(serviceProvider); + + await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); _logger.LogInformation( $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); }); @@ -353,14 +359,19 @@ public void StartDebugService( throw new NotSupportedException($"The transport {config.TransportType} is not supported"); } - if(!alreadySubscribedDebug) + // If the instance of PSES is being used for debugging only, then we don't want to allow automatic restarting + // because the user can simply spin up a new PSES if they need to. + // This design decision was done since this "debug-only PSES" is used in the "Temporary Integrated Console debugging" + // feature which does not want PSES to be restarted so that the user can see the output of the last debug + // session. + if(!alreadySubscribedDebug && !useTempSession) { alreadySubscribedDebug = true; _debugServer.SessionEnded += (sender, eventArgs) => { _debugServer.Dispose(); alreadySubscribedDebug = false; - StartDebugService(config, profilePaths, useExistingSession); + StartDebugService(config, profilePaths, useTempSession); }; } } @@ -378,8 +389,20 @@ public void StopServices() /// public void WaitForCompletion() { - // TODO: We need a way to know when to complete this task! - _languageServer.WaitForShutdown().Wait(); + // If _languageServer is not null, then we are either using: + // Stdio - that only uses a LanguageServer so we return when that has shutdown. + // NamedPipes - that uses both LanguageServer and DebugServer, but LanguageServer + // is the core of PowerShell Editor Services and if that shuts down, + // we want the whole process to shutdown. + if (_languageServer != null) + { + _languageServer.WaitForShutdown().GetAwaiter().GetResult(); + return; + } + + // If there is no LanguageServer, then we must be running with the DebugServiceOnly switch + // (used in Temporary console debugging) and we need to wait for the DebugServer to shutdown. + _debugServer.WaitForShutdown().GetAwaiter().GetResult(); } #endregion diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 860dbc845..98474b0be 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -26,6 +26,8 @@ public class PsesDebugServer : IDisposable private PowerShellContextService _powerShellContextService; + private readonly TaskCompletionSource _serverStopped; + public PsesDebugServer( ILoggerFactory factory, Stream inputStream, @@ -34,9 +36,10 @@ public PsesDebugServer( _loggerFactory = factory; _inputStream = inputStream; _outputStream = outputStream; + _serverStopped = new TaskCompletionSource(); } - public async Task StartAsync(IServiceProvider languageServerServiceProvider) + public async Task StartAsync(IServiceProvider languageServerServiceProvider, bool useTempSession) { _jsonRpcServer = await JsonRpcServer.From(options => { @@ -50,14 +53,13 @@ public async Task StartAsync(IServiceProvider languageServerServiceProvider) _powerShellContextService = languageServerServiceProvider.GetService(); _powerShellContextService.IsDebugServerActive = true; + // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. + _powerShellContextService + .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") + .Wait(); + options.Services = new ServiceCollection() - .AddSingleton(_powerShellContextService) - .AddSingleton(languageServerServiceProvider.GetService()) - .AddSingleton(languageServerServiceProvider.GetService()) - .AddSingleton(this) - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddPsesDebugServices(languageServerServiceProvider, this, useTempSession); options .WithInput(_inputStream) @@ -95,6 +97,12 @@ public void Dispose() { _powerShellContextService.IsDebugServerActive = false; _jsonRpcServer.Dispose(); + _serverStopped.SetResult(true); + } + + public async Task WaitForShutdown() + { + await _serverStopped.Task; } #region Events diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 3d6901479..caddc5e5f 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -65,43 +65,13 @@ public async Task StartAsync() .WithInput(input) .WithOutput(output) .WithServices(serviceCollection => serviceCollection - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - PowerShellContextService.Create( - provider.GetService(), - provider.GetService(), - _profilePaths, - _featureFlags, - _enableConsoleRepl, - _internalHost, - _hostDetails, - _additionalModules)) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton( - (provider) => - { - var extensionService = new ExtensionService( - provider.GetService(), - provider.GetService()); - extensionService.InitializeAsync( - serviceProvider: provider, - editorOperations: provider.GetService()) - .Wait(); - return extensionService; - }) - .AddSingleton( - (provider) => - { - return AnalysisService.Create( - provider.GetService(), - provider.GetService(), - provider.GetService().CreateLogger()); - })) + .AddPsesLanguageServices( + _profilePaths, + _featureFlags, + _enableConsoleRepl, + _internalHost, + _hostDetails, + _additionalModules)) .ConfigureLogging(builder => builder .AddSerilog(Log.Logger) .SetMinimumLevel(LogLevel.Trace)) diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs new file mode 100644 index 000000000..cd66847b0 --- /dev/null +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Management.Automation.Host; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Services; + +namespace Microsoft.PowerShell.EditorServices.Server +{ + internal static class PsesServiceCollectionExtensions + { + public static IServiceCollection AddPsesLanguageServices ( + this IServiceCollection collection, + ProfilePaths profilePaths, + HashSet featureFlags, + bool enableConsoleRepl, + PSHost internalHost, + HostDetails hostDetails, + string[] additionalModules) + { + return collection.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + PowerShellContextService.Create( + provider.GetService(), + provider.GetService(), + profilePaths, + featureFlags, + enableConsoleRepl, + internalHost, + hostDetails, + additionalModules)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton( + (provider) => + { + var extensionService = new ExtensionService( + provider.GetService(), + provider.GetService()); + extensionService.InitializeAsync( + serviceProvider: provider, + editorOperations: provider.GetService()) + .Wait(); + return extensionService; + }) + .AddSingleton( + (provider) => + { + return AnalysisService.Create( + provider.GetService(), + provider.GetService(), + provider.GetService().CreateLogger()); + }); + } + + public static IServiceCollection AddPsesDebugServices( + this IServiceCollection collection, + IServiceProvider languageServiceProvider, + PsesDebugServer psesDebugServer, + bool useTempSession) + { + return collection.AddSingleton(languageServiceProvider.GetService()) + .AddSingleton(languageServiceProvider.GetService()) + .AddSingleton(languageServiceProvider.GetService()) + .AddSingleton(psesDebugServer) + .AddSingleton() + .AddSingleton(new DebugStateService + { + OwnsEditorSession = useTempSession + }) + .AddSingleton(); + } + } +} diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index addf829a1..747a8a9b2 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -43,10 +43,17 @@ public ConfigurationDoneHandler( _workspaceService = workspaceService; } - public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) + public async Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken) { _debugService.IsClientAttached = true; + if (_debugStateService.OwnsEditorSession) + { + // If this is a debug-only session, we need to start + // the command loop manually + _powerShellContextService.ConsoleReader.StartCommandLoop(); + } + if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch)) { if (_powerShellContextService.SessionState == PowerShellContextState.Ready) @@ -63,14 +70,6 @@ public Task Handle(ConfigurationDoneArguments request if (_debugStateService.IsInteractiveDebugSession) { - if (_debugStateService.OwnsEditorSession) - { - // If this is a debug-only session, we need to start - // the command loop manually - // TODO: Bring this back - //_editorSession.HostInput.StartCommandLoop(); - } - if (_debugService.IsDebuggerStopped) { if (_debugService.CurrentDebuggerStoppedEventArgs != null) @@ -88,7 +87,7 @@ public Task Handle(ConfigurationDoneArguments request } } - return Task.FromResult(new ConfigurationDoneResponse()); + return new ConfigurationDoneResponse(); } private async Task LaunchScriptAsync(string scriptToLaunch) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index dfe550c18..a1f54a0de 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -1793,7 +1793,7 @@ private void OnExecutionStatusChanged( private void PowerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e) { - _languageServer.SendNotification( + _languageServer?.SendNotification( "powerShell/runspaceChanged", new MinifiedRunspaceDetails(e.NewRunspace)); } @@ -1831,7 +1831,7 @@ public MinifiedRunspaceDetails(RunspaceDetails eventArgs) /// details of the execution status change private void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e) { - _languageServer.SendNotification( + _languageServer?.SendNotification( "powerShell/executionStatusChanged", e); }