diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
index 31f6a988f..6fbecce9c 100644
--- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs
+++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
@@ -117,6 +117,7 @@ public async Task StartAsync()
response.SupportsHitConditionalBreakpoints = true;
response.SupportsLogPoints = true;
response.SupportsSetVariable = true;
+ response.SupportsDelayedStackTraceLoading = true;
return Task.CompletedTask;
});
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
index 667a26bdd..94a957cf9 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
@@ -6,8 +6,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
-using System.Management.Automation.Language;
-using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -50,7 +48,6 @@ internal class DebugService
private VariableContainerDetails scriptScopeVariables;
private VariableContainerDetails localScopeVariables;
private StackFrameDetails[] stackFrameDetails;
- private readonly PropertyInfo invocationTypeScriptPositionProperty;
private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
#endregion
@@ -75,6 +72,12 @@ internal class DebugService
///
public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }
+ ///
+ /// Returns a task that completes when script frames and variables have completed population
+ ///
+ public Task StackFramesAndVariablesFetched { get; private set; }
+
+
///
/// Tracks whether we are running Debug-Runspace in an out-of-process runspace.
///
@@ -111,12 +114,6 @@ public DebugService(
_debugContext.DebuggerResuming += OnDebuggerResuming;
_debugContext.BreakpointUpdated += OnBreakpointUpdated;
_remoteFileManager = remoteFileManager;
-
- invocationTypeScriptPositionProperty =
- typeof(InvocationInfo)
- .GetProperty(
- "ScriptPosition",
- BindingFlags.NonPublic | BindingFlags.Instance);
}
#endregion
@@ -981,8 +978,8 @@ await _executionService.ExecutePSCommandAsync(
}
}
- // Get call stack and variables.
- await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
+ // Begin call stack and variables fetch. We don't need to block here.
+ StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);
// If this is a remote connection and the debugger stopped at a line
// in a script file, get the file contents
@@ -996,53 +993,6 @@ await _remoteFileManager.FetchRemoteFileAsync(
_psesHost.CurrentRunspace).ConfigureAwait(false);
}
- if (stackFrameDetails.Length > 0)
- {
- // Augment the top stack frame with details from the stop event
- if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
- {
- StackFrameDetails targetFrame = stackFrameDetails[0];
-
- // Certain context changes (like stepping into the default value expression
- // of a parameter) do not create a call stack frame. In order to represent
- // this context change we create a fake call stack frame.
- if (!string.IsNullOrEmpty(scriptExtent.File)
- && !PathUtils.IsPathEqual(scriptExtent.File, targetFrame.ScriptPath))
- {
- await debugInfoHandle.WaitAsync().ConfigureAwait(false);
- try
- {
- targetFrame = new StackFrameDetails
- {
- ScriptPath = scriptExtent.File,
- // Just use the last frame's variables since we don't have a
- // good way to get real values.
- AutoVariables = targetFrame.AutoVariables,
- CommandVariables = targetFrame.CommandVariables,
- // Ideally we'd get a real value here but since there's no real
- // call stack frame for this, we'd need to replicate a lot of
- // engine code.
- FunctionName = "",
- };
-
- StackFrameDetails[] newFrames = new StackFrameDetails[stackFrameDetails.Length + 1];
- newFrames[0] = targetFrame;
- stackFrameDetails.CopyTo(newFrames, 1);
- stackFrameDetails = newFrames;
- }
- finally
- {
- debugInfoHandle.Release();
- }
- }
-
- targetFrame.StartLineNumber = scriptExtent.StartLineNumber;
- targetFrame.EndLineNumber = scriptExtent.EndLineNumber;
- targetFrame.StartColumnNumber = scriptExtent.StartColumnNumber;
- targetFrame.EndColumnNumber = scriptExtent.EndColumnNumber;
- }
- }
-
CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
index 04adf185e..e74bd1eb9 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs
@@ -20,14 +20,17 @@ internal class ScopesHandler : IScopesHandler
public Task Handle(ScopesArguments request, CancellationToken cancellationToken)
{
- VariableScope[] variableScopes =
- _debugService.GetVariableScopes(
- (int)request.FrameId);
+ //We have an artificial breakpoint label, so just copy the stacktrace from the first stack entry for this.
+ int frameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;
+
+ VariableScope[] variableScopes = _debugService.GetVariableScopes(frameId);
return Task.FromResult(new ScopesResponse
{
- Scopes = new Container(variableScopes
- .Select(LspDebugUtils.CreateScope))
+ Scopes = new Container(
+ variableScopes
+ .Select(LspDebugUtils.CreateScope)
+ )
});
}
}
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
index f53e094e7..560e18733 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
@@ -1,64 +1,132 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using System.Management.Automation;
using Microsoft.PowerShell.EditorServices.Services;
-using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
-using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
+using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
+using System.Linq;
+
+namespace Microsoft.PowerShell.EditorServices.Handlers;
-namespace Microsoft.PowerShell.EditorServices.Handlers
+internal class StackTraceHandler(DebugService debugService) : IStackTraceHandler
{
- internal class StackTraceHandler : IStackTraceHandler
+ ///
+ /// Because we don't know the size of the stacktrace beforehand, we will tell the client that there are more frames available, this is effectively a paging size, as the client should request this many frames after the first one.
+ ///
+ private const int INITIAL_PAGE_SIZE = 20;
+
+ public async Task Handle(StackTraceArguments request, CancellationToken cancellationToken)
{
- private readonly DebugService _debugService;
+ if (!debugService.IsDebuggerStopped)
+ {
+ throw new NotSupportedException("Stacktrace was requested while we are not stopped at a breakpoint.");
+ }
- public StackTraceHandler(DebugService debugService) => _debugService = debugService;
+ // Adapting to int to let us use LINQ, realistically if you have a stacktrace larger than this that the client is requesting, you have bigger problems...
+ int skip = Convert.ToInt32(request.StartFrame ?? 0);
+ int take = Convert.ToInt32(request.Levels ?? 0);
- public async Task Handle(StackTraceArguments request, CancellationToken cancellationToken)
- {
- StackFrameDetails[] stackFrameDetails = await _debugService.GetStackFramesAsync(cancellationToken).ConfigureAwait(false);
+ // We generate a label for the breakpoint and can return that immediately if the client is supporting DelayedStackTraceLoading.
+ InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo
+ ?? throw new NotSupportedException("InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug.");
- // Handle a rare race condition where the adapter requests stack frames before they've
- // begun building.
- if (stackFrameDetails is null)
- {
- return new StackTraceResponse
- {
- StackFrames = Array.Empty(),
- TotalFrames = 0
- };
- }
-
- List newStackFrames = new();
-
- long startFrameIndex = request.StartFrame ?? 0;
- long maxFrameCount = stackFrameDetails.Length;
-
- // If the number of requested levels == 0 (or null), that means get all stack frames
- // after the specified startFrame index. Otherwise get all the stack frames.
- long requestedFrameCount = request.Levels ?? 0;
- if (requestedFrameCount > 0)
- {
- maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount);
- }
+ StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo);
- for (long i = startFrameIndex; i < maxFrameCount; i++)
+ if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the susequent requests.
+ {
+ return new StackTraceResponse()
{
- // Create the new StackFrame object with an ID that can
- // be referenced back to the current list of stack frames
- newStackFrames.Add(LspDebugUtils.CreateStackFrame(stackFrameDetails[i], id: i));
- }
+ StackFrames = new StackFrame[] { breakpointLabel },
+ TotalFrames = INITIAL_PAGE_SIZE //Indicate to the client that there are more frames available
+ };
+ }
+
+ // Wait until the stack frames and variables have been fetched.
+ await debugService.StackFramesAndVariablesFetched.ConfigureAwait(false);
+
+ StackFrameDetails[] stackFrameDetails = await debugService.GetStackFramesAsync(cancellationToken)
+ .ConfigureAwait(false);
+ // Handle a rare race condition where the adapter requests stack frames before they've
+ // begun building.
+ if (stackFrameDetails is null)
+ {
return new StackTraceResponse
{
- StackFrames = newStackFrames,
- TotalFrames = newStackFrames.Count
+ StackFrames = Array.Empty(),
+ TotalFrames = 0
+ };
+ }
+
+ List newStackFrames = new();
+ if (skip == 0)
+ {
+ newStackFrames.Add(breakpointLabel);
+ }
+
+ newStackFrames.AddRange(
+ stackFrameDetails
+ .Skip(skip != 0 ? skip - 1 : skip)
+ .Take(take != 0 ? take - 1 : take)
+ .Select((frame, index) => CreateStackFrame(frame, index + 1))
+ );
+
+ return new StackTraceResponse
+ {
+ StackFrames = newStackFrames,
+ TotalFrames = newStackFrames.Count
+ };
+ }
+
+ public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id)
+ {
+ SourcePresentationHint sourcePresentationHint =
+ stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;
+
+ // When debugging an interactive session, the ScriptPath is which is not a valid source file.
+ // We need to make sure the user can't open the file associated with this stack frame.
+ // It will generate a VSCode error in this case.
+ Source? source = null;
+ if (!stackFrame.ScriptPath.Contains("<"))
+ {
+ source = new Source
+ {
+ Path = stackFrame.ScriptPath,
+ PresentationHint = sourcePresentationHint
};
}
+
+ return new StackFrame
+ {
+ Id = id,
+ Name = (source is not null) ? stackFrame.FunctionName : "Interactive Session",
+ Line = (source is not null) ? stackFrame.StartLineNumber : 0,
+ EndLine = stackFrame.EndLineNumber,
+ Column = (source is not null) ? stackFrame.StartColumnNumber : 0,
+ EndColumn = stackFrame.EndColumnNumber,
+ Source = source
+ };
}
+
+ public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new()
+ {
+ Name = "",
+ Id = id,
+ Source = new()
+ {
+ Path = invocationInfo.ScriptName
+ },
+ Line = invocationInfo.ScriptLineNumber,
+ Column = invocationInfo.OffsetInLine,
+ PresentationHint = StackFramePresentationHint.Label
+ };
+
}
+
diff --git a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs
index 115689586..36677743c 100644
--- a/src/PowerShellEditorServices/Utility/LspDebugUtils.cs
+++ b/src/PowerShellEditorServices/Utility/LspDebugUtils.cs
@@ -56,38 +56,6 @@ public static Breakpoint CreateBreakpoint(
};
}
- public static StackFrame CreateStackFrame(
- StackFrameDetails stackFrame,
- long id)
- {
- SourcePresentationHint sourcePresentationHint =
- stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;
-
- // When debugging an interactive session, the ScriptPath is which is not a valid source file.
- // We need to make sure the user can't open the file associated with this stack frame.
- // It will generate a VSCode error in this case.
- Source source = null;
- if (!stackFrame.ScriptPath.Contains("<"))
- {
- source = new Source
- {
- Path = stackFrame.ScriptPath,
- PresentationHint = sourcePresentationHint
- };
- }
-
- return new StackFrame
- {
- Id = id,
- Name = (source != null) ? stackFrame.FunctionName : "Interactive Session",
- Line = (source != null) ? stackFrame.StartLineNumber : 0,
- EndLine = stackFrame.EndLineNumber,
- Column = (source != null) ? stackFrame.StartColumnNumber : 0,
- EndColumn = stackFrame.EndColumnNumber,
- Source = source
- };
- }
-
public static Scope CreateScope(VariableScope scope)
{
return new Scope