Skip to content

Fix attach to process debugging #1752

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

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
5 changes: 5 additions & 0 deletions PowerShellEditorServices.build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ task TestE2E Build, SetupHelpForTests, {

# Run E2E tests in ConstrainedLanguage mode.
if (!$script:IsNix) {
if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown("BuiltInAdministratorsSid")) {
Write-Warning 'Skipping E2E CLM tests as they must be ran in an elevated process.'
return
}

try {
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine);
exec { & dotnet $script:dotnetTestArgs $script:NetRuntime.PS7 }
Expand Down
2 changes: 1 addition & 1 deletion src/PowerShellEditorServices/Server/PsesDebugServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void Dispose()
// It represents the debugger on the PowerShell process we're in,
// while a new debug server is spun up for every debugging session
_psesHost.DebugContext.IsDebugServerActive = false;
_debugAdapterServer.Dispose();
_debugAdapterServer?.Dispose();
_inputStream.Dispose();
_outputStream.Dispose();
_serverStopped.SetResult(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;

namespace Microsoft.PowerShell.EditorServices.Services
{
Expand Down Expand Up @@ -43,6 +44,7 @@ public async Task<List<Breakpoint>> GetBreakpointsAsync()
{
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
{
_editorServicesHost.Runspace.ThrowCancelledIfUnusable();
return BreakpointApiUtils.GetBreakpoints(
_editorServicesHost.Runspace.Debugger,
_debugStateService.RunspaceId);
Expand Down
172 changes: 100 additions & 72 deletions src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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.Utility;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Utility;

Expand Down Expand Up @@ -74,6 +75,15 @@ internal class DebugService
/// </summary>
public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }

/// <summary>
/// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
/// </summary>
public bool IsDebuggingRemoteRunspace
{
get => _debugContext.IsDebuggingRemoteRunspace;
set => _debugContext.IsDebuggingRemoteRunspace = value;
}

#endregion

#region Constructors
Expand Down Expand Up @@ -128,6 +138,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).ConfigureAwait(false);

string scriptPath = scriptFile.FilePath;

_psesHost.Runspace.ThrowCancelledIfUnusable();
// Make sure we're using the remote script path
if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
{
Expand Down Expand Up @@ -774,34 +786,35 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";

_psesHost.Runspace.ThrowCancelledIfUnusable();
// 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
// correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we
// just return its results. In a remote runspace we serialize it first and then later
// deserialize it.
bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote;
string returnSerializedIfInRemoteRunspace = isRemoteRunspace
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
: callStackVarName;

// PSObject is used here instead of the specific type because we get deserialized
// objects from remote sessions and want a common interface.
PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}");
PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}");
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);

IEnumerable callStack = isOnRemoteMachine
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList
IEnumerable callStack = isRemoteRunspace
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList
: results;

List<StackFrameDetails> stackFrameDetailList = new();
bool isTopStackFrame = true;
foreach (object callStackFrameItem in callStack)
{
// We have to use reflection to get the variable dictionary.
IList callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList;
IList callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList;
PSObject callStackFrame = callStackFrameComponents[0] as PSObject;
IDictionary callStackVariables = isOnRemoteMachine
? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary
IDictionary callStackVariables = isRemoteRunspace
? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary
: callStackFrameComponents[1] as IDictionary;

VariableContainerDetails autoVariables = new(
Expand Down Expand Up @@ -864,7 +877,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
{
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
}
else if (isOnRemoteMachine
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
&& _remoteFileManager is not null
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
{
Expand Down Expand Up @@ -908,83 +921,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref

internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
{
bool noScriptName = false;
string localScriptPath = e.InvocationInfo.ScriptName;

// If there's no ScriptName, get the "list" of the current source
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
try
Copy link
Member

Choose a reason for hiding this comment

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

Was there anything other than the wrap in a try/catch/finally that changed here? I didn't see anything but GitHub made it hard to compare.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nah just that

{
// Get the current script listing and create the buffer
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
bool noScriptName = false;
string localScriptPath = e.InvocationInfo.ScriptName;

IReadOnlyList<PSObject> scriptListingLines =
await _executionService.ExecutePSCommandAsync<PSObject>(
command, CancellationToken.None).ConfigureAwait(false);

if (scriptListingLines is not null)
// If there's no ScriptName, get the "list" of the current source
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
{
int linePrefixLength = 0;
// Get the current script listing and create the buffer
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");

string scriptListing =
string.Join(
Environment.NewLine,
scriptListingLines
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
.Where(s => s is not null));
IReadOnlyList<PSObject> scriptListingLines =
await _executionService.ExecutePSCommandAsync<PSObject>(
command, CancellationToken.None).ConfigureAwait(false);

temporaryScriptListingPath =
_remoteFileManager.CreateTemporaryFile(
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
scriptListing,
_psesHost.CurrentRunspace);
if (scriptListingLines is not null)
{
int linePrefixLength = 0;

string scriptListing =
string.Join(
Environment.NewLine,
scriptListingLines
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
.Where(s => s is not null));

temporaryScriptListingPath =
_remoteFileManager.CreateTemporaryFile(
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
scriptListing,
_psesHost.CurrentRunspace);

localScriptPath =
temporaryScriptListingPath
?? StackFrameDetails.NoFileScriptPath;

noScriptName = localScriptPath is not null;
}
else
{
_logger.LogWarning("Could not load script context");
}
}

localScriptPath =
temporaryScriptListingPath
?? StackFrameDetails.NoFileScriptPath;
// Get call stack and variables.
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);

noScriptName = localScriptPath is not null;
// 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
&& _remoteFileManager is not null
&& !noScriptName)
{
localScriptPath =
await _remoteFileManager.FetchRemoteFileAsync(
e.InvocationInfo.ScriptName,
_psesHost.CurrentRunspace).ConfigureAwait(false);
}
else

if (stackFrameDetails.Length > 0)
{
_logger.LogWarning("Could not load script context");
// Augment the top stack frame with details from the stop event
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
{
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
}
}
}

// Get call stack and variables.
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
_psesHost.CurrentRunspace,
localScriptPath);

// 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
&& _remoteFileManager is not null
&& !noScriptName)
// Notify the host that the debugger is stopped.
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
}
catch (OperationCanceledException)
{
localScriptPath =
await _remoteFileManager.FetchRemoteFileAsync(
e.InvocationInfo.ScriptName,
_psesHost.CurrentRunspace).ConfigureAwait(false);
// Ignore, likely means that a remote runspace has closed.
}

if (stackFrameDetails.Length > 0)
catch (Exception exception)
{
// Augment the top stack frame with details from the stop event
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
{
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
}
// Log in a catch all so we don't crash the process.
_logger.LogError(
exception,
"Error occurred while obtaining debug info. Message: {message}",
exception.Message);
}

CurrentDebuggerStoppedEventArgs =
new DebuggerStoppedEventArgs(
e,
_psesHost.CurrentRunspace,
localScriptPath);

// Notify the host that the debugger is stopped.
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
}

private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) => CurrentDebuggerStoppedEventArgs = null;
Expand Down
Loading