Skip to content

Commit 70f31d9

Browse files
Merge pull request #1570 from PowerShell/andschwa/stop-debugger
Send `stopDebugger` notification when appropriate
2 parents 8c62e0e + 1b362fb commit 70f31d9

File tree

5 files changed

+51
-49
lines changed

5 files changed

+51
-49
lines changed

.editorconfig

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ indent_size = 4
1212
trim_trailing_whitespace = true
1313
csharp_space_before_open_square_brackets = true
1414
csharp_space_after_keywords_in_control_flow_statements = true
15+
csharp_space_before_open_square_brackets = false
1516

1617
# CS0168: The variable 'var' is declared but never used
1718
dotnet_diagnostic.CS0168.severity = error

src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ private void WriteStartupBanner()
308308

309309
private void DebugServer_OnSessionEnded(object sender, EventArgs args)
310310
{
311-
_logger.Log(PsesLogLevel.Verbose, "Debug session ended. Restarting debug service");
311+
_logger.Log(PsesLogLevel.Verbose, "Debug session ended, restarting debug service...");
312312
var oldServer = (PsesDebugServer)sender;
313313
oldServer.Dispose();
314314
_alreadySubscribedDebug = false;

src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs

+43-35
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,18 @@ public RunspaceDetails CurrentRunspace
162162
/// </summary>
163163
public string InitialWorkingDirectory { get; private set; }
164164

165+
/// <summary>
166+
/// Tracks the state of the LSP debug server (not the PowerShell debugger).
167+
/// </summary>
165168
internal bool IsDebugServerActive { get; set; }
166169

170+
/// <summary>
171+
/// Tracks if the PowerShell session started the debug server itself (true), or if it was
172+
/// started by an LSP notification (false). Essentially, this marks if we're responsible for
173+
/// stopping the debug server (and thus need to send a notification to do so).
174+
/// </summary>
175+
internal bool OwnsDebugServerState { get; set; }
176+
167177
internal DebuggerStopEventArgs CurrentDebuggerStopEventArgs { get; private set; }
168178

169179
#endregion
@@ -182,7 +192,6 @@ public PowerShellContextService(
182192
OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer,
183193
bool isPSReadLineEnabled)
184194
{
185-
logger.LogTrace("Instantiating PowerShellContextService and adding event handlers");
186195
_languageServer = languageServer;
187196
this.logger = logger;
188197
this.isPSReadLineEnabled = isPSReadLineEnabled;
@@ -204,7 +213,7 @@ public static PowerShellContextService Create(
204213
// Respect a user provided bundled module path.
205214
if (Directory.Exists(hostStartupInfo.BundledModulePath))
206215
{
207-
logger.LogTrace($"Using new bundled module path: {hostStartupInfo.BundledModulePath}");
216+
logger.LogDebug($"Using new bundled module path: {hostStartupInfo.BundledModulePath}");
208217
s_bundledModulePath = hostStartupInfo.BundledModulePath;
209218
}
210219

@@ -228,7 +237,6 @@ public static PowerShellContextService Create(
228237
hostUserInterface,
229238
logger);
230239

231-
logger.LogTrace("Creating initial PowerShell runspace");
232240
Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost, hostStartupInfo.InitialSessionState);
233241
powerShellContext.Initialize(hostStartupInfo.ProfilePaths, initialRunspace, true, hostUserInterface);
234242
powerShellContext.ImportCommandsModuleAsync();
@@ -317,7 +325,6 @@ public void Initialize(
317325
IHostOutput consoleHost)
318326
{
319327
Validate.IsNotNull("initialRunspace", initialRunspace);
320-
this.logger.LogTrace($"Initializing PowerShell context with runspace {initialRunspace.Name}");
321328

322329
this.ownsInitialRunspace = ownsInitialRunspace;
323330
this.SessionState = PowerShellContextState.NotStarted;
@@ -353,6 +360,7 @@ public void Initialize(
353360
}
354361
else
355362
{
363+
// TODO: Also throw for PowerShell 6
356364
throw new NotSupportedException(
357365
"This computer has an unsupported version of PowerShell installed: " +
358366
powerShellVersion.ToString());
@@ -567,10 +575,9 @@ public Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
567575
cancellationToken);
568576
}
569577

570-
571578
/// <summary>
572579
/// Executes a PSCommand against the session's runspace and returns
573-
/// a collection of results of the expected type.
580+
/// a collection of results of the expected type. This function needs help.
574581
/// </summary>
575582
/// <typeparam name="TResult">The expected result type.</typeparam>
576583
/// <param name="psCommand">The PSCommand to be executed.</param>
@@ -591,8 +598,6 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
591598
Validate.IsNotNull(nameof(psCommand), psCommand);
592599
Validate.IsNotNull(nameof(executionOptions), executionOptions);
593600

594-
this.logger.LogTrace($"Attempting to execute command(s): {GetStringForPSCommand(psCommand)}");
595-
596601
// Add history to PSReadLine before cancelling, otherwise it will be restored as the
597602
// cancelled prompt when it's called again.
598603
if (executionOptions.AddToHistory)
@@ -626,8 +631,6 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
626631
this.ShouldExecuteWithEventing(executionOptions) ||
627632
(PromptNest.IsRemote && executionOptions.IsReadLine)))
628633
{
629-
this.logger.LogTrace("Passing command execution to pipeline thread");
630-
631634
if (shouldCancelReadLine && PromptNest.IsReadLineBusy())
632635
{
633636
// If a ReadLine pipeline is running in the debugger then we'll stop responding here
@@ -705,6 +708,7 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
705708
}
706709
try
707710
{
711+
this.logger.LogTrace($"Executing in debugger: {GetStringForPSCommand(psCommand)}");
708712
return this.ExecuteCommandInDebugger<TResult>(
709713
psCommand,
710714
executionOptions.WriteOutputToHost);
@@ -732,8 +736,6 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
732736
AddToHistory = executionOptions.AddToHistory
733737
};
734738

735-
this.logger.LogTrace("Passing to PowerShell");
736-
737739
PowerShell shell = this.PromptNest.GetPowerShell(executionOptions.IsReadLine);
738740

739741
// Due to the following PowerShell bug, we can't just assign shell.Commands to psCommand
@@ -757,6 +759,8 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
757759
: this.CurrentRunspace.Runspace;
758760
try
759761
{
762+
this.logger.LogDebug($"Invoking: {GetStringForPSCommand(psCommand)}");
763+
760764
// Nested PowerShell instances can't be invoked asynchronously. This occurs
761765
// in nested prompts and pipeline requests from eventing.
762766
if (shell.IsNested)
@@ -777,6 +781,21 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
777781
await this.sessionStateLock.ReleaseForExecuteCommand().ConfigureAwait(false);
778782
}
779783

784+
// This is the edge case where the debug server is running because it was
785+
// started by PowerShell (and not by an LSP event), and we're no longer in the
786+
// debugger within PowerShell, so since we own the state we need to stop the
787+
// debug server too.
788+
//
789+
// Strangely one would think we could check `!PromptNest.IsInDebugger` but that
790+
// doesn't work, we have to check if the shell is nested instead. Therefore this
791+
// is a bit fragile, and I don't know how it'll work in a remoting scenario.
792+
if (IsDebugServerActive && OwnsDebugServerState && !shell.IsNested)
793+
{
794+
logger.LogDebug("Stopping LSP debugger because PowerShell debugger stopped running!");
795+
OwnsDebugServerState = false;
796+
_languageServer?.SendNotification("powerShell/stopDebugger");
797+
}
798+
780799
if (shell.HadErrors)
781800
{
782801
var strBld = new StringBuilder(1024);
@@ -810,15 +829,11 @@ public async Task<IEnumerable<TResult>> ExecuteCommandAsync<TResult>(
810829

811830
hadErrors = true;
812831
}
813-
else
814-
{
815-
this.logger.LogTrace("Execution completed successfully");
816-
}
817832
}
818833
}
819834
catch (PSRemotingDataStructureException e)
820835
{
821-
this.logger.LogHandledException("Pipeline stopped while executing command", e);
836+
this.logger.LogHandledException("PSRemotingDataStructure exception while executing command", e);
822837
errorMessages?.Append(e.Message);
823838
}
824839
catch (PipelineStoppedException e)
@@ -1063,7 +1078,7 @@ public async Task ExecuteScriptWithArgsAsync(string script, string arguments = n
10631078
.FirstOrDefault()
10641079
.ProviderPath;
10651080

1066-
this.logger.LogTrace($"Prepending working directory {workingDir} to script path {script}");
1081+
this.logger.LogDebug($"Prepending working directory {workingDir} to script path {script}");
10671082
script = Path.Combine(workingDir, script);
10681083
}
10691084
catch (System.Management.Automation.DriveNotFoundException e)
@@ -1095,7 +1110,6 @@ public async Task ExecuteScriptWithArgsAsync(string script, string arguments = n
10951110
strBld.Append(' ').Append(arguments);
10961111

10971112
var launchedScript = strBld.ToString();
1098-
this.logger.LogTrace($"Launch script is: {launchedScript}");
10991113

11001114
command.AddScript(launchedScript, false);
11011115
}
@@ -1237,15 +1251,15 @@ public void AbortExecution()
12371251
/// </param>
12381252
public void AbortExecution(bool shouldAbortDebugSession)
12391253
{
1254+
this.logger.LogTrace("Execution abort requested...");
1255+
12401256
if (this.SessionState == PowerShellContextState.Aborting
12411257
|| this.SessionState == PowerShellContextState.Disposed)
12421258
{
12431259
this.logger.LogTrace($"Execution abort requested when already aborted (SessionState = {this.SessionState})");
12441260
return;
12451261
}
12461262

1247-
this.logger.LogTrace("Execution abort requested...");
1248-
12491263
if (shouldAbortDebugSession)
12501264
{
12511265
this.ExitAllNestedPrompts();
@@ -1391,7 +1405,7 @@ private void ResumeDebugger(DebuggerResumeAction resumeAction, bool shouldWaitFo
13911405
/// </summary>
13921406
public void Close()
13931407
{
1394-
logger.LogDebug("Closing PowerShellContextService...");
1408+
logger.LogTrace("Closing PowerShellContextService...");
13951409
this.PromptNest.Dispose();
13961410
this.SessionState = PowerShellContextState.Disposed;
13971411

@@ -1829,13 +1843,7 @@ private void OnSessionStateChanged(object sender, SessionStateChangedEventArgs e
18291843
{
18301844
if (this.SessionState != PowerShellContextState.Disposed)
18311845
{
1832-
this.logger.LogTrace(
1833-
string.Format(
1834-
"Session state changed --\r\n\r\n Old state: {0}\r\n New state: {1}\r\n Result: {2}",
1835-
this.SessionState.ToString(),
1836-
e.NewSessionState.ToString(),
1837-
e.ExecutionResult));
1838-
1846+
this.logger.LogTrace($"Session state was: {SessionState}, is now: {e.NewSessionState}, result: {e.ExecutionResult}");
18391847
this.SessionState = e.NewSessionState;
18401848
this.SessionStateChanged?.Invoke(sender, e);
18411849
}
@@ -1881,8 +1889,6 @@ private void OnExecutionStatusChanged(
18811889
/// </remarks>
18821890
private void PowerShellContext_RunspaceChangedAsync(object sender, RunspaceChangedEventArgs e)
18831891
{
1884-
this.logger.LogTrace("Sending runspaceChanged notification");
1885-
18861892
_languageServer?.SendNotification(
18871893
"powerShell/runspaceChanged",
18881894
new MinifiedRunspaceDetails(e.NewRunspace));
@@ -1927,8 +1933,6 @@ public MinifiedRunspaceDetails(RunspaceDetails eventArgs)
19271933
/// <param name="e">details of the execution status change</param>
19281934
private void PowerShellContext_ExecutionStatusChangedAsync(object sender, ExecutionStatusChangedEventArgs e)
19291935
{
1930-
this.logger.LogTrace("Sending executionStatusChanged notification");
1931-
19321936
// The cancelling of the prompt (PSReadLine) causes an ExecutionStatus.Aborted to be sent after every
19331937
// actual execution (ExecutionStatus.Running) on the pipeline. We ignore that event since it's counterintuitive to
19341938
// the goal of this method which is to send updates when the pipeline is actually running something.
@@ -1948,8 +1952,6 @@ private void PowerShellContext_ExecutionStatusChangedAsync(object sender, Execut
19481952

19491953
private IEnumerable<TResult> ExecuteCommandInDebugger<TResult>(PSCommand psCommand, bool sendOutputToHost)
19501954
{
1951-
this.logger.LogTrace($"Attempting to execute command(s) in the debugger: {GetStringForPSCommand(psCommand)}");
1952-
19531955
IEnumerable<TResult> output =
19541956
this.versionSpecificOperations.ExecuteCommandInDebugger<TResult>(
19551957
this,
@@ -2422,8 +2424,14 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e)
24222424
// when the DebugServer is fully started.
24232425
CurrentDebuggerStopEventArgs = e;
24242426

2427+
// If this event has fired but the LSP debug server is not active, it means that the
2428+
// PowerShell debugger has started some other way (most likely an existing PSBreakPoint
2429+
// was executed). So not only do we have to start the server, but later we will be
2430+
// responsible for stopping it too.
24252431
if (!IsDebugServerActive)
24262432
{
2433+
logger.LogDebug("Starting LSP debugger because PowerShell debugger is running!");
2434+
OwnsDebugServerState = true;
24272435
_languageServer?.SendNotification("powerShell/startDebugger");
24282436
}
24292437

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,7 @@ public async Task CanStepPastSystemWindowsForms()
260260
new SetFunctionBreakpointsArguments
261261
{
262262
Breakpoints = new FunctionBreakpoint[]
263-
{
264-
new FunctionBreakpoint
265-
{
266-
Name = "Write-Host",
267-
}
268-
}
263+
{ new FunctionBreakpoint { Name = "Write-Host", } }
269264
}).ConfigureAwait(false);
270265

271266
var breakpoint = setBreakpointsResponse.Breakpoints.First();

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+5-7
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ await this.debugService.SetCommandBreakpointsAsync(
114114
await executeTask.ConfigureAwait(false);
115115

116116
StackFrameDetails[] stackFrames = debugService.GetStackFrames();
117-
Assert.Equal(StackFrameDetails.NoFileScriptPath, stackFrames [0].ScriptPath);
117+
Assert.Equal(StackFrameDetails.NoFileScriptPath, stackFrames[0].ScriptPath);
118118

119119
VariableDetailsBase[] variables =
120120
debugService.GetVariables(stackFrames[0].LocalVariables.Id);
@@ -151,7 +151,7 @@ public async Task DebuggerAcceptsScriptArgs(string[] args)
151151

152152
await this.debugService.SetLineBreakpointsAsync(
153153
debugWithParamsFile,
154-
new [] { BreakpointDetails.Create(debugWithParamsFile.FilePath, 3) }).ConfigureAwait(false);
154+
new[] { BreakpointDetails.Create(debugWithParamsFile.FilePath, 3) }).ConfigureAwait(false);
155155

156156
string arguments = string.Join(" ", args);
157157

@@ -580,8 +580,6 @@ await this.AssertStateChange(
580580
PowerShellContextState.Ready,
581581
PowerShellExecutionResult.Stopped).ConfigureAwait(false);
582582

583-
// Abort script execution early and wait for completion
584-
this.debugService.Abort();
585583
await executeTask.ConfigureAwait(false);
586584
}
587585

@@ -872,7 +870,7 @@ await this.debugService.SetLineBreakpointsAsync(
872870
Assert.Equal("[1]", childVars[1].Name);
873871

874872
var childVarStrs = new HashSet<string>(childVars.Select(v => v.ValueString));
875-
var expectedVars = new [] {
873+
var expectedVars = new[] {
876874
"[firstChild, \"Child\"]",
877875
"[secondChild, 42]"
878876
};
@@ -1026,15 +1024,15 @@ await this.debugService.SetLineBreakpointsAsync(
10261024
await executeTask.ConfigureAwait(false);
10271025
}
10281026

1029-
public async Task AssertDebuggerPaused()
1027+
private async Task AssertDebuggerPaused()
10301028
{
10311029
DebuggerStoppedEventArgs eventArgs =
10321030
await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(10000).Token).ConfigureAwait(false);
10331031

10341032
Assert.Empty(eventArgs.OriginalEvent.Breakpoints);
10351033
}
10361034

1037-
public async Task AssertDebuggerStopped(
1035+
private async Task AssertDebuggerStopped(
10381036
string scriptPath,
10391037
int lineNumber = -1)
10401038
{

0 commit comments

Comments
 (0)