Skip to content

Commit 871b468

Browse files
JustinGroteandyleejordanSeeminglyScience
committed
Remove local variables from stack frames
Thanks to PowerShell idiosyncracies around scopes, it is impossible for the debug service to get the "local variables" for each stack frame. The prior behavior used an assumption that from the 0 index, each increment to the stack frame corresponded 1-to-1 to an increment in scope, and that at each of those frames we could get local variables. This does not work because that 1-to-1 assumption does not hold, as evidenced by crashes exhibited in the preview extension because there are often more stack frames than there are scopes. Since this was but a guess, it was decided that it is worse to provide inaccurate information than a "best guess" and so each stack frame now only gets auto variables. In the process of fixing this, the variable detection mechanism was improved to rely on the raw results of `Get-PSCallStack` when available locally, and only pass it through a serialization and deserialization process when necessary for remote debugging. Finally, the `StackFrameDetails.Create` method had an ancient TODO from 2019 that we decided to drop the skeleton support for, namely passing the workspace root path and processing the invoation information. Co-authored-by: Andy Schwartzmeyer <andrew@schwartzmeyer.com> Co-authored-by: Patrick Meinecke <SeeminglyScience@users.noreply.github.com>
1 parent 8af84e7 commit 871b468

File tree

2 files changed

+82
-85
lines changed

2 files changed

+82
-85
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System;
@@ -17,6 +17,7 @@
1717
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
1818
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
1919
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
20+
using System.Collections;
2021

2122
namespace Microsoft.PowerShell.EditorServices.Services
2223
{
@@ -45,6 +46,7 @@ internal class DebugService
4546
private List<VariableDetailsBase> variables;
4647
private VariableContainerDetails globalScopeVariables;
4748
private VariableContainerDetails scriptScopeVariables;
49+
private VariableContainerDetails localScopeVariables;
4850
private StackFrameDetails[] stackFrameDetails;
4951
private readonly PropertyInfo invocationTypeScriptPositionProperty;
5052

@@ -445,11 +447,6 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
445447
for (int i = 0; i < stackFrames.Length; i++)
446448
{
447449
var stackFrame = stackFrames[i];
448-
if (stackFrame.LocalVariables.ContainsVariable(variable.Id))
449-
{
450-
scope = i.ToString();
451-
break;
452-
}
453450
}
454451
}
455452

@@ -626,13 +623,12 @@ internal async Task<StackFrameDetails[]> GetStackFramesAsync(CancellationToken c
626623
public VariableScope[] GetVariableScopes(int stackFrameId)
627624
{
628625
var stackFrames = this.GetStackFrames();
629-
int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id;
630626
int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id;
631627

632628
return new VariableScope[]
633629
{
634630
new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName),
635-
new VariableScope(localStackFrameVariableId, VariableContainerDetails.LocalScopeName),
631+
new VariableScope(this.localScopeVariables.Id, VariableContainerDetails.LocalScopeName),
636632
new VariableScope(this.scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName),
637633
new VariableScope(this.globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName),
638634
};
@@ -655,30 +651,27 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride)
655651
new VariableDetails("Dummy", null)
656652
};
657653

658-
// Must retrieve global/script variales before stack frame variables
659-
// as we check stack frame variables against globals.
660-
await FetchGlobalAndScriptVariablesAsync().ConfigureAwait(false);
654+
655+
// Must retrieve in order of broadest to narrowest scope for efficient deduplication: global, script, local
656+
this.globalScopeVariables =
657+
await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false);
658+
659+
this.scriptScopeVariables =
660+
await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false);
661+
662+
this.localScopeVariables =
663+
await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false);
664+
661665
await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false);
666+
662667
}
663668
finally
664669
{
665670
this.debugInfoHandle.Release();
666671
}
667672
}
668673

669-
private async Task FetchGlobalAndScriptVariablesAsync()
670-
{
671-
// Retrieve globals first as script variable retrieval needs to search globals.
672-
this.globalScopeVariables =
673-
await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null).ConfigureAwait(false);
674-
675-
this.scriptScopeVariables =
676-
await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false);
677-
}
678-
679-
private async Task<VariableContainerDetails> FetchVariableContainerAsync(
680-
string scope,
681-
VariableContainerDetails autoVariables)
674+
private async Task<VariableContainerDetails> FetchVariableContainerAsync(string scope)
682675
{
683676
PSCommand psCommand = new PSCommand()
684677
.AddCommand("Get-Variable")
@@ -704,11 +697,6 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(
704697
var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ };
705698
this.variables.Add(variableDetails);
706699
scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails);
707-
708-
if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope))
709-
{
710-
autoVariables.Children.Add(variableDetails.Name, variableDetails);
711-
}
712700
}
713701
}
714702

@@ -792,55 +780,90 @@ private bool AddToAutoVariables(PSObject psvariable, string scope)
792780
private async Task FetchStackFramesAsync(string scriptNameOverride)
793781
{
794782
PSCommand psCommand = new PSCommand();
783+
// The serialization depth to retrieve variables from remote runspaces.
784+
const int serializationDepth = 3;
795785

796786
// This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame
797787
// objects (or "deserialized" CallStackFrames) when attached to a runspace in another
798788
// process. Without the intermediate variable Get-PSCallStack inexplicably returns
799789
// an array of strings containing the formatted output of the CallStackFrame list.
800-
var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
801-
psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}");
790+
string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
791+
792+
string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}";
793+
794+
// If we're attached to a remote runspace, we need to serialize the callstack prior to transport
795+
// because the default depth is too shallow
796+
bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
797+
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
798+
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})"
799+
: callStackVarName;
802800

803-
var results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
801+
// We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information
802+
psCommand.AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}");
804803

805-
var callStackFrames = results.ToArray();
806804

807-
this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length];
805+
// PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface
806+
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
808807

809-
for (int i = 0; i < callStackFrames.Length; i++)
808+
IEnumerable callStack = isOnRemoteMachine
809+
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList
810+
: results;
811+
812+
List<StackFrameDetails> stackFrameDetailList = new List<StackFrameDetails>();
813+
foreach (var callStackFrameItem in callStack)
810814
{
811-
VariableContainerDetails autoVariables =
812-
new VariableContainerDetails(
813-
this.nextVariableId++,
814-
VariableContainerDetails.AutoVariablesName);
815+
var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList;
816+
var callStackFrame = callStackFrameComponents[0] as PSObject;
817+
IDictionary callStackVariables = isOnRemoteMachine
818+
? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary
819+
: callStackFrameComponents[1] as IDictionary;
815820

816-
this.variables.Add(autoVariables);
821+
var autoVariables = new VariableContainerDetails(
822+
nextVariableId++,
823+
VariableContainerDetails.AutoVariablesName);
817824

818-
VariableContainerDetails localVariables =
819-
await FetchVariableContainerAsync(i.ToString(), autoVariables).ConfigureAwait(false);
825+
variables.Add(autoVariables);
820826

821-
// When debugging, this is the best way I can find to get what is likely the workspace root.
822-
// This is controlled by the "cwd:" setting in the launch config.
823-
string workspaceRootPath = _psesHost.InitialWorkingDirectory;
827+
foreach (DictionaryEntry entry in callStackVariables)
828+
{
829+
// TODO: This should be deduplicated into a new function for the other variable handling as well
830+
object psVarValue = isOnRemoteMachine
831+
? (entry.Value as PSObject).Properties["Value"].Value
832+
: (entry.Value as PSVariable).Value;
833+
// The constructor we are using here does not automatically add the dollar prefix
834+
string psVarName = VariableDetails.DollarPrefix + entry.Key.ToString();
835+
var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ };
836+
variables.Add(variableDetails);
837+
838+
if (AddToAutoVariables(new PSObject(entry.Value), scope: null))
839+
{
840+
autoVariables.Children.Add(variableDetails.Name, variableDetails);
841+
}
842+
}
824843

825-
this.stackFrameDetails[i] =
826-
StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath);
844+
var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables);
827845

828-
string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath;
829-
if (scriptNameOverride != null &&
846+
string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath;
847+
if (scriptNameOverride is not null &&
830848
string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
831849
{
832-
this.stackFrameDetails[i].ScriptPath = scriptNameOverride;
850+
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
833851
}
834-
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
835-
&& this.remoteFileManager != null
852+
else if (isOnRemoteMachine
853+
&& remoteFileManager is not null
836854
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
837855
{
838-
this.stackFrameDetails[i].ScriptPath =
839-
this.remoteFileManager.GetMappedPath(
856+
stackFrameDetailsEntry.ScriptPath =
857+
remoteFileManager.GetMappedPath(
840858
stackFrameScriptPath,
841859
_psesHost.CurrentRunspace);
842860
}
861+
862+
stackFrameDetailList.Add(
863+
stackFrameDetailsEntry);
843864
}
865+
866+
stackFrameDetails = stackFrameDetailList.ToArray();
844867
}
845868

846869
private static string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength)

src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ internal class StackFrameDetails
6464
/// </summary>
6565
public VariableContainerDetails AutoVariables { get; private set; }
6666

67-
/// <summary>
68-
/// Gets or sets the VariableContainerDetails that contains the local variables.
69-
/// </summary>
70-
public VariableContainerDetails LocalVariables { get; private set; }
71-
7267
#endregion
7368

7469
#region Constructors
@@ -83,47 +78,26 @@ internal class StackFrameDetails
8378
/// <param name="autoVariables">
8479
/// A variable container with all the filtered, auto variables for this stack frame.
8580
/// </param>
86-
/// <param name="localVariables">
87-
/// A variable container with all the local variables for this stack frame.
88-
/// </param>
89-
/// <param name="workspaceRootPath">
90-
/// Specifies the path to the root of an open workspace, if one is open. This path is used to
91-
/// determine whether individua stack frames are external to the workspace.
92-
/// </param>
9381
/// <returns>A new instance of the StackFrameDetails class.</returns>
9482
static internal StackFrameDetails Create(
9583
PSObject callStackFrameObject,
96-
VariableContainerDetails autoVariables,
97-
VariableContainerDetails localVariables,
98-
string workspaceRootPath = null)
84+
VariableContainerDetails autoVariables)
9985
{
100-
string moduleId = string.Empty;
101-
var isExternal = false;
102-
103-
var invocationInfo = callStackFrameObject.Properties["InvocationInfo"]?.Value as InvocationInfo;
10486
string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath;
10587
int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0);
10688

107-
// TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add
108-
// settings to control this feature.
109-
//if (workspaceRootPath != null &&
110-
// invocationInfo != null &&
111-
// !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase))
112-
//{
113-
// isExternal = true;
114-
//}
115-
11689
return new StackFrameDetails
11790
{
11891
ScriptPath = scriptPath,
11992
FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string,
12093
StartLineNumber = startLineNumber,
12194
EndLineNumber = startLineNumber, // End line number isn't given in PowerShell stack frames
122-
StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames
95+
StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames
12396
EndColumnNumber = 0,
12497
AutoVariables = autoVariables,
125-
LocalVariables = localVariables,
126-
IsExternalCode = isExternal
98+
// TODO: Re-enable `isExternal` detection along with a setting. Will require
99+
// `workspaceRootPath`, see Git blame.
100+
IsExternalCode = false
127101
};
128102
}
129103

0 commit comments

Comments
 (0)