-
Notifications
You must be signed in to change notification settings - Fork 235
Improve debugger's variable population mechanism #1620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
8685daf
d34bf1b
df4d194
e9013f3
491bff8
43bbaec
1bf4c79
b56245d
eab71ea
bf4ceeb
c624aeb
6e6f73f
2105145
d8dfbbc
8253a9a
56044e2
16b294c
5ed6fe8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 | ||||||||||||||
{ | ||||||||||||||
|
@@ -687,8 +688,20 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync( | |||||||||||||
var scopeVariableContainer = new VariableContainerDetails(this.nextVariableId++, "Scope: " + scope); | ||||||||||||||
this.variables.Add(scopeVariableContainer); | ||||||||||||||
|
||||||||||||||
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None) | ||||||||||||||
.ConfigureAwait(false); | ||||||||||||||
IReadOnlyList<PSObject> results; | ||||||||||||||
try | ||||||||||||||
{ | ||||||||||||||
results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None) | ||||||||||||||
.ConfigureAwait(false); | ||||||||||||||
} | ||||||||||||||
catch (CmdletInvocationException ex) | ||||||||||||||
{ | ||||||||||||||
if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException")) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Suggested comment to resolve Paul's request. |
||||||||||||||
{ | ||||||||||||||
throw; | ||||||||||||||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
} | ||||||||||||||
results = null; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
if (results != null) | ||||||||||||||
{ | ||||||||||||||
|
@@ -792,55 +805,92 @@ 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"; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be reviewed for possible injection opportunities, and ensure that arbitrary script content cannot be injected from external sources. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While true, the only change here is from |
||||||||||||||
|
||||||||||||||
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; | ||||||||||||||
andyleejordan marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit confusing for future maintainers. Please add comments that clearly show the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Suggested comment to resolve Paul's request. |
||||||||||||||
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, {serializationDepth})" | ||||||||||||||
: callStackVarName; | ||||||||||||||
|
||||||||||||||
var results = await _executionService.ExecutePSCommandAsync<PSObject>(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<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(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 | ||||||||||||||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
: results; | ||||||||||||||
|
||||||||||||||
List<StackFrameDetails> stackFrameDetailList = new List<StackFrameDetails>(); | ||||||||||||||
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; | ||||||||||||||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
IDictionary callStackVariables = isOnRemoteMachine | ||||||||||||||
? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary | ||||||||||||||
: callStackFrameComponents[1] as IDictionary; | ||||||||||||||
|
||||||||||||||
var autoVariables = new VariableContainerDetails( | ||||||||||||||
nextVariableId++, | ||||||||||||||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
VariableContainerDetails.AutoVariablesName); | ||||||||||||||
|
||||||||||||||
this.variables.Add(autoVariables); | ||||||||||||||
variables.Add(autoVariables); | ||||||||||||||
|
||||||||||||||
VariableContainerDetails localVariables = | ||||||||||||||
await FetchVariableContainerAsync(i.ToString(), autoVariables).ConfigureAwait(false); | ||||||||||||||
var localVariables = new VariableContainerDetails(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 | ||||||||||||||
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); | ||||||||||||||
|
||||||||||||||
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, localVariables, workspaceRootPath: _psesHost.InitialWorkingDirectory); | ||||||||||||||
|
||||||||||||||
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(); | ||||||||||||||
JustinGrote marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
} | ||||||||||||||
|
||||||||||||||
private static string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength) | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment as to why this exception is caught and eaten here, except for 'psargumentoutofrangeexception' types.