Skip to content

Commit f890a75

Browse files
Merge pull request #1685 from PowerShell/andschwa/stopDebugger
Synchronize PowerShell debugger and DAP server state
2 parents 624fe30 + 0f52aa2 commit f890a75

File tree

2 files changed

+99
-97
lines changed

2 files changed

+99
-97
lines changed

src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs

+48-78
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
using System;
55
using System.Management.Automation;
66
using System.Threading;
7+
using System.Threading.Tasks;
78
using Microsoft.Extensions.Logging;
89
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
9-
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
10-
using System.Threading.Tasks;
1110

1211
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging
1312
{
@@ -16,44 +15,57 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging
1615
/// </summary>
1716
/// <remarks>
1817
/// <para>
19-
/// Debugging through a PowerShell Host is implemented by registering a handler
20-
/// for the <see cref="System.Management.Automation.Debugger.DebuggerStop"/> event.
21-
/// Registering that handler causes debug actions in PowerShell like Set-PSBreakpoint
22-
/// and Wait-Debugger to drop into the debugger and trigger the handler.
23-
/// The handler is passed a mutable <see cref="System.Management.Automation.DebuggerStopEventArgs"/> object
24-
/// and the debugger stop lasts for the duration of the handler call.
25-
/// The handler sets the <see cref="System.Management.Automation.DebuggerStopEventArgs.ResumeAction"/> property
26-
/// when after it returns, the PowerShell debugger uses that as the direction on how to proceed.
18+
/// Debugging through a PowerShell Host is implemented by registering a handler for the <see
19+
/// cref="Debugger.DebuggerStop"/> event. Registering that handler causes debug actions in
20+
/// PowerShell like Set-PSBreakpoint and Wait-Debugger to drop into the debugger and trigger the
21+
/// handler. The handler is passed a mutable <see cref="DebuggerStopEventArgs"/> object and the
22+
/// debugger stop lasts for the duration of the handler call. The handler sets the <see
23+
/// cref="DebuggerStopEventArgs.ResumeAction"/> property when after it returns, the PowerShell
24+
/// debugger uses that as the direction on how to proceed.
2725
/// </para>
2826
/// <para>
29-
/// When we handle the <see cref="System.Management.Automation.Debugger.DebuggerStop"/> event,
30-
/// we drop into a nested debug prompt and execute things in the debugger with <see cref="System.Management.Automation.Debugger.ProcessCommand(PSCommand, PSDataCollection{PSObject})"/>,
31-
/// which enables debugger commands like <c>l</c>, <c>c</c>, <c>s</c>, etc.
32-
/// <see cref="Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging.PowerShellDebugContext"/> saves the event args object in its state,
33-
/// and when one of the debugger commands is used, the result returned is used to set <see cref="System.Management.Automation.DebuggerStopEventArgs.ResumeAction"/>
34-
/// on the saved event args object so that when the event handler returns, the PowerShell debugger takes the correct action.
27+
/// When we handle the <see cref="Debugger.DebuggerStop"/> event, we drop into a nested debug
28+
/// prompt and execute things in the debugger with <see cref="Debugger.ProcessCommand(PSCommand,
29+
/// PSDataCollection{PSObject})"/>, which enables debugger commands like <c>l</c>, <c>c</c>,
30+
/// <c>s</c>, etc. <see cref="PowerShellDebugContext"/> saves the event args object in its
31+
/// state, and when one of the debugger commands is used, the result returned is used to set
32+
/// <see cref="DebuggerStopEventArgs.ResumeAction"/> on the saved event args object so that when
33+
/// the event handler returns, the PowerShell debugger takes the correct action.
3534
/// </para>
3635
/// </remarks>
3736
internal class PowerShellDebugContext : IPowerShellDebugContext
3837
{
3938
private readonly ILogger _logger;
4039

41-
private readonly ILanguageServerFacade _languageServer;
42-
4340
private readonly PsesInternalHost _psesHost;
4441

4542
public PowerShellDebugContext(
4643
ILoggerFactory loggerFactory,
47-
ILanguageServerFacade languageServer,
4844
PsesInternalHost psesHost)
4945
{
5046
_logger = loggerFactory.CreateLogger<PowerShellDebugContext>();
51-
_languageServer = languageServer;
5247
_psesHost = psesHost;
5348
}
5449

50+
/// <summary>
51+
/// Tracks if the debugger is currently stopped at a breakpoint.
52+
/// </summary>
5553
public bool IsStopped { get; private set; }
5654

55+
/// <summary>
56+
/// Tracks the state of the PowerShell debugger. This is NOT the same as <see
57+
/// cref="Debugger.IsActive">, which is true whenever breakpoints are set. Instead, this is
58+
/// set to true when the first <see cref="PsesInternalHost.OnDebuggerStopped"> event is
59+
/// fired, and set to false in <see cref="PsesInternalHost.DoOneRepl"> when <see
60+
/// cref="Debugger.IsInBreakpoint"> is false. This is used to send the
61+
/// 'powershell/stopDebugger' notification to the LSP debug server in the cases where the
62+
/// server was started or ended by the PowerShell session instead of by Code's GUI.
63+
/// </summary>
64+
public bool IsActive { get; set; }
65+
66+
/// <summary>
67+
/// Tracks the state of the LSP debug server (not the PowerShell debugger).
68+
/// </summary>
5769
public bool IsDebugServerActive { get; set; }
5870

5971
public DebuggerStopEventArgs LastStopEventArgs { get; private set; }
@@ -67,42 +79,24 @@ public Task<DscBreakpointCapability> GetDscBreakpointCapabilityAsync(Cancellatio
6779
return _psesHost.CurrentRunspace.GetDscBreakpointCapabilityAsync(_logger, _psesHost, cancellationToken);
6880
}
6981

82+
// This is required by the PowerShell API so that remote debugging works. Without it, a
83+
// runspace may not have these options set and attempting to set breakpoints remotely fails.
7084
public void EnableDebugMode()
7185
{
72-
// This is required by the PowerShell API so that remote debugging works.
73-
// Without it, a runspace may not have these options set and attempting to set breakpoints remotely can fail.
7486
_psesHost.Runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript);
7587
}
7688

77-
public void Abort()
78-
{
79-
SetDebugResuming(DebuggerResumeAction.Stop);
80-
}
89+
public void Abort() => SetDebugResuming(DebuggerResumeAction.Stop);
8190

82-
public void BreakExecution()
83-
{
84-
_psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true);
85-
}
91+
public void BreakExecution() => _psesHost.Runspace.Debugger.SetDebuggerStepMode(enabled: true);
8692

87-
public void Continue()
88-
{
89-
SetDebugResuming(DebuggerResumeAction.Continue);
90-
}
93+
public void Continue() => SetDebugResuming(DebuggerResumeAction.Continue);
9194

92-
public void StepInto()
93-
{
94-
SetDebugResuming(DebuggerResumeAction.StepInto);
95-
}
95+
public void StepInto() => SetDebugResuming(DebuggerResumeAction.StepInto);
9696

97-
public void StepOut()
98-
{
99-
SetDebugResuming(DebuggerResumeAction.StepOut);
100-
}
97+
public void StepOut() => SetDebugResuming(DebuggerResumeAction.StepOut);
10198

102-
public void StepOver()
103-
{
104-
SetDebugResuming(DebuggerResumeAction.StepOver);
105-
}
99+
public void StepOver() => SetDebugResuming(DebuggerResumeAction.StepOver);
106100

107101
public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction)
108102
{
@@ -127,27 +121,19 @@ public void SetDebugResuming(DebuggerResumeAction debuggerResumeAction)
127121
}
128122

129123
// This must be called AFTER the new PowerShell has been pushed
130-
public void EnterDebugLoop()
131-
{
132-
RaiseDebuggerStoppedEvent();
133-
}
124+
public void EnterDebugLoop() => RaiseDebuggerStoppedEvent();
134125

135126
// This must be called BEFORE the debug PowerShell has been popped
136127
[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")]
137-
public void ExitDebugLoop()
138-
{
139-
}
128+
public void ExitDebugLoop() { }
140129

141-
public void SetDebuggerStopped(DebuggerStopEventArgs debuggerStopEventArgs)
130+
public void SetDebuggerStopped(DebuggerStopEventArgs args)
142131
{
143132
IsStopped = true;
144-
LastStopEventArgs = debuggerStopEventArgs;
133+
LastStopEventArgs = args;
145134
}
146135

147-
public void SetDebuggerResumed()
148-
{
149-
IsStopped = false;
150-
}
136+
public void SetDebuggerResumed() { IsStopped = false; }
151137

152138
public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult)
153139
{
@@ -158,26 +144,10 @@ public void ProcessDebuggerResult(DebuggerCommandResults debuggerResult)
158144
}
159145
}
160146

161-
public void HandleBreakpointUpdated(BreakpointUpdatedEventArgs breakpointUpdatedEventArgs)
162-
{
163-
BreakpointUpdated?.Invoke(this, breakpointUpdatedEventArgs);
164-
}
147+
public void HandleBreakpointUpdated(BreakpointUpdatedEventArgs args) => BreakpointUpdated?.Invoke(this, args);
165148

166-
private void RaiseDebuggerStoppedEvent()
167-
{
168-
if (!IsDebugServerActive)
169-
{
170-
// NOTE: The language server is not necessarily connected, so this must be
171-
// conditional access. This shows up in unit tests.
172-
_languageServer?.SendNotification("powerShell/startDebugger");
173-
}
149+
private void RaiseDebuggerStoppedEvent() => DebuggerStopped?.Invoke(this, LastStopEventArgs);
174150

175-
DebuggerStopped?.Invoke(this, LastStopEventArgs);
176-
}
177-
178-
private void RaiseDebuggerResumingEvent(DebuggerResumingEventArgs debuggerResumingEventArgs)
179-
{
180-
DebuggerResuming?.Invoke(this, debuggerResumingEventArgs);
181-
}
151+
private void RaiseDebuggerResumingEvent(DebuggerResumingEventArgs args) => DebuggerResuming?.Invoke(this, args);
182152
}
183153
}

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+51-19
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
3232
private static string s_bundledModulePath = Path.GetFullPath(Path.Combine(
3333
Path.GetDirectoryName(typeof(PsesInternalHost).Assembly.Location), "..", "..", ".."));
3434

35-
private static string s_commandsModulePath => Path.GetFullPath(Path.Combine(
35+
private static string CommandsModulePath => Path.GetFullPath(Path.Combine(
3636
s_bundledModulePath, "PowerShellEditorServices", "Commands", "PowerShellEditorServices.Commands.psd1"));
3737

3838
private readonly ILoggerFactory _loggerFactory;
@@ -112,7 +112,7 @@ public PsesInternalHost(
112112
Name = hostInfo.Name;
113113
Version = hostInfo.Version;
114114

115-
DebugContext = new PowerShellDebugContext(loggerFactory, languageServer, this);
115+
DebugContext = new PowerShellDebugContext(loggerFactory, this);
116116
UI = hostInfo.ConsoleReplEnabled
117117
? new EditorServicesConsolePSHostUserInterface(loggerFactory, _readLineProvider, hostInfo.PSHost.UI)
118118
: new NullPSHostUI();
@@ -513,8 +513,7 @@ private void PopPowerShell(RunspaceChangeAction runspaceChangeAction = RunspaceC
513513
{
514514
// If we're changing runspace, make sure we move the handlers over. If we just
515515
// popped the last frame, then we're exiting and should pop the runspace too.
516-
if (_psFrameStack.Count == 0
517-
|| _runspaceStack.Peek().Runspace != _psFrameStack.Peek().PowerShell.Runspace)
516+
if (_psFrameStack.Count == 0 || CurrentRunspace.Runspace != CurrentPowerShell.Runspace)
518517
{
519518
RunspaceFrame previousRunspaceFrame = _runspaceStack.Pop();
520519
RemoveRunspaceEventHandlers(previousRunspaceFrame.Runspace);
@@ -566,7 +565,6 @@ private void RunTopLevelExecutionLoop()
566565
_stopped.SetResult(true);
567566
}
568567

569-
570568
private void RunDebugExecutionLoop()
571569
{
572570
try
@@ -584,16 +582,14 @@ private void RunExecutionLoop()
584582
{
585583
while (!ShouldExitExecutionLoop)
586584
{
587-
using (CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false))
588-
{
589-
DoOneRepl(cancellationScope.CancellationToken);
585+
using CancellationScope cancellationScope = _cancellationContext.EnterScope(isIdleScope: false);
586+
DoOneRepl(cancellationScope.CancellationToken);
590587

591-
while (!ShouldExitExecutionLoop
592-
&& !cancellationScope.CancellationToken.IsCancellationRequested
593-
&& _taskQueue.TryTake(out ISynchronousTask task))
594-
{
595-
task.ExecuteSynchronously(cancellationScope.CancellationToken);
596-
}
588+
while (!ShouldExitExecutionLoop
589+
&& !cancellationScope.CancellationToken.IsCancellationRequested
590+
&& _taskQueue.TryTake(out ISynchronousTask task))
591+
{
592+
task.ExecuteSynchronously(cancellationScope.CancellationToken);
597593
}
598594
}
599595
}
@@ -605,6 +601,16 @@ private void DoOneRepl(CancellationToken cancellationToken)
605601
return;
606602
}
607603

604+
// We use the REPL as a poll to check if the debug context is active but PowerShell
605+
// indicates we're no longer debugging. This happens when PowerShell was used to start
606+
// the debugger (instead of using a Code launch configuration) via Wait-Debugger or
607+
// simply hitting a PSBreakpoint. We need to synchronize the state and stop the debug
608+
// context (and likely the debug server).
609+
if (DebugContext.IsActive && !CurrentRunspace.Runspace.Debugger.InBreakpoint)
610+
{
611+
StopDebugContext();
612+
}
613+
608614
// When a task must run in the foreground, we cancel out of the idle loop and return to the top level.
609615
// At that point, we would normally run a REPL, but we need to immediately execute the task.
610616
// So we set _skipNextPrompt to do that.
@@ -629,8 +635,7 @@ private void DoOneRepl(CancellationToken cancellationToken)
629635
// However, we must distinguish the last two scenarios, since PSRL will not print a new line in those cases.
630636
if (string.IsNullOrEmpty(userInput))
631637
{
632-
if (cancellationToken.IsCancellationRequested
633-
|| LastKeyWasCtrlC())
638+
if (cancellationToken.IsCancellationRequested || LastKeyWasCtrlC())
634639
{
635640
UI.WriteLine();
636641
}
@@ -742,7 +747,7 @@ private static PowerShell CreatePowerShellForRunspace(Runspace runspace)
742747
pwsh.SetCorrectExecutionPolicy(_logger);
743748
}
744749

745-
pwsh.ImportModule(s_commandsModulePath);
750+
pwsh.ImportModule(CommandsModulePath);
746751

747752
if (hostStartupInfo.AdditionalModules?.Count > 0)
748753
{
@@ -830,7 +835,12 @@ private void OnPowerShellIdle(CancellationToken idleCancellationToken)
830835

831836
private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
832837
{
838+
// We need to cancel the current task.
833839
_cancellationContext.CancelCurrentTask();
840+
841+
// If the current task was running under the debugger, we need to synchronize the
842+
// cancelation with our debug context (and likely the debug server).
843+
StopDebugContext();
834844
}
835845

836846
private ConsoleKeyInfo ReadKey(bool intercept)
@@ -850,9 +860,31 @@ private bool LastKeyWasCtrlC()
850860
&& _lastKey.Value.IsCtrlC();
851861
}
852862

863+
private void StopDebugContext()
864+
{
865+
// We are officially stopping the debugger.
866+
DebugContext.IsActive = false;
867+
868+
// If the debug server is active, we need to synchronize state and stop it.
869+
if (DebugContext.IsDebugServerActive)
870+
{
871+
_languageServer?.SendNotification("powerShell/stopDebugger");
872+
}
873+
}
874+
853875
private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs)
854876
{
877+
// The debugger has officially started. We use this to later check if we should stop it.
878+
DebugContext.IsActive = true;
879+
880+
// If the debug server is NOT active, we need to synchronize state and start it.
881+
if (!DebugContext.IsDebugServerActive)
882+
{
883+
_languageServer?.SendNotification("powerShell/startDebugger");
884+
}
885+
855886
DebugContext.SetDebuggerStopped(debuggerStopEventArgs);
887+
856888
try
857889
{
858890
CurrentPowerShell.WaitForRemoteOutputIfNeeded();
@@ -875,7 +907,7 @@ private void OnRunspaceStateChanged(object sender, RunspaceStateEventArgs runspa
875907
if (!ShouldExitExecutionLoop && !_resettingRunspace && !runspaceStateEventArgs.RunspaceStateInfo.IsUsable())
876908
{
877909
_resettingRunspace = true;
878-
PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger);
910+
Task _ = PopOrReinitializeRunspaceAsync().HandleErrorsAsync(_logger);
879911
}
880912
}
881913

@@ -889,7 +921,7 @@ private Task PopOrReinitializeRunspaceAsync()
889921
return ExecuteDelegateAsync(
890922
nameof(PopOrReinitializeRunspaceAsync),
891923
new ExecutionOptions { InterruptCurrentForeground = true },
892-
(cancellationToken) =>
924+
(_) =>
893925
{
894926
while (_psFrameStack.Count > 0
895927
&& !_psFrameStack.Peek().PowerShell.Runspace.RunspaceStateInfo.IsUsable())

0 commit comments

Comments
 (0)