From 8685daf5a3c052a0274971d46b38a8be46034325 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 4 Nov 2021 17:35:42 -0700 Subject: [PATCH 01/18] Ignore scoping issues with fetching local variables --- .../Services/DebugAdapter/DebugService.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 56b6ecb11..d5757964f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -687,8 +687,22 @@ private async Task FetchVariableContainerAsync( 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") && + ex.Message.Contains("exceeds the number of active scopes"))) + { + throw; + } + results = null; + } if (results != null) { From d34bf1bf8b685d001ad1db2f59539c69635ee464 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 6 Nov 2021 09:56:53 -0700 Subject: [PATCH 02/18] New Serializer for PS Variables to support remoting. BROKEN: ID references not matching up --- .../Services/DebugAdapter/DebugService.cs | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index d5757964f..c418e3f8f 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 { @@ -806,55 +807,81 @@ 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. + var serializationDepth = 10; // 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}"); + var callStackVarName = $"$GLOBAL:{PsesGlobalVariableNamePrefix}CallStack"; - var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); + var getPSCallStack = $"Get-PSCallStack | % {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; - var callStackFrames = results.ToArray(); + // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information + // TODO: Don't use serializer for local runspaces, or implement a way to specify depth ExecutePSCommandAsync + string getPSCallStackScript = $"[Collections.ArrayList]{callStackVarName}=@();{getPSCallStack};[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})"; + psCommand.AddScript(getPSCallStackScript); - 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 + var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); + string serializedResult = results[0].BaseObject as string; + var callStack = (PSSerializer.Deserialize(serializedResult) as PSObject).BaseObject as ArrayList; - for (int i = 0; i < callStackFrames.Length; i++) + List stackFrameDetailList = new(); + foreach (var callStackFrameItem in callStack) { - VariableContainerDetails autoVariables = - new VariableContainerDetails( - this.nextVariableId++, - VariableContainerDetails.AutoVariablesName); + var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as ArrayList; + var callStackFrame = callStackFrameComponents[0] as PSObject; + var callStackVariables = (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary; + + VariableContainerDetails autoVariables = new( + nextVariableId++, + VariableContainerDetails.AutoVariablesName); - this.variables.Add(autoVariables); + variables.Add(autoVariables); - VariableContainerDetails localVariables = - await FetchVariableContainerAsync(i.ToString(), autoVariables).ConfigureAwait(false); + var localVariables = new VariableContainerDetails(this.nextVariableId++, callStackFrame.ToString()); + variables.Add(localVariables); - // 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 + var psVar = entry.Value as PSObject; + var variableDetails = new VariableDetails(entry.Key.ToString(), psVar.Properties["Value"].Value) { Id = nextVariableId++ }; + variables.Add(variableDetails); + localVariables.Children.Add(variableDetails.Name, variableDetails); + + if (AddToAutoVariables(new PSObject(entry.Value), null)) + { + autoVariables.Children.Add(variableDetails.Name, variableDetails); + } + } - this.stackFrameDetails[i] = - StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, null); - string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath; + string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; if (scriptNameOverride != null && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { - this.stackFrameDetails[i].ScriptPath = scriptNameOverride; + stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } else if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && this.remoteFileManager != null + && remoteFileManager != 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) From df4d1948361fad79a273fabeb57ffb7e2cacfc2a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 6 Nov 2021 12:01:39 -0700 Subject: [PATCH 03/18] Match on both properties and noteproperties --- .../Services/DebugAdapter/Debugging/VariableDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs index e34604927..44c86b4bb 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs @@ -290,7 +290,7 @@ private VariableDetails[] GetChildren(object obj, ILogger logger) childVariables.AddRange( psObject .Properties - .Where(p => p.MemberType == PSMemberTypes.NoteProperty) + .Where(p => p.MemberType == PSMemberTypes.NoteProperty || p.MemberType == PSMemberTypes.Property) .Select(p => new VariableDetails(p))); obj = psObject.BaseObject; From e9013f328b66ba4de9c4c45c6dfa39f0e9e94b32 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 6 Nov 2021 13:03:31 -0700 Subject: [PATCH 04/18] Make cmdletinvocationexception less specific --- .../Services/DebugAdapter/DebugService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index c418e3f8f..3bddee74b 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -697,9 +697,7 @@ private async Task FetchVariableContainerAsync( catch (CmdletInvocationException ex) { if (!( - ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException") && - ex.Message.Contains("exceeds the number of active scopes"))) - { + ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException") { throw; } results = null; From 491bff8d9cf563c937e903e2fa97853923d2d284 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 6 Nov 2021 14:41:26 -0700 Subject: [PATCH 05/18] Don't serialize local codepaths --- .../Services/DebugAdapter/DebugService.cs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 3bddee74b..6f351e42f 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -696,8 +696,8 @@ private async Task FetchVariableContainerAsync( } catch (CmdletInvocationException ex) { - if (!( - ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException") { + if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException")) + { throw; } results = null; @@ -806,7 +806,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) { PSCommand psCommand = new PSCommand(); // The serialization depth to retrieve variables from remote runspaces. - var serializationDepth = 10; + var 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 @@ -816,22 +816,34 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) var getPSCallStack = $"Get-PSCallStack | % {{ [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 + var isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; + var returnSerializedIfOnRemoteMachine = isOnRemoteMachine + ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" + : callStackVarName; + // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information // TODO: Don't use serializer for local runspaces, or implement a way to specify depth ExecutePSCommandAsync - string getPSCallStackScript = $"[Collections.ArrayList]{callStackVarName}=@();{getPSCallStack};[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})"; + string getPSCallStackScript = $"[Collections.ArrayList]{callStackVarName}=@();{getPSCallStack};{returnSerializedIfOnRemoteMachine}"; psCommand.AddScript(getPSCallStackScript); + // PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); - string serializedResult = results[0].BaseObject as string; - var callStack = (PSSerializer.Deserialize(serializedResult) as PSObject).BaseObject as ArrayList; + + IEnumerable callStack = isOnRemoteMachine + ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList + : results; List stackFrameDetailList = new(); foreach (var callStackFrameItem in callStack) { - var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as ArrayList; + var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; var callStackFrame = callStackFrameComponents[0] as PSObject; - var callStackVariables = (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary; + IDictionary callStackVariables = isOnRemoteMachine + ? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary + : callStackFrameComponents[1] as IDictionary; VariableContainerDetails autoVariables = new( nextVariableId++, @@ -845,8 +857,10 @@ 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 - var psVar = entry.Value as PSObject; - var variableDetails = new VariableDetails(entry.Key.ToString(), psVar.Properties["Value"].Value) { Id = nextVariableId++ }; + object psVarValue = isOnRemoteMachine + ? (entry.Value as PSObject).Properties["Value"].Value + : (entry.Value as PSVariable).Value; + var variableDetails = new VariableDetails(entry.Key.ToString(), psVarValue) { Id = nextVariableId++ }; variables.Add(variableDetails); localVariables.Children.Add(variableDetails.Name, variableDetails); From 43bbaec8401714491211b8f3bc26b767790192de Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 9 Nov 2021 12:02:23 -0800 Subject: [PATCH 06/18] Make serializationDepth constant 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 6f351e42f..1fe3b5a0c 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -806,7 +806,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) { PSCommand psCommand = new PSCommand(); // The serialization depth to retrieve variables from remote runspaces. - var serializationDepth = 3; + 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 From 1bf4c791f35b4803ee56735d6c1390796402dc37 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 9 Nov 2021 12:03:19 -0800 Subject: [PATCH 07/18] callStackVarName explicit typing 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 1fe3b5a0c..ae01eb830 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -812,7 +812,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // 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"; + string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; var getPSCallStack = $"Get-PSCallStack | % {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; From b56245dbf06392a3ea0698326acfeb938ca928a8 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 9 Nov 2021 12:03:38 -0800 Subject: [PATCH 08/18] getPSCallStack explicit string 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 ae01eb830..73bbd7461 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -814,7 +814,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // an array of strings containing the formatted output of the CallStackFrame list. string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; - var getPSCallStack = $"Get-PSCallStack | % {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; + string getPSCallStack = $"Get-PSCallStack | % {{ [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 From eab71ea84a90f107e5b4f3c1726ee4bf0a77ea41 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 9 Nov 2021 12:04:33 -0800 Subject: [PATCH 09/18] Explicit type for isOnRemoteMachine 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 73bbd7461..75619ce70 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -818,7 +818,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // If we're attached to a remote runspace, we need to serialize the callstack prior to transport // because the default depth is too shallow - var isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; + bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; var returnSerializedIfOnRemoteMachine = isOnRemoteMachine ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" : callStackVarName; From bf4ceeb65b11e9177738f865ffd65a06c4620997 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Tue, 9 Nov 2021 12:06:16 -0800 Subject: [PATCH 10/18] Explicit Typing for returnSerializedIfOnRemoteMachine 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 75619ce70..feb909a57 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -819,7 +819,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // 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; - var returnSerializedIfOnRemoteMachine = isOnRemoteMachine + string returnSerializedIfOnRemoteMachine = isOnRemoteMachine ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" : callStackVarName; From c624aebbc5965d382d3ef22a74931cd08272908d Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 10 Nov 2021 09:30:57 -0800 Subject: [PATCH 11/18] Address nits --- .../Services/DebugAdapter/DebugService.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index feb909a57..90074a1f6 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -830,13 +830,13 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface - var results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); + IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); IEnumerable callStack = isOnRemoteMachine ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList : results; - List stackFrameDetailList = new(); + List stackFrameDetailList = new List(); foreach (var callStackFrameItem in callStack) { var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; @@ -845,7 +845,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) ? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary : callStackFrameComponents[1] as IDictionary; - VariableContainerDetails autoVariables = new( + var autoVariables = new VariableContainerDetails( nextVariableId++, VariableContainerDetails.AutoVariablesName); @@ -864,13 +864,13 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) variables.Add(variableDetails); localVariables.Children.Add(variableDetails.Name, variableDetails); - if (AddToAutoVariables(new PSObject(entry.Value), null)) + if (AddToAutoVariables(new PSObject(entry.Value), scope: null)) { autoVariables.Children.Add(variableDetails.Name, variableDetails); } } - var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, null); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, workspaceRootPath: null); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; if (scriptNameOverride != null && @@ -889,8 +889,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) } stackFrameDetailList.Add( - stackFrameDetailsEntry - ); + stackFrameDetailsEntry); } stackFrameDetails = stackFrameDetailList.ToArray(); From 6e6f73f5c3fb25d0129087472ce697b9157386a8 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 10 Nov 2021 11:59:23 -0800 Subject: [PATCH 12/18] Address @andschwa feedback --- .../Services/DebugAdapter/DebugService.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 90074a1f6..ffe6fe9ff 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -814,7 +814,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // an array of strings containing the formatted output of the CallStackFrame list. string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack"; - string getPSCallStack = $"Get-PSCallStack | % {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}"; + 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 @@ -824,9 +824,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) : callStackVarName; // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information - // TODO: Don't use serializer for local runspaces, or implement a way to specify depth ExecutePSCommandAsync - string getPSCallStackScript = $"[Collections.ArrayList]{callStackVarName}=@();{getPSCallStack};{returnSerializedIfOnRemoteMachine}"; - psCommand.AddScript(getPSCallStackScript); + 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 @@ -851,7 +849,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) variables.Add(autoVariables); - var localVariables = new VariableContainerDetails(this.nextVariableId++, callStackFrame.ToString()); + var localVariables = new VariableContainerDetails(nextVariableId++, callStackFrame.ToString()); variables.Add(localVariables); foreach (DictionaryEntry entry in callStackVariables) @@ -870,16 +868,16 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) } } - var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, workspaceRootPath: null); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, workspaceRootPath: _psesHost.InitialWorkingDirectory); string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath; - if (scriptNameOverride != null && + if (scriptNameOverride is not null && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { stackFrameDetailsEntry.ScriptPath = scriptNameOverride; } - else if (_psesHost.CurrentRunspace.IsOnRemoteMachine - && remoteFileManager != null + else if (isOnRemoteMachine + && remoteFileManager is not null && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath)) { stackFrameDetailsEntry.ScriptPath = From 21051454cb8b279a27d51030d09c2c0697bf8118 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Wed, 10 Nov 2021 16:39:47 -0800 Subject: [PATCH 13/18] More comprehensive parameter matching --- .../Services/DebugAdapter/Debugging/VariableDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs index 44c86b4bb..abc16027a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs @@ -290,7 +290,7 @@ private VariableDetails[] GetChildren(object obj, ILogger logger) childVariables.AddRange( psObject .Properties - .Where(p => p.MemberType == PSMemberTypes.NoteProperty || p.MemberType == PSMemberTypes.Property) + .Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0) // Filter out non-properties .Select(p => new VariableDetails(p))); obj = psObject.BaseObject; From d8dfbbc91f0cbff84c05da6bba98490c45a711a8 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 11 Nov 2021 17:02:10 -0800 Subject: [PATCH 14/18] First Pass: Simplify Local and Stack Variable Collection --- .../Services/DebugAdapter/DebugService.cs | 40 +++++++------------ .../Debugging/StackFrameDetails.cs | 13 +----- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index ffe6fe9ff..3d6073223 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -46,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; @@ -446,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; - } } } @@ -627,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), }; @@ -656,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, null).ConfigureAwait(false); + + this.scriptScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); + + this.localScopeVariables = + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); + await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); + } finally { @@ -667,16 +671,6 @@ 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) @@ -849,9 +843,6 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) variables.Add(autoVariables); - var localVariables = new VariableContainerDetails(nextVariableId++, callStackFrame.ToString()); - variables.Add(localVariables); - foreach (DictionaryEntry entry in callStackVariables) { // TODO: This should be deduplicated into a new function for the other variable handling as well @@ -860,7 +851,6 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) : (entry.Value as PSVariable).Value; var variableDetails = new VariableDetails(entry.Key.ToString(), psVarValue) { Id = nextVariableId++ }; variables.Add(variableDetails); - localVariables.Children.Add(variableDetails.Name, variableDetails); if (AddToAutoVariables(new PSObject(entry.Value), scope: null)) { @@ -868,7 +858,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) } } - var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, workspaceRootPath: _psesHost.InitialWorkingDirectory); + var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables); 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 6d03c6494..cce7afd40 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 @@ -93,14 +88,9 @@ internal class StackFrameDetails /// 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); @@ -122,7 +112,6 @@ static internal StackFrameDetails Create( StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames EndColumnNumber = 0, AutoVariables = autoVariables, - LocalVariables = localVariables, IsExternalCode = isExternal }; } From 8253a9a9ec81a78081ec2bd48895c56cbda1c960 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 11 Nov 2021 19:51:49 -0800 Subject: [PATCH 15/18] Fix fetching script when I wanted to fetch local --- .../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 3d6073223..44c08f8e8 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -660,7 +660,7 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); this.localScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); + await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName, null).ConfigureAwait(false); await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); From 56044e204d567b21a3b1c50eea7e8af41fa86286 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 11 Nov 2021 20:18:41 -0800 Subject: [PATCH 16/18] Cleanup unnecessary autoVariable assignments --- .../Services/DebugAdapter/DebugService.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 44c08f8e8..2c350acfb 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -654,13 +654,13 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) // Must retrieve in order of broadest to narrowest scope for efficient deduplication: global, script, local this.globalScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName, null).ConfigureAwait(false); + await FetchVariableContainerAsync(VariableContainerDetails.GlobalScopeName).ConfigureAwait(false); this.scriptScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName, null).ConfigureAwait(false); + await FetchVariableContainerAsync(VariableContainerDetails.ScriptScopeName).ConfigureAwait(false); this.localScopeVariables = - await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName, null).ConfigureAwait(false); + await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false); await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false); @@ -671,9 +671,7 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride) } } - private async Task FetchVariableContainerAsync( - string scope, - VariableContainerDetails autoVariables) + private async Task FetchVariableContainerAsync(string scope) { PSCommand psCommand = new PSCommand() .AddCommand("Get-Variable") @@ -711,11 +709,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); - } } } @@ -763,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")) { return true; } From 16b294cc5721c7e21bc6cf4e8c0cadc93065ee05 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 11 Nov 2021 20:37:21 -0800 Subject: [PATCH 17/18] Expose both _ and PSItem --- .../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 2c350acfb..e2cabdd72 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("PSItem")) + if (variableName.Equals("PSItem") || variableName.Equals("_")) { return true; } From 5ed6fe8df25d3f77755cf9e4f4960180efdc5766 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Thu, 11 Nov 2021 21:13:07 -0800 Subject: [PATCH 18/18] Fix auto variable names --- .../Services/DebugAdapter/DebugService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index e2cabdd72..d1366622a 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -842,7 +842,9 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) object psVarValue = isOnRemoteMachine ? (entry.Value as PSObject).Properties["Value"].Value : (entry.Value as PSVariable).Value; - var variableDetails = new VariableDetails(entry.Key.ToString(), psVarValue) { Id = nextVariableId++ }; + // 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))