From d4256bbbafe07542888d0c1b0c69b3b3e1f2781a Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 7 Jun 2022 12:10:52 -0400 Subject: [PATCH] Exit debugger stop early if cause is PSE If the `ErrorActionPreference` is set to `Break` and the user hits the Stop button in the UI, then we need to exit early on reentrance of OnDebuggerStop. Currently we deadlock until they hit stop again. --- .../PowerShell/Host/PsesInternalHost.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 271518add..d86b56254 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Management.Automation.Host; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -39,6 +40,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns private static string CommandsModulePath => Path.GetFullPath(Path.Combine( s_bundledModulePath, "PowerShellEditorServices", "Commands", "PowerShellEditorServices.Commands.psd1")); + private static readonly PropertyInfo s_scriptDebuggerTriggerObjectProperty; + private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; @@ -89,6 +92,21 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns private bool _resettingRunspace; + static PsesInternalHost() + { + Type scriptDebuggerType = typeof(PSObject).Assembly + .GetType("System.Management.Automation.ScriptDebugger"); + + if (scriptDebuggerType is null) + { + return; + } + + s_scriptDebuggerTriggerObjectProperty = scriptDebuggerType.GetProperty( + "TriggerObject", + BindingFlags.Instance | BindingFlags.NonPublic); + } + public PsesInternalHost( ILoggerFactory loggerFactory, ILanguageServerFacade languageServer, @@ -1142,6 +1160,34 @@ internal void WaitForExternalDebuggerStops() private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs) { + // If ErrorActionPreference is set to Break, any engine exception is going to trigger a + // pipeline stop. Technically this is the same behavior as a standalone PowerShell + // process, but we use pipeline stops with greater frequency due to features like run + // selection and terminating the debugger. Without this, if the "Stop" button is pressed + // then we hit this repeatedly. + // + // This info is publically accessible via `PSDebugContext` but we'd need to access it + // via a script. At this point in the call I'd prefer this to be as light as possible so + // we can escape ASAP but we may want to consider switching to that at some point. + if (!Runspace.RunspaceIsRemote && s_scriptDebuggerTriggerObjectProperty is not null) + { + object triggerObject = null; + try + { + triggerObject = s_scriptDebuggerTriggerObjectProperty.GetValue(Runspace.Debugger); + } + catch + { + // Ignore all exceptions. There shouldn't be any, but as this is implementation + // detail that is subject to change it's best to be overly cautious. + } + + if (triggerObject is PipelineStoppedException pse) + { + throw pse; + } + } + // The debugger has officially started. We use this to later check if we should stop it. DebugContext.IsActive = true;