Skip to content
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

Improve debugger's variable population mechanism #1620

Closed
wants to merge 18 commits into from
Closed

Improve debugger's variable population mechanism #1620

wants to merge 18 commits into from

Conversation

JustinGrote
Copy link
Collaborator

@JustinGrote JustinGrote commented Nov 5, 2021

@JustinGrote
Copy link
Collaborator Author

Thoughts: Do we need to do any kind of additional verification to make sure the variable "scope" matches the stacktrace? Or is this lazy iterator method considered pretty consistent?

@SeeminglyScience
Copy link
Collaborator

Thoughts: Do we need to do any kind of additional verification to make sure the variable "scope" matches the stacktrace? Or is this lazy iterator method considered pretty consistent?

Might be worth looking into whether CallStackFrame.GetFrameVariables() can be used. I'm guessing there is a reason it was avoided (though maybe that was for PSv3 support). @rkeithhill do you happen to remember?

@JustinGrote
Copy link
Collaborator Author

JustinGrote commented Nov 5, 2021

@SeeminglyScience could be because the stack frames are being returned deserialized here, maybe some more formatting can go into the Get-PSCallStack request? @daviwil any recollection?

https://github.com/JustinGrote/PowerShellEditorServices/blob/c93f3edd02c0714c7c24664da800083aecd1599c/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs#L810-L815

@JustinGrote
Copy link
Collaborator Author

JustinGrote commented Nov 5, 2021

@JustinGrote
Copy link
Collaborator Author

JustinGrote commented Nov 5, 2021

Seems to work fine when generic'd to CallStackFrame, methods still work fine, I can probably cut a lot of code out here.
image

@JustinGrote
Copy link
Collaborator Author

Oh, the reason may be when debugging remote runspaces, need to test that.

@JustinGrote
Copy link
Collaborator Author

Yep, that's the reason. Shouldn't the generic ExecutePSCommandAsync throw an exception rather than silently filter objects if they don't match the generic profile?
image

@JustinGrote
Copy link
Collaborator Author

I'll adjust the pscommand here to return the variable info as well so the subsequent queries are unnecessary.

@SeeminglyScience
Copy link
Collaborator

Yeah I always forget about that. Serialization is the bane of my existence -_-

Yep, that's the reason. Shouldn't the generic ExecutePSCommandAsync throw an exception rather than silently filter objects if they don't match the generic profile? image

I always thought it should as well, every time I saw that OfType call. At least a Debug.Assert or something since you don't really want a crash and it's really a design time error. Ultimately it doesn't seem to hurt much but a debug assert would be nice for safety.

@andyleejordan
Copy link
Member

Oh, the reason may be when debugging remote runspaces, need to test that.

I know Rob spent a good chunk of time making sure this worked, though sadly there wasn't really a way to put tests around it.

@andyleejordan
Copy link
Member

Yep, that's the reason. Shouldn't the generic ExecutePSCommandAsync throw an exception rather than silently filter objects if they don't match the generic profile?
image

If it's not already in a big old comment explaining so, would you mind adding @JustinGrote?

@SeeminglyScience
Copy link
Collaborator

I know Rob spent a good chunk of time making sure this worked, though sadly there wasn't really a way to put tests around it.

Oh yeah I'm sure how it's set up works fine for remote runspaces. It's just my GetFrameVariables idea that explodes 😁

@JustinGrote JustinGrote marked this pull request as draft November 5, 2021 17:23
@JustinGrote
Copy link
Collaborator Author

This now uses serialization only for the remote runspace pathway. I'm not very happy about all the "isOnRemoteMachine" ternaries, this should probably instead be abstracted out to a CallStackProvider class or something to make it easier to test.

@JustinGrote JustinGrote changed the title Ignore scoping issues with fetching local variables Fix: Rework Variable Handler to not rely on scope iteration and use serializer for remote runspaces Nov 6, 2021
@JustinGrote JustinGrote marked this pull request as ready for review November 6, 2021 21:56
Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, thanks for doing this! ❤️ Got a few requests and questions

@JustinGrote
Copy link
Collaborator Author

@andschwa should be good to merge if the CI tests pass

@andyleejordan andyleejordan changed the title Fix: Rework Variable Handler to not rely on scope iteration and use serializer for remote runspaces Improve debugger's variable population mechanism Nov 11, 2021
Copy link
Member

@andyleejordan andyleejordan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're having to go back to the drawing board on this after testing. We accidentally dropped all the local variables 😅

@JustinGrote JustinGrote marked this pull request as draft November 11, 2021 18:52
@JustinGrote
Copy link
Collaborator Author

Per discussion looks like GetFrameVariables() doesn't work like it should, it's not returning local vars.

@andyleejordan
Copy link
Member

Per discussion looks like GetFrameVariables() doesn't work like it should, it's not returning local vars.

😭

@JustinGrote
Copy link
Collaborator Author

JustinGrote commented Nov 11, 2021

OK, new plan:

  1. Global/Script/Local will reflect just what the gv version of each of those is at the current breakpoint, we're not going to try go up the scope stack and associate those to stackframes because they aren't always 1:1 (modules, etc.)
  2. Auto will contain variables in the current getFrameVariables as well as some whitelisted pull-forward items such as $input,$_, and $args

We will be able to vastly simplify things this way, and it will be more obvious and presentable to users as it would be what they would expect (e.g. these are the variables that can be seen at script scope, not the "script scope" variables, so it would include globals)

Initially the auto pull forward vars will be hardcoded but I want to make this a setting.

@JustinGrote JustinGrote marked this pull request as ready for review November 12, 2021 05:47
@JustinGrote
Copy link
Collaborator Author

OK this is much better now. Auto now includes only $args and $PSItem (as the original code was meant to it seems) but it comes directly from the callstack frame, so if you have a deep callstack, it will show $args and $PSItem at the level of that callstack. Local/Script/Global basically reflect what is shown in get-variable, with script and local respectively de-duped as they are processed (this is also original behavior).

The only difference in behavior is that local does not change with the stack layer, because stack layers and scopes are not 1:1 pairings. Instead, a future PR should add "Scope x" variable headings for the top stack frame only as with the current implementation it is not possible to accurately correlate scope layers to stack frames (may be possible in the future)

Also updated so "Auto" shows both $_ and $PSItem even though they represent the same thing. @SeeminglyScience and I discussed and while it would be nice to only present $PSItem to encourage people to use it, that may confuse people who still use or learn $_ and are ignorant that they are the same.

results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None)
.ConfigureAwait(false);
}
catch (CmdletInvocationException ex)

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.

// 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

Choose a reason for hiding this comment

The 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 Get-PSCallStack script that is run for both local and remote (serialized results) cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
// The next command 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.
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine

Suggested comment to resolve Paul's request.


// 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";

Choose a reason for hiding this comment

The 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. ExecutePSCommandAsync appears to really be 'ExecuteScriptAsync' since it takes a general script string instead of command plus arguments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While true, the only change here is from var to string. We can do this later.

@andyleejordan
Copy link
Member

Ok the only thing I'd like to know if we can still keep working (even admitting that it's funky) is to have the Auto variable groupset update per stack frame. In the (admittedly broken in several ways) current version of the code, if you hit a breakpoint and click through the frames in the callstack, the current frame's local and auto variables update in the VS Code debugger. I'll play with this and see if I can get it to work.

@JustinGrote
Copy link
Collaborator Author

JustinGrote commented Nov 16, 2021

Auto variable does update per stack frame for the auto variables, or at least it did in my testing.

Local and Global do not because, as discussed, there is no actual correlary between stack frames and scopes, and the previous behavior that enabled this (upcycling through the scopes via -scope 0, -scope 1, etc.) was inaccurate especially if done in a module scope, and presenting inaccurate variables are considered worse than not presenting them at all.

Rather than try to edit Local and Global, it would be better to have a "Stack" or "CurrentFrame" group list all variables in GetFrameVariables.

@andyleejordan
Copy link
Member

Auto variable does update per stack frame for the auto variables, or at least it did in my testing.

Auto is not in my testing.

Local and Global do not because, as discussed, there is no actual correlary between stack frames and scopes

Yup, yup, no I get that there isn't a precise correlation and that it doesn't hold up across the board. It's annoying because there's at least some correlation in that the top stack frame is scope 0, and its parent scope is (most likely? always? at least a vast majority of the time?) scope 1...but yeah ok, you're right with "presenting inaccurate variables are considered worse than not presenting them at all." I think I agree, I just wish there was a better solution.

Rather than try to edit Local and Global, it would be better to have a "Stack" or "CurrentFrame" group list all variables in GetFrameVariables.

Well and this won't work well at all because we found GetFrameVariables is broken for our needs.

Ugh, ok, this is an improvement and a bug fix. Let me see if the Auto variables can be updated per frame, as you said it was working for you.

}
catch (CmdletInvocationException ex)
{
if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!ex.ErrorRecord.CategoryInfo.Reason.Equals("PSArgumentOutOfRangeException"))
// 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 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"))

Suggested comment to resolve Paul's request.


// 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";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While true, the only change here is from var to string. We can do this later.

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
// The next command 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.
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine

Suggested comment to resolve Paul's request.

@JustinGrote
Copy link
Collaborator Author

@andschwa example of the auto variables updating thru the stack during a Pester Test on PoshNmap. Remember we only currently capture $args and $_/$PSItem to Auto, we can either expand this to include all "current" stack variables that aren't some common noisy ones, or put them in another variable pane named "CurrentFrame"

Capture.mp4

@andyleejordan
Copy link
Member

andyleejordan commented Nov 17, 2021

@JustinGrote I'm not seeing any auto variables now. I've built your branch (and double checked it's up-to-date except rebased on master, and that my extension is using the right build through the symlink). I'm in a Debug configuration. How are you testing? Also FYI I'm on macOS with PowerShell 7.2.

@JustinGrote
Copy link
Collaborator Author

Closing in favor of #1630

@JustinGrote JustinGrote deleted the justingrote/IgnoreScopeIssue branch November 19, 2021 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Null Stack if debugging in separate process Debugging with active breakpoint crashes Powershell session
4 participants