diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs index e9c3cab87..40482fc3f 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorContextService.cs @@ -84,10 +84,10 @@ public interface IEditorContextService internal class EditorContextService : IEditorContextService { - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; internal EditorContextService( - ILanguageServer languageServer) + ILanguageServerFacade languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs index da3cf0629..582543663 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs @@ -43,12 +43,12 @@ public class EditorExtensionServiceProvider internal EditorExtensionServiceProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; - LanguageServer = new LanguageServerService(_serviceProvider.GetService()); + LanguageServer = new LanguageServerService(_serviceProvider.GetService()); //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(_serviceProvider.GetService()); + EditorUI = new EditorUIService(_serviceProvider.GetService()); } /// @@ -143,7 +143,7 @@ public object GetServiceByAssemblyQualifiedName(string asmQualifiedTypeName) /// /// This method is intended as a trapdoor and should not be used in the first instance. /// Consider using the public extension services if possible. - /// + /// /// Also note that services in PSES may live in a separate assembly load context, /// meaning that a type of the seemingly correct name may fail to fetch to a service /// that is known under a type of the same name but loaded in a different context. diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs index 0e58d7d64..1dd3e8738 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorUIService.cs @@ -103,9 +103,9 @@ internal class EditorUIService : IEditorUIService { private static string[] s_choiceResponseLabelSeparators = new[] { ", " }; - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; - public EditorUIService(ILanguageServer languageServer) + public EditorUIService(ILanguageServerFacade languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs index 0e7be9043..0d6e9a98a 100644 --- a/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs +++ b/src/PowerShellEditorServices/Extensions/Api/LanguageServerService.cs @@ -66,9 +66,9 @@ public interface ILanguageServerService internal class LanguageServerService : ILanguageServerService { - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; - internal LanguageServerService(ILanguageServer languageServer) + internal LanguageServerService(ILanguageServerFacade languageServer) { _languageServer = languageServer; } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index cfb49699b..e15a78940 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -122,11 +122,11 @@ public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Strea .ClearProviders() .AddSerilog() .SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(provider => null) + .AddSingleton(provider => null) .AddPsesLanguageServices(hostStartupInfo) // For a Temp session, there is no LanguageServer so just set it to null .AddSingleton( - typeof(ILanguageServer), + typeof(ILanguageServerFacade), _ => null) .BuildServiceProvider(); diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index d490dee74..4657dfbcb 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 3e74221a5..042fccceb 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -13,7 +13,9 @@ using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Utility; +using OmniSharp.Extensions.DebugAdapter.Protocol; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.DebugAdapter.Server; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; @@ -41,7 +43,7 @@ internal class PsesDebugServer : IDisposable private readonly bool _usePSReadLine; private readonly TaskCompletionSource _serverStopped; - private IJsonRpcServer _jsonRpcServer; + private DebugAdapterServer _debugAdapterServer; private PowerShellContextService _powerShellContextService; protected readonly ILoggerFactory _loggerFactory; @@ -71,13 +73,8 @@ public PsesDebugServer( /// A task that completes when the server is ready. public async Task StartAsync() { - _jsonRpcServer = await JsonRpcServer.From(options => + _debugAdapterServer = await DebugAdapterServer.From(options => { - options.Serializer = new DapProtocolSerializer(); - options.Receiver = new DapReceiver(); - options.LoggerFactory = _loggerFactory; - ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); - // We need to let the PowerShell Context Service know that we are in a debug session // so that it doesn't send the powerShell/startDebugger message. _powerShellContextService = ServiceProvider.GetService(); @@ -97,45 +94,53 @@ public async Task StartAsync() .GetResult(); } - options.Services = new ServiceCollection() - .AddPsesDebugServices(ServiceProvider, this, _useTempSession); - options .WithInput(_inputStream) - .WithOutput(_outputStream); - - logger.LogInformation("Adding handlers"); - - options - .WithHandler() - .WithHandler() - .WithHandler() + .WithOutput(_outputStream) + .WithServices(serviceCollection => serviceCollection + .AddLogging() + .AddOptions() + .AddPsesDebugServices(ServiceProvider, this, _useTempSession)) + .WithHandler() .WithHandler() - .WithHandler() - .WithHandler() + .WithHandler() .WithHandler() .WithHandler() - .WithHandler() .WithHandler() .WithHandler() .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() - .WithHandler() + .WithHandler() .WithHandler() .WithHandler() - .WithHandler(); - - logger.LogInformation("Handlers added"); + .WithHandler() + // The OnInitialize delegate gets run when we first receive the _Initialize_ request: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialize(async (server, request, cancellationToken) => { + var breakpointService = server.GetService(); + // Clear any existing breakpoints before proceeding + await breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); + }) + // The OnInitialized delegate gets run right before the server responds to the _Initialize_ request: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialized((server, request, response, cancellationToken) => { + response.SupportsConditionalBreakpoints = true; + response.SupportsConfigurationDoneRequest = true; + response.SupportsFunctionBreakpoints = true; + response.SupportsHitConditionalBreakpoints = true; + response.SupportsLogPoints = true; + response.SupportsSetVariable = true; + + return Task.CompletedTask; + }); }).ConfigureAwait(false); } public void Dispose() { _powerShellContextService.IsDebugServerActive = false; - _jsonRpcServer.Dispose(); + _debugAdapterServer.Dispose(); + _inputStream.Dispose(); + _outputStream.Dispose(); _serverStopped.SetResult(true); } diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 63d30070d..11b196cd4 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ public static IServiceCollection AddPsesLanguageServices( (provider) => PowerShellContextService.Create( provider.GetService(), - provider.GetService(), + provider.GetService(), hostStartupInfo)) .AddSingleton() .AddSingleton() @@ -36,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/Services/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs index de4bcf01e..439d85b34 100644 --- a/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Services/Analysis/AnalysisService.cs @@ -84,7 +84,7 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) private readonly ILogger _logger; - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; private readonly ConfigurationService _configurationService; @@ -109,7 +109,7 @@ internal static string GetUniqueIdFromDiagnostic(Diagnostic diagnostic) /// The workspace service for file handling within a workspace. public AnalysisService( ILoggerFactory loggerFactory, - ILanguageServer languageServer, + ILanguageServerFacade languageServer, ConfigurationService configurationService, WorkspaceService workspaceService) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs index 9e48625b5..d91295d76 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs @@ -9,7 +9,7 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; -using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.DebugAdapter.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Services { @@ -19,20 +19,20 @@ internal class DebugEventHandlerService private readonly PowerShellContextService _powerShellContextService; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; - private readonly IJsonRpcServer _jsonRpcServer; + private readonly IDebugAdapterServerFacade _debugAdapterServer; public DebugEventHandlerService( ILoggerFactory factory, PowerShellContextService powerShellContextService, DebugService debugService, DebugStateService debugStateService, - IJsonRpcServer jsonRpcServer) + IDebugAdapterServerFacade debugAdapterServer) { _logger = factory.CreateLogger(); _powerShellContextService = powerShellContextService; _debugService = debugService; _debugStateService = debugStateService; - _jsonRpcServer = jsonRpcServer; + _debugAdapterServer = debugAdapterServer; } internal void RegisterEventHandlers() @@ -79,7 +79,7 @@ e.OriginalEvent.Breakpoints[0] is CommandBreakpoint : "breakpoint"; } - _jsonRpcServer.SendNotification(EventNames.Stopped, + _debugAdapterServer.SendNotification(EventNames.Stopped, new StoppedEvent { ThreadId = 1, @@ -93,10 +93,10 @@ private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEve e.ChangeAction == RunspaceChangeAction.Enter && e.NewRunspace.Context == RunspaceContext.DebuggedRunspace) { - // Send the InitializedEvent so that the debugger will continue + // Sends the InitializedEvent so that the debugger will continue // sending configuration requests _debugStateService.WaitingForAttach = false; - _jsonRpcServer.SendNotification(EventNames.Initialized); + _debugStateService.ServerStarted.SetResult(true); } else if ( e.ChangeAction == RunspaceChangeAction.Exit && @@ -105,7 +105,7 @@ private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEve // Exited the session while the debugger is stopped, // send a ContinuedEvent so that the client changes the // UI to appear to be running again - _jsonRpcServer.SendNotification(EventNames.Continued, + _debugAdapterServer.SendNotification(EventNames.Continued, new ContinuedEvent { ThreadId = 1, @@ -116,7 +116,7 @@ private void PowerShellContext_RunspaceChanged(object sender, RunspaceChangedEve private void PowerShellContext_DebuggerResumed(object sender, DebuggerResumeAction e) { - _jsonRpcServer.SendNotification(EventNames.Continued, + _debugAdapterServer.SendNotification(EventNames.Continued, new ContinuedEvent { AllThreadsContinued = true, @@ -164,7 +164,7 @@ private void DebugService_BreakpointUpdated(object sender, BreakpointUpdatedEven breakpoint.Verified = e.UpdateType != BreakpointUpdateType.Disabled; - _jsonRpcServer.SendNotification(EventNames.Breakpoint, + _debugAdapterServer.SendNotification(EventNames.Breakpoint, new BreakpointEvent { Reason = reason, diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index f60d945a6..515fa9baa 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -38,6 +38,9 @@ internal class DebugStateService internal bool IsUsingTempIntegratedConsole { get; set; } + // This gets set at the end of the Launch/Attach handler which set debug state. + internal TaskCompletionSource ServerStarted { get; set; } + internal void ReleaseSetBreakpointHandle() { _setBreakpointInProgressHandle.Release(); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs index a1c7a030a..777d032e3 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs @@ -19,121 +19,20 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { - internal class SetFunctionBreakpointsHandler : ISetFunctionBreakpointsHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - private readonly DebugStateService _debugStateService; - - public SetFunctionBreakpointsHandler( - ILoggerFactory loggerFactory, - DebugService debugService, - DebugStateService debugStateService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - _debugStateService = debugStateService; - } - - public async Task Handle(SetFunctionBreakpointsArguments request, CancellationToken cancellationToken) - { - CommandBreakpointDetails[] breakpointDetails = request.Breakpoints - .Select((funcBreakpoint) => CommandBreakpointDetails.Create( - funcBreakpoint.Name, - funcBreakpoint.Condition, - funcBreakpoint.HitCondition)) - .ToArray(); - - // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. - CommandBreakpointDetails[] updatedBreakpointDetails = breakpointDetails; - if (!_debugStateService.NoDebug) - { - await _debugStateService.WaitForSetBreakpointHandleAsync().ConfigureAwait(false); - - try - { - updatedBreakpointDetails = - await _debugService.SetCommandBreakpointsAsync( - breakpointDetails).ConfigureAwait(false); - } - catch (Exception e) - { - // Log whatever the error is - _logger.LogException($"Caught error while setting command breakpoints", e); - } - finally - { - _debugStateService.ReleaseSetBreakpointHandle(); - } - } - - return new SetFunctionBreakpointsResponse - { - Breakpoints = updatedBreakpointDetails - .Select(LspDebugUtils.CreateBreakpoint) - .ToArray() - }; - } - } - - internal class SetExceptionBreakpointsHandler : ISetExceptionBreakpointsHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - private readonly DebugStateService _debugStateService; - - public SetExceptionBreakpointsHandler( - ILoggerFactory loggerFactory, - DebugService debugService, - DebugStateService debugStateService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - _debugStateService = debugStateService; - } - - public Task Handle(SetExceptionBreakpointsArguments request, CancellationToken cancellationToken) - { - // TODO: When support for exception breakpoints (unhandled and/or first chance) - // are added to the PowerShell engine, wire up the VSCode exception - // breakpoints here using the pattern below to prevent bug regressions. - //if (!noDebug) - //{ - // setBreakpointInProgress = true; - - // try - // { - // // Set exception breakpoints in DebugService - // } - // catch (Exception e) - // { - // // Log whatever the error is - // Logger.WriteException($"Caught error while setting exception breakpoints", e); - // } - // finally - // { - // setBreakpointInProgress = false; - // } - //} - - return Task.FromResult(new SetExceptionBreakpointsResponse()); - } - } - - internal class SetBreakpointsHandler : ISetBreakpointsHandler + internal class BreakpointHandlers : ISetFunctionBreakpointsHandler, ISetBreakpointsHandler, ISetExceptionBreakpointsHandler { private readonly ILogger _logger; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly WorkspaceService _workspaceService; - public SetBreakpointsHandler( + public BreakpointHandlers( ILoggerFactory loggerFactory, DebugService debugService, DebugStateService debugStateService, WorkspaceService workspaceService) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _debugService = debugService; _debugStateService = debugStateService; _workspaceService = workspaceService; @@ -218,5 +117,72 @@ await _debugService.SetLineBreakpointsAsync( .Select(LspDebugUtils.CreateBreakpoint)) }; } + + public async Task Handle(SetFunctionBreakpointsArguments request, CancellationToken cancellationToken) + { + CommandBreakpointDetails[] breakpointDetails = request.Breakpoints + .Select((funcBreakpoint) => CommandBreakpointDetails.Create( + funcBreakpoint.Name, + funcBreakpoint.Condition, + funcBreakpoint.HitCondition)) + .ToArray(); + + // If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints. + CommandBreakpointDetails[] updatedBreakpointDetails = breakpointDetails; + if (!_debugStateService.NoDebug) + { + await _debugStateService.WaitForSetBreakpointHandleAsync().ConfigureAwait(false); + + try + { + updatedBreakpointDetails = + await _debugService.SetCommandBreakpointsAsync( + breakpointDetails).ConfigureAwait(false); + } + catch (Exception e) + { + // Log whatever the error is + _logger.LogException($"Caught error while setting command breakpoints", e); + } + finally + { + _debugStateService.ReleaseSetBreakpointHandle(); + } + } + + return new SetFunctionBreakpointsResponse + { + Breakpoints = updatedBreakpointDetails + .Select(LspDebugUtils.CreateBreakpoint) + .ToArray() + }; + } + + public Task Handle(SetExceptionBreakpointsArguments request, CancellationToken cancellationToken) + { + // TODO: When support for exception breakpoints (unhandled and/or first chance) + // is added to the PowerShell engine, wire up the VSCode exception + // breakpoints here using the pattern below to prevent bug regressions. + //if (!noDebug) + //{ + // setBreakpointInProgress = true; + + // try + // { + // // Set exception breakpoints in DebugService + // } + // catch (Exception e) + // { + // // Log whatever the error is + // Logger.WriteException($"Caught error while setting exception breakpoints", e); + // } + // finally + // { + // setBreakpointInProgress = false; + // } + //} + + return Task.FromResult(new SetExceptionBreakpointsResponse()); + } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 12e77552e..f0c9674ae 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -14,14 +14,14 @@ using Microsoft.PowerShell.EditorServices.Services.TextDocument; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; -using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.DebugAdapter.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Handlers { internal class ConfigurationDoneHandler : IConfigurationDoneHandler { private readonly ILogger _logger; - private readonly IJsonRpcServer _jsonRpcServer; + private readonly IDebugAdapterServerFacade _debugAdapterServer; private readonly DebugService _debugService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; @@ -30,7 +30,7 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler public ConfigurationDoneHandler( ILoggerFactory loggerFactory, - IJsonRpcServer jsonRpcServer, + IDebugAdapterServerFacade debugAdapterServer, DebugService debugService, DebugStateService debugStateService, DebugEventHandlerService debugEventHandlerService, @@ -38,7 +38,7 @@ public ConfigurationDoneHandler( WorkspaceService workspaceService) { _logger = loggerFactory.CreateLogger(); - _jsonRpcServer = jsonRpcServer; + _debugAdapterServer = debugAdapterServer; _debugService = debugService; _debugStateService = debugStateService; _debugEventHandlerService = debugEventHandlerService; @@ -127,7 +127,7 @@ await _powerShellContextService .ExecuteScriptWithArgsAsync(scriptToLaunch, _debugStateService.Arguments, writeInputToHost: true).ConfigureAwait(false); } - _jsonRpcServer.SendNotification(EventNames.Terminated); + _debugAdapterServer.SendNotification(EventNames.Terminated); } } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs index 21c520881..fd5a4f9ba 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebuggerActionHandlers.cs @@ -4,7 +4,6 @@ // using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -14,12 +13,12 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { - internal class ContinueHandler : IContinueHandler + internal class DebuggerActionHandlers : IContinueHandler, INextHandler, IPauseHandler, IStepInHandler, IStepOutHandler { private readonly ILogger _logger; private readonly DebugService _debugService; - public ContinueHandler( + public DebuggerActionHandlers( ILoggerFactory loggerFactory, DebugService debugService) { @@ -32,40 +31,12 @@ public Task Handle(ContinueArguments request, CancellationToke _debugService.Continue(); return Task.FromResult(new ContinueResponse()); } - } - - internal class NextHandler : INextHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - - public NextHandler( - ILoggerFactory loggerFactory, - DebugService debugService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - } public Task Handle(NextArguments request, CancellationToken cancellationToken) { _debugService.StepOver(); return Task.FromResult(new NextResponse()); } - } - - internal class PauseHandler : IPauseHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - - public PauseHandler( - ILoggerFactory loggerFactory, - DebugService debugService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - } public Task Handle(PauseArguments request, CancellationToken cancellationToken) { @@ -79,40 +50,12 @@ public Task Handle(PauseArguments request, CancellationToken canc throw new RpcErrorException(0, e.Message); } } - } - - internal class StepInHandler : IStepInHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - - public StepInHandler( - ILoggerFactory loggerFactory, - DebugService debugService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - } public Task Handle(StepInArguments request, CancellationToken cancellationToken) { _debugService.StepIn(); return Task.FromResult(new StepInResponse()); } - } - - internal class StepOutHandler : IStepOutHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - - public StepOutHandler( - ILoggerFactory loggerFactory, - DebugService debugService) - { - _logger = loggerFactory.CreateLogger(); - _debugService = debugService; - } public Task Handle(StepOutArguments request, CancellationToken cancellationToken) { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs deleted file mode 100644 index 4fb305090..000000000 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/InitializeHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services; -using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; - -namespace Microsoft.PowerShell.EditorServices.Handlers -{ - internal class InitializeHandler : IInitializeHandler - { - private readonly ILogger _logger; - private readonly DebugService _debugService; - private readonly BreakpointService _breakpointService; - - public InitializeHandler( - ILoggerFactory factory, - DebugService debugService, - BreakpointService breakpointService) - { - _logger = factory.CreateLogger(); - _debugService = debugService; - _breakpointService = breakpointService; - } - - public async Task Handle(InitializeRequestArguments request, CancellationToken cancellationToken) - { - // Clear any existing breakpoints before proceeding - await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(false); - - // Now send the Initialize response to continue setup - return new InitializeResponse - { - SupportsConditionalBreakpoints = true, - SupportsConfigurationDoneRequest = true, - SupportsFunctionBreakpoints = true, - SupportsHitConditionalBreakpoints = true, - SupportsLogPoints = true, - SupportsSetVariable = true - }; - } - } -} diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 02e62a5f9..0f065baa8 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -15,31 +15,20 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; -using MediatR; using OmniSharp.Extensions.JsonRpc; using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Protocol.Server; namespace Microsoft.PowerShell.EditorServices.Handlers { - [Serial, Method("launch")] - internal interface IPsesLaunchHandler : IJsonRpcRequestHandler { } - - [Serial, Method("attach")] - internal interface IPsesAttachHandler : IJsonRpcRequestHandler { } - - internal class PsesLaunchRequestArguments : IRequest + internal class PsesLaunchRequestArguments : LaunchRequestArguments { /// /// Gets or sets the absolute path to the script to debug. /// public string Script { get; set; } - /// - /// Gets or sets a boolean value that indicates whether the script should be - /// run with (false) or without (true) debugging support. - /// - public bool NoDebug { get; set; } - /// /// Gets or sets a boolean value that determines whether to automatically stop /// target after launch. If not specified, target does not stop. @@ -53,7 +42,7 @@ internal class PsesLaunchRequestArguments : IRequest /// /// Gets or sets the working directory of the launched debuggee (specified as an absolute path). - /// If omitted the debuggee is lauched in its own directory. + /// If omitted the debuggee is launched in its own directory. /// public string Cwd { get; set; } @@ -81,7 +70,7 @@ internal class PsesLaunchRequestArguments : IRequest public Dictionary Env { get; set; } } - internal class PsesAttachRequestArguments : IRequest + internal class PsesAttachRequestArguments : AttachRequestArguments { public string ComputerName { get; set; } @@ -94,35 +83,40 @@ internal class PsesAttachRequestArguments : IRequest public string CustomPipeName { get; set; } } - internal class LaunchHandler : IPsesLaunchHandler + internal class LaunchAndAttachHandler : ILaunchHandler, IAttachHandler, IOnDebugAdapterServerStarted { - private readonly ILogger _logger; + private static readonly Version s_minVersionForCustomPipeName = new Version(6, 2); + private readonly ILogger _logger; + private readonly BreakpointService _breakpointService; private readonly DebugService _debugService; private readonly PowerShellContextService _powerShellContextService; private readonly DebugStateService _debugStateService; private readonly DebugEventHandlerService _debugEventHandlerService; - private readonly IJsonRpcServer _jsonRpcServer; + private readonly IDebugAdapterServerFacade _debugAdapterServer; private readonly RemoteFileManagerService _remoteFileManagerService; - public LaunchHandler( + public LaunchAndAttachHandler( ILoggerFactory factory, - IJsonRpcServer jsonRpcServer, + IDebugAdapterServerFacade debugAdapterServer, + BreakpointService breakpointService, + DebugEventHandlerService debugEventHandlerService, DebugService debugService, - PowerShellContextService powerShellContextService, DebugStateService debugStateService, - DebugEventHandlerService debugEventHandlerService, + PowerShellContextService powerShellContextService, RemoteFileManagerService remoteFileManagerService) { - _logger = factory.CreateLogger(); - _jsonRpcServer = jsonRpcServer; + _logger = factory.CreateLogger(); + _debugAdapterServer = debugAdapterServer; + _breakpointService = breakpointService; + _debugEventHandlerService = debugEventHandlerService; _debugService = debugService; - _powerShellContextService = powerShellContextService; _debugStateService = debugStateService; - _debugEventHandlerService = debugEventHandlerService; + _debugStateService.ServerStarted = new TaskCompletionSource(); + _powerShellContextService = powerShellContextService; _remoteFileManagerService = remoteFileManagerService; } - public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken) + public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken) { _debugEventHandlerService.RegisterEventHandlers(); @@ -168,12 +162,12 @@ public async Task Handle(PsesLaunchRequestArguments request, CancellationT await _powerShellContextService.SetWorkingDirectoryAsync(workingDir, isPathAlreadyEscaped: false).ConfigureAwait(false); } - _logger.LogTrace($"Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); + _logger.LogTrace("Working dir " + (string.IsNullOrEmpty(workingDir) ? "not set." : $"set to '{workingDir}'")); } // Prepare arguments to the script - if specified string arguments = null; - if ((request.Args != null) && (request.Args.Length > 0)) + if (request.Args?.Length > 0) { arguments = string.Join(" ", request.Args); _logger.LogTrace("Script arguments are: " + arguments); @@ -207,45 +201,14 @@ public async Task Handle(PsesLaunchRequestArguments request, CancellationT // debugging session _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(_debugStateService.ScriptToLaunch); - // Send the InitializedEvent so that the debugger will continue + // Sends the InitializedEvent so that the debugger will continue // sending configuration requests - _jsonRpcServer.SendNotification(EventNames.Initialized); + _debugStateService.ServerStarted.SetResult(true); - return Unit.Value; + return new LaunchResponse(); } - } - internal class AttachHandler : IPsesAttachHandler - { - private static readonly Version s_minVersionForCustomPipeName = new Version(6, 2); - - private readonly ILogger _logger; - private readonly DebugService _debugService; - private readonly BreakpointService _breakpointService; - private readonly PowerShellContextService _powerShellContextService; - private readonly DebugStateService _debugStateService; - private readonly DebugEventHandlerService _debugEventHandlerService; - private readonly IJsonRpcServer _jsonRpcServer; - - public AttachHandler( - ILoggerFactory factory, - IJsonRpcServer jsonRpcServer, - DebugService debugService, - PowerShellContextService powerShellContextService, - DebugStateService debugStateService, - BreakpointService breakpointService, - DebugEventHandlerService debugEventHandlerService) - { - _logger = factory.CreateLogger(); - _jsonRpcServer = jsonRpcServer; - _debugService = debugService; - _breakpointService = breakpointService; - _powerShellContextService = powerShellContextService; - _debugStateService = debugStateService; - _debugEventHandlerService = debugEventHandlerService; - } - - public async Task Handle(PsesAttachRequestArguments request, CancellationToken cancellationToken) + public async Task Handle(PsesAttachRequestArguments request, CancellationToken cancellationToken) { _debugStateService.IsAttachSession = true; @@ -279,7 +242,7 @@ public async Task Handle(PsesAttachRequestArguments request, CancellationT } else if (_powerShellContextService.CurrentRunspace.Location == RunspaceLocation.Remote) { - throw new RpcErrorException(0, $"Cannot attach to a process in a remote session when already in a remote session."); + throw new RpcErrorException(0, "Cannot attach to a process in a remote session when already in a remote session."); } await _powerShellContextService.ExecuteScriptStringAsync( @@ -303,7 +266,7 @@ await _powerShellContextService.ExecuteScriptStringAsync( await _powerShellContextService.ExecuteScriptStringAsync( $"Enter-PSHostProcess -Id {processId}", - errorMessages).ConfigureAwait(false); + errorMessages); if (errorMessages.Length > 0) { @@ -319,7 +282,7 @@ await _powerShellContextService.ExecuteScriptStringAsync( await _powerShellContextService.ExecuteScriptStringAsync( $"Enter-PSHostProcess -CustomPipeName {request.CustomPipeName}", - errorMessages).ConfigureAwait(false); + errorMessages); if (errorMessages.Length > 0) { @@ -385,9 +348,29 @@ await _powerShellContextService.ExecuteScriptStringAsync( if (runspaceVersion.Version.Major >= 7) { - _jsonRpcServer.SendNotification(EventNames.Initialized); + _debugStateService.ServerStarted.SetResult(true); } - return Unit.Value; + return new AttachResponse(); + } + + // PSES follows the following flow: + // Receive a Initialize request + // Run Initialize handler and send response back + // Receive a Launch/Attach request + // Run Launch/Attach handler and send response back + // PSES sends the initialized event at the end of the Launch/Attach handler + + // The way that the Omnisharp server works is that this OnStarted handler runs after OnInitialized + // (after the Initialize DAP response is sent to the client) but before the _Initalized_ DAP event + // gets sent to the client. Because of the way PSES handles breakpoints, + // we can't send the Initialized event until _after_ we finish the Launch/Attach handler. + // The flow above depicts this. To achieve this, we wait until _debugStateService.ServerStarted + // is set, which will be done by the Launch/Attach handlers. + public async Task OnStarted(IDebugAdapterServer server, CancellationToken cancellationToken) + { + // We wait for this task to be finished before triggering the initialized message to + // be sent to the client. + await _debugStateService.ServerStarted.Task.ConfigureAwait(false); } private async Task OnExecutionCompletedAsync(Task executeTask) @@ -431,7 +414,7 @@ private async Task OnExecutionCompletedAsync(Task executeTask) } _debugService.IsClientAttached = false; - _jsonRpcServer.SendNotification(EventNames.Terminated); + _debugAdapterServer.SendNotification(EventNames.Terminated); } } } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs index 78e907acb..e6dc46c1d 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/EditorOperationsService.cs @@ -19,12 +19,12 @@ internal class EditorOperationsService : IEditorOperations private readonly WorkspaceService _workspaceService; private readonly PowerShellContextService _powerShellContextService; - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; public EditorOperationsService( WorkspaceService workspaceService, PowerShellContextService powerShellContextService, - ILanguageServer languageServer) + ILanguageServerFacade languageServer) { _workspaceService = workspaceService; _powerShellContextService = powerShellContextService; diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs index 28cd3e2a1..b70c39c48 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/ExtensionService.cs @@ -25,7 +25,7 @@ internal sealed class ExtensionService private readonly Dictionary editorCommands = new Dictionary(); - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; #endregion @@ -57,7 +57,7 @@ internal sealed class ExtensionService /// PowerShellContext for loading and executing extension code. /// /// A PowerShellContext used to execute extension code. - internal ExtensionService(PowerShellContextService powerShellContext, ILanguageServer languageServer) + internal ExtensionService(PowerShellContextService powerShellContext, ILanguageServerFacade languageServer) { this.PowerShellContext = powerShellContext; _languageServer = languageServer; diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs index b61e32798..4d5897359 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Handlers/GetVersionHandler.cs @@ -23,13 +23,13 @@ internal class GetVersionHandler : IGetVersionHandler private readonly ILogger _logger; private readonly PowerShellContextService _powerShellContextService; - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; private readonly ConfigurationService _configurationService; public GetVersionHandler( ILoggerFactory factory, PowerShellContextService powerShellContextService, - ILanguageServer languageServer, + ILanguageServerFacade languageServer, ConfigurationService configurationService) { _logger = factory.CreateLogger(); diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 26ae9bd93..556d86a6c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -69,7 +69,7 @@ static PowerShellContextService() private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); private readonly SessionStateLock sessionStateLock = new SessionStateLock(); - private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer _languageServer; + private readonly OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade _languageServer; private readonly bool isPSReadLineEnabled; private readonly ILogger logger; @@ -173,7 +173,7 @@ public RunspaceDetails CurrentRunspace /// public PowerShellContextService( ILogger logger, - OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, bool isPSReadLineEnabled) { _languageServer = languageServer; @@ -187,7 +187,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.ILanguageServer languageServer, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade 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 0fbfa9636..dc1a08168 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/PromptHandlers.cs @@ -14,12 +14,12 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler { - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; private readonly IHostInput _hostInput; private TaskCompletionSource _readLineTask; public ProtocolChoicePromptHandler( - ILanguageServer languageServer, + ILanguageServerFacade languageServer, IHostInput hostInput, IHostOutput hostOutput, ILogger logger) @@ -96,12 +96,12 @@ private void HandlePromptResponse( internal class ProtocolInputPromptHandler : ConsoleInputPromptHandler { - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; private readonly IHostInput hostInput; private TaskCompletionSource readLineTask; public ProtocolInputPromptHandler( - ILanguageServer languageServer, + ILanguageServerFacade 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 b45f0d4a4..e8e051cbd 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -15,7 +15,7 @@ internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface { #region Private Fields - private readonly ILanguageServer _languageServer; + private readonly ILanguageServerFacade _languageServer; #endregion @@ -27,7 +27,7 @@ internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface /// /// public ProtocolPSHostUserInterface( - ILanguageServer languageServer, + ILanguageServerFacade languageServer, PowerShellContextService powerShellContext, ILogger logger) : base ( diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs index a43dac341..4bcd45a91 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeLensHandlers.cs @@ -24,11 +24,13 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { internal class PsesCodeLensHandlers : ICodeLensHandler, ICodeLensResolveHandler { + private readonly Guid _id = new Guid(); private readonly ILogger _logger; private readonly SymbolsService _symbolsService; private readonly WorkspaceService _workspaceService; private CodeLensCapability _capability; + public Guid Id => _id; public PsesCodeLensHandlers(ILoggerFactory factory, SymbolsService symbolsService, WorkspaceService workspaceService, ConfigurationService configurationService) { diff --git a/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs new file mode 100644 index 000000000..a56cf6ff0 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/DAPTestsFixures.cs @@ -0,0 +1,86 @@ +// +// 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.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; + +namespace PowerShellEditorServices.Test.E2E +{ + public class DAPTestsFixture : TestsFixture + { + public override bool IsDebugAdapterTests => true; + + public DebugAdapterClient PsesDebugAdapterClient { get; private set; } + + public TaskCompletionSource Started { get; } = new TaskCompletionSource(); + + public async override Task CustomInitializeAsync( + ILoggerFactory factory, + Stream inputStream, + Stream outputStream) + { + var initialized = new TaskCompletionSource(); + PsesDebugAdapterClient = DebugAdapterClient.Create(options => + { + options + .WithInput(inputStream) + .WithOutput(outputStream) + // The OnStarted delegate gets run when we receive the _Initialized_ event from the server: + // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized + .OnStarted((client, token) => { + Started.SetResult(true); + return Task.CompletedTask; + }) + // The OnInitialized delegate gets run when we first receive the _Initialize_ response: + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + .OnInitialized((client, request, response, token) => { + initialized.SetResult(true); + return Task.CompletedTask; + }); + }); + + // PSES follows the following flow: + // Receive a Initialize request + // Run Initialize handler and send response back + // Receive a Launch/Attach request + // Run Launch/Attach handler and send response back + // PSES sends the initialized event at the end of the Launch/Attach handler + + // The way that the Omnisharp client works is that this Initialize method doesn't return until + // after OnStarted is run... which only happens when Initialized is received from the server. + // so if we would await this task, it would deadlock. + // To get around this, we run the Initialize() without await but use a `TaskCompletionSource` + // that gets completed when we receive the response to Initialize + // This tells us that we are ready to send messages to PSES... but are not stuck waiting for + // Initialized. + PsesDebugAdapterClient.Initialize(CancellationToken.None).ConfigureAwait(false); + await initialized.Task.ConfigureAwait(false); + } + + public override async Task DisposeAsync() + { + try + { + await PsesDebugAdapterClient.RequestDisconnect(new DisconnectArguments + { + Restart = false, + TerminateDebuggee = true + }).ConfigureAwait(false); + await _psesProcess.Stop().ConfigureAwait(false); + PsesDebugAdapterClient?.Dispose(); + } + catch (ObjectDisposedException) + { + // Language client has a disposal bug in it + } + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs new file mode 100644 index 000000000..7bd21f6b4 --- /dev/null +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -0,0 +1,82 @@ +// +// 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.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.PowerShell.EditorServices.Handlers; +using Xunit; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using System.Threading; + +namespace PowerShellEditorServices.Test.E2E +{ + public class DebugAdapterProtocolMessageTests : IClassFixture + { + private readonly static string s_binDir = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + private readonly DebugAdapterClient PsesDebugAdapterClient; + private readonly DAPTestsFixture _dapTestsFixture; + + public DebugAdapterProtocolMessageTests(DAPTestsFixture data) + { + _dapTestsFixture = data; + PsesDebugAdapterClient = data.PsesDebugAdapterClient; + } + + private string NewTestFile(string script, bool isPester = false) + { + string fileExt = isPester ? ".Tests.ps1" : ".ps1"; + string filePath = Path.Combine(s_binDir, Path.GetRandomFileName() + fileExt); + File.WriteAllText(filePath, script); + + return filePath; + } + + [Fact] + public void CanInitializeWithCorrectServerSettings() + { + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsConditionalBreakpoints); + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsConfigurationDoneRequest); + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsFunctionBreakpoints); + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsHitConditionalBreakpoints); + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsLogPoints); + Assert.True(PsesDebugAdapterClient.ServerSettings.SupportsSetVariable); + } + + [Fact] + public async Task CanLaunchScriptWithNoBreakpointsAsync() + { + string filePath = NewTestFile("'works' > \"$PSScriptRoot/testFile.txt\""); + LaunchResponse launchResponse = await PsesDebugAdapterClient.RequestLaunch(new PsesLaunchRequestArguments + { + NoDebug = false, + Script = filePath, + Cwd = "", + CreateTemporaryIntegratedConsole = false, + }).ConfigureAwait(false); + + Assert.NotNull(launchResponse); + + // This will check to see if we received the Initialized event from the server. + await Task.Run( + async () => await _dapTestsFixture.Started.Task.ConfigureAwait(false), + new CancellationTokenSource(2000).Token).ConfigureAwait(false); + + ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()).ConfigureAwait(false); + Assert.NotNull(configDoneResponse); + + // At this point the script should be running so lets give it time + await Task.Delay(2000).ConfigureAwait(false); + + string testFile = Path.Join(Path.GetDirectoryName(filePath), "testFile.txt"); + string contents = await File.ReadAllTextAsync(testFile).ConfigureAwait(false); + Assert.Equal($"works{Environment.NewLine}", contents); + } + } +} diff --git a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs index 11ea4a49a..168c8d261 100644 --- a/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs +++ b/test/PowerShellEditorServices.Test.E2E/LSPTestsFixures.cs @@ -1,4 +1,9 @@ -using System; +// +// 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.IO; using System.Linq; @@ -42,27 +47,20 @@ public async override Task CustomInitializeAsync( .WithInput(inputStream) .WithOutput(outputStream) .WithRootUri(DocumentUri.FromFileSystemPath(testdir.FullName)) - .OnPublishDiagnostics(diagnosticParams => - { - Diagnostics.AddRange(diagnosticParams.Diagnostics.Where(d => d != null)); - }) - .OnLogMessage(logMessageParams => - { - Output?.WriteLine($"{logMessageParams.Type.ToString()}: {logMessageParams.Message}"); - }); + .OnPublishDiagnostics(diagnosticParams => Diagnostics.AddRange(diagnosticParams.Diagnostics.Where(d => d != null))) + .OnLogMessage(logMessageParams => Output?.WriteLine($"{logMessageParams.Type.ToString()}: {logMessageParams.Message}")); // Enable all capabilities this this is for testing. // This will be a built in feature of the Omnisharp client at some point. var capabilityTypes = typeof(ICapability).Assembly.GetExportedTypes() - .Where(z => typeof(ICapability).IsAssignableFrom(z)) - .Where(z => z.IsClass && !z.IsAbstract); + .Where(z => typeof(ICapability).IsAssignableFrom(z) && z.IsClass && !z.IsAbstract); foreach (Type capabilityType in capabilityTypes) { options.WithCapability(Activator.CreateInstance(capabilityType, Array.Empty()) as ICapability); } }); - await PsesLanguageClient.Initialize(CancellationToken.None); + await PsesLanguageClient.Initialize(CancellationToken.None).ConfigureAwait(false); // Make sure Script Analysis is enabled because we'll need it in the tests. PsesLanguageClient.Workspace.DidChangeConfiguration( @@ -84,8 +82,8 @@ public override async Task DisposeAsync() { try { - await PsesLanguageClient.Shutdown(); - await _psesProcess.Stop(); + await PsesLanguageClient.Shutdown().ConfigureAwait(false); + await _psesProcess.Stop().ConfigureAwait(false); PsesLanguageClient?.Dispose(); } catch (ObjectDisposedException) diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj index 8d28f6f0d..83c921171 100644 --- a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -10,7 +10,8 @@ - + + @@ -26,4 +27,3 @@ - diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 8823c88f6..4d80ae1d9 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -30,7 +30,7 @@ - +