Skip to content

Commit 843d66d

Browse files
Fix attach to process debugging (#1752)
* Fix an occassional dead lock on restarting debug * Fix attach to process and other debugging issues * Fixes #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 * Use FilterText instead of Label for Assert * If multiple Write-Host's exist then this test will fail. FilterText contains the module qualified name when dupes are present. * Use Contains instead of Collection in assert * If the testing machine has a pester template installed the collection will have an extra item. * Add some safety to disposals * Skip CLM E2E tests in unelevated process * Add preset ExecutionOptions.ImmediateInteractive * Cleanup extra null conditionals * Add comment describing disconnect code path * Add `Is*` properties for context frame types * Use the new `Is*` props in a few more places * Fix new analyzers
1 parent b703dee commit 843d66d

16 files changed

+707
-155
lines changed

Diff for: PowerShellEditorServices.build.ps1

+5
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ task TestE2E Build, SetupHelpForTests, {
202202

203203
# Run E2E tests in ConstrainedLanguage mode.
204204
if (!$script:IsNix) {
205+
if (-not [Security.Principal.WindowsIdentity]::GetCurrent().Owner.IsWellKnown("BuiltInAdministratorsSid")) {
206+
Write-Warning 'Skipping E2E CLM tests as they must be ran in an elevated process.'
207+
return
208+
}
209+
205210
try {
206211
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine);
207212
exec { & dotnet $script:dotnetTestArgs $script:NetRuntime.PS7 }

Diff for: src/PowerShellEditorServices/Server/PsesDebugServer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public void Dispose()
121121
// It represents the debugger on the PowerShell process we're in,
122122
// while a new debug server is spun up for every debugging session
123123
_psesHost.DebugContext.IsDebugServerActive = false;
124-
_debugAdapterServer.Dispose();
124+
_debugAdapterServer?.Dispose();
125125
_inputStream.Dispose();
126126
_outputStream.Dispose();
127127
_loggerFactory.Dispose();

Diff for: 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);

Diff for: src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+97-69
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
{
@@ -771,22 +783,23 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
771783
const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
772784
const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";
773785

786+
_psesHost.Runspace.ThrowCancelledIfUnusable();
774787
// If we're attached to a remote runspace, we need to serialize the list prior to
775788
// transport because the default depth is too shallow. From testing, we determined the
776-
// correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we
777-
// just return its results. On a remote machine we serialize it first and then later
789+
// correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we
790+
// just return its results. In a remote runspace we serialize it first and then later
778791
// deserialize it.
779-
bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine;
780-
string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
792+
bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote;
793+
string returnSerializedIfInRemoteRunspace = isRemoteRunspace
781794
? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
782795
: callStackVarName;
783796

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

789-
IEnumerable callStack = isOnRemoteMachine
802+
IEnumerable callStack = isRemoteRunspace
790803
? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList
791804
: results;
792805

@@ -797,7 +810,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
797810
// We have to use reflection to get the variable dictionary.
798811
IList callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList;
799812
PSObject callStackFrame = callStackFrameComponents[0] as PSObject;
800-
IDictionary callStackVariables = isOnRemoteMachine
813+
IDictionary callStackVariables = isRemoteRunspace
801814
? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary
802815
: callStackFrameComponents[1] as IDictionary;
803816

@@ -861,7 +874,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
861874
{
862875
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
863876
}
864-
else if (isOnRemoteMachine
877+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
865878
&& _remoteFileManager is not null
866879
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
867880
{
@@ -905,83 +918,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref
905918

906919
internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
907920
{
908-
bool noScriptName = false;
909-
string localScriptPath = e.InvocationInfo.ScriptName;
910-
911-
// If there's no ScriptName, get the "list" of the current source
912-
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
921+
try
913922
{
914-
// Get the current script listing and create the buffer
915-
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
923+
bool noScriptName = false;
924+
string localScriptPath = e.InvocationInfo.ScriptName;
916925

917-
IReadOnlyList<PSObject> scriptListingLines =
918-
await _executionService.ExecutePSCommandAsync<PSObject>(
919-
command, CancellationToken.None).ConfigureAwait(false);
920-
921-
if (scriptListingLines is not null)
926+
// If there's no ScriptName, get the "list" of the current source
927+
if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath))
922928
{
923-
int linePrefixLength = 0;
929+
// Get the current script listing and create the buffer
930+
PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}");
924931

925-
string scriptListing =
926-
string.Join(
927-
Environment.NewLine,
928-
scriptListingLines
929-
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
930-
.Where(s => s is not null));
932+
IReadOnlyList<PSObject> scriptListingLines =
933+
await _executionService.ExecutePSCommandAsync<PSObject>(
934+
command, CancellationToken.None).ConfigureAwait(false);
931935

932-
temporaryScriptListingPath =
933-
_remoteFileManager.CreateTemporaryFile(
934-
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
935-
scriptListing,
936-
_psesHost.CurrentRunspace);
936+
if (scriptListingLines is not null)
937+
{
938+
int linePrefixLength = 0;
939+
940+
string scriptListing =
941+
string.Join(
942+
Environment.NewLine,
943+
scriptListingLines
944+
.Select(o => TrimScriptListingLine(o, ref linePrefixLength))
945+
.Where(s => s is not null));
946+
947+
temporaryScriptListingPath =
948+
_remoteFileManager.CreateTemporaryFile(
949+
$"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}",
950+
scriptListing,
951+
_psesHost.CurrentRunspace);
952+
953+
localScriptPath =
954+
temporaryScriptListingPath
955+
?? StackFrameDetails.NoFileScriptPath;
956+
957+
noScriptName = localScriptPath is not null;
958+
}
959+
else
960+
{
961+
_logger.LogWarning("Could not load script context");
962+
}
963+
}
937964

938-
localScriptPath =
939-
temporaryScriptListingPath
940-
?? StackFrameDetails.NoFileScriptPath;
965+
// Get call stack and variables.
966+
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
941967

942-
noScriptName = localScriptPath is not null;
968+
// If this is a remote connection and the debugger stopped at a line
969+
// in a script file, get the file contents
970+
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
971+
&& _remoteFileManager is not null
972+
&& !noScriptName)
973+
{
974+
localScriptPath =
975+
await _remoteFileManager.FetchRemoteFileAsync(
976+
e.InvocationInfo.ScriptName,
977+
_psesHost.CurrentRunspace).ConfigureAwait(false);
943978
}
944-
else
979+
980+
if (stackFrameDetails.Length > 0)
945981
{
946-
_logger.LogWarning("Could not load script context");
982+
// Augment the top stack frame with details from the stop event
983+
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
984+
{
985+
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
986+
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
987+
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
988+
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
989+
}
947990
}
948-
}
949991

950-
// Get call stack and variables.
951-
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
992+
CurrentDebuggerStoppedEventArgs =
993+
new DebuggerStoppedEventArgs(
994+
e,
995+
_psesHost.CurrentRunspace,
996+
localScriptPath);
952997

953-
// If this is a remote connection and the debugger stopped at a line
954-
// in a script file, get the file contents
955-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
956-
&& _remoteFileManager is not null
957-
&& !noScriptName)
998+
// Notify the host that the debugger is stopped.
999+
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
1000+
}
1001+
catch (OperationCanceledException)
9581002
{
959-
localScriptPath =
960-
await _remoteFileManager.FetchRemoteFileAsync(
961-
e.InvocationInfo.ScriptName,
962-
_psesHost.CurrentRunspace).ConfigureAwait(false);
1003+
// Ignore, likely means that a remote runspace has closed.
9631004
}
964-
965-
if (stackFrameDetails.Length > 0)
1005+
catch (Exception exception)
9661006
{
967-
// Augment the top stack frame with details from the stop event
968-
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
969-
{
970-
stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber;
971-
stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber;
972-
stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber;
973-
stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber;
974-
}
1007+
// Log in a catch all so we don't crash the process.
1008+
_logger.LogError(
1009+
exception,
1010+
"Error occurred while obtaining debug info. Message: {message}",
1011+
exception.Message);
9751012
}
976-
977-
CurrentDebuggerStoppedEventArgs =
978-
new DebuggerStoppedEventArgs(
979-
e,
980-
_psesHost.CurrentRunspace,
981-
localScriptPath);
982-
983-
// Notify the host that the debugger is stopped.
984-
DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs);
9851013
}
9861014

9871015
private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs) => CurrentDebuggerStoppedEventArgs = null;

0 commit comments

Comments
 (0)