From a2185961b5494e3742310b10157b250502715957 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:57:25 -0700 Subject: [PATCH] Fix debugging script blocks that aren't in files The code relied on running `list 1 ` but that wasn't being run in the debug context because we allow-list commands to prevent pollution of the history, and missed it. Like the `prompt` and interactive commands (which `list` could be but is not when we run it) we need to check for this exact `list` command and run it under the debugger. Moreover, we also weren't locking the `debugInfoHandle`, nor were we correctly checking if `scriptListingLines` was empty (it was never null), and our shortcut to skip allocation was broken. Actually we can't skip allocation, but we can at least skip superfluous conversions. --- .../Services/DebugAdapter/DebugService.cs | 21 ++++++++++++++----- .../Execution/SynchronousPowerShellTask.cs | 19 ++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 0439f4484..9328e5440 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -930,13 +930,24 @@ internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) { // Get the current script listing and create the buffer - PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); + IReadOnlyList scriptListingLines; + await debugInfoHandle.WaitAsync().ConfigureAwait(false); + try + { + // This command must be run through `ExecuteInDebugger`! + PSCommand psCommand = new PSCommand().AddScript($"list 1 {int.MaxValue}"); - IReadOnlyList scriptListingLines = - await _executionService.ExecutePSCommandAsync( - command, CancellationToken.None).ConfigureAwait(false); + scriptListingLines = + await _executionService.ExecutePSCommandAsync( + psCommand, + CancellationToken.None).ConfigureAwait(false); + } + finally + { + debugInfoHandle.Release(); + } - if (scriptListingLines is not null) + if (scriptListingLines.Count > 0) { int linePrefixLength = 0; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs index c7a983bd8..7c05b3da2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs @@ -73,9 +73,12 @@ public override IReadOnlyList Run(CancellationToken cancellationToken) // state that we sync with the LSP debugger. The former commands we want to send // through PowerShell's `Debugger.ProcessCommand` so that they work as expected, but // the latter we must not send through it else they pollute the history as this - // PowerShell API does not let us exclude them from it. + // PowerShell API does not let us exclude them from it. Notably we also need to send + // the `prompt` command and our special `list 1 ` through the debugger too. + // The former needs the context in order to show `DBG 1>` etc., and the latter is + // used to gather the lines when debugging a script that isn't in a file. return _pwsh.Runspace.Debugger.InBreakpoint - && (PowerShellExecutionOptions.AddToHistory || IsPromptCommand(_psCommand) || _pwsh.Runspace.RunspaceIsRemote) + && (PowerShellExecutionOptions.AddToHistory || IsPromptOrListCommand(_psCommand) || _pwsh.Runspace.RunspaceIsRemote) ? ExecuteInDebugger(cancellationToken) : ExecuteNormally(cancellationToken); } @@ -87,7 +90,7 @@ public override IReadOnlyList Run(CancellationToken cancellationToken) public override string ToString() => _psCommand.GetInvocationText(); - private static bool IsPromptCommand(PSCommand command) + private static bool IsPromptOrListCommand(PSCommand command) { if (command.Commands.Count is not 1 || command.Commands[0] is { IsScript: false } or { Parameters.Count: > 0 }) @@ -96,7 +99,8 @@ private static bool IsPromptCommand(PSCommand command) } string commandText = command.Commands[0].CommandText; - return commandText.Equals("prompt", StringComparison.OrdinalIgnoreCase); + return commandText.Equals("prompt", StringComparison.OrdinalIgnoreCase) + || commandText.Equals($"list 1 {int.MaxValue}", StringComparison.OrdinalIgnoreCase); } private IReadOnlyList ExecuteNormally(CancellationToken cancellationToken) @@ -301,11 +305,10 @@ private IReadOnlyList ExecuteInDebugger(CancellationToken cancellationT return Array.Empty(); } - // If we've been asked for a PSObject, no need to allocate a new collection - if (typeof(TResult) == typeof(PSObject) - && outputCollection is IReadOnlyList resultCollection) + // If we've been asked for a PSObject, no need to convert + if (typeof(TResult) == typeof(PSObject)) { - return resultCollection; + return new List(outputCollection) as IReadOnlyList; } // Otherwise, convert things over