From 3a8937ffa807de787a3a4f1fedbc02f0f5dc2aa7 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 18 Nov 2021 15:31:35 -0800 Subject: [PATCH 01/12] 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 Co-authored-by: Patrick Meinecke --- .../Services/DebugAdapter/DebugService.cs | 129 +++++++++++------- .../Debugging/StackFrameDetails.cs | 36 +---- 2 files changed, 81 insertions(+), 84 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 56b6ecb11..35a1c012b 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -17,6 +17,7 @@ using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; +using System.Collections; namespace Microsoft.PowerShell.EditorServices.Services { @@ -45,6 +46,7 @@ internal class DebugService private List variables; private VariableContainerDetails globalScopeVariables; private VariableContainerDetails scriptScopeVariables; + private VariableContainerDetails localScopeVariables; private StackFrameDetails[] stackFrameDetails; private readonly PropertyInfo invocationTypeScriptPositionProperty; @@ -445,11 +447,6 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str for (int i = 0; i < stackFrames.Length; i++) { var stackFrame = stackFrames[i]; - if (stackFrame.LocalVariables.ContainsVariable(variable.Id)) - { - scope = i.ToString(); - break; - } } } @@ -626,13 +623,12 @@ internal async Task GetStackFramesAsync(CancellationToken c public VariableScope[] GetVariableScopes(int stackFrameId) { var stackFrames = this.GetStackFrames(); - int localStackFrameVariableId = stackFrames[stackFrameId].LocalVariables.Id; int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; return new VariableScope[] { new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName), - new VariableScope(localStackFrameVariableId, VariableContainerDetails.LocalScopeName), + new VariableScope(this.localScopeVariables.Id, VariableContainerDetails.LocalScopeName), new VariableScope(this.scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), new VariableScope(this.globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), }; @@ -655,10 +651,19 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) new VariableDetails("Dummy", null) }; - // Must retrieve global/script variales before stack frame variables - // as we check stack frame variables against globals. - await FetchGlobalAndScriptVariablesAsync().ConfigureAwait(false); + + // Must retrieve in order of broadest to narrowest scope for efficient deduplication: global, script, local + this.globalScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false); + + this.scriptScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false); + + this.localScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false); + await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); + } finally { @@ -666,19 +671,7 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) } } - private async Task FetchGlobalAndScriptVariablesAsync() - { - // Retrieve globals first as script variable retrieval needs to search globals. - this.globalScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null).ConfigureAwait(false); - - this.scriptScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); - } - - private async Task FetchVariableContainerAsync( - string scope, - VariableContainerDetails autoVariables) + private async Task FetchVariableContainerAsync(string scope) { PSCommand psCommand = new PSCommand() .AddCommand("Get-Variable") @@ -704,11 +697,6 @@ private async Task FetchVariableContainerAsync( var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; this.variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); - - if ((autoVariables != null) && AddToAutoVariables(psVariableObject, scope)) - { - autoVariables.Children.Add(variableDetails.Name, variableDetails); - } } } @@ -792,55 +780,90 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) private async Task FetchStackFramesAsync(string scriptNameOverride) { PSCommand psCommand = new PSCommand(); + // The serialization depth to retrieve variables from remote runspaces. + const int serializationDepth = 3; // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame // objects (or "deserialized" CallStackFrames) when attached to a runspace in another // process. Without the intermediate variable Get-PSCallStack inexplicably returns // an array of strings containing the formatted output of the CallStackFrame list. - var callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; - psCommand.AddScript($"{callStackVarName} = Get-PSCallStack; {callStackVarName}"); + string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; + + string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; + + // If we're attached to a remote runspace, we need to serialize the callstack prior to transport + // because the default depth is too shallow + bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; + string returnSerializedIfOnRemoteMachine = isOnRemoteMachine + ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" + : callStackVarName; - var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); + // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information + psCommand.AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}"); - var callStackFrames = results.ToArray(); - this.stackFrameDetails = new StackFrameDetails[callStackFrames.Length]; + // PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface + IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); - for (int i = 0; i < callStackFrames.Length; i++) + IEnumerable callStack = isOnRemoteMachine + ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList + : results; + + List stackFrameDetailList = new List(); + foreach (var callStackFrameItem in callStack) { - VariableContainerDetails autoVariables = - new VariableContainerDetails( - this.nextVariableId++, - VariableContainerDetails.AutoVariablesName); + var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; + var callStackFrame = callStackFrameComponents[0] as PSObject; + IDictionary callStackVariables = isOnRemoteMachine + ? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary + : callStackFrameComponents[1] as IDictionary; - this.variables.Add(autoVariables); + var autoVariables = new VariableContainerDetails( + nextVariableId++, + VariableContainerDetails.AutoVariablesName); - VariableContainerDetails localVariables = - await FetchVariableContainerAsync(i.ToString(), autoVariables).ConfigureAwait(false); + variables.Add(autoVariables); - // When debugging, this is the best way I can find to get what is likely the workspace root. - // This is controlled by the "cwd:" setting in the launch config. - string workspaceRootPath = _psesHost.InitialWorkingDirectory; + foreach (DictionaryEntry entry in callStackVariables) + { + // TODO: This should be deduplicated into a new function for the other variable handling as well + object psVarValue = isOnRemoteMachine + ? (entry.Value as PSObject).Properties["Value"].Value + : (entry.Value as PSVariable).Value; + // The constructor we are using here does not automatically add the dollar prefix + string psVarName = VariableDetails.DollarPrefix + entry.Key.ToString(); + var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ }; + variables.Add(variableDetails); + + if (AddToAutoVariables(new PSObject(entry.Value), scope: null)) + { + autoVariables.Children.Add(variableDetails.Name, variableDetails); + } + } - this.stackFrameDetails[i] = - StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables); - string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; - if (scriptNameOverride != null && + string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; + if (scriptNameOverride is not null && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { - this.stackFrameDetails[i].ScriptPath = scriptNameOverride; + stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } - else if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && this.remoteFileManager != null + else if (isOnRemoteMachine + && remoteFileManager is not null && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { - this.stackFrameDetails[i].ScriptPath = - this.remoteFileManager.GetMappedPath( + stackFrameDetailsEntry.ScriptPath = + remoteFileManager.GetMappedPath( stackFrameScriptPath, _psesHost.CurrentRunspace); } + + stackFrameDetailList.Add( + stackFrameDetailsEntry); } + + stackFrameDetails = stackFrameDetailList.ToArray(); } private static string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs index 6d03c6494..2b0037eb8 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs @@ -64,11 +64,6 @@ internal class StackFrameDetails /// public VariableContainerDetails AutoVariables { get; private set; } - /// - /// Gets or sets the VariableContainerDetails that contains the local variables. - /// - public VariableContainerDetails LocalVariables { get; private set; } - #endregion #region Constructors @@ -83,47 +78,26 @@ internal class StackFrameDetails /// /// A variable container with all the filtered, auto variables for this stack frame. /// - /// - /// A variable container with all the local variables for this stack frame. - /// - /// - /// Specifies the path to the root of an open workspace, if one is open. This path is used to - /// determine whether individua stack frames are external to the workspace. - /// /// A new instance of the StackFrameDetails class. static internal StackFrameDetails Create( PSObject callStackFrameObject, - VariableContainerDetails autoVariables, - VariableContainerDetails localVariables, - string workspaceRootPath = null) + VariableContainerDetails autoVariables) { - string moduleId = string.Empty; - var isExternal = false; - - var invocationInfo = callStackFrameObject.Properties["InvocationInfo"]?.Value as InvocationInfo; string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); - // TODO: RKH 2019-03-07 Temporarily disable "external" code until I have a chance to add - // settings to control this feature. - //if (workspaceRootPath != null && - // invocationInfo != null && - // !scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase)) - //{ - // isExternal = true; - //} - return new StackFrameDetails { ScriptPath = scriptPath, FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string, StartLineNumber = startLineNumber, EndLineNumber = startLineNumber, // End line number isn't given in PowerShell stack frames - StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames + StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames EndColumnNumber = 0, AutoVariables = autoVariables, - LocalVariables = localVariables, - IsExternalCode = isExternal + // TODO: Re-enable `isExternal` detection along with a setting. Will require + // `workspaceRootPath`, see Git blame. + IsExternalCode = false }; } From 6e955be4e7c7a541d029252e2ba8bd07c27b5cd7 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 18 Nov 2021 15:41:21 -0800 Subject: [PATCH 02/12] Handle `PSArgumentOutOfRangeException` when fetching variables --- .../Services/DebugAdapter/DebugService.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 35a1c012b..da56ca052 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -680,8 +680,20 @@ private async Task FetchVariableContainerAsync(string var scopeVariableContainer = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); this.variables.Add(scopeVariableContainer); - IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None) - .ConfigureAwait(false); + IReadOnlyList results; + try + { + results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None) + .ConfigureAwait(false); + } + catch (CmdletInvocationException ex) + { + if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException")) + { + throw; + } + results = null; + } if (results != null) { From 581a67cbf801aca533501715b05cce03b31c5f9c Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 18 Nov 2021 15:42:28 -0800 Subject: [PATCH 03/12] Include `$PSItem` in auto variables in addition to `$_` While it would be nice to only present `$PSItem` to encourage people to use it, that may confuse people who still use `$_` and don't know that they are the same. Co-authored-by: Patrick Meinecke --- .../Services/DebugAdapter/DebugService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index da56ca052..d1366622a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -756,7 +756,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) // Some local variables, if they exist, should be displayed by default if (psvariable.TypeNames[0].EndsWith("LocalVariable")) { - if (variableName.Equals("_")) + if (variableName.Equals("PSItem") || variableName.Equals("_")) { return true; } From d40bf7e39e48805e133bb129266ac58e9f2d13fb Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 18 Nov 2021 15:49:37 -0800 Subject: [PATCH 04/12] Improve properties selection for variables Co-authored-by: Patrick Meinecke Co-authored-by: Andy Schwartzmeyer --- .../Services/DebugAdapter/Debugging/VariableDetails.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs index e34604927..970a20745 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs @@ -290,7 +290,10 @@ private VariableDetails[] GetChildren(object obj, ILogger logger) childVariables.AddRange( psObject .Properties - .Where(p => p.MemberType == PSMemberTypes.NoteProperty) + // Here we check the object's MemberType against the `Properties` + // bit-mask to determine if this is a property. Hence the selection + // will only include properties. + .Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0) .Select(p => new VariableDetails(p))); obj = psObject.BaseObject; From 931ad228366fbbd80f491c07f4af427bf5ade771 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 15:01:14 -0800 Subject: [PATCH 05/12] Change language version to `latest` e.g. 10 with .NET SDK 6 Mostly so we can use const interpolated strings. --- PowerShellEditorServices.Common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props index 4f85f1157..a93f1e8b2 100644 --- a/PowerShellEditorServices.Common.props +++ b/PowerShellEditorServices.Common.props @@ -4,7 +4,7 @@ Microsoft © Microsoft Corporation. - 9.0 + latest PowerShell;editor;development;language;debugging https://raw.githubusercontent.com/PowerShell/PowerShellEditorServices/master/LICENSE true From c940fa6ade820ef846609831957d4b7225812eee Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 15:00:41 -0800 Subject: [PATCH 06/12] Readability cleanups in `DebugService.cs` --- .../Services/DebugAdapter/DebugService.cs | 264 ++++++++---------- 1 file changed, 119 insertions(+), 145 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index d1366622a..02aec129d 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -2,22 +2,22 @@ // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Reflection; -using System.Threading.Tasks; -using Microsoft.PowerShell.EditorServices.Utility; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging; -using System.Collections; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services { @@ -111,7 +111,7 @@ public DebugService( { Validate.IsNotNull(nameof(executionService), executionService); - this._logger = factory.CreateLogger(); + _logger = factory.CreateLogger(); _executionService = executionService; _breakpointService = breakpointService; _psesHost = psesHost; @@ -122,7 +122,7 @@ public DebugService( this.remoteFileManager = remoteFileManager; - this.invocationTypeScriptPositionProperty = + invocationTypeScriptPositionProperty = typeof(InvocationInfo) .GetProperty( "ScriptPosition", @@ -149,30 +149,23 @@ public async Task SetLineBreakpointsAsync( string scriptPath = scriptFile.FilePath; // Make sure we're using the remote script path - if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && this.remoteFileManager != null) + if (_psesHost.CurrentRunspace.IsOnRemoteMachine && remoteFileManager is not null) { - if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath)) + if (!remoteFileManager.IsUnderRemoteTempPath(scriptPath)) { - this._logger.LogTrace( - $"Could not set breakpoints for local path '{scriptPath}' in a remote session."); + _logger.LogTrace($"Could not set breakpoints for local path '{scriptPath}' in a remote session."); return Array.Empty(); } - string mappedPath = - this.remoteFileManager.GetMappedPath( - scriptPath, - _psesHost.CurrentRunspace); + string mappedPath = remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace); scriptPath = mappedPath; } - else if ( - this.temporaryScriptListingPath != null && - this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase)) + else if (temporaryScriptListingPath is not null + && temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase)) { - this._logger.LogTrace( - $"Could not set breakpoint on temporary script listing path '{scriptPath}'."); + _logger.LogTrace($"Could not set breakpoint on temporary script listing path '{scriptPath}'."); return Array.Empty(); } @@ -181,7 +174,7 @@ public async Task SetLineBreakpointsAsync( // quoted and have those wildcard chars escaped. string escapedScriptPath = PathUtils.WildcardEscapePath(scriptPath); - if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) + if (dscBreakpoints is null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath)) { if (clearExisting) { @@ -259,7 +252,7 @@ public void StepOut() /// /// Causes the debugger to break execution wherever it currently - /// is at the time. This is equivalent to clicking "Pause" in a + /// is at the time. This is equivalent to clicking "Pause" in a /// debugger UI. /// public void Break() @@ -285,26 +278,26 @@ public void Abort() public VariableDetailsBase[] GetVariables(int variableReferenceId) { VariableDetailsBase[] childVariables; - this.debugInfoHandle.Wait(); + debugInfoHandle.Wait(); try { - if ((variableReferenceId < 0) || (variableReferenceId >= this.variables.Count)) + if ((variableReferenceId < 0) || (variableReferenceId >= variables.Count)) { _logger.LogWarning($"Received request for variableReferenceId {variableReferenceId} that is out of range of valid indices."); return Array.Empty(); } - VariableDetailsBase parentVariable = this.variables[variableReferenceId]; + VariableDetailsBase parentVariable = variables[variableReferenceId]; if (parentVariable.IsExpandable) { - childVariables = parentVariable.GetChildren(this._logger); + childVariables = parentVariable.GetChildren(_logger); foreach (var child in childVariables) { // Only add child if it hasn't already been added. if (child.Id < 0) { - child.Id = this.nextVariableId++; - this.variables.Add(child); + child.Id = nextVariableId++; + variables.Add(child); } } } @@ -317,13 +310,13 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId) } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } /// /// Evaluates a variable expression in the context of the stopped - /// debugger. This method decomposes the variable expression to + /// debugger. This method decomposes the variable expression to /// walk the cached variable data for the specified stack frame. /// /// The variable expression string to evaluate. @@ -332,8 +325,8 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId) public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) { // NOTE: From a watch we will get passed expressions that are not naked variables references. - // Probably the right way to do this woudld be to examine the AST of the expr before calling - // this method to make sure it is a VariableReference. But for the most part, non-naked variable + // Probably the right way to do this would be to examine the AST of the expr before calling + // this method to make sure it is a VariableReference. But for the most part, non-naked variable // references are very unlikely to find a matching variable e.g. "$i+5.2" will find no var matching "$i+5". // Break up the variable path @@ -343,21 +336,21 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, IEnumerable variableList; // Ensure debug info isn't currently being built. - this.debugInfoHandle.Wait(); + debugInfoHandle.Wait(); try { - variableList = this.variables; + variableList = variables; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } foreach (var variableName in variablePathParts) { - if (variableList == null) + if (variableList is null) { - // If there are no children left to search, break out early + // If there are no children left to search, break out early. return null; } @@ -369,11 +362,10 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, variableName, StringComparison.CurrentCultureIgnoreCase)); - if (resolvedVariable != null && - resolvedVariable.IsExpandable) + if (resolvedVariable is not null && resolvedVariable.IsExpandable) { - // Continue by searching in this variable's children - variableList = this.GetVariables(resolvedVariable.Id); + // Continue by searching in this variable's children. + variableList = GetVariables(resolvedVariable.Id); } } @@ -382,7 +374,7 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression, /// /// Sets the specified variable by container variableReferenceId and variable name to the - /// specified new value. If the variable cannot be set or converted to that value this + /// specified new value. If the variable cannot be set or converted to that value this /// method will throw InvalidPowerShellExpressionException, ArgumentTransformationMetadataException, or /// SessionStateUnauthorizedAccessException. /// @@ -396,7 +388,7 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str Validate.IsNotNull(nameof(name), name); Validate.IsNotNull(nameof(value), value); - this._logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); + _logger.LogTrace($"SetVariableRequest for '{name}' to value string (pre-quote processing): '{value}'"); // An empty or whitespace only value is not a valid expression for SetVariable. if (value.Trim().Length == 0) @@ -407,7 +399,9 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str // Evaluate the expression to get back a PowerShell object from the expression string. // This may throw, in which case the exception is propagated to the caller PSCommand evaluateExpressionCommand = new PSCommand().AddScript(value); - object expressionResult = (await _executionService.ExecutePSCommandAsync(evaluateExpressionCommand, CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); + object expressionResult = + (await _executionService.ExecutePSCommandAsync(evaluateExpressionCommand, CancellationToken.None) + .ConfigureAwait(false)).FirstOrDefault(); // If PowerShellContext.ExecuteCommand returns an ErrorRecord as output, the expression failed evaluation. // Ideally we would have a separate means from communicating error records apart from normal output. @@ -419,14 +413,14 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str // OK, now we have a PS object from the supplied value string (expression) to assign to a variable. // Get the variable referenced by variableContainerReferenceId and variable name. VariableContainerDetails variableContainer = null; - await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); + await debugInfoHandle.WaitAsync().ConfigureAwait(false); try { - variableContainer = (VariableContainerDetails)this.variables[variableContainerReferenceId]; + variableContainer = (VariableContainerDetails)variables[variableContainerReferenceId]; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } VariableDetailsBase variable = variableContainer.Children[name]; @@ -463,7 +457,7 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str .AddParameter("Scope", scope); PSVariable psVariable = (await _executionService.ExecutePSCommandAsync(getVariableCommand, CancellationToken.None).ConfigureAwait(false)).FirstOrDefault(); - if (psVariable == null) + if (psVariable is null) { throw new Exception($"Failed to retrieve PSVariable object for '{name}' from scope '{scope}'."); } @@ -484,7 +478,7 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str } } - if (argTypeConverterAttr != null) + if (argTypeConverterAttr is not null) { _logger.LogTrace($"Setting variable '{name}' using conversion to value: {expressionResult ?? ""}"); @@ -543,7 +537,7 @@ public async Task EvaluateExpressionAsync( // we can assume that Out-String will be getting used to format results // of command executions into string output. However, if null is returned // then return null so that no output gets displayed. - if (writeResultAsOutput || results == null || results.Count == 0) + if (writeResultAsOutput || results is null || results.Count == 0) { return null; } @@ -564,53 +558,53 @@ public async Task EvaluateExpressionAsync( /// public StackFrameDetails[] GetStackFrames() { - this.debugInfoHandle.Wait(); + debugInfoHandle.Wait(); try { - return this.stackFrameDetails; + return stackFrameDetails; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } internal StackFrameDetails[] GetStackFrames(CancellationToken cancellationToken) { - this.debugInfoHandle.Wait(cancellationToken); + debugInfoHandle.Wait(cancellationToken); try { - return this.stackFrameDetails; + return stackFrameDetails; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } internal async Task GetStackFramesAsync() { - await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); + await debugInfoHandle.WaitAsync().ConfigureAwait(false); try { - return this.stackFrameDetails; + return stackFrameDetails; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } internal async Task GetStackFramesAsync(CancellationToken cancellationToken) { - await this.debugInfoHandle.WaitAsync(cancellationToken).ConfigureAwait(false); + await debugInfoHandle.WaitAsync(cancellationToken).ConfigureAwait(false); try { - return this.stackFrameDetails; + return stackFrameDetails; } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } @@ -622,15 +616,15 @@ internal async Task GetStackFramesAsync(CancellationToken c /// The list of VariableScope instances which describe the available variable scopes. public VariableScope[] GetVariableScopes(int stackFrameId) { - var stackFrames = this.GetStackFrames(); + var stackFrames = GetStackFrames(); int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; return new VariableScope[] { new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName), - new VariableScope(this.localScopeVariables.Id, VariableContainerDetails.LocalScopeName), - new VariableScope(this.scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), - new VariableScope(this.globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), + new VariableScope(localScopeVariables.Id, VariableContainerDetails.LocalScopeName), + new VariableScope(scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), + new VariableScope(globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), }; } @@ -640,34 +634,30 @@ public VariableScope[] GetVariableScopes(int stackFrameId) private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) { - await this.debugInfoHandle.WaitAsync().ConfigureAwait(false); + await debugInfoHandle.WaitAsync().ConfigureAwait(false); try { - this.nextVariableId = VariableDetailsBase.FirstVariableId; - this.variables = new List + nextVariableId = VariableDetailsBase.FirstVariableId; + variables = new List { // Create a dummy variable for index 0, should never see this. new VariableDetails("Dummy", null) }; + // Must retrieve in order of broadest to narrowest scope for efficient + // deduplication: global, script, local. + globalScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false); - // Must retrieve in order of broadest to narrowest scope for efficient deduplication: global, script, local - this.globalScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false); + scriptScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false); - this.scriptScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false); - - this.localScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false); + localScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false); await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); - } finally { - this.debugInfoHandle.Release(); + debugInfoHandle.Release(); } } @@ -677,14 +667,13 @@ private async Task FetchVariableContainerAsync(string .AddCommand("Get-Variable") .AddParameter("Scope", scope); - var scopeVariableContainer = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); - this.variables.Add(scopeVariableContainer); + var scopeVariableContainer = new VariableContainerDetails(nextVariableId++, "Scope: " + scope); + variables.Add(scopeVariableContainer); IReadOnlyList results; try { - results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None) - .ConfigureAwait(false); + results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); } catch (CmdletInvocationException ex) { @@ -695,19 +684,19 @@ private async Task FetchVariableContainerAsync(string results = null; } - if (results != null) + if (results is not null) { foreach (PSObject psVariableObject in results) { // Under some circumstances, we seem to get variables back with no "Name" field - // We skip over those here + // We skip over those here. if (psVariableObject.Properties["Name"] is null) { continue; } - var variableDetails = new VariableDetails(psVariableObject) { Id = this.nextVariableId++ }; - this.variables.Add(variableDetails); + var variableDetails = new VariableDetails(psVariableObject) { Id = nextVariableId++ }; + variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); } } @@ -717,8 +706,8 @@ private async Task FetchVariableContainerAsync(string private bool AddToAutoVariables(PSObject psvariable, string scope) { - if ((scope == VariableContainerDetails.GlobalScopeName) || - (scope == VariableContainerDetails.ScriptScopeName)) + if ((scope == VariableContainerDetails.GlobalScopeName) + || (scope == VariableContainerDetails.ScriptScopeName)) { // We don't A) have a good way of distinguishing built-in from user created variables // and B) globalScopeVariables.Children.ContainsKey() doesn't work for built-in variables @@ -730,8 +719,8 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) object variableValue = psvariable.Properties["Value"].Value; // Don't put any variables created by PSES in the Auto variable container. - if (variableName.StartsWith(PsesGlobalVariableNamePrefix) || - variableName.Equals("PSDebugContext")) + if (variableName.StartsWith(PsesGlobalVariableNamePrefix) + || variableName.Equals("PSDebugContext")) { return false; } @@ -744,8 +733,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) optionsProperty.Value as string, out variableScope)) { - this._logger.LogWarning( - $"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); + _logger.LogWarning($"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); } } else if (optionsProperty.Value is ScopedItemOptions) @@ -762,8 +750,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) } else if (variableName.Equals("args", StringComparison.OrdinalIgnoreCase)) { - return variableValue is Array array - && array.Length > 0; + return variableValue is Array array && array.Length > 0; } return false; @@ -776,11 +763,11 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) var constantAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.Constant; var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; - if (((variableScope & constantAllScope) == constantAllScope) || - ((variableScope & readonlyAllScope) == readonlyAllScope)) + if (((variableScope & constantAllScope) == constantAllScope) + || ((variableScope & readonlyAllScope) == readonlyAllScope)) { string prefixedVariableName = VariableDetails.DollarPrefix + variableName; - if (this.globalScopeVariables.Children.ContainsKey(prefixedVariableName)) + if (globalScopeVariables.Children.ContainsKey(prefixedVariableName)) { return false; } @@ -821,7 +808,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList : results; - List stackFrameDetailList = new List(); + var stackFrameDetailList = new List(); foreach (var callStackFrameItem in callStack) { var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; @@ -856,8 +843,8 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; - if (scriptNameOverride is not null && - string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) + if (scriptNameOverride is not null + && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } @@ -866,13 +853,10 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { stackFrameDetailsEntry.ScriptPath = - remoteFileManager.GetMappedPath( - stackFrameScriptPath, - _psesHost.CurrentRunspace); + remoteFileManager.GetMappedPath(stackFrameScriptPath, _psesHost.CurrentRunspace); } - stackFrameDetailList.Add( - stackFrameDetailsEntry); + stackFrameDetailList.Add(stackFrameDetailsEntry); } stackFrameDetails = stackFrameDetailList.ToArray(); @@ -912,17 +896,16 @@ internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e) string localScriptPath = e.InvocationInfo.ScriptName; // If there's no ScriptName, get the "list" of the current source - if (this.remoteFileManager != null && string.IsNullOrEmpty(localScriptPath)) + if (remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) { // Get the current script listing and create the buffer - PSCommand command = new PSCommand(); - command.AddScript($"list 1 {int.MaxValue}"); + PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); IReadOnlyList scriptListingLines = await _executionService.ExecutePSCommandAsync( command, CancellationToken.None).ConfigureAwait(false); - if (scriptListingLines != null) + if (scriptListingLines is not null) { int linePrefixLength = 0; @@ -930,72 +913,68 @@ await _executionService.ExecutePSCommandAsync( string.Join( Environment.NewLine, scriptListingLines - .Select(o => DebugService.TrimScriptListingLine(o, ref linePrefixLength)) - .Where(s => s != null)); + .Select(o => TrimScriptListingLine(o, ref linePrefixLength)) + .Where(s => s is not null)); - this.temporaryScriptListingPath = - this.remoteFileManager.CreateTemporaryFile( + temporaryScriptListingPath = + remoteFileManager.CreateTemporaryFile( $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", scriptListing, _psesHost.CurrentRunspace); localScriptPath = - this.temporaryScriptListingPath + temporaryScriptListingPath ?? StackFrameDetails.NoFileScriptPath; - noScriptName = localScriptPath != null; + noScriptName = localScriptPath is not null; } else { - this._logger.LogWarning($"Could not load script context"); + _logger.LogWarning("Could not load script context"); } } // Get call stack and variables. - await this.FetchStackFramesAndVariablesAsync( - noScriptName ? localScriptPath : null).ConfigureAwait(false); + await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false); // If this is a remote connection and the debugger stopped at a line // in a script file, get the file contents if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && this.remoteFileManager != null + && remoteFileManager is not null && !noScriptName) { localScriptPath = - await this.remoteFileManager.FetchRemoteFileAsync( + await remoteFileManager.FetchRemoteFileAsync( e.InvocationInfo.ScriptName, _psesHost.CurrentRunspace).ConfigureAwait(false); } - if (this.stackFrameDetails.Length > 0) + if (stackFrameDetails.Length > 0) { // Augment the top stack frame with details from the stop event - if (this.invocationTypeScriptPositionProperty - .GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) + if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) { - this.stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; - this.stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; - this.stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; - this.stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; + stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; + stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; + stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; + stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; } } - this.CurrentDebuggerStoppedEventArgs = + CurrentDebuggerStoppedEventArgs = new DebuggerStoppedEventArgs( e, _psesHost.CurrentRunspace, localScriptPath); - // Notify the host that the debugger is stopped - this.DebuggerStopped?.Invoke( - sender, - this.CurrentDebuggerStoppedEventArgs); + // Notify the host that the debugger is stopped. + DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs); } private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) { - this.CurrentDebuggerStoppedEventArgs = null; + CurrentDebuggerStoppedEventArgs = null; } /// @@ -1015,18 +994,13 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) // TODO: This could be either a path or a script block! string scriptPath = lineBreakpoint.Script; if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && this.remoteFileManager != null) + && remoteFileManager is not null) { - string mappedPath = - this.remoteFileManager.GetMappedPath( - scriptPath, - _psesHost.CurrentRunspace); + string mappedPath = remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace); - if (mappedPath == null) + if (mappedPath is null) { - this._logger.LogError( - $"Could not map remote path '{scriptPath}' to a local path."); - + _logger.LogError($"Could not map remote path '{scriptPath}' to a local path."); return; } @@ -1064,7 +1038,7 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) } } - this.BreakpointUpdated?.Invoke(sender, e); + BreakpointUpdated?.Invoke(sender, e); } #endregion From 16f3f4acda91e06f93f4ba039355fb4cebcc1caf Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 15:02:56 -0800 Subject: [PATCH 07/12] Remove unused `stackFrameId` argument --- .../Services/DebugAdapter/DebugService.cs | 5 +---- .../Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 02aec129d..bcb8fe370 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -320,9 +320,8 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId) /// walk the cached variable data for the specified stack frame. /// /// The variable expression string to evaluate. - /// The ID of the stack frame in which the expression should be evaluated. /// A VariableDetailsBase object containing the result. - public VariableDetailsBase GetVariableFromExpression(string variableExpression, int stackFrameId) + public VariableDetailsBase GetVariableFromExpression(string variableExpression) { // NOTE: From a watch we will get passed expressions that are not naked variables references. // Probably the right way to do this would be to examine the AST of the expr before calling @@ -517,14 +516,12 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str /// PowerShellContext. /// /// The expression string to execute. - /// The ID of the stack frame in which the expression should be executed. /// /// If true, writes the expression result as host output rather than returning the results. /// In this case, the return value of this function will be null. /// A VariableDetails object containing the result. public async Task EvaluateExpressionAsync( string expressionString, - int stackFrameId, bool writeResultAsOutput) { var command = new PSCommand().AddScript(expressionString); diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs index 0e1a9ce5f..952a4e9ca 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs @@ -61,8 +61,7 @@ public async Task Handle(EvaluateRequestArguments request, if (_debugContext.IsStopped) { // First check to see if the watch expression refers to a naked variable reference. - result = - _debugService.GetVariableFromExpression(request.Expression, request.FrameId); + result = _debugService.GetVariableFromExpression(request.Expression); // If the expression is not a naked variable reference, then evaluate the expression. if (result == null) @@ -70,7 +69,6 @@ public async Task Handle(EvaluateRequestArguments request, result = await _debugService.EvaluateExpressionAsync( request.Expression, - request.FrameId, isFromRepl).ConfigureAwait(false); } } From 92818c8e638c9adca9c7ccfbe581f02d7ed463ba Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 15:03:52 -0800 Subject: [PATCH 08/12] Fix `SetVariableAsync` for new local variable container --- .../Services/DebugAdapter/DebugService.cs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index bcb8fe370..87f4ae6a2 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -423,29 +423,23 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str } VariableDetailsBase variable = variableContainer.Children[name]; - // Determine scope in which the variable lives. This is required later for the call to Get-Variable -Scope. - string scope = null; - if (variableContainerReferenceId == this.scriptScopeVariables.Id) + // Determine scope in which the variable lives so we can pass it to `Get-Variable -Scope`. + string scope = null; // TODO: Can this use a fancy pattern matcher? + if (variableContainerReferenceId == localScopeVariables.Id) { - scope = "Script"; + scope = VariableContainerDetails.LocalScopeName; } - else if (variableContainerReferenceId == this.globalScopeVariables.Id) + else if (variableContainerReferenceId == scriptScopeVariables.Id) { - scope = "Global"; + scope = VariableContainerDetails.ScriptScopeName; } - else + else if (variableContainerReferenceId == globalScopeVariables.Id) { - // Determine which stackframe's local scope the variable is in. - StackFrameDetails[] stackFrames = await this.GetStackFramesAsync().ConfigureAwait(false); - for (int i = 0; i < stackFrames.Length; i++) - { - var stackFrame = stackFrames[i]; - } + scope = VariableContainerDetails.GlobalScopeName; } - - if (scope == null) + else { - // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. + // Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200. throw new Exception("Could not find the scope for this variable."); } From 26f978cf925d83fac86ec0472ef579c7ed4afd62 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 15:05:29 -0800 Subject: [PATCH 09/12] Document and improve `FetchStackFramesAsync` --- .../Services/DebugAdapter/DebugService.cs | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 87f4ae6a2..61a7a3d81 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -668,6 +668,11 @@ private async Task FetchVariableContainerAsync(string } catch (CmdletInvocationException ex) { + // It's possible to be asked to run `Get-Variable -Scope N` where N is a number that + // exceeds the available scopes. In this case, the command throws this exception, + // but there's nothing we can do about it, nor can we know the number of scopes that + // exist, and we shouldn't crash the debugger, so we just return no results instead. + // All other exceptions should be thrown again. if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException")) { throw; @@ -757,6 +762,8 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) if (((variableScope & constantAllScope) == constantAllScope) || ((variableScope & readonlyAllScope) == readonlyAllScope)) { + // The constructor we are using here does not automatically add the dollar prefix, + // so we do it manually. string prefixedVariableName = VariableDetails.DollarPrefix + variableName; if (globalScopeVariables.Children.ContainsKey(prefixedVariableName)) { @@ -769,30 +776,27 @@ private bool AddToAutoVariables(PSObject psvariable, string scope) private async Task FetchStackFramesAsync(string scriptNameOverride) { - PSCommand psCommand = new PSCommand(); - // The serialization depth to retrieve variables from remote runspaces. - const int serializationDepth = 3; - // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame // objects (or "deserialized" CallStackFrames) when attached to a runspace in another - // process. Without the intermediate variable Get-PSCallStack inexplicably returns - // an array of strings containing the formatted output of the CallStackFrame list. - string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; - - string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; - - // If we're attached to a remote runspace, we need to serialize the callstack prior to transport - // because the default depth is too shallow + // process. Without the intermediate variable Get-PSCallStack inexplicably returns an + // array of strings containing the formatted output of the CallStackFrame list. So we + // run a script that builds the list of CallStackFrames and their variables. + const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; + const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}"; + + // If we're attached to a remote runspace, we need to serialize the list prior to + // transport because the default depth is too shallow. From testing, we determined the + // correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we + // just return its results. On a remote machine we serialize it first and then later + // deserialize it. bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; string returnSerializedIfOnRemoteMachine = isOnRemoteMachine - ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" + ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)" : callStackVarName; - // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information - psCommand.AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}"); - - - // PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface + // PSObject is used here instead of the specific type because we get deserialized + // objects from remote sessions and want a common interface. + var psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}"); IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); IEnumerable callStack = isOnRemoteMachine @@ -816,11 +820,13 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) foreach (DictionaryEntry entry in callStackVariables) { - // TODO: This should be deduplicated into a new function for the other variable handling as well + // TODO: This should be deduplicated into a new function. object psVarValue = isOnRemoteMachine ? (entry.Value as PSObject).Properties["Value"].Value : (entry.Value as PSVariable).Value; - // The constructor we are using here does not automatically add the dollar prefix + + // The constructor we are using here does not automatically add the dollar + // prefix, so we do it manually. string psVarName = VariableDetails.DollarPrefix + entry.Key.ToString(); var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ }; variables.Add(variableDetails); From 0bd5490e3eb047058f68b16d828cbac62e0bfbd2 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 18 Nov 2021 17:13:21 -0800 Subject: [PATCH 10/12] Remove unused `scope` argument from `AddToAutoVariables` --- .../Services/DebugAdapter/DebugService.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 61a7a3d81..a4a37e3de 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -700,17 +700,9 @@ private async Task FetchVariableContainerAsync(string return scopeVariableContainer; } - private bool AddToAutoVariables(PSObject psvariable, string scope) + // TODO: This function needs explanation, thought, and improvement. + private bool AddToAutoVariables(PSObject psvariable) { - if ((scope == VariableContainerDetails.GlobalScopeName) - || (scope == VariableContainerDetails.ScriptScopeName)) - { - // We don't A) have a good way of distinguishing built-in from user created variables - // and B) globalScopeVariables.Children.ContainsKey() doesn't work for built-in variables - // stored in a child variable container within the globals variable container. - return false; - } - string variableName = psvariable.Properties["Name"].Value as string; object variableValue = psvariable.Properties["Value"].Value; @@ -831,7 +823,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ }; variables.Add(variableDetails); - if (AddToAutoVariables(new PSObject(entry.Value), scope: null)) + if (AddToAutoVariables(new PSObject(entry.Value))) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } From 29cfe733ef2feaebc78e6645014ed6a737081a67 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 18 Nov 2021 19:00:13 -0800 Subject: [PATCH 11/12] Add `Command` variables container --- .../Services/DebugAdapter/DebugService.cs | 11 ++++++++++- .../DebugAdapter/Debugging/StackFrameDetails.cs | 9 ++++++++- .../Debugging/VariableContainerDetails.cs | 11 ++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index a4a37e3de..b52fd92bb 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -609,10 +609,12 @@ public VariableScope[] GetVariableScopes(int stackFrameId) { var stackFrames = GetStackFrames(); int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id; + int commandVariablesId = stackFrames[stackFrameId].CommandVariables.Id; return new VariableScope[] { new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName), + new VariableScope(commandVariablesId, VariableContainerDetails.CommandVariablesName), new VariableScope(localScopeVariables.Id, VariableContainerDetails.LocalScopeName), new VariableScope(scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName), new VariableScope(globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName), @@ -810,6 +812,12 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) variables.Add(autoVariables); + var commandVariables = new VariableContainerDetails( + nextVariableId++, + VariableContainerDetails.CommandVariablesName); + + variables.Add(commandVariables); + foreach (DictionaryEntry entry in callStackVariables) { // TODO: This should be deduplicated into a new function. @@ -823,13 +831,14 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ }; variables.Add(variableDetails); + commandVariables.Children.Add(variableDetails.Name, variableDetails); if (AddToAutoVariables(new PSObject(entry.Value))) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } } - var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; if (scriptNameOverride is not null diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs index 2b0037eb8..cf31a17ea 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs @@ -64,6 +64,11 @@ internal class StackFrameDetails /// public VariableContainerDetails AutoVariables { get; private set; } + /// + /// Gets or sets the VariableContainerDetails that contains the call stack frame variables. + /// + public VariableContainerDetails CommandVariables { get; private set; } + #endregion #region Constructors @@ -81,7 +86,8 @@ internal class StackFrameDetails /// A new instance of the StackFrameDetails class. static internal StackFrameDetails Create( PSObject callStackFrameObject, - VariableContainerDetails autoVariables) + VariableContainerDetails autoVariables, + VariableContainerDetails commandVariables) { string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath; int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0); @@ -95,6 +101,7 @@ static internal StackFrameDetails Create( StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames EndColumnNumber = 0, AutoVariables = autoVariables, + CommandVariables = commandVariables, // TODO: Re-enable `isExternal` detection along with a setting. Will require // `workspaceRootPath`, see Git blame. IsExternalCode = false diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs index df7ec30a7..25d2b2f3f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableContainerDetails.cs @@ -20,17 +20,22 @@ namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter internal class VariableContainerDetails : VariableDetailsBase { /// - /// Provides a constant for the name of the Global scope. + /// Provides a constant for the name of the filtered auto variables. /// public const string AutoVariablesName = "Auto"; /// - /// Provides a constant for the name of the Global scope. + /// Provides a constant for the name of the current stack frame variables. + /// + public const string CommandVariablesName = "Command"; + + /// + /// Provides a constant for the name of the global scope variables. /// public const string GlobalScopeName = "Global"; /// - /// Provides a constant for the name of the Local scope. + /// Provides a constant for the name of the local scope variables. /// public const string LocalScopeName = "Local"; From 9f17d7fc247612f38286d276d40181b457b0630f Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Fri, 19 Nov 2021 18:12:42 -0800 Subject: [PATCH 12/12] Fix debugger's "auto" variables container We want this container to hold everything that is most likely contextually relevant to the user while debugging. It is specifically NOT analagous to PowerShell's automatic variables, but to Visual Studio's "auto" view of a debugger's variables. This means that for the top of the callstack, we merge the local scope's variables with those in the frame and filter to our own curated list. We also cleaned up our variable property rehydration path. --- .../Services/DebugAdapter/DebugService.cs | 172 ++++++++++++------ 1 file changed, 117 insertions(+), 55 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index b52fd92bb..6c8d94ae8 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -41,6 +41,7 @@ internal class DebugService private readonly IPowerShellDebugContext _debugContext; + // The LSP protocol refers to variables by individual IDs, this is an iterator for that purpose. private int nextVariableId; private string temporaryScriptListingPath; private List variables; @@ -654,7 +655,12 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) } } - private async Task FetchVariableContainerAsync(string scope) + private Task FetchVariableContainerAsync(string scope) + { + return FetchVariableContainerAsync(scope, autoVarsOnly: false); + } + + private async Task FetchVariableContainerAsync(string scope, bool autoVarsOnly) { PSCommand psCommand = new PSCommand() .AddCommand("Get-Variable") @@ -692,8 +698,17 @@ private async Task FetchVariableContainerAsync(string { continue; } + var variableInfo = TryVariableInfo(psVariableObject); + if (variableInfo is null || !ShouldAddAsVariable(variableInfo)) + { + continue; + } + if (autoVarsOnly && !ShouldAddToAutoVariables(variableInfo)) + { + continue; + } - var variableDetails = new VariableDetails(psVariableObject) { Id = nextVariableId++ }; + var variableDetails = new VariableDetails(variableInfo.Variable) { Id = nextVariableId++ }; variables.Add(variableDetails); scopeVariableContainer.Children.Add(variableDetails.Name, variableDetails); } @@ -702,70 +717,95 @@ private async Task FetchVariableContainerAsync(string return scopeVariableContainer; } - // TODO: This function needs explanation, thought, and improvement. - private bool AddToAutoVariables(PSObject psvariable) + // This is a helper type for FetchStackFramesAsync to preserve the variable Type after deserialization. + private record VariableInfo(string[] Types, PSVariable Variable); + + // Create a VariableInfo for both serialized and deserialized variables. + private static VariableInfo TryVariableInfo(PSObject psObject) { - string variableName = psvariable.Properties["Name"].Value as string; - object variableValue = psvariable.Properties["Value"].Value; + if (psObject.TypeNames.Contains("System.Management.Automation.PSVariable")) + { + return new VariableInfo(psObject.TypeNames.ToArray(), psObject.BaseObject as PSVariable); + } + if (psObject.TypeNames.Contains("Deserialized.System.Management.Automation.PSVariable")) + { + // Rehydrate the relevant variable properties and recreate it. + ScopedItemOptions options = (ScopedItemOptions)Enum.Parse(typeof(ScopedItemOptions), psObject.Properties["Options"].Value.ToString()); + PSVariable reconstructedVar = new( + psObject.Properties["Name"].Value.ToString(), + psObject.Properties["Value"].Value, + options + ); + return new VariableInfo(psObject.TypeNames.ToArray(), reconstructedVar); + } - // Don't put any variables created by PSES in the Auto variable container. - if (variableName.StartsWith(PsesGlobalVariableNamePrefix) - || variableName.Equals("PSDebugContext")) + return null; + } + + /// + /// Filters out variables we don't care about such as built-ins + /// + private static bool ShouldAddAsVariable(VariableInfo variableInfo) + { + // Filter built-in constant or readonly variables like $true, $false, $null, etc. + ScopedItemOptions variableScope = variableInfo.Variable.Options; + var constantAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.Constant; + var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; + if (((variableScope & constantAllScope) == constantAllScope) + || ((variableScope & readonlyAllScope) == readonlyAllScope)) { return false; } - ScopedItemOptions variableScope = ScopedItemOptions.None; - PSPropertyInfo optionsProperty = psvariable.Properties["Options"]; - if (string.Equals(optionsProperty.TypeNameOfValue, "System.String")) + if (variableInfo.Variable.Name switch { "null" => true, _ => false }) { - if (!Enum.TryParse( - optionsProperty.Value as string, - out variableScope)) - { - _logger.LogWarning($"Could not parse a variable's ScopedItemOptions value of '{optionsProperty.Value}'"); - } + return false; } - else if (optionsProperty.Value is ScopedItemOptions) + + return true; + } + + // This method curates variables that should be added to the "auto" view, which we define as variables that are + // very likely to be contextually relevant to the user, in an attempt to reduce noise when debugging. + // Variables not listed here can still be found in the other containers like local and script, this is + // provided as a convenience. + private bool ShouldAddToAutoVariables(VariableInfo variableInfo) + { + var variableToAdd = variableInfo.Variable; + if (!ShouldAddAsVariable(variableInfo)) { - variableScope = (ScopedItemOptions)optionsProperty.Value; + return false; } - // Some local variables, if they exist, should be displayed by default - if (psvariable.TypeNames[0].EndsWith("LocalVariable")) + // Filter internal variables created by Powershell Editor Services. + if (variableToAdd.Name.StartsWith(PsesGlobalVariableNamePrefix) + || variableToAdd.Name.Equals("PSDebugContext")) { - if (variableName.Equals("PSItem") || variableName.Equals("_")) - { - return true; - } - else if (variableName.Equals("args", StringComparison.OrdinalIgnoreCase)) - { - return variableValue is Array array && array.Length > 0; - } - return false; } - else if (!psvariable.TypeNames[0].EndsWith(nameof(PSVariable))) + + // Filter Global-Scoped variables. We first cast to VariableDetails to ensure the prefix + // is added for purposes of comparison. + VariableDetails variableToAddDetails = new(variableToAdd); + if (globalScopeVariables.Children.ContainsKey(variableToAddDetails.Name)) { return false; } - var constantAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.Constant; - var readonlyAllScope = ScopedItemOptions.AllScope | ScopedItemOptions.ReadOnly; - - if (((variableScope & constantAllScope) == constantAllScope) - || ((variableScope & readonlyAllScope) == readonlyAllScope)) + // We curate a list of LocalVariables that, if they exist, should be displayed by default. + if (variableInfo.Types[0].EndsWith("LocalVariable")) { - // The constructor we are using here does not automatically add the dollar prefix, - // so we do it manually. - string prefixedVariableName = VariableDetails.DollarPrefix + variableName; - if (globalScopeVariables.Children.ContainsKey(prefixedVariableName)) + return variableToAdd.Name switch { - return false; - } + "PSItem" or "_" or "" => true, + "args" or "input" => variableToAdd.Value is Array array && array.Length > 0, + "PSBoundParameters" => variableToAdd.Value is IDictionary dict && dict.Count > 0, + _ => false + }; } - return true; + // Any other PSVariables that survive the above criteria should be included. + return variableInfo.Types[0].EndsWith("PSVariable"); } private async Task FetchStackFramesAsync(string scriptNameOverride) @@ -798,8 +838,10 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) : results; var stackFrameDetailList = new List(); + bool isTopStackFrame = true; foreach (var callStackFrameItem in callStack) { + // We have to use reflection to get the variable dictionary. var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; var callStackFrame = callStackFrameComponents[0] as PSObject; IDictionary callStackVariables = isOnRemoteMachine @@ -820,27 +862,47 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) foreach (DictionaryEntry entry in callStackVariables) { - // TODO: This should be deduplicated into a new function. - object psVarValue = isOnRemoteMachine - ? (entry.Value as PSObject).Properties["Value"].Value - : (entry.Value as PSVariable).Value; - - // The constructor we are using here does not automatically add the dollar - // prefix, so we do it manually. - string psVarName = VariableDetails.DollarPrefix + entry.Key.ToString(); - var variableDetails = new VariableDetails(psVarName, psVarValue) { Id = nextVariableId++ }; + VariableInfo psVarInfo = TryVariableInfo(new PSObject(entry.Value)); + if (psVarInfo is null) + { + _logger.LogError($"A object was received that is not a PSVariable object"); + continue; + } + + var variableDetails = new VariableDetails(psVarInfo.Variable) { Id = nextVariableId++ }; variables.Add(variableDetails); commandVariables.Children.Add(variableDetails.Name, variableDetails); - if (AddToAutoVariables(new PSObject(entry.Value))) + + if (ShouldAddToAutoVariables(psVarInfo)) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } } - var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables); + // If this is the top stack frame, we also want to add relevant local variables to + // the "Auto" container (not to be confused with Automatic PowerShell variables). + // + // TODO: We can potentially use `Get-Variable -Scope x` to add relevant local + // variables to other frames but frames and scopes are not perfectly analagous and + // we'd need a way to detect things such as module borders and dot-sourced files. + if (isTopStackFrame) + { + var localScopeAutoVariables = await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName, autoVarsOnly: true).ConfigureAwait(false); + foreach (KeyValuePair entry in localScopeAutoVariables.Children) + { + // NOTE: `TryAdd` doesn't work on `IDictionary`. + if (!autoVariables.Children.ContainsKey(entry.Key)) + { + autoVariables.Children.Add(entry.Key, entry.Value); + } + } + isTopStackFrame = false; + } + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; + if (scriptNameOverride is not null && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) {