diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs index 74e088d47..a6c1d27e1 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs @@ -4,10 +4,9 @@ using System; using System.Management.Automation; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging { @@ -16,44 +15,57 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging /// /// /// - /// Debugging through a PowerShell Host is implemented by registering a handler - /// for the event. - /// Registering that handler causes debug actions in PowerShell like Set-PSBreakpoint - /// and Wait-Debugger to drop into the debugger and trigger the handler. - /// The handler is passed a mutable object - /// and the debugger stop lasts for the duration of the handler call. - /// The handler sets the property - /// when after it returns, the PowerShell debugger uses that as the direction on how to proceed. + /// Debugging through a PowerShell Host is implemented by registering a handler for the event. Registering that handler causes debug actions in + /// PowerShell like Set-PSBreakpoint and Wait-Debugger to drop into the debugger and trigger the + /// handler. The handler is passed a mutable object and the + /// debugger stop lasts for the duration of the handler call. The handler sets the property when after it returns, the PowerShell + /// debugger uses that as the direction on how to proceed. /// /// - /// When we handle the event, - /// we drop into a nested debug prompt and execute things in the debugger with , - /// which enables debugger commands like l, c, s, etc. - /// saves the event args object in its state, - /// and when one of the debugger commands is used, the result returned is used to set - /// on the saved event args object so that when the event handler returns, the PowerShell debugger takes the correct action. + /// When we handle the event, we drop into a nested debug + /// prompt and execute things in the debugger with , which enables debugger commands like l, c, + /// s, etc. saves the event args object in its + /// state, and when one of the debugger commands is used, the result returned is used to set + /// on the saved event args object so that when + /// the event handler returns, the PowerShell debugger takes the correct action. /// /// internal class PowerShellDebugContext : IPowerShellDebugContext { private readonly ILogger _logger; - private readonly ILanguageServerFacade _languageServer; - private readonly PsesInternalHost _psesHost; public PowerShellDebugContext( ILoggerFactory loggerFactory, - ILanguageServerFacade languageServer, PsesInternalHost psesHost) { _logger = loggerFactory.CreateLogger(); - _languageServer = languageServer; _psesHost = psesHost; } + /// + /// Tracks if the debugger is currently stopped at a breakpoint. + /// public bool IsStopped { get; private set; } + /// + /// Tracks the state of the PowerShell debugger. This is NOT the same as , which is true whenever breakpoints are set. Instead, this is + /// set to true when the first event is + /// fired, and set to false in when is false. This is used to send the + /// 'powershell/stopDebugger' notification to the LSP debug server in the cases where the + /// server was started or ended by the PowerShell session instead of by Code's GUI. + /// + public bool IsActive { get; set; } + + /// + /// Tracks the state of the LSP debug server (not the PowerShell debugger). + /// public bool IsDebugServerActive { get; set; } public DebuggerStopEventArgs LastStopEventArgs { get; private set; } @@ -67,42 +79,24 @@ public Task GetDscBreakpointCapabilityAsync(Cancellatio return _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken); } + // This is required by the PowerShell API so that remote debugging works. Without it, a + // runspace may not have these options set and attempting to set breakpoints remotely fails. public void EnableDebugMode() { - // This is required by the PowerShell API so that remote debugging works. - // Without it, a runspace may not have these options set and attempting to set breakpoints remotely can fail. _psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); } - public void Abort() - { - SetDebugResuming(DebuggerResumeAction.Stop); - } + public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop); - public void BreakExecution() - { - _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true); - } + public void BreakExecution() => _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true); - public void Continue() - { - SetDebugResuming(DebuggerResumeAction.Continue); - } + public void Continue() => SetDebugResuming(DebuggerResumeAction.Continue); - public void StepInto() - { - SetDebugResuming(DebuggerResumeAction.StepInto); - } + public void StepInto() => SetDebugResuming(DebuggerResumeAction.StepInto); - public void StepOut() - { - SetDebugResuming(DebuggerResumeAction.StepOut); - } + public void StepOut() => SetDebugResuming(DebuggerResumeAction.StepOut); - public void StepOver() - { - SetDebugResuming(DebuggerResumeAction.StepOver); - } + public void StepOver() => SetDebugResuming(DebuggerResumeAction.StepOver); public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction) { @@ -127,27 +121,19 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction) } // This must be called AFTER the new PowerShell has been pushed - public void EnterDebugLoop() - { - RaiseDebuggerStoppedEvent(); - } + public void EnterDebugLoop() => RaiseDebuggerStoppedEvent(); // This must be called BEFORE the debug PowerShell has been popped [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "This method may acquire an implementation later, at which point it will need instance data")] - public void ExitDebugLoop() - { - } + public void ExitDebugLoop() { } - public void SetDebuggerStopped(DebuggerStopEventArgs debuggerStopEventArgs) + public void SetDebuggerStopped(DebuggerStopEventArgs args) { IsStopped = true; - LastStopEventArgs = debuggerStopEventArgs; + LastStopEventArgs = args; } - public void SetDebuggerResumed() - { - IsStopped = false; - } + public void SetDebuggerResumed() { IsStopped = false; } public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult) { @@ -158,26 +144,10 @@ public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult) } } - public void HandleBreakpointUpdated(BreakpointUpdatedEventArgs breakpointUpdatedEventArgs) - { - BreakpointUpdated?.Invoke(this, breakpointUpdatedEventArgs); - } + public void HandleBreakpointUpdated(BreakpointUpdatedEventArgs args) => BreakpointUpdated?.Invoke(this, args); - private void RaiseDebuggerStoppedEvent() - { - if (!IsDebugServerActive) - { - // NOTE: The language server is not necessarily connected, so this must be - // conditional access. This shows up in unit tests. - _languageServer?.SendNotification("powerShell/startDebugger"); - } + private void RaiseDebuggerStoppedEvent() => DebuggerStopped?.Invoke(this, LastStopEventArgs); - DebuggerStopped?.Invoke(this, LastStopEventArgs); - } - - private void RaiseDebuggerResumingEvent(DebuggerResumingEventArgs debuggerResumingEventArgs) - { - DebuggerResuming?.Invoke(this, debuggerResumingEventArgs); - } + private void RaiseDebuggerResumingEvent(DebuggerResumingEventArgs args) => DebuggerResuming?.Invoke(this, args); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index a1ccee7b5..1901dabf4 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -32,7 +32,7 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns private static string s_bundledModulePath = Path.GetFullPath(Path.Combine( Path.GetDirectoryName(typeof(PsesInternalHost).Assembly.Location), "..", "..", "..")); - private static string s_commandsModulePath => Path.GetFullPath(Path.Combine( + private static string CommandsModulePath => Path.GetFullPath(Path.Combine( s_bundledModulePath, "PowerShellEditorServices", "Commands", "PowerShellEditorServices.Commands.psd1")); private readonly ILoggerFactory _loggerFactory; @@ -112,7 +112,7 @@ public PsesInternalHost( Name = hostInfo.Name; Version = hostInfo.Version; - DebugContext = new PowerShellDebugContext(loggerFactory, languageServer, this); + DebugContext = new PowerShellDebugContext(loggerFactory, this); UI = hostInfo.ConsoleReplEnabled ? new EditorServicesConsolePSHostUserInterface(loggerFactory, _readLineProvider, hostInfo.PSHost.UI) : new NullPSHostUI(); @@ -513,8 +513,7 @@ private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceC { // If we're changing runspace, make sure we move the handlers over. If we just // popped the last frame, then we're exiting and should pop the runspace too. - if (_psFrameStack.Count == 0 - || _runspaceStack.Peek().Runspace != _psFrameStack.Peek().PowerShell.Runspace) + if (_psFrameStack.Count == 0 || CurrentRunspace.Runspace != CurrentPowerShell.Runspace) { RunspaceFrame previousRunspaceFrame = _runspaceStack.Pop(); RemoveRunspaceEventHandlers(previousRunspaceFrame.Runspace); @@ -566,7 +565,6 @@ private void RunTopLevelExecutionLoop() _stopped.SetResult(true); } - private void RunDebugExecutionLoop() { try @@ -584,16 +582,14 @@ private void RunExecutionLoop() { while (!ShouldExitExecutionLoop) { - using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false)) - { - DoOneRepl(cancellationScope.CancellationToken); + using CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false); + DoOneRepl(cancellationScope.CancellationToken); - while (!ShouldExitExecutionLoop - && !cancellationScope.CancellationToken.IsCancellationRequested - && _taskQueue.TryTake(out ISynchronousTask task)) - { - task.ExecuteSynchronously(cancellationScope.CancellationToken); - } + while (!ShouldExitExecutionLoop + && !cancellationScope.CancellationToken.IsCancellationRequested + && _taskQueue.TryTake(out ISynchronousTask task)) + { + task.ExecuteSynchronously(cancellationScope.CancellationToken); } } } @@ -605,6 +601,16 @@ private void DoOneRepl(CancellationToken cancellationToken) return; } + // We use the REPL as a poll to check if the debug context is active but PowerShell + // indicates we're no longer debugging. This happens when PowerShell was used to start + // the debugger (instead of using a Code launch configuration) via Wait-Debugger or + // simply hitting a PSBreakpoint. We need to synchronize the state and stop the debug + // context (and likely the debug server). + if (DebugContext.IsActive && !CurrentRunspace.Runspace.Debugger.InBreakpoint) + { + StopDebugContext(); + } + // When a task must run in the foreground, we cancel out of the idle loop and return to the top level. // At that point, we would normally run a REPL, but we need to immediately execute the task. // So we set _skipNextPrompt to do that. @@ -629,8 +635,7 @@ private void DoOneRepl(CancellationToken cancellationToken) // However, we must distinguish the last two scenarios, since PSRL will not print a new line in those cases. if (string.IsNullOrEmpty(userInput)) { - if (cancellationToken.IsCancellationRequested - || LastKeyWasCtrlC()) + if (cancellationToken.IsCancellationRequested || LastKeyWasCtrlC()) { UI.WriteLine(); } @@ -742,7 +747,7 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace) pwsh.SetCorrectExecutionPolicy(_logger); } - pwsh.ImportModule(s_commandsModulePath); + pwsh.ImportModule(CommandsModulePath); if (hostStartupInfo.AdditionalModules?.Count > 0) { @@ -830,7 +835,12 @@ private void OnPowerShellIdle(CancellationToken idleCancellationToken) private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args) { + // We need to cancel the current task. _cancellationContext.CancelCurrentTask(); + + // If the current task was running under the debugger, we need to synchronize the + // cancelation with our debug context (and likely the debug server). + StopDebugContext(); } private ConsoleKeyInfo ReadKey(bool intercept) @@ -850,9 +860,31 @@ private bool LastKeyWasCtrlC() && _lastKey.Value.IsCtrlC(); } + private void StopDebugContext() + { + // We are officially stopping the debugger. + DebugContext.IsActive = false; + + // If the debug server is active, we need to synchronize state and stop it. + if (DebugContext.IsDebugServerActive) + { + _languageServer?.SendNotification("powerShell/stopDebugger"); + } + } + private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs) { + // The debugger has officially started. We use this to later check if we should stop it. + DebugContext.IsActive = true; + + // If the debug server is NOT active, we need to synchronize state and start it. + if (!DebugContext.IsDebugServerActive) + { + _languageServer?.SendNotification("powerShell/startDebugger"); + } + DebugContext.SetDebuggerStopped(debuggerStopEventArgs); + try { CurrentPowerShell.WaitForRemoteOutputIfNeeded(); @@ -875,7 +907,7 @@ private void OnRunspaceStateChanged(object sender, RunspaceStateEventArgs runspa if (!ShouldExitExecutionLoop && !_resettingRunspace && !runspaceStateEventArgs.RunspaceStateInfo.IsUsable()) { _resettingRunspace = true; - PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger); + Task _ = PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger); } } @@ -889,7 +921,7 @@ private Task PopOrReinitializeRunspaceAsync() return ExecuteDelegateAsync( nameof(PopOrReinitializeRunspaceAsync), new ExecutionOptions { InterruptCurrentForeground = true }, - (cancellationToken) => + (_) => { while (_psFrameStack.Count > 0 && !_psFrameStack.Peek().PowerShell.Runspace.RunspaceStateInfo.IsUsable())