Skip to content

Commit 8980a36

Browse files
Fix attach to process and other debugging issues
Fixes PowerShell#1736 - Add a new context frame for runspace entrance and REPL loops - Ensure runspace access is not attempted on a broken runspace - When a remote runspace hits a debugger stop, ensure that the origin pipeline thread does not run a REPL concurrently - Fix many race conditions
1 parent 73dddca commit 8980a36

12 files changed

+663
-144
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
1313
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
1414
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
15+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
1516

1617
namespace Microsoft.PowerShell.EditorServices.Services
1718
{
@@ -43,6 +44,7 @@ public async Task<List<Breakpoint>> GetBreakpointsAsync()
4344
{
4445
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
4546
{
47+
_editorServicesHost.Runspace.ThrowCancelledIfUnusable();
4648
return BreakpointApiUtils.GetBreakpoints(
4749
_editorServicesHost.Runspace.Debugger,
4850
_debugStateService.RunspaceId);

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+100-72
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
1717
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
1818
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
19+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
1920
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
2021
using Microsoft.PowerShell.EditorServices.Utility;
2122

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

78+
/// <summary>
79+
/// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
80+
/// </summary>
81+
public bool IsDebuggingRemoteRunspace
82+
{
83+
get => _debugContext.IsDebuggingRemoteRunspace;
84+
set => _debugContext.IsDebuggingRemoteRunspace = value;
85+
}
86+
7787
#endregion
7888

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

130140
string scriptPath = scriptFile.FilePath;
141+
142+
_psesHost.Runspace.ThrowCancelledIfUnusable();
131143
// Make sure we're using the remote script path
132144
if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
133145
{
@@ -795,34 +807,35 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
795807
const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
796808
const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";
797809

810+
_psesHost.Runspace.ThrowCancelledIfUnusable();
798811
// If we're attached to a remote runspace, we need to serialize the list prior to
799812
// transport because the default depth is too shallow. From testing, we determined the
800-
// correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we
801-
// just return its results. On a remote machine we serialize it first and then later
813+
// correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we
814+
// just return its results. In a remote runspace we serialize it first and then later
802815
// deserialize it.
803-
bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
804-
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
816+
bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote;
817+
string returnSerializedIfInRemoteRunspace = isRemoteRunspace
805818
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
806819
: callStackVarName;
807820

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

813-
IEnumerable callStack = isOnRemoteMachine
814-
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList
826+
IEnumerable callStack = isRemoteRunspace
827+
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList
815828
: results;
816829

817830
var stackFrameDetailList = new List<StackFrameDetails>();
818831
bool isTopStackFrame = true;
819832
foreach (var callStackFrameItem in callStack)
820833
{
821834
// We have to use reflection to get the variable dictionary.
822-
var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList;
835+
var callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList;
823836
var callStackFrame = callStackFrameComponents[0] as PSObject;
824-
IDictionary callStackVariables = isOnRemoteMachine
825-
? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary
837+
IDictionary callStackVariables = isRemoteRunspace
838+
? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary
826839
: callStackFrameComponents[1] as IDictionary;
827840

828841
var autoVariables = new VariableContainerDetails(
@@ -885,7 +898,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
885898
{
886899
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
887900
}
888-
else if (isOnRemoteMachine
901+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
889902
&& _remoteFileManager is not null
890903
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
891904
{
@@ -929,83 +942,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref
929942

930943
internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
931944
{
932-
bool noScriptName = false;
933-
string localScriptPath = e.InvocationInfo.ScriptName;
934-
935-
// If there's no ScriptName, get the "list" of the current source
936-
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
945+
try
937946
{
938-
// Get the current script listing and create the buffer
939-
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
947+
bool noScriptName = false;
948+
string localScriptPath = e.InvocationInfo.ScriptName;
940949

941-
IReadOnlyList<PSObject> scriptListingLines =
942-
await _executionService.ExecutePSCommandAsync<PSObject>(
943-
command, CancellationToken.None).ConfigureAwait(false);
944-
945-
if (scriptListingLines is not null)
950+
// If there's no ScriptName, get the "list" of the current source
951+
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
946952
{
947-
int linePrefixLength = 0;
953+
// Get the current script listing and create the buffer
954+
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
948955

949-
string scriptListing =
950-
string.Join(
951-
Environment.NewLine,
952-
scriptListingLines
953-
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
954-
.Where(s => s is not null));
956+
IReadOnlyList<PSObject> scriptListingLines =
957+
await _executionService.ExecutePSCommandAsync<PSObject>(
958+
command, CancellationToken.None).ConfigureAwait(false);
955959

956-
temporaryScriptListingPath =
957-
_remoteFileManager.CreateTemporaryFile(
958-
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
959-
scriptListing,
960-
_psesHost.CurrentRunspace);
960+
if (scriptListingLines is not null)
961+
{
962+
int linePrefixLength = 0;
963+
964+
string scriptListing =
965+
string.Join(
966+
Environment.NewLine,
967+
scriptListingLines
968+
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
969+
.Where(s => s is not null));
970+
971+
temporaryScriptListingPath =
972+
_remoteFileManager.CreateTemporaryFile(
973+
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
974+
scriptListing,
975+
_psesHost.CurrentRunspace);
976+
977+
localScriptPath =
978+
temporaryScriptListingPath
979+
?? StackFrameDetails.NoFileScriptPath;
980+
981+
noScriptName = localScriptPath is not null;
982+
}
983+
else
984+
{
985+
_logger.LogWarning("Could not load script context");
986+
}
987+
}
961988

962-
localScriptPath =
963-
temporaryScriptListingPath
964-
?? StackFrameDetails.NoFileScriptPath;
989+
// Get call stack and variables.
990+
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
965991

966-
noScriptName = localScriptPath is not null;
992+
// If this is a remote connection and the debugger stopped at a line
993+
// in a script file, get the file contents
994+
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
995+
&& _remoteFileManager is not null
996+
&& !noScriptName)
997+
{
998+
localScriptPath =
999+
await _remoteFileManager.FetchRemoteFileAsync(
1000+
e.InvocationInfo.ScriptName,
1001+
_psesHost.CurrentRunspace).ConfigureAwait(false);
9671002
}
968-
else
1003+
1004+
if (stackFrameDetails.Length > 0)
9691005
{
970-
_logger.LogWarning("Could not load script context");
1006+
// Augment the top stack frame with details from the stop event
1007+
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
1008+
{
1009+
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
1010+
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
1011+
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
1012+
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
1013+
}
9711014
}
972-
}
9731015

974-
// Get call stack and variables.
975-
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
1016+
CurrentDebuggerStoppedEventArgs =
1017+
new DebuggerStoppedEventArgs(
1018+
e,
1019+
_psesHost.CurrentRunspace,
1020+
localScriptPath);
9761021

977-
// If this is a remote connection and the debugger stopped at a line
978-
// in a script file, get the file contents
979-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
980-
&& _remoteFileManager is not null
981-
&& !noScriptName)
1022+
// Notify the host that the debugger is stopped.
1023+
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
1024+
}
1025+
catch (OperationCanceledException)
9821026
{
983-
localScriptPath =
984-
await _remoteFileManager.FetchRemoteFileAsync(
985-
e.InvocationInfo.ScriptName,
986-
_psesHost.CurrentRunspace).ConfigureAwait(false);
1027+
// Ignore, likely means that a remote runspace has closed.
9871028
}
988-
989-
if (stackFrameDetails.Length > 0)
1029+
catch (Exception exception)
9901030
{
991-
// Augment the top stack frame with details from the stop event
992-
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
993-
{
994-
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
995-
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
996-
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
997-
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
998-
}
1031+
// Log in a catch all so we don't crash the process.
1032+
_logger.LogError(
1033+
exception,
1034+
"Error occurred while obtaining debug info. Message: {message}",
1035+
exception.Message);
9991036
}
1000-
1001-
CurrentDebuggerStoppedEventArgs =
1002-
new DebuggerStoppedEventArgs(
1003-
e,
1004-
_psesHost.CurrentRunspace,
1005-
localScriptPath);
1006-
1007-
// Notify the host that the debugger is stopped.
1008-
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
10091037
}
10101038

10111039
private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs)

0 commit comments

Comments
 (0)