diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index 4d9809acf76..d8338324ba9 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -104,6 +104,13 @@ public DebuggerStopEventArgs( /// DebuggerAction.StepToLine is only valid when debugging an script. /// public DebuggerResumeAction ResumeAction { get; set; } + + /// + /// This property is used internally for remote debug stops only. It is used to signal the remote debugger proxy + /// that it should *not* send a resume action to the remote debugger. This is used by runspace debug processing to + /// leave pending runspace debug sessions suspended until a debugger is attached. + /// + internal bool SuspendRemote { get; set; } }; /// @@ -253,6 +260,70 @@ private DebugSource() { } #endregion + #region Runspace Debug Processing + + /// + /// StartRunspaceDebugProcessing event arguments + /// + public sealed class StartRunspaceDebugProcessingEventArgs : EventArgs + { + /// The runspace to process + public Runspace Runspace + { + get; + private set; + } + + /// + /// When set to true this will cause PowerShell to process this runspace debug session through its + /// script debugger. To use the default processing return from this event call after setting + /// this property to true. + /// + public bool UseDefaultProcessing + { + get; + set; + } + + /// + /// Constructor + /// + public StartRunspaceDebugProcessingEventArgs(Runspace runspace) + { + if (runspace == null) { throw new PSArgumentNullException("runspace"); } + + Runspace = runspace; + } + } + + /// + /// ProcessRunspaceDebugEnd event arguments + /// + public sealed class ProcessRunspaceDebugEndEventArgs : EventArgs + { + /// + /// The runspace where internal debug processing has ended + /// + public Runspace Runspace + { + get; + private set; + } + + /// + /// Constructor + /// + /// + public ProcessRunspaceDebugEndEventArgs(Runspace runspace) + { + if (runspace == null) { throw new PSArgumentNullException("runspace"); } + + Runspace = runspace; + } + } + + #endregion + #endregion #region Enums @@ -327,6 +398,26 @@ public abstract class Debugger /// internal event EventHandler NestedDebuggingCancelledEvent; + #region Runspace Debug Processing Events + + /// + /// Event raised when a runspace debugger needs breakpoint processing. + /// + public event EventHandler StartRunspaceDebugProcessing; + + /// + /// Event raised when a runspace debugger is finished being processed. + /// + public event EventHandler RunspaceDebugProcessingCompleted; + + /// + /// Event raised to indicate that the debugging session is over and runspace debuggers queued for + /// processing should be released. + /// + public event EventHandler CancelRunspaceDebugProcessing; + + #endregion + #endregion #region Properties @@ -470,6 +561,36 @@ protected bool IsDebuggerBreakpointUpdatedEventSubscribed() return (BreakpointUpdated != null); } + #region Runspace Debug Processing + + /// + protected void RaiseStartRunspaceDebugProcessingEvent(StartRunspaceDebugProcessingEventArgs args) + { + if (args == null) { throw new PSArgumentNullException("args"); } + StartRunspaceDebugProcessing.SafeInvoke(this, args); + } + + /// + protected void RaiseRunspaceProcessingCompletedEvent(ProcessRunspaceDebugEndEventArgs args) + { + if (args == null) { throw new PSArgumentNullException("args"); } + RunspaceDebugProcessingCompleted.SafeInvoke(this, args); + } + + /// + protected bool IsStartRunspaceDebugProcessingEventSubscribed() + { + return (StartRunspaceDebugProcessing != null); + } + + /// + protected void RaiseCancelRunspaceDebugProcessingEvent() + { + CancelRunspaceDebugProcessing.SafeInvoke(this, null); + } + + #endregion + #endregion #region Public Methods @@ -712,6 +833,30 @@ internal void RaiseNestedDebuggingCancelEvent() #endregion + #region Runspace Debug Processing Methods + + /// + /// Adds the provided Runspace object to the runspace debugger processing queue. + /// The queue will then raise the StartRunspaceDebugProcessing events for each runspace to allow + /// a host script debugger implementation to provide an active debugging session. + /// + /// Runspace to debug + internal virtual void QueueRunspaceForDebug(Runspace runspace) + { + throw new PSNotImplementedException(); + } + + /// + /// Causes the CancelRunspaceDebugProcessing event to be raised which notifies subscribers that current debugging + /// sessions should be cancelled. + /// + public virtual void CancelDebuggerProcessing() + { + throw new PSNotImplementedException(); + } + + #endregion + #region Members internal const string CannotProcessCommandNotStopped = "Debugger:CannotProcessCommandNotStopped"; @@ -1575,6 +1720,11 @@ internal void Clear() private bool _preserveUnhandledDebugStopEvent; private ManualResetEventSlim _preserveDebugStopEvent; + // Process runspace debugger + private Lazy> _runspaceDebugQueue = new Lazy>(); + private volatile Int32 _processingRunspaceDebugQueue; + private ManualResetEventSlim _runspaceDebugCompleteEvent; + private static readonly string s_processDebugPromptMatch; #endregion private members @@ -2634,6 +2784,95 @@ internal override void StopDebugRunspace(Runspace runspace) #endregion + #region Runspace Debug Processing + + /// + /// Adds the provided Runspace object to the runspace debugger processing queue. + /// The queue will then raise the StartRunspaceDebugProcessing events for each runspace to allow + /// a host script debugger implementation to provide an active debugging session. + /// + /// Runspace to debug + internal override void QueueRunspaceForDebug(Runspace runspace) + { + runspace.StateChanged += RunspaceStateChangedHandler; + runspace.AvailabilityChanged += RunspaceAvailabilityChangedHandler; + _runspaceDebugQueue.Value.Enqueue(new StartRunspaceDebugProcessingEventArgs(runspace)); + StartRunspaceForDebugQueueProcessing(); + } + + /// + /// Causes the CancelRunspaceDebugProcessing event to be raised which notifies subscribers that these debugging + /// sessions should be cancelled. + /// + public override void CancelDebuggerProcessing() + { + // Empty runspace debugger processing queue and then notify any subscribers. + ReleaseInternalRunspaceDebugProcessing(null, true); + + try + { + RaiseCancelRunspaceDebugProcessingEvent(); + } + catch (Exception) + { } + } + + private void ReleaseInternalRunspaceDebugProcessing(object sender, bool emptyQueue = false) + { + Runspace runspace = sender as Runspace; + if (runspace != null) + { + runspace.StateChanged -= RunspaceStateChangedHandler; + runspace.AvailabilityChanged -= RunspaceAvailabilityChangedHandler; + } + + if (emptyQueue && _runspaceDebugQueue.IsValueCreated) + { + StartRunspaceDebugProcessingEventArgs args; + while (_runspaceDebugQueue.Value.TryDequeue(out args)) + { + args.Runspace.StateChanged -= RunspaceStateChangedHandler; + args.Runspace.AvailabilityChanged -= RunspaceAvailabilityChangedHandler; + try + { + args.Runspace.Debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Ignore; + } + catch (Exception) { } + } + } + + if (_runspaceDebugCompleteEvent != null) + { + try + { + _runspaceDebugCompleteEvent.Set(); + } + catch (ObjectDisposedException) { } + } + } + + private void RunspaceStateChangedHandler(object sender, RunspaceStateEventArgs args) + { + switch (args.RunspaceStateInfo.State) + { + case RunspaceState.Closed: + case RunspaceState.Broken: + case RunspaceState.Disconnected: + ReleaseInternalRunspaceDebugProcessing(sender); + break; + } + } + + private void RunspaceAvailabilityChangedHandler(object sender, RunspaceAvailabilityEventArgs args) + { + if (args.RunspaceAvailability == RunspaceAvailability.Available) + { + ReleaseInternalRunspaceDebugProcessing(sender); + } + } + + #endregion + #endregion #region Job debugger integration @@ -3037,6 +3276,7 @@ private void HandleMonitorRunningJobsDebuggerStop(object sender, DebuggerStopEve } Debugger senderDebugger = sender as Debugger; + bool pushSucceeded = false; lock (_syncActiveDebuggerStopObject) { Debugger activeDebugger = null; @@ -3055,11 +3295,14 @@ private void HandleMonitorRunningJobsDebuggerStop(object sender, DebuggerStopEve } } - if (PushActiveDebugger(senderDebugger, _jobCallStackOffset)) - { - // Forward the debug stop event. - HandleActiveJobDebuggerStop(sender, args); - } + pushSucceeded = PushActiveDebugger(senderDebugger, _jobCallStackOffset); + } + + // Handle debugger stop outside lock. + if (pushSucceeded) + { + // Forward the debug stop event. + HandleActiveJobDebuggerStop(sender, args); } } @@ -3436,6 +3679,7 @@ private void HandleMonitorRunningRSDebuggerStop(object sender, DebuggerStopEvent if (sender == null || args == null) { return; } Debugger senderDebugger = sender as Debugger; + bool pushSucceeded = false; lock (_syncActiveDebuggerStopObject) { Debugger activeDebugger; @@ -3491,12 +3735,15 @@ private void HandleMonitorRunningRSDebuggerStop(object sender, DebuggerStopEvent args.InvocationInfo = nestedDebugger.FixupInvocationInfo(args.InvocationInfo); // Finally push the runspace debugger. - if (PushActiveDebugger(senderDebugger, _runspaceCallStackOffset)) - { - // Forward the debug stop event. - // This method will always pop the debugger after debugger stop completes. - HandleActiveRunspaceDebuggerStop(sender, args); - } + pushSucceeded = PushActiveDebugger(senderDebugger, _runspaceCallStackOffset); + } + + // Handle debugger stop outside lock. + if (pushSucceeded) + { + // Forward the debug stop event. + // This method will always pop the debugger after debugger stop completes. + HandleActiveRunspaceDebuggerStop(sender, args); } } @@ -3588,6 +3835,125 @@ private bool SetUpDebuggerOnRunspace(Runspace runspace) #endregion + #region Runspace Debug Processing + + private void StartRunspaceForDebugQueueProcessing() + { + Int32 startThread = Interlocked.CompareExchange(ref _processingRunspaceDebugQueue, 1, 0); + + if (startThread == 0) + { + var thread = new System.Threading.Thread( + new ThreadStart(DebuggerQueueThreadProc)); + thread.Start(); + } + } + + private void DebuggerQueueThreadProc() + { + StartRunspaceDebugProcessingEventArgs runspaceDebugProcessArgs; + while (_runspaceDebugQueue.Value.TryDequeue(out runspaceDebugProcessArgs)) + { + if (IsStartRunspaceDebugProcessingEventSubscribed()) + { + try + { + RaiseStartRunspaceDebugProcessingEvent(runspaceDebugProcessArgs); + } + catch (Exception) { } + } + else + { + // If there are no ProcessDebugger event subscribers then default to handling internally. + runspaceDebugProcessArgs.UseDefaultProcessing = true; + } + + // Check for internal handling request. + if (runspaceDebugProcessArgs.UseDefaultProcessing) + { + try + { + ProcessRunspaceDebugInternally(runspaceDebugProcessArgs.Runspace); + } + catch (Exception) { } + } + } + + Interlocked.CompareExchange(ref _processingRunspaceDebugQueue, 0, 1); + + if (_runspaceDebugQueue.Value.Count > 0) + { + StartRunspaceForDebugQueueProcessing(); + } + } + + private void ProcessRunspaceDebugInternally(Runspace runspace) + { + WaitForReadyDebug(); + + DebugRunspace(runspace); + + // Block this event thread until debugging has ended. + WaitForDebugComplete(); + + // Ensure runspace debugger is not stopped in break mode. + if (runspace.Debugger.InBreakpoint) + { + try + { + runspace.Debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Ignore; + } + catch (Exception) { } + } + + StopDebugRunspace(runspace); + + // If we return to local script execution in step mode then ensure the debugger is enabled. + _nestedDebuggerStop = false; + if ((_steppingMode == SteppingMode.StepIn) && (_currentDebuggerAction != DebuggerResumeAction.Stop) && (_context._debuggingMode == 0)) + { + SetInternalDebugMode(InternalDebugMode.Enabled); + } + + RaiseRunspaceProcessingCompletedEvent( + new ProcessRunspaceDebugEndEventArgs(runspace)); + } + + private void WaitForReadyDebug() + { + // Wait up to ten seconds + System.Threading.Thread.Sleep(500); + int count = 0; + bool debugReady = false; + do + { + System.Threading.Thread.Sleep(250); + debugReady = IsDebuggerReady(); + } while (!debugReady && (count++ < 40)); + + if (!debugReady) { throw new PSInvalidOperationException(); } + } + + private bool IsDebuggerReady() + { + return (!this.IsPushed && !this.InBreakpoint && (this._context._debuggingMode > -1) && (this._context.InternalHost.NestedPromptCount == 0)); + } + + private void WaitForDebugComplete() + { + if (_runspaceDebugCompleteEvent == null) + { + _runspaceDebugCompleteEvent = new ManualResetEventSlim(false); + } + else + { + _runspaceDebugCompleteEvent.Reset(); + } + _runspaceDebugCompleteEvent.Wait(); + } + + #endregion + #region IDisposable /// @@ -3753,6 +4119,7 @@ internal abstract class NestedRunspaceDebugger : Debugger, IDisposable { #region Members + private bool _isDisposed; protected Runspace _runspace; protected Debugger _wrappedDebugger; @@ -3821,6 +4188,8 @@ public NestedRunspaceDebugger( /// DebuggerCommandResults public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataCollection output) { + if (_isDisposed) { return new DebuggerCommandResults(null, false); } + // Preprocess debugger commands. String cmd = command.Commands[0].CommandText.Trim(); @@ -3909,6 +4278,8 @@ public override bool IsActive /// public virtual void Dispose() { + _isDisposed = true; + if (_wrappedDebugger != null) { _wrappedDebugger.BreakpointUpdated -= HandleBreakpointUpdated; @@ -4042,6 +4413,21 @@ internal void CheckStateAndRaiseStopEvent() } } + /// + /// Gets the callstack of the nested runspace. + /// + /// + internal PSDataCollection GetRSCallStack() + { + // Get call stack from wrapped debugger + PSCommand cmd = new PSCommand(); + cmd.AddCommand("Get-PSCallStack"); + PSDataCollection callStackOutput = new PSDataCollection(); + _wrappedDebugger.ProcessCommand(cmd, callStackOutput); + + return callStackOutput; + } + #endregion } @@ -4069,11 +4455,7 @@ public StandaloneRunspaceDebugger( protected override DebuggerCommandResults HandleCallStack(PSDataCollection output) { - // Get call stack from wrapped debugger - PSCommand cmd = new PSCommand(); - cmd.AddCommand("Get-PSCallStack"); - PSDataCollection callStackOutput = new PSDataCollection(); - _wrappedDebugger.ProcessCommand(cmd, callStackOutput); + PSDataCollection callStackOutput = GetRSCallStack(); // Display call stack info as formatted. using (PowerShell ps = PowerShell.Create()) @@ -4087,7 +4469,7 @@ protected override DebuggerCommandResults HandleCallStack(PSDataCollection - /// Removes job data aggregation callbacks. Used for jobs - /// stopped in debugger so that debugger can access data. - /// - internal void RemoveJobAggregation() - { - RemoveAggreateCallbacksFromHelper(Helper); - } - #endregion #region stop @@ -3077,32 +3068,6 @@ protected void HandleURIDirectionReported(object sender, RemoteDataEventArgs - /// Used to detect an Invoke-Command running command breakpoint hit. - /// In this case disconnect the runspace so that a debugger can be - /// attached later by the user. - /// - /// - /// - protected void HandleRunspaceAvailabilityChangedForInvoke(object sender, RunspaceAvailabilityEventArgs e) - { - RemoteRunspace remoteRunspace = sender as RemoteRunspace; - if (remoteRunspace != null && - e.RunspaceAvailability == RunspaceAvailability.RemoteDebug) - { - remoteRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; - - try - { - remoteRunspace.DisconnectAsync(); - } - catch (PSNotImplementedException) { } - catch (InvalidRunspacePoolStateException) { } - catch (InvalidRunspaceStateException) { } - catch (PSInvalidOperationException) { } - } - } - /// /// Handle method executor stream events. /// @@ -3146,10 +3111,6 @@ protected virtual void HandlePipelineStateChanged(object sender, PipelineStateEv // since we got state changed event..we dont need to listen on // URI redirections anymore ((RemoteRunspace)Runspace).URIRedirectionReported -= HandleURIDirectionReported; - - // We monitor runspace RemoteDebug availability only while - // this pipeline is running. - ((RemoteRunspace)Runspace).AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; } PipelineState state = e.PipelineStateInfo.State; @@ -3628,12 +3589,6 @@ private void HandleInformationAdded(object sender, DataAddedEventArgs eventArgs) /// aggregation has to be stopped protected void StopAggregateResultsFromHelper(ExecutionCmdletHelper helper) { - // Ensure the Runspace availability handler is removed on command completion. - if (helper.PipelineRunspace != null) - { - helper.PipelineRunspace.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; - } - // Get the pipeline associated with this helper and register for appropriate events RemoveAggreateCallbacksFromHelper(helper); @@ -4123,7 +4078,6 @@ internal PSInvokeExpressionSyncJob(List operations, Throttle RemoteRunspace remoteRS = helper.Pipeline.Runspace as RemoteRunspace; if (null != remoteRS) { - remoteRS.AvailabilityChanged += HandleRunspaceAvailabilityChangedForInvoke; remoteRS.StateChanged += HandleRunspaceStateChanged; if (remoteRS.RunspaceStateInfo.State == RunspaceState.BeforeOpen) @@ -4357,7 +4311,6 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs e) if (e.RunspaceStateInfo.State != RunspaceState.Opened) { remoteRS.StateChanged -= HandleRunspaceStateChanged; - remoteRS.AvailabilityChanged -= HandleRunspaceAvailabilityChangedForInvoke; } } } diff --git a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs index 0a3c8b05985..5e9cc54a540 100644 --- a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs +++ b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs @@ -1351,7 +1351,7 @@ private bool SetDebugInfo(PSPrimitiveDictionary psApplicationPrivateData) var psVersionTable = psApplicationPrivateData[PSVersionInfo.PSVersionTableName] as PSPrimitiveDictionary; if (psVersionTable.ContainsKey(PSVersionInfo.PSVersionName)) { - ServerVersion = psVersionTable[PSVersionInfo.PSVersionName] as Version; + ServerVersion = PSObject.Base(psVersionTable[PSVersionInfo.PSVersionName]) as Version; } } } @@ -2435,7 +2435,7 @@ private void ProcessDebuggerStopEventProc(object state) finally { _handleDebuggerStop = false; - if (!_detachCommand) + if (!_detachCommand && !args.SuspendRemote) { SetDebuggerAction(args.ResumeAction); } @@ -2468,7 +2468,7 @@ private void ProcessDebuggerStopEventProc(object state) finally { // Restore runspace availability. - if (restoreAvailability) + if (restoreAvailability && (_runspace.RunspaceAvailability == RunspaceAvailability.RemoteDebug)) { SetRemoteDebug(false, prevAvailability); } diff --git a/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs b/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs index 41420e9b957..0b7640c97c3 100644 --- a/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs @@ -739,6 +739,35 @@ public override Hashtable[] SSHConnection #endregion + #region Remote Debug Parameters + + /// + /// When selected this parameter causes a debugger Step-Into action for each running remote session. + /// + [Parameter(ParameterSetName = InvokeCommandCommand.ComputerNameParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.SessionParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.UriParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathComputerNameParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathSessionParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathUriParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.VMIdParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.VMNameParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.ContainerIdParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathVMIdParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathVMNameParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathContainerIdParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.SSHHostParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.SSHHostHashParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostParameterSet)] + [Parameter(ParameterSetName = InvokeCommandCommand.FilePathSSHHostHashParameterSet)] + public virtual SwitchParameter RemoteDebug + { + get; + set; + } + + #endregion + #endregion Parameters #region Overrides @@ -767,6 +796,19 @@ protected override void BeginProcessing() throw new InvalidOperationException(RemotingErrorIdStrings.SessionNameWithoutInvokeDisconnected); } + // Adjust RemoteDebug value based on current state + var hostDebugger = GetHostDebugger(); + if (hostDebugger == null) + { + // Do not allow RemoteDebug if there is no host debugger available. Otherwise script will hang indefinitely. + RemoteDebug = false; + } + else if (hostDebugger.IsDebuggerSteppingEnabled) + { + // If host debugger is in step-in mode then always make RemoteDebug true + RemoteDebug = true; + } + // Checking session's availability and reporting errors in early stage, unless '-AsJob' is specified. // When '-AsJob' is specified, Invoke-Command should return a job object without throwing error, even // 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() _inputStreamClosed = true; } - if (!ParameterSetName.Equals("InProcess")) + if (!ParameterSetName.Equals(InProcParameterSet)) { // at this point there is nothing to do for // inproc case. The script block is executed @@ -1209,6 +1251,17 @@ protected override void EndProcessing() /// protected override void StopProcessing() { + // Ensure that any runspace debug processing is ended + var hostDebugger = GetHostDebugger(); + if (hostDebugger != null) + { + try + { + hostDebugger.CancelDebuggerProcessing(); + } + catch (PSNotImplementedException) { } + } + if (!ParameterSetName.Equals(InvokeCommandCommand.InProcParameterSet)) { if (!_asjob) @@ -1247,6 +1300,20 @@ protected override void StopProcessing() #region Private Methods + private Debugger GetHostDebugger() + { + Debugger hostDebugger = null; + try + { + System.Management.Automation.Internal.Host.InternalHost chost = + this.Host as System.Management.Automation.Internal.Host.InternalHost; + hostDebugger = chost.Runspace.Debugger; + } + catch (PSNotImplementedException) { } + + return hostDebugger; + } + /// /// Handle event from the throttle manager indicating that all /// operations are complete @@ -1298,11 +1365,32 @@ private void CreateAndRunSyncJob() // Add robust connection retry notification handler. AddConnectionRetryHandler(_job); + // Enable all Invoke-Command synchronous jobs for remote debugging (in case Wait-Debugger or + // or line breakpoints are set in script). + foreach (var operation in Operations) + { + operation.RunspaceDebuggingEnabled = true; + operation.RunspaceDebugStepInEnabled = RemoteDebug; + operation.RunspaceDebugStop += HandleRunspaceDebugStop; + } + _job.StartOperations(Operations); } } } + private void HandleRunspaceDebugStop(object sender, StartRunspaceDebugProcessingEventArgs args) + { + var operation = sender as IThrottleOperation; + operation.RunspaceDebugStop -= HandleRunspaceDebugStop; + + var hostDebugger = GetHostDebugger(); + if (hostDebugger != null) + { + hostDebugger.QueueRunspaceForDebug(args.Runspace); + } + } + private void HandleJobStateChanged(object sender, JobStateEventArgs e) { JobState state = e.JobStateInfo.State; @@ -1617,8 +1705,6 @@ private void WriteJobResults(bool nonblocking) // pipelines. _asjob = true; - List removedDebugStopJobs = new List(); - // Write warnings to user about each disconnect. foreach (var cjob in rtnJob.ChildJobs) { @@ -1629,41 +1715,17 @@ private void WriteJobResults(bool nonblocking) PSSession session = GetPSSession(childJob.Runspace.InstanceId); if (session != null) { - RemoteDebugger remoteDebugger = session.Runspace.Debugger as RemoteDebugger; - if (remoteDebugger != null && - remoteDebugger.IsRemoteDebug) - { - // The session was disconnected because it hit a debug breakpoint. + // Write network failed, auto-disconnect error + WriteNetworkFailedError(session); - // Remove child job data aggregation so debugger can show data. - childJob.RemoveJobAggregation(); - removedDebugStopJobs.Add(childJob); - - // Write appropriate warning. - WriteWarning( - StringUtil.Format(RemotingErrorIdStrings.RCDisconnectDebug, + // Session disconnected message. + WriteWarning( + StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession, session.Name, session.InstanceId, session.ComputerName)); - } - else - { - // Write network failed, auto-disconnect error - WriteNetworkFailedError(session); - - // Session disconnected message. - WriteWarning( - StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession, - session.Name, session.InstanceId, session.ComputerName)); - } } } } - // Remove debugger stopped jobs - foreach (var dJob in removedDebugStopJobs) - { - rtnJob.ChildJobs.Remove(dJob); - } - if (rtnJob.ChildJobs.Count > 0) { JobRepository.Add(rtnJob); @@ -1686,25 +1748,13 @@ private void WriteJobResults(bool nonblocking) // Add to session repository. this.RunspaceRepository.AddOrReplace(session); - RemoteRunspace remoteRunspace = session.Runspace as RemoteRunspace; - if (remoteRunspace != null && - remoteRunspace.RunspacePool.RemoteRunspacePoolInternal.IsRemoteDebugStop) - { - // The session was disconnected because it hit a debug breakpoint. - WriteWarning( - StringUtil.Format(RemotingErrorIdStrings.RCDisconnectDebug, - session.Name, session.InstanceId, session.ComputerName)); - } - else - { - // Write network failed, auto-disconnect error - WriteNetworkFailedError(session); + // Write network failed, auto-disconnect error + WriteNetworkFailedError(session); - // Session disconnected message. - WriteWarning( - StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession, - session.Name, session.InstanceId, session.ComputerName)); - } + // Session disconnected message. + WriteWarning( + StringUtil.Format(RemotingErrorIdStrings.RCDisconnectSession, + session.Name, session.InstanceId, session.ComputerName)); // Session created message. WriteWarning( diff --git a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs index 772c3375205..cf94a916d31 100644 --- a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs +++ b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs @@ -3118,6 +3118,49 @@ internal Runspace PipelineRunspace set; get; } + + #region Runspace Debug + + internal void ConfigureRunspaceDebugging(Runspace runspace) + { + if (!RunspaceDebuggingEnabled || (runspace == null) || (runspace.Debugger == null)) { return; } + + runspace.Debugger.DebuggerStop += HandleDebuggerStop; + + // Configure runspace debugger to preserve unhandled stops (wait for debugger attach) + runspace.Debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Wait; + + if (RunspaceDebugStepInEnabled) + { + // Configure runspace debugger to run script in step mode + try + { + runspace.Debugger.SetDebuggerStepMode(true); + } + catch (PSInvalidOperationException) { } + } + } + + internal void CleanupRunspaceDebugging(Runspace runspace) + { + if ((runspace == null) || (runspace.Debugger == null)) { return; } + + runspace.Debugger.DebuggerStop -= HandleDebuggerStop; + } + + private void HandleDebuggerStop(object sender, DebuggerStopEventArgs args) + { + PipelineRunspace.Debugger.DebuggerStop -= HandleDebuggerStop; + + // Forward event + RaiseRunspaceDebugStopEvent(PipelineRunspace); + + // Signal remote session to remain stopped in debuger + args.SuspendRemote = true; + } + + #endregion + } // ExecutionCmdletHelper /// @@ -3152,6 +3195,8 @@ internal ExecutionCmdletHelperRunspace(Pipeline pipeline) /// internal override void StartOperation() { + ConfigureRunspaceDebugging(PipelineRunspace); + try { if (ShouldUseSteppablePipelineOnServer) @@ -3244,6 +3289,8 @@ private void RaiseOperationCompleteEvent() /// raises this operation complete private void RaiseOperationCompleteEvent(EventArgs baseEventArgs) { + CleanupRunspaceDebugging(PipelineRunspace); + if (pipeline != null) { // Dispose the pipeline object and release data and remoting resources. @@ -3386,6 +3433,8 @@ private void HandleRunspaceStateChanged(object sender, case RunspaceState.Opened: { + ConfigureRunspaceDebugging(RemoteRunspace); + // if successfully opened // Call InvokeAsync() on the pipeline try diff --git a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs index b50f21da228..b62337fadec 100644 --- a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs +++ b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs @@ -2139,6 +2139,10 @@ private static System.Diagnostics.Process StartSSHProcessImpl( return sshProcess; } + // Process creation flags + private const int CREATE_NEW_PROCESS_GROUP = 0x00000200; + private const int CREATE_SUSPENDED = 0x00000004; + /// /// CreateProcessWithRedirectedStd /// @@ -2205,8 +2209,12 @@ private static Process CreateProcessWithRedirectedStd( // No new window: Inherit the parent process's console window creationFlags = 0x00000000; + // Create the new process in its own group, so that Ctrl+C is not sent to ssh.exe. We want to handle this + // control signal internally so that it can be passed via PSRP to the remote session. + creationFlags |= CREATE_NEW_PROCESS_GROUP; + // Create the new process suspended so we have a chance to get a corresponding Process object in case it terminates quickly. - creationFlags |= 0x00000004; + creationFlags |= CREATE_SUSPENDED; PlatformInvokes.SECURITY_ATTRIBUTES lpProcessAttributes = new PlatformInvokes.SECURITY_ATTRIBUTES(); PlatformInvokes.SECURITY_ATTRIBUTES lpThreadAttributes = new PlatformInvokes.SECURITY_ATTRIBUTES(); diff --git a/src/System.Management.Automation/engine/remoting/common/throttlemanager.cs b/src/System.Management.Automation/engine/remoting/common/throttlemanager.cs index 785bbe252b6..808916d9d14 100644 --- a/src/System.Management.Automation/engine/remoting/common/throttlemanager.cs +++ b/src/System.Management.Automation/engine/remoting/common/throttlemanager.cs @@ -130,6 +130,44 @@ internal bool IgnoreStop } } private bool _ignoreStop = false; + + #region Runspace Debug + + /// + /// When true enables runspace debugging for operations involving runspaces. + /// + internal bool RunspaceDebuggingEnabled + { + get; + set; + } + + /// + /// When true configures runspace debugging to stop at first opportunity. + /// + internal bool RunspaceDebugStepInEnabled + { + get; + set; + } + + /// + /// Event raised when operation runspace enters a debugger stopped state. + /// + internal event EventHandler RunspaceDebugStop; + + /// + /// RaiseRunspaceDebugStopEvent + /// + /// Runspace + internal void RaiseRunspaceDebugStopEvent(System.Management.Automation.Runspaces.Runspace runspace) + { + RunspaceDebugStop.SafeInvoke(this, new StartRunspaceDebugProcessingEventArgs(runspace)); + } + + + #endregion + } // IThrottleOperation #endregion IThrottleOperation diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx index 69c3a356605..c826acf0ac5 100644 --- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx +++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx @@ -936,7 +936,7 @@ Do you want to continue? {0} may need to restart the WinRM service if a configuration using this name has recently been unregistered, certain system data structures may still be cached. In that case, a restart of WinRM may be required. All WinRM sessions connected to Windows PowerShell session configurations, such as Microsoft.PowerShell and session configurations that are created with the Register-PSSessionConfiguration cmdlet, are disconnected. - + You are running in a remote session and have selected the Force option which means the WinRM service may restart.If the WinRM service restarts then this remote session will be terminated and you will need to create a new session to continue @@ -1359,9 +1359,6 @@ All WinRM sessions connected to Windows PowerShell session configurations, such The remote session command is currently stopped in the debugger. Use the Enter-PSSession cmdlet to connect interactively to the remote session and automatically enter into the console debugger. - - Session {0} with instance ID {1} on computer {2} has been disconnected because the script running on the session has stopped at a breakpoint. Use the Enter-PSSession cmdlet on this session to connect back to the session and begin interactive debugging. - The remote session to which you are connected does not support remote debugging. You must connect to a remote computer that is running Windows PowerShell {0} or greater. diff --git a/test/powershell/engine/Remoting/InvokeCommandRemoteDebug.Tests.ps1 b/test/powershell/engine/Remoting/InvokeCommandRemoteDebug.Tests.ps1 new file mode 100644 index 00000000000..dfcd9decc8e --- /dev/null +++ b/test/powershell/engine/Remoting/InvokeCommandRemoteDebug.Tests.ps1 @@ -0,0 +1,216 @@ +## +## PowerShell Invoke-Command -RemoteDebug Tests +## + +if ($IsWindows) +{ + $remotingModule = Join-Path $PSScriptRoot "../Common/TestRemoting.psm1" + Import-Module $remotingModule -ErrorAction SilentlyContinue + + $typeDef = @' + using System; + using System.Globalization; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using System.Management.Automation.Host; + + namespace TestRunner + { + public class DummyHost : PSHost, IHostSupportsInteractiveSession + { + public Runspace _runspace; + private Guid _instanceId = Guid.NewGuid(); + + public override CultureInfo CurrentCulture + { + get { return CultureInfo.CurrentCulture; } + } + public override CultureInfo CurrentUICulture + { + get { return CultureInfo.CurrentUICulture; } + } + public override Guid InstanceId + { + get { return _instanceId; } + } + public override string Name + { + get { return "DummyTestHost"; } + } + public override PSHostUserInterface UI + { + get { return null; } + } + public override Version Version + { + get { return new Version(1, 0); } + } + public override void EnterNestedPrompt() { } + public override void ExitNestedPrompt() { } + public override void NotifyBeginApplication() { } + public override void NotifyEndApplication() { } + public override void SetShouldExit(int exitCode) { } + public void PushRunspace(Runspace runspace) { } + public void PopRunspace() { } + public bool IsRunspacePushed { get { return false; } } + public Runspace Runspace { get { return _runspace; } private set { _runspace = value; } } + } + + public class TestDebugger : Debugger + { + private Runspace _runspace; + public int DebugStopCount + { + private set; + get; + } + public int RunspaceDebugProcessingCount + { + private set; + get; + } + public bool RunspaceDebugProcessCancelled + { + private set; + get; + } + + private void HandleDebuggerStop(object sender, DebuggerStopEventArgs args) + { + DebugStopCount++; + var debugger = sender as Debugger; + var command = new PSCommand(); + command.AddScript("prompt"); + var output = new PSDataCollection(); + debugger.ProcessCommand(command, output); + } + private void HandleStartRunspaceDebugProcessing(object sender, StartRunspaceDebugProcessingEventArgs args) + { + args.UseDefaultProcessing = true; + RunspaceDebugProcessingCount++; + } + private void HandleCancelRunspaceDebugProcessing(object sender, EventArgs args) + { + RunspaceDebugProcessCancelled = true; + } + public TestDebugger(Runspace runspace) + { + _runspace = runspace; + _runspace.Debugger.DebuggerStop += HandleDebuggerStop; + _runspace.Debugger.StartRunspaceDebugProcessing += HandleStartRunspaceDebugProcessing; + _runspace.Debugger.CancelRunspaceDebugProcessing += HandleCancelRunspaceDebugProcessing; + } + public void Release() + { + if (_runspace == null) { return; } + _runspace.Debugger.DebuggerStop -= HandleDebuggerStop; + _runspace.Debugger.StartRunspaceDebugProcessing -= HandleStartRunspaceDebugProcessing; + _runspace.Debugger.CancelRunspaceDebugProcessing -= HandleCancelRunspaceDebugProcessing; + } + + public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataCollection output) { return null; } + public override void SetDebuggerAction(DebuggerResumeAction resumeAction) { } + public override DebuggerStopEventArgs GetDebuggerStopArgs() { return null; } + public override void StopProcessCommand() { } + } + } +'@ +} + +Describe "Invoke-Command remote debugging tests" -Tags 'Feature' { + + BeforeAll { + + if (!$IsWindows) + { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:Pending"] = $true + } + else + { + $sb = [scriptblock]::Create(@' + "Hello!" +'@) + + Add-Type -TypeDefinition $typeDef -ReferencedAssemblies "System.Globalization","System.Management.Automation" + + $dummyHost = [TestRunner.DummyHost]::new() + [runspace] $rs = [runspacefactory]::CreateRunspace($dummyHost) + $rs.Open() + $dummyHost._runspace = $rs + + $testDebugger = [TestRunner.TestDebugger]::new($rs) + + [runspace] $rs2 = [runspacefactory]::CreateRunspace() + $rs2.Open() + + [powershell] $ps = [powershell]::Create() + $ps.Runspace = $rs + + [powershell] $ps2 = [powershell]::Create() + $ps2.Runspace = $rs2 + + $remoteSession = New-RemoteRunspace + } + } + + AfterAll { + + if (!$IsWindows) + { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } + else + { + if ($testDebugger -ne $null) { $testDebugger.Release() } + if ($ps -ne $null) { $ps.Dispose() } + if ($ps2 -ne $null) { $ps2.Dispose() } + if ($rs -ne $null) { $rs.Dispose() } + if ($rs2 -ne $null) { $rs2.Dispose() } + if ($remoteSession -ne $null) { Remove-PSSession $remoteSession -ErrorAction SilentlyContinue } + } + } + + AfterEach { + $ps.Commands.Clear() + $ps2.Commands.Clear() + } + + It "Verifies that asynchronous 'Invoke-Command -RemoteDebug' is ignored" { + + $ps.AddCommand("Invoke-Command"). + AddParameter("Session", $remoteSession). + AddParameter("ScriptBlock", $sb). + AddParameter("RemoteDebug", $true). + AddParameter("AsJob", $true) + $result = $ps.Invoke() + $testDebugger.DebugStopCount | Should Be 0 + } + + It "Verifies that synchronous 'Invoke-Command -RemoteDebug' invokes debugger" { + + $ps.AddCommand("Invoke-Command"). + AddParameter("Session", $remoteSession). + AddParameter("ScriptBlock", $sb). + AddParameter("RemoteDebug", $true) + $result = $ps.Invoke() + $testDebugger.RunspaceDebugProcessingCount | Should Be 1 + $testDebugger.DebugStopCount | Should Be 1 + } + + It "Verifies the debugger 'CancelDebuggerProcessing' API method" { + + $rs.Debugger.CancelDebuggerProcessing() + $testDebugger.RunspaceDebugProcessCancelled | Should Be $true + } + + It "Verifies that 'Invoke-Command -RemoteDebug' running in a runspace without PSHost is ignored" { + + $ps2.AddCommand("Invoke-Command"). + AddParameter("Session", $remoteSession). + AddParameter("ScriptBlock", $sb). + AddParameter("RemoteDebug", $true) + $result = $ps2.Invoke() + $result | Should Be "Hello!" + } +}