From cbb9e096546bdef169ec4d4ae6a776eaabab80e0 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:07:17 -0800 Subject: [PATCH] WIP: Fix up debugger attach etc. Also such a mess. --- .../Handlers/LaunchAndAttachHandler.cs | 227 +++++++++--------- .../Handlers/IGetRunspaceHandler.cs | 2 +- .../PSHostProcessAndRunspaceHandlers.cs | 91 +++---- .../LanguageServerProtocolMessageTests.cs | 2 +- 4 files changed, 151 insertions(+), 171 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index ac6490c7b..682f4e96c 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -76,9 +76,9 @@ internal record PsesAttachRequestArguments : AttachRequestArguments { public string ComputerName { get; set; } - public string ProcessId { get; set; } + public int ProcessId { get; set; } - public string RunspaceId { get; set; } + public int RunspaceId { get; set; } public string RunspaceName { get; set; } @@ -86,7 +86,8 @@ internal record PsesAttachRequestArguments : AttachRequestArguments } internal class LaunchAndAttachHandler : ILaunchHandler, IAttachHandler, IOnDebugAdapterServerStarted - { +{ + private static readonly int currentProcessId = System.Diagnostics.Process.GetCurrentProcess().Id; private static readonly Version s_minVersionForCustomPipeName = new(6, 2); private readonly ILogger _logger; private readonly BreakpointService _breakpointService; @@ -239,16 +240,12 @@ private async Task HandleImpl(PsesAttachRequestArguments request { // The debugger has officially started. We use this to later check if we should stop it. ((PsesInternalHost)_executionService).DebugContext.IsActive = true; - _debugStateService.IsAttachSession = true; - _debugEventHandlerService.RegisterEventHandlers(); - bool processIdIsSet = !string.IsNullOrEmpty(request.ProcessId) && request.ProcessId != "undefined"; + bool processIdIsSet = request.ProcessId != 0; bool customPipeNameIsSet = !string.IsNullOrEmpty(request.CustomPipeName) && request.CustomPipeName != "undefined"; - PowerShellVersionDetails runspaceVersion = _runspaceContext.CurrentRunspace.PowerShellVersionDetails; - // If there are no host processes to attach to or the user cancels selection, we get a null for the process id. // This is not an error, just a request to stop the original "attach to" request. // Testing against "undefined" is a HACK because I don't know how to make "Cancel" on quick pick loading @@ -261,37 +258,9 @@ private async Task HandleImpl(PsesAttachRequestArguments request throw new RpcErrorException(0, "User aborted attach to PowerShell host process."); } - if (request.ComputerName != null) + if (!string.IsNullOrEmpty(request.ComputerName)) { - if (runspaceVersion.Version.Major < 4) - { - throw new RpcErrorException(0, $"Remote sessions are only available with PowerShell 4 and higher (current session is {runspaceVersion.Version})."); - } - else if (_runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) - { - throw new RpcErrorException(0, "Cannot attach to a process in a remote session when already in a remote session."); - } - - PSCommand enterPSSessionCommand = new PSCommand() - .AddCommand("Enter-PSSession") - .AddParameter("ComputerName", request.ComputerName); - - try - { - await _executionService.ExecutePSCommandAsync( - enterPSSessionCommand, - cancellationToken, - PowerShellExecutionOptions.ImmediateInteractive) - .ConfigureAwait(false); - } - catch (Exception e) - { - string msg = $"Could not establish remote session to computer '{request.ComputerName}'"; - _logger.LogError(e, msg); - throw new RpcErrorException(0, msg); - } - - _debugStateService.IsRemoteAttach = true; + await AttachToComputer(request.ComputerName, cancellationToken).ConfigureAwait(false); } // Set up a temporary runspace changed event handler so we can ensure @@ -305,79 +274,36 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) runspaceChanged.TrySetResult(true); } - _executionService.RunspaceChanged += RunspaceChangedHandler; - - if (processIdIsSet && int.TryParse(request.ProcessId, out int processId) && (processId > 0)) + if (processIdIsSet) { - if (runspaceVersion.Version.Major < 5) + // Skip this if we're targetting ourself. + if (request.ProcessId != currentProcessId) { - throw new RpcErrorException(0, $"Attaching to a process is only available with PowerShell 5 and higher (current session is {runspaceVersion.Version})."); - } - - PSCommand enterPSHostProcessCommand = new PSCommand() - .AddCommand("Enter-PSHostProcess") - .AddParameter("Id", processId); - - try - { - await _executionService.ExecutePSCommandAsync( - enterPSHostProcessCommand, - cancellationToken, - PowerShellExecutionOptions.ImmediateInteractive) - .ConfigureAwait(false); - } - catch (Exception e) - { - string msg = $"Could not attach to process with Id: '{request.ProcessId}'"; - _logger.LogError(e, msg); - throw new RpcErrorException(0, msg); + _executionService.RunspaceChanged += RunspaceChangedHandler; + await AttachToProcess(request.ProcessId, cancellationToken).ConfigureAwait(false); + await runspaceChanged.Task.ConfigureAwait(false); } } else if (customPipeNameIsSet) { - if (runspaceVersion.Version < s_minVersionForCustomPipeName) - { - throw new RpcErrorException(0, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); - } - - PSCommand enterPSHostProcessCommand = new PSCommand() - .AddCommand("Enter-PSHostProcess") - .AddParameter("CustomPipeName", request.CustomPipeName); - - try - { - await _executionService.ExecutePSCommandAsync( - enterPSHostProcessCommand, - cancellationToken, - PowerShellExecutionOptions.ImmediateInteractive) - .ConfigureAwait(false); - } - catch (Exception e) - { - string msg = $"Could not attach to process with CustomPipeName: '{request.CustomPipeName}'"; - _logger.LogError(e, msg); - throw new RpcErrorException(0, msg); - } + _executionService.RunspaceChanged += RunspaceChangedHandler; + await AttachToPipe(request.CustomPipeName, cancellationToken).ConfigureAwait(false); + await runspaceChanged.Task.ConfigureAwait(false); } - else if (request.ProcessId != "current") + else { - _logger.LogError( - $"Attach request failed, '{request.ProcessId}' is an invalid value for the processId."); - - throw new RpcErrorException(0, "A positive integer must be specified for the processId field."); + throw new RpcErrorException(0, "Invalid configuration with no process ID nor custom pipe name!"); } - await runspaceChanged.Task.ConfigureAwait(false); // Execute the Debug-Runspace command but don't await it because it - // will block the debug adapter initialization process. The + // will block the debug adapter initialization process. The // InitializedEvent will be sent as soon as the RunspaceChanged // event gets fired with the attached runspace. - PSCommand debugRunspaceCmd = new PSCommand().AddCommand("Debug-Runspace"); - if (request.RunspaceName != null) + if (!string.IsNullOrEmpty(request.RunspaceName)) { - PSCommand getRunspaceIdCommand = new PSCommand() + PSCommand psCommand = new PSCommand() .AddCommand(@"Microsoft.PowerShell.Utility\Get-Runspace") .AddParameter("Name", request.RunspaceName) .AddCommand(@"Microsoft.PowerShell.Utility\Select-Object") @@ -386,7 +312,7 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) try { IEnumerable ids = await _executionService.ExecutePSCommandAsync( - getRunspaceIdCommand, + psCommand, cancellationToken) .ConfigureAwait(false); @@ -395,38 +321,27 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) _debugStateService.RunspaceId = id; break; - // TODO: If we don't end up setting this, we should throw + // TODO: If we don't end up setting this, we should throw! } } - catch (Exception getRunspaceException) + catch (Exception e) { _logger.LogError( - getRunspaceException, + e, "Unable to determine runspace to attach to. Message: {message}", - getRunspaceException.Message); + e.Message); } - // TODO: We have the ID, why not just use that? debugRunspaceCmd.AddParameter("Name", request.RunspaceName); } - else if (request.RunspaceId != null) + else if (request.RunspaceId > 0) { - if (!int.TryParse(request.RunspaceId, out int runspaceId) || runspaceId <= 0) - { - _logger.LogError( - $"Attach request failed, '{request.RunspaceId}' is an invalid value for the processId."); - - throw new RpcErrorException(0, "A positive integer must be specified for the RunspaceId field."); - } - - _debugStateService.RunspaceId = runspaceId; - - debugRunspaceCmd.AddParameter("Id", runspaceId); + _debugStateService.RunspaceId = request.RunspaceId; + debugRunspaceCmd.AddParameter("Id", request.RunspaceId); } else { _debugStateService.RunspaceId = 1; - debugRunspaceCmd.AddParameter("Id", 1); } @@ -438,11 +353,89 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _) .ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive) .ContinueWith(OnExecutionCompletedAsync, TaskScheduler.Default); - if (runspaceVersion.Version.Major >= 7) + _debugStateService.ServerStarted.TrySetResult(true); + + return new AttachResponse(); + } + + private async Task AttachToComputer(string computerName, CancellationToken cancellationToken) + { + _debugStateService.IsRemoteAttach = true; + + if (_runspaceContext.CurrentRunspace.RunspaceOrigin != RunspaceOrigin.Local) { - _debugStateService.ServerStarted.TrySetResult(true); + throw new RpcErrorException(0, "Cannot attach to a process in a remote session when already in a remote session."); + } + + PSCommand psCommand = new PSCommand() + .AddCommand("Enter-PSSession") + .AddParameter("ComputerName", computerName); + + try + { + await _executionService.ExecutePSCommandAsync( + psCommand, + cancellationToken, + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); + } + catch (Exception e) + { + string msg = $"Could not establish remote session to computer {computerName}"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); + } + } + + private async Task AttachToProcess(int processId, CancellationToken cancellationToken) + { + PSCommand enterPSHostProcessCommand = new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Core\Enter-PSHostProcess") + .AddParameter("Id", processId); + + try + { + await _executionService.ExecutePSCommandAsync( + enterPSHostProcessCommand, + cancellationToken, + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); + } + catch (Exception e) + { + string msg = $"Could not attach to process with ID: {processId}"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); + } + } + + private async Task AttachToPipe(string pipeName, CancellationToken cancellationToken) + { + PowerShellVersionDetails runspaceVersion = _runspaceContext.CurrentRunspace.PowerShellVersionDetails; + + if (runspaceVersion.Version < s_minVersionForCustomPipeName) + { + throw new RpcErrorException(0, $"Attaching to a process with CustomPipeName is only available with PowerShell 6.2 and higher (current session is {runspaceVersion.Version})."); + } + + PSCommand enterPSHostProcessCommand = new PSCommand() + .AddCommand(@"Microsoft.PowerShell.Core\Enter-PSHostProcess") + .AddParameter("CustomPipeName", pipeName); + + try + { + await _executionService.ExecutePSCommandAsync( + enterPSHostProcessCommand, + cancellationToken, + PowerShellExecutionOptions.ImmediateInteractive) + .ConfigureAwait(false); + } + catch (Exception e) + { + string msg = $"Could not attach to process with CustomPipeName: {pipeName}"; + _logger.LogError(e, msg); + throw new RpcErrorException(0, msg); } - return new AttachResponse(); } // PSES follows the following flow: diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs index 7cf86e45e..7ca2c86a4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/IGetRunspaceHandler.cs @@ -11,7 +11,7 @@ internal interface IGetRunspaceHandler : IJsonRpcRequestHandler { - public string ProcessId { get; set; } + public int ProcessId { get; set; } } internal class RunspaceResponse diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs index bff586d44..c0d91c5cb 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/PSHostProcessAndRunspaceHandlers.cs @@ -16,6 +16,7 @@ internal class PSHostProcessAndRunspaceHandlers : IGetPSHostProcessesHandler, IG { private readonly ILogger _logger; private readonly IInternalPowerShellExecutionService _executionService; + private static readonly int currentProcessId = System.Diagnostics.Process.GetCurrentProcess().Id; public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory, IInternalPowerShellExecutionService executionService) { @@ -23,80 +24,66 @@ public PSHostProcessAndRunspaceHandlers(ILoggerFactory factory, IInternalPowerSh _executionService = executionService; } - public Task Handle(GetPSHostProcessesParams request, CancellationToken cancellationToken) + public async Task Handle(GetPSHostProcessesParams request, CancellationToken cancellationToken) { - List psHostProcesses = new(); + PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Core\Get-PSHostProcessInfo"); + IReadOnlyList processes = await _executionService.ExecutePSCommandAsync( + psCommand, cancellationToken).ConfigureAwait(false); - int processId = System.Diagnostics.Process.GetCurrentProcess().Id; - - using (PowerShell pwsh = PowerShell.Create()) + List psHostProcesses = []; + foreach (dynamic p in processes) { - pwsh.AddCommand("Get-PSHostProcessInfo") - .AddCommand("Where-Object") - .AddParameter("Property", "ProcessId") - .AddParameter("NE") - .AddParameter("Value", processId.ToString()); - - System.Collections.ObjectModel.Collection processes = pwsh.Invoke(); - - if (processes != null) + PSHostProcessResponse response = new() { - foreach (dynamic p in processes) - { - psHostProcesses.Add( - new PSHostProcessResponse - { - ProcessName = p.ProcessName, - ProcessId = p.ProcessId, - AppDomainName = p.AppDomainName, - MainWindowTitle = p.MainWindowTitle - }); - } + ProcessName = p.ProcessName, + ProcessId = p.ProcessId, + AppDomainName = p.AppDomainName, + MainWindowTitle = p.MainWindowTitle + }; + + // Specially identify our process to the user. + if (response.ProcessId == currentProcessId) { + response.MainWindowTitle = "PowerShell Editor Services"; } + + psHostProcesses.Add(response); } - return Task.FromResult(psHostProcesses.ToArray()); + return psHostProcesses.ToArray(); } public async Task Handle(GetRunspaceParams request, CancellationToken cancellationToken) { - IEnumerable runspaces = null; - - request.ProcessId ??= "current"; + IReadOnlyList runspaces = []; - // If the processId is a valid int, we need to run Get-Runspace within that process - // otherwise just use the current runspace. - if (int.TryParse(request.ProcessId, out int pid)) + // If we're the host process we just use Get-Runspace. + if (request.ProcessId == currentProcessId) + { + PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-Runspace"); + // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. + runspaces = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); + } + else { // Create a remote runspace that we will invoke Get-Runspace in. - using Runspace rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(pid)); + using Runspace rs = RunspaceFactory.CreateRunspace(new NamedPipeConnectionInfo(request.ProcessId)); using PowerShell ps = PowerShell.Create(); rs.Open(); ps.Runspace = rs; // Returns deserialized Runspaces. For simpler code, we use PSObject and rely on dynamic later. runspaces = ps.AddCommand(@"Microsoft.PowerShell.Utility\Get-Runspace").Invoke(); } - else - { - PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-Runspace"); - // returns (not deserialized) Runspaces. For simpler code, we use PSObject and rely on dynamic later. - runspaces = await _executionService.ExecutePSCommandAsync(psCommand, cancellationToken).ConfigureAwait(false); - } - - List runspaceResponses = new(); - if (runspaces != null) + List runspaceResponses = []; + foreach (dynamic runspace in runspaces) { - foreach (dynamic runspace in runspaces) - { - runspaceResponses.Add( - new RunspaceResponse - { - Id = runspace.Id, - Name = runspace.Name, - Availability = runspace.RunspaceAvailability.ToString() - }); - } + runspaceResponses.Add( + new RunspaceResponse + { + Id = runspace.Id, + Name = runspace.Name, + Availability = runspace.RunspaceAvailability.ToString() + }); } return runspaceResponses.ToArray(); diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index cfbc5daf7..b04679e12 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -601,7 +601,7 @@ await PsesLanguageClient "powerShell/getRunspace", new GetRunspaceParams { - ProcessId = $"{process.Id}" + ProcessId = process.Id }) .Returning(CancellationToken.None); }