8000 Implementation for Invoke-Command step-in remote debugging (#3015) · PowerShell/PowerShell@b4cb5e9 · GitHub
[go: up one dir, main page]

Skip to content

Commit b4cb5e9

Browse files
PaulHigindaxian-dbw
authored andcommitted
Implementation for Invoke-Command step-in remote debugging (#3015)
These changes provide the ability to debug remote running scripts started with the Invoke-Command cmdlet. The design is event based and provides new public events that allow subscribers to be notified when an Invoke-Command remote session is ready for debugging. Since Invoke-Command allows running scripts on multiple targets at once (fan-out) the notification event is raised for each remote session as it becomes ready for debugging. The subscriber to these events will be a script debugger implementation (such as PowerShell console, ISE, or VSCode) and will handle all debugging details such as simultaneously debugging multiple remote sessions at once in separate windows. But these changes also include an internal implementation which is used by default if host debuggers don't want to handle the debugging details. This internal implementation is what PowerShell console, ISE uses so they can have this new behavior without having to modify their debugger implementations. The internal implementation serializes each remote session of Invoke-Command so that they can be debugged one at a time. The remote session debugger is "pushed" onto the internal debugger stack so that debugging transitions to the remote session. Existing debugging commands work so that the "quit" debugging command will stop the current remote session script from running and allow the next remote session to be debugged. Similarly the "continue" debugging command allows the script to continue running outside step mode and again go to the next remote session for debugging. The "stepout" debugging command steps out of all Invoke-Command remote sessions and lets the script continue to run for each remote session in parallel as they are normally run. The purpose of Invoke-Command step-in remote debugging is allow seamless debugging of a local script that calls Invoke-Command on remote targets. But there is also a new Invoke-Command "-RemoteDebug" parameter that lets you Invoke-Command on the command line and have it drop directly into the debugger. An example from the PowerShell command line looks like this: ``` PS C:\> C:\TestICM.ps1 Entering debug mode. Use h or ? for help. Hit Command breakpoint on 'Invoke-Command' At C:\TestICM.ps1:2 char:1 + Invoke-Command -cn $computerName,paulhig-3 -File c:\LinuxScript.ps1 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [DBG]: PS C:\>> list 1: $computerName = "localhost" 2:* Invoke-Command -cn $computerName,paulhig-3 -File c:\LinuxScript.ps1 3: "Test Complete!" [DBG]: PS C:\>> stepin At line:1 char:1 + Write-Output "Running script on Linux!" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [paulhig-3]:[DBG]: [Process:14072]: [Runspace5]: PS C:\Users\paulhi\Documents> ``` Notice that the debugger "stepin" command transitioned from local script debugging to debugging the remote session on computer "paulhig-3", as can be seen by the change in the debugger prompt. You can also do this from the command line to drop directly into the debugger ``` Invoke-Command -cn localhost -Script $scriptblock -RemoteDebug ``` These changes also remove an old behavior that was incompatible with this new step-in feature. Previously if a remote session running script hit a break point it would stop in the debugger and go to the "disconnected session" state. This was to allow the user to reconnect using Enter-PSSession and then interactively debug the remote session script. This behavior has been removed and now the user needs to attach a debugger using the newer Debug-Runspace cmdlet.
1 parent 875ea10 commit b4cb5e9

File tree

9 files changed

+842
-132
lines changed

9 files changed

+842
-132
lines changed

src/System.Management.Automation/engine/debugger/debugger.cs

Lines changed: 427 additions & 28 deletions
Large diffs are not rendered by default.

src/System.Management.Automation/engine/remoting/client/Job.cs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2803,15 +2803,6 @@ internal void ConnectAsync()
28032803
_remotePipeline.ConnectAsync();
28042804
}
28052805

2806-
/// <summary>
2807-
/// Removes job data aggregation callbacks. Used for jobs
2808-
/// stopped in debugger so that debugger can access data.
2809-
/// </summary>
2810-
internal void RemoveJobAggregation()
2811-
{
2812-
RemoveAggreateCallbacksFromHelper(Helper);
2813-
}
2814-
28152806
#endregion
28162807

28172808
#region stop
@@ -3077,32 +3068,6 @@ protected void HandleURIDirectionReported(object sender, RemoteDataEventArgs<Uri
30773068
this.WriteWarning(message);
30783069
}
30793070

3080-
/// <summary>
3081-
/// Used to detect an Invoke-Command running command breakpoint hit.
3082-
/// In this case disconnect the runspace so that a debugger can be
3083-
/// attached later by the user.
3084-
/// </summary>
3085-
/// <param name="sender"></param>
3086-
/// <param name="e"></param>
3087-
protected void HandleRunspaceAvailabilityChangedForInvoke(object sender, RunspaceAvailabilityEventArgs e)
3088-
{
3089-
RemoteRunspace remoteRunspace = sender as RemoteRunspace;
3090-
if (remoteRunspace != null &&
3091-
e.RunspaceAvailability == RunspaceAvailability.RemoteDebug)
3092-
{
3093-
remoteRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke;
3094-
3095-
try
3096-
{
3097-
remoteRunspace.DisconnectAsync();
3098-
}
3099-
catch (PSNotImplementedException) { }
3100-
catch (InvalidRunspacePoolStateException) { }
3101-
catch (InvalidRunspaceStateException) { }
3102-
catch (PSInvalidOperationException) { }
3103-
}
3104-
}
3105-
31063071
/// <summary>
31073072
/// Handle method executor stream events.
31083073
/// </summary>
@@ -3146,10 +3111,6 @@ protected virtual void HandlePipelineStateChanged(object sender, PipelineStateEv
31463111
// since we got state changed event..we dont need to listen on
31473112
// URI redirections anymore
31483113
((RemoteRunspace)Runspace).URIRedirectionReported -= HandleURIDirectionReported;
3149-
3150-
// We monitor runspace RemoteDebug availability only while
3151-
// this pipeline is running.
3152-
((RemoteRunspace)Runspace).AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke;
31533114
}
31543115

31553116
PipelineState state = e.PipelineStateInfo.State;
@@ -3628,12 +3589,6 @@ private void HandleInformationAdded(object sender, DataAddedEventArgs eventArgs)
36283589
/// aggregation has to be stopped</param>
36293590
protected void StopAggregateResultsFromHelper(ExecutionCmdletHelper helper)
36303591
{
3631-
// Ensure the Runspace availability handler is removed on command completion.
3632-
if (helper.PipelineRunspace != null)
3633-
{
3634-
helper.PipelineRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke;
3635-
}
3636-
36373592
// Get the pipeline associated with this helper and register for appropriate events
36383593
RemoveAggreateCallbacksFromHelper(helper);
36393594

@@ -4123,7 +4078,6 @@ internal PSInvokeExpressionSyncJob(List<IThrottleOperation> operations, Throttle
41234078
RemoteRunspace remoteRS = helper.Pipeline.Runspace as RemoteRunspace;
41244079
if (null != remoteRS)
41254080
{
4126-
remoteRS.AvailabilityChanged += HandleRunspaceAvailabilityChangedForInvoke;
41274081
remoteRS.StateChanged += HandleRunspaceStateChanged;
41284082

41294083
if (remoteRS.RunspaceStateInfo.State == RunspaceState.BeforeOpen)
@@ -4357,7 +4311,6 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs e)
43574311
if (e.RunspaceStateInfo.State != RunspaceState.Opened)
43584312
{
43594313
remoteRS.StateChanged -= HandleRunspaceStateChanged;
4360-
remoteRS.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke;
43614314
}
43624315
}
43634316
}

src/System.Management.Automation/engine/remoting/client/remoterunspace.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,7 +1351,7 @@ private bool SetDebugInfo(PSPrimitiveDictionary psApplicationPrivateData)
13511351
var psVersionTable = psApplicationPrivateData[PSVersionInfo.PSVersionTableName] as PSPrimitiveDictionary;
13521352
if (psVersionTable.ContainsKey(PSVersionInfo.PSVersionName))
13531353
{
1354-
ServerVersion = psVersionTable[PSVersionInfo.PSVersionName] as Version;
1354+
ServerVersion = PSObject.Base(psVersionTable[PSVersionInfo.PSVersionName]) as Version;
13551355
}
13561356
}
13571357
}
@@ -2435,7 +2435,7 @@ private void ProcessDebuggerStopEventProc(object state)
24352435
finally
24362436
{
24372437
_handleDebuggerStop = false;
2438-
if (!_detachCommand)
2438+
if (!_detachCommand && !args.SuspendRemote)
24392439
{
24402440
SetDebuggerAction(args.ResumeAction);
24412441
}
@@ -2468,7 +2468,7 @@ private void ProcessDebuggerStopEventProc(object state)
24682468
finally
24692469
{
24702470
// Restore runspace availability.
2471-
if (restoreAvailability)
2471+
if (restoreAvailability && (_runspace.RunspaceAvailability == RunspaceAvailability.RemoteDebug))
24722472
{
24732473
SetRemoteDebug(false, prevAvailability);
24742474
}

src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,35 @@ public override Hashtable[] SSHConnection
739739

740740
#endregion
741741

742+
#region Remote Debug Parameters
743+
744+
/// <summary>
745+
/// When selected this parameter causes a debugger Step-Into action for each running remote session.
746+
/// </summary>
747+
[Parameter(ParameterSetName = InvokeCommandCommand.ComputerNameParameterSet)]
748+
[Parameter(ParameterSetName = InvokeCommandCommand.SessionParameterSet)]
749+
[Parameter(ParameterSetName = InvokeCommandCommand.UriParameterSet)]
750+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathComputerNameParameterSet)]
751+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathSessionParameterSet)]
752+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathUriParameterSet)]
753+
[Parameter(ParameterSetName = InvokeCommandCommand.VMIdParameterSet)]
754+
[Parameter(ParameterSetName = InvokeCommandCommand.VMNameParameterSet)]
755+
[Parameter(ParameterSetName = InvokeCommandCommand.ContainerIdParameterSet)]
756+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathVMIdParameterSet)]
757+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathVMNameParameterSet)]
758+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathContainerIdParameterSet)]
759+
[Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)]
760+
[Parameter(ParameterSetName = InvokeCommandCommand.SSHHostHashParameterSet)]
761+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)]
762+
[Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostHashParameterSet)]
763+
public virtual SwitchParameter RemoteDebug
764+
{
765+
get;
766+
set;
767+
}
768+
769+
#endregion
770+
742771
#endregion Parameters
743772

744773
#region Overrides
@@ -767,6 +796,19 @@ protected override void BeginProcessing()
767796
throw new InvalidOperationException(RemotingErrorIdStrings.SessionNameWithoutInvokeDisconnected);
768797
}
769798

799+
// Adjust RemoteDebug value based on current state
800+
var hostDebugger = GetHostDebugger();
801+
if (hostDebugger == null)
802+
{
803+
// Do not allow RemoteDebug if there is no host debugger available. Otherwise script will hang indefinitely.
804+
RemoteDebug = false;
805+
}
806+
else if (hostDebugger.IsDebuggerSteppingEnabled)
807+
{
808+
// If host debugger is in step-in mode then always make RemoteDebug true
809+
RemoteDebug = true;
810+
}
811+
770812
// Checking session's availability and reporting errors in early stage, unless '-AsJob' is specified.
771813
// When '-AsJob' is specified, Invoke-Command should return a job object without throwing error, even
772814
// if the session is not in available state -- this is the PSv3 behavior and we should not break it.
@@ -985,7 +1027,7 @@ protected override void ProcessRecord()
9851027
_inputStreamClosed = true;
9861028
}
9871029

988-
if (!ParameterSetName.Equals("InProcess"))
1030+
if (!ParameterSetName.Equals(InProcParameterSet))
9891031
{
9901032
// at this point there is nothing to do for
9911033
// inproc case. The script block is executed
@@ -1209,6 +1251,17 @@ protected override void EndProcessing()
12091251
/// </remarks>
12101252
protected override void StopProcessing()
12111253
{
1254+
// Ensure that any runspace debug processing is ended
1255+
var hostDebugger = GetHostDebugger();
1256+
if (hostDebugger != null)
1257+
{
1258+
try
1259+
{
1260+
hostDebugger.CancelDebuggerProcessing();
1261+
}
1262+
catch (PSNotImplementedException) { }
1263+
}
1264+
12121265
if (!ParameterSetName.Equals(InvokeCommandCommand.InProcParameterSet))
12131266
{
12141267
if (!_asjob)
@@ -1247,6 +1300,20 @@ protected override void StopProcessing()
12471300

12481301
#region Private Methods
12491302

1303+
private Debugger GetHostDebugger()
1304+
{
1305+
Debugger hostDebugger = null;
1306+
try
1307+
{
1308+
System.Management.Automation.Internal.Host.InternalHost chost =
1309+
this.Host as System.Management.Automation.Internal.Host.InternalHost;
1310+
hostDebugger = chost.Runspace.Debugger;
1311+
}
1312+
catch (PSNotImplementedException) { }
1313+
1314+
return hostDebugger;
1315+
}
1316+
12501317
/// <summary>
12511318
/// Handle event from the throttle manager indicating that all
12521319
/// operations are complete
@@ -1298,11 +1365,32 @@ private void CreateAndRunSyncJob()
12981365
// Add robust connection retry notification handler.
12991366
AddConnectionRetryHandler(_job);
13001367

1368+
// Enable all Invoke-Command synchronous jobs for remote debugging (in case Wait-Debugger or
1369+
// or line breakpoints are set in script).
1370+
foreach (var operation in Operations)
1371+
{
1372+
operation.RunspaceDebuggingEnabled = true;
1373+
operation.RunspaceDebugStepInEnabled = RemoteDebug;
1374+
operation.RunspaceDebugStop += HandleRunspaceDebugStop;
1375+
}
1376+
13011377
_job.StartOperations(Operations);
13021378
}
13031379
}
13041380
}
13051381

1382+
private void HandleRunspaceDebugStop(object sender, StartRunspaceDebugProcessingEventArgs args)
1383+
{
1384+
var operation = sender as IThrottleOperation;
1385+
operation.RunspaceDebugStop -= HandleRunspaceDebugStop;
1386+
1387+
var hostDebugger = GetHostDebugger();
1388+
if (hostDebugger != null)
1389+
{
1390+
hostDebugger.QueueRunspaceForDebug(args.Runspace);
1391+
}
1392+
}
1393+
13061394
private void HandleJobStateChanged(object sender, JobStateEventArgs e)
13071395
{
13081396
JobState state = e.JobStateInfo.State;
@@ -1617,8 +1705,6 @@ private void WriteJobResults(bool nonblocking)
16171705
// pipelines.
16181706
_asjob = true;
16191707

1620-
List<Job> removedDebugStopJobs = new List<Job>();
1621-
16221708
// Write warnings to user about each disconnect.
16231709
foreach (var cjob in rtnJob.ChildJobs)
16241710
{
@@ -1629,41 +1715,17 @@ private void WriteJobResults(bool nonblocking)
16291715
PSSession session = GetPSSession(childJob.Runspace.InstanceId);
16301716
if (session != null)
16311717
{
1632-
RemoteDebugger remoteDebugger = session.Runspace.Debugger as RemoteDebugger;
1633-
if (remoteDebugger != null &&
1634-
remoteDebugger.IsRemoteDebug)
1635-
{
1636-
// The session was disconnected because it hit a debug breakpoint.
1718+
// Write network failed, auto-disconnect error
1719+
WriteNetworkFailedError(session);
16371720

1638-
// Remove child job data aggregation so debugger can show data.
1639-
childJob.RemoveJobAggregation();
1640-
removedDebugStopJobs.Add(childJob);
1641-
1642-
// Write appropriate warning.
1643-
WriteWarning(
1644-
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectDebug,
1721+
// Session disconnected message.
1722+
WriteWarning(
1723+
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession,
16451724
session.Name, session.InstanceId, session.ComputerName));
1646-
}
1647-
else
1648-
{
1649-
// Write network failed, auto-disconnect error
1650-
WriteNetworkFailedError(session);
1651-
1652-
// Session disconnected message.
1653-
WriteWarning(
1654-
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession,
1655-
session.Name, session.InstanceId, session.ComputerName));
1656-
}
16571725
}
16581726
}
16591727
}
16601728

1661-
// Remove debugger stopped jobs
1662-
foreach (var dJob in removedDebugStopJobs)
1663-
{
1664-
rtnJob.ChildJobs.Remove(dJob);
1665-
}
1666-
16671729
if (rtnJob.ChildJobs.Count > 0)
16681730
{
16691731
JobRepository.Add(rtnJob);
@@ -1686,25 +1748,13 @@ private void WriteJobResults(bool nonblocking)
16861748
// Add to session repository.
16871749
this.RunspaceRepository.AddOrReplace(session);
16881750

1689-
RemoteRunspace remoteRunspace = session.Runspace as RemoteRunspace;
1690-
if (remoteRunspace != null &&
1691-
remoteRunspace.RunspacePool.RemoteRunspacePoolInternal.IsRemoteDebugStop)
1692-
{
1693-
// The session was disconnected because it hit a debug breakpoint.
1694-
WriteWarning(
1695-
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectDebug,
1696-
session.Name, session.InstanceId, session.ComputerName));
1697-
}
1698-
else
1699-
{
1700-
// Write network failed, auto-disconnect error
1701-
WriteNetworkFailedError(session);
1751+
// Write network failed, auto-disconnect error
1752+
WriteNetworkFailedError(session);
17021753

1703-
// Session disconnected message.
1704-
WriteWarning(
1705-
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession,
1706-
session.Name, session.InstanceId, session.ComputerName));
1707-
}
1754+
// Session disconnected message.
1755+
WriteWarning(
1756+
StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession,
1757+
session.Name, session.InstanceId, session.ComputerName));
17081758

17091759
// Session created message.
17101760
WriteWarning(

0 commit comments

Comments
 (0)
0