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
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
New Serializer for PS Variables to support remoting. BROKEN: ID refer…
…ences not matching up
  • Loading branch information
JustinGrote committed Nov 9, 2021
commit d34bf1bf8b685d001ad1db2f59539c69635ee464
73 changes: 50 additions & 23 deletions src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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";
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved

var results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
var getPSCallStack = $"Get-PSCallStack | % {{ [void]{callStackVarName}.add(@($PSItem,$PSItem.GetFrameVariables())) }}";
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved

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<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
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<StackFrameDetails> stackFrameDetailList = new();
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
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;
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
var callStackVariables = (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary;

VariableContainerDetails autoVariables = new(
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
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(this.nextVariableId++, callStackFrame.ToString());
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
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))
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
{
autoVariables.Children.Add(variableDetails.Name, variableDetails);
}
}

this.stackFrameDetails[i] =
StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath);
var stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, localVariables, null);
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved

string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath;
string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath;
if (scriptNameOverride != null &&
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
{
this.stackFrameDetails[i].ScriptPath = scriptNameOverride;
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
}
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
&& this.remoteFileManager != null
&& remoteFileManager != null
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
{
this.stackFrameDetails[i].ScriptPath =
this.remoteFileManager.GetMappedPath(
stackFrameDetailsEntry.ScriptPath =
remoteFileManager.GetMappedPath(
stackFrameScriptPath,
_psesHost.CurrentRunspace);
}

stackFrameDetailList.Add(
stackFrameDetailsEntry
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
);
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
}

stackFrameDetails = stackFrameDetailList.ToArray();
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
}

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