From ca202da6437f5c9ed334725469f6d8f1cb7e5a2c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 12 Dec 2019 17:23:12 -0800 Subject: [PATCH] Improvements in breakpoint APIs for remote scenarios (#11312) --- .../engine/debugger/debugger.cs | 228 +++++++++-- .../engine/hostifaces/PSTask.cs | 91 ++++- .../engine/remoting/client/Job.cs | 92 ++++- .../engine/remoting/client/remoterunspace.cs | 259 +++++++++--- .../remoting/server/ServerPowerShellDriver.cs | 12 +- .../server/ServerRunspacePoolDriver.cs | 383 +++++++++++------- .../resources/DebuggerStrings.resx | 12 + .../resources/RemotingErrorIdStrings.resx | 3 + test/powershell/SDK/Breakpoint.Tests.ps1 | 114 +++++- 9 files changed, 902 insertions(+), 292 deletions(-) diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index 77992fcf86..2da8856222 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -622,20 +622,23 @@ namespace System.Management.Automation /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. /// /// Id of the breakpoint you want. - public virtual Breakpoint GetBreakpoint(int id) => + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public virtual Breakpoint GetBreakpoint(int id, int? runspaceId = null) => throw new PSNotImplementedException(); /// /// Adds the provided set of breakpoints to the debugger. /// /// Breakpoints. - public virtual void SetBreakpoints(IEnumerable breakpoints) => + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public virtual void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) => throw new PSNotImplementedException(); /// /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. /// - public virtual List GetBreakpoints() => + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public virtual List GetBreakpoints(int? runspaceId = null) => throw new PSNotImplementedException(); /// @@ -644,8 +647,9 @@ namespace System.Management.Automation /// The name of the command that will trigger the breakpoint. This value is required and may not be null. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// The command breakpoint that was set. - public virtual CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) => + public virtual CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) => throw new PSNotImplementedException(); /// @@ -655,8 +659,9 @@ namespace System.Management.Automation /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// The line breakpoint that was set. - public virtual LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) => + public virtual LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) => throw new PSNotImplementedException(); /// @@ -666,32 +671,36 @@ namespace System.Management.Automation /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// The variable breakpoint that was set. - public virtual VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) => + public virtual VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) => throw new PSNotImplementedException(); /// /// Removes a breakpoint from the debugger. /// /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// True if the breakpoint was removed from the debugger; false otherwise. - public virtual bool RemoveBreakpoint(Breakpoint breakpoint) => + public virtual bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => throw new PSNotImplementedException(); /// /// Enables a breakpoint in the debugger. /// /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. - public virtual Breakpoint EnableBreakpoint(Breakpoint breakpoint) => + public virtual Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => throw new PSNotImplementedException(); /// /// Disables a breakpoint in the debugger. /// /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. - public virtual Breakpoint DisableBreakpoint(Breakpoint breakpoint) => + public virtual Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => throw new PSNotImplementedException(); /// @@ -2590,9 +2599,16 @@ namespace System.Management.Automation /// /// Adds the provided set of breakpoints to the debugger. /// - /// - public override void SetBreakpoints(IEnumerable breakpoints) + /// The breakpoints to set. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) { + if (runspaceId.HasValue) + { + GetRunspaceDebugger(runspaceId.Value).SetBreakpoints(breakpoints); + return; + } + foreach (Breakpoint bp in breakpoints) { switch (bp) @@ -2616,8 +2632,14 @@ namespace System.Management.Automation /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. /// /// Id of the breakpoint you want. - public override Breakpoint GetBreakpoint(int id) + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).GetBreakpoint(id); + } + _idToBreakpoint.TryGetValue(id, out Breakpoint breakpoint); return breakpoint; } @@ -2625,8 +2647,14 @@ namespace System.Management.Automation /// /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. /// - public override List GetBreakpoints() + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override List GetBreakpoints(int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).GetBreakpoints(); + } + return (from bp in _idToBreakpoint.Values orderby bp.Id select bp).ToList(); } @@ -2636,9 +2664,15 @@ namespace System.Management.Automation /// The name of the command that will trigger the breakpoint. This value is required and may not be null. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). /// - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).SetCommandBreakpoint(command, action, path); + } + Diagnostics.Assert(!string.IsNullOrEmpty(command), "Caller to verify command is not null or empty."); WildcardPattern pattern = WildcardPattern.Get(command, WildcardOptions.Compiled | WildcardOptions.IgnoreCase); @@ -2654,9 +2688,15 @@ namespace System.Management.Automation /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. - /// - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A LineBreakpoint + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).SetLineBreakpoint(path, line, column, action); + } + Diagnostics.Assert(path != null, "Caller to verify path is not null."); Diagnostics.Assert(line > 0, "Caller to verify line is greater than 0."); @@ -2671,18 +2711,33 @@ namespace System.Management.Automation /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. - /// - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A VariableBreakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).SetVariableBreakpoint(variableName, accessMode, action, path); + } + Diagnostics.Assert(!string.IsNullOrEmpty(variableName), "Caller to verify variableName is not null or empty."); CheckForBreakpointSupport(); return AddVariableBreakpoint(new VariableBreakpoint(path, variableName, accessMode, action)); } - // This is the implementation of the Remove-PSBreakpoint cmdlet. - public override bool RemoveBreakpoint(Breakpoint breakpoint) + /// + /// This is the implementation of the Remove-PSBreakpoint cmdlet. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).RemoveBreakpoint(breakpoint); + } + Diagnostics.Assert(breakpoint != null, "Caller to verify the breakpoint is not null."); if (_idToBreakpoint.Remove(breakpoint.Id, out _)) @@ -2702,10 +2757,18 @@ namespace System.Management.Automation return false; } - - // This is the implementation of the Enable-PSBreakpoint cmdlet. - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) + /// + /// This is the implementation of the Enable-PSBreakpoint cmdlet. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).EnableBreakpoint(breakpoint); + } + Diagnostics.Assert(breakpoint != null, "Caller to verify the breakpoint is not null."); if (_idToBreakpoint.TryGetValue(breakpoint.Id, out _)) @@ -2719,9 +2782,18 @@ namespace System.Management.Automation return null; } - // This is the implementation of the Disable-PSBreakpoint cmdlet. - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) + /// + /// This is the implementation of the Disable-PSBreakpoint cmdlet. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { + if (runspaceId.HasValue) + { + return GetRunspaceDebugger(runspaceId.Value).DisableBreakpoint(breakpoint); + } + Diagnostics.Assert(breakpoint != null, "Caller to verify the breakpoint is not null."); if (_idToBreakpoint.TryGetValue(breakpoint.Id, out _)) @@ -2735,6 +2807,21 @@ namespace System.Management.Automation return null; } + private Debugger GetRunspaceDebugger(int runspaceId) + { + if (!Runspace.RunspaceDictionary.TryGetValue(runspaceId, out WeakReference wr)) + { + throw new PSArgumentException(string.Format(DebuggerStrings.InvalidRunspaceId, runspaceId)); + } + + if (!wr.TryGetTarget(out Runspace rs)) + { + throw new PSArgumentException(DebuggerStrings.UnableToGetRunspace); + } + + return rs.Debugger; + } + #endregion Breakpoints #region Job Debugging @@ -4084,8 +4171,9 @@ namespace System.Management.Automation /// Adds the provided set of breakpoints to the debugger. /// /// Breakpoints. - public override void SetBreakpoints(IEnumerable breakpoints) => - _wrappedDebugger.SetBreakpoints(breakpoints); + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) => + _wrappedDebugger.SetBreakpoints(breakpoints, runspaceId); /// /// Process debugger or PowerShell command/script. @@ -4123,29 +4211,83 @@ namespace System.Management.Automation return _wrappedDebugger.ProcessCommand(command, output); } - public override Breakpoint GetBreakpoint(int id) => - _wrappedDebugger.GetBreakpoint(id); + /// + /// Get a breakpoint by id. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) => + _wrappedDebugger.GetBreakpoint(id, runspaceId); - public override List GetBreakpoints() => - _wrappedDebugger.GetBreakpoints(); + /// + /// Returns breakpoints on a runspace. + /// + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A list of breakpoints in a runspace. + public override List GetBreakpoints(int? runspaceId = null) => + _wrappedDebugger.GetBreakpoints(runspaceId); - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetCommandBreakpoint(command, action, path); + /// + /// Sets a command breakpoint in the debugger. + /// + /// The name of the command that will trigger the breakpoint. This value is required and may not be null. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The command breakpoint that was set. + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetCommandBreakpoint(command, action, path, runspaceId); - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) => - _wrappedDebugger.SetLineBreakpoint(path, line, column, action); + /// + /// Sets a line breakpoint in the debugger. + /// + /// The path to the script file where the breakpoint may be hit. This value is required and may not be null. + /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. + /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The line breakpoint that was set. + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) => + _wrappedDebugger.SetLineBreakpoint(path, line, column, action, runspaceId); - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path); + /// + /// Sets a variable breakpoint in the debugger. + /// + /// The name of the variable that will trigger the breakpoint. This value is required and may not be null. + /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The variable breakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path, runspaceId); - public override bool RemoveBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.RemoveBreakpoint(breakpoint); + /// + /// Removes a breakpoint from the debugger. + /// + /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// True if the breakpoint was removed from the debugger; false otherwise. + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.RemoveBreakpoint(breakpoint, runspaceId); - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.EnableBreakpoint(breakpoint); + /// + /// Enables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.EnableBreakpoint(breakpoint, runspaceId); - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.DisableBreakpoint(breakpoint); + /// + /// Disables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.DisableBreakpoint(breakpoint, runspaceId); /// /// SetDebuggerAction. diff --git a/src/System.Management.Automation/engine/hostifaces/PSTask.cs b/src/System.Management.Automation/engine/hostifaces/PSTask.cs index 9a17e20252..c68a84a9d0 100644 --- a/src/System.Management.Automation/engine/hostifaces/PSTask.cs +++ b/src/System.Management.Automation/engine/hostifaces/PSTask.cs @@ -1091,8 +1091,9 @@ namespace System.Management.Automation.PSTasks /// Adds the provided set of breakpoints to the debugger. /// /// List of breakpoints. - public override void SetBreakpoints(IEnumerable breakpoints) => - _wrappedDebugger.SetBreakpoints(breakpoints); + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) => + _wrappedDebugger.SetBreakpoints(breakpoints, runspaceId); /// /// Sets the debugger resume action. @@ -1103,26 +1104,84 @@ namespace System.Management.Automation.PSTasks _wrappedDebugger.SetDebuggerAction(resumeAction); } - public override Breakpoint GetBreakpoint(int id) => - _wrappedDebugger.GetBreakpoint(id); + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The breakpoint with the specified id. + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) => + _wrappedDebugger.GetBreakpoint(id, runspaceId); - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetCommandBreakpoint(command, action, path); + /// + /// Returns breakpoints on a runspace. + /// + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A list of breakpoints in a runspace. + public override List GetBreakpoints(int? runspaceId = null) => + _wrappedDebugger.GetBreakpoints(runspaceId); - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path); + /// + /// Sets a command breakpoint in the debugger. + /// + /// The name of the command that will trigger the breakpoint. This value is required and may not be null. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The command breakpoint that was set. + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetCommandBreakpoint(command, action, path, runspaceId); - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) => - _wrappedDebugger.SetLineBreakpoint(path, line, column, action); + /// + /// Sets a variable breakpoint in the debugger. + /// + /// The name of the variable that will trigger the breakpoint. This value is required and may not be null. + /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The variable breakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path, runspaceId); - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.EnableBreakpoint(breakpoint); + /// + /// Sets a line breakpoint in the debugger. + /// + /// The path to the script file where the breakpoint may be hit. This value is required and may not be null. + /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. + /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The line breakpoint that was set. + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) => + _wrappedDebugger.SetLineBreakpoint(path, line, column, action, runspaceId); - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.DisableBreakpoint(breakpoint); + /// + /// Enables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.EnableBreakpoint(breakpoint, runspaceId); - public override bool RemoveBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.RemoveBreakpoint(breakpoint); + /// + /// Disables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.DisableBreakpoint(breakpoint, runspaceId); + + /// + /// Removes a breakpoint from the debugger. + /// + /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// True if the breakpoint was removed from the debugger; false otherwise. + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.RemoveBreakpoint(breakpoint, runspaceId); /// /// Stops a running command. diff --git a/src/System.Management.Automation/engine/remoting/client/Job.cs b/src/System.Management.Automation/engine/remoting/client/Job.cs index 93061df4a8..d01e555d71 100644 --- a/src/System.Management.Automation/engine/remoting/client/Job.cs +++ b/src/System.Management.Automation/engine/remoting/client/Job.cs @@ -3913,32 +3913,88 @@ namespace System.Management.Automation /// Adds the provided set of breakpoints to the debugger. /// /// Breakpoints to set. - public override void SetBreakpoints(IEnumerable breakpoints) => - _wrappedDebugger.SetBreakpoints(breakpoints); + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) => + _wrappedDebugger.SetBreakpoints(breakpoints, runspaceId); - public override Breakpoint GetBreakpoint(int id) => - _wrappedDebugger.GetBreakpoint(id); + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A a breakpoint with the specified id. + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) => + _wrappedDebugger.GetBreakpoint(id, runspaceId); - public override List GetBreakpoints() => - _wrappedDebugger.GetBreakpoints(); + /// + /// Returns breakpoints on a runspace. + /// + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A list of breakpoints in a runspace. + public override List GetBreakpoints(int? runspaceId = null) => + _wrappedDebugger.GetBreakpoints(runspaceId); - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetCommandBreakpoint(command, action, path); + /// + /// Sets a command breakpoint in the debugger. + /// + /// The name of the command that will trigger the breakpoint. This value is required and may not be null. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The command breakpoint that was set. + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetCommandBreakpoint(command, action, path, runspaceId); - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) => - _wrappedDebugger.SetLineBreakpoint(path, line, column, action); + /// + /// Sets a line breakpoint in the debugger. + /// + /// The path to the script file where the breakpoint may be hit. This value is required and may not be null. + /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. + /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The line breakpoint that was set. + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) => + _wrappedDebugger.SetLineBreakpoint(path, line, column, action, runspaceId); - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) => - _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path); + /// + /// Sets a variable breakpoint in the debugger. + /// + /// The name of the variable that will trigger the breakpoint. This value is required and may not be null. + /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The variable breakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.SetVariableBreakpoint(variableName, accessMode, action, path, runspaceId); - public override bool RemoveBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.RemoveBreakpoint(breakpoint); + /// + /// Removes a breakpoint from the debugger. + /// + /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// True if the breakpoint was removed from the debugger; false otherwise. + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.RemoveBreakpoint(breakpoint, runspaceId); - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.EnableBreakpoint(breakpoint); + /// + /// Enables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.EnableBreakpoint(breakpoint, runspaceId); - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.DisableBreakpoint(breakpoint); + /// + /// Disables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.DisableBreakpoint(breakpoint, runspaceId); /// /// Sets the debugger resume action. diff --git a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs index dc9b2bab40..ce7abd0749 100644 --- a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs +++ b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs @@ -2010,30 +2010,55 @@ namespace System.Management.Automation /// Adds the provided set of breakpoints to the debugger. /// /// Breakpoints to set. - public override void SetBreakpoints(IEnumerable breakpoints) => - _runspace.Debugger?.SetBreakpoints(breakpoints); + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) + { + // This is supported only for PowerShell versions >= 7.0 + CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.SetBreakpoint); + + var functionParameters = new Dictionary + { + { "BreakpointList", breakpoints }, + }; + + if (runspaceId.HasValue) + { + functionParameters.Add("RunspaceId", runspaceId.Value); + } + + InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.SetBreakpoint, functionParameters); + } /// /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. /// /// Id of the breakpoint you want. - public override Breakpoint GetBreakpoint(int id) + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The breakpoint with the specified id. + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.GetBreakpoint); - return InvokeRemoteBreakpointFunction( - RemoteDebuggingCommands.GetBreakpoint, - new Dictionary - { - { "Id", id }, - }); + var functionParameters = new Dictionary + { + { "Id", id }, + }; + + if (runspaceId.HasValue) + { + functionParameters.Add("RunspaceId", runspaceId.Value); + } + + return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.GetBreakpoint, functionParameters); } /// /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. /// - public override List GetBreakpoints() + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A list of breakpoints in a runspace. + public override List GetBreakpoints(int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.GetBreakpoint); @@ -2046,6 +2071,11 @@ namespace System.Management.Automation { ps.AddCommand(RemoteDebuggingCommands.GetBreakpoint); + if (runspaceId.HasValue) + { + ps.AddParameter("RunspaceId", runspaceId.Value); + } + Collection output = ps.Invoke(); foreach (var item in output) { @@ -2053,124 +2083,186 @@ namespace System.Management.Automation { breakpoints.Add(bp); } + else if (TryGetRemoteDebuggerException(item, out Exception ex)) + { + throw ex; + } } } return breakpoints; } - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) + /// + /// Sets a command breakpoint in the debugger. + /// + /// The name of the command that will trigger the breakpoint. This value is required and may not be null. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The command breakpoint that was set. + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.SetBreakpoint); - + + Breakpoint breakpoint = new CommandBreakpoint(path, null, command, action); var functionParameters = new Dictionary { - { "Command", command }, + { "Breakpoint", breakpoint }, }; - if (action != null) + if (runspaceId.HasValue) { - functionParameters.Add("Action", action); - } - - if (path != null) - { - functionParameters.Add("Script", path); + functionParameters.Add("RunspaceId", runspaceId.Value); } return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.SetBreakpoint, functionParameters); } - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) + /// + /// Sets a line breakpoint in the debugger. + /// + /// The path to the script file where the breakpoint may be hit. This value is required and may not be null. + /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. + /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The line breakpoint that was set. + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.SetBreakpoint); + Breakpoint breakpoint = new LineBreakpoint(path, line, column, action); + var functionParameters = new Dictionary { - { "Script", path }, - { "Line", line }, + { "Breakpoint", breakpoint }, }; - if (column != 0) + if (runspaceId.HasValue) { - functionParameters.Add("Column", column); - } - - if (action != null) - { - functionParameters.Add("Action", action); + functionParameters.Add("RunspaceId", runspaceId.Value); } return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.SetBreakpoint, functionParameters); } - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) + /// + /// Sets a variable breakpoint in the debugger. + /// + /// The name of the variable that will trigger the breakpoint. This value is required and may not be null. + /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The variable breakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.SetBreakpoint); + Breakpoint breakpoint = new VariableBreakpoint(path, variableName, accessMode, action); + var functionParameters = new Dictionary { - { "Variable", variableName }, + { "Breakpoint", breakpoint }, }; - if (accessMode != VariableAccessMode.Write) + if (runspaceId.HasValue) { - functionParameters.Add("Mode", accessMode); - } - - if (action != null) - { - functionParameters.Add("Action", action); - } - - if (path != null) - { - functionParameters.Add("Script", path); + functionParameters.Add("RunspaceId", runspaceId.Value); } return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.SetBreakpoint, functionParameters); } - public override bool RemoveBreakpoint(Breakpoint breakpoint) + /// + /// Removes a breakpoint from the debugger. + /// + /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// True if the breakpoint was removed from the debugger; false otherwise. + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.RemoveBreakpoint); - return InvokeRemoteBreakpointFunction( - RemoteDebuggingCommands.RemoveBreakpoint, - new Dictionary - { - { "Id", breakpoint.Id }, - }); + if (breakpoint == null) + { + return false; + } + + var functionParameters = new Dictionary + { + { "Id", breakpoint.Id }, + }; + + if (runspaceId.HasValue) + { + functionParameters.Add("RunspaceId", runspaceId.Value); + } + + return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.RemoveBreakpoint, functionParameters); } - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) + /// + /// Enables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.EnableBreakpoint); - return InvokeRemoteBreakpointFunction( - RemoteDebuggingCommands.EnableBreakpoint, - new Dictionary - { - { "Id", breakpoint.Id }, - }); + if (breakpoint == null) + { + return null; + } + + var functionParameters = new Dictionary + { + { "Id", breakpoint.Id }, + }; + + if (runspaceId.HasValue) + { + functionParameters.Add("RunspaceId", runspaceId.Value); + } + + return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.EnableBreakpoint, functionParameters); } - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) + /// + /// Disables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) { // This is supported only for PowerShell versions >= 7.0 CheckRemoteBreakpointManagementSupport(RemoteDebuggingCommands.DisableBreakpoint); - return InvokeRemoteBreakpointFunction( - RemoteDebuggingCommands.DisableBreakpoint, - new Dictionary - { - { "Id", breakpoint.Id }, - }); + if (breakpoint == null) + { + return null; + } + + var functionParameters = new Dictionary + { + { "Id", breakpoint.Id }, + }; + + if (runspaceId.HasValue) + { + functionParameters.Add("RunspaceId", runspaceId.Value); + } + + return InvokeRemoteBreakpointFunction(RemoteDebuggingCommands.DisableBreakpoint, functionParameters); } /// @@ -2518,6 +2610,38 @@ namespace System.Management.Automation #region Private Methods + private static bool TryGetRemoteDebuggerException( + PSObject item, + out Exception exception) + { + exception = null; + if (item == null) + { + return false; + } + + bool haveExceptionType = false; + foreach (var typeName in item.TypeNames) + { + if (typeName.Equals("Deserialized.System.Exception")) + { + haveExceptionType = true; + break; + } + } + + if (haveExceptionType) + { + var errorMessage = item.Properties["Message"]?.Value ?? string.Empty; + exception = new RemoteException( + StringUtil.Format( + RemotingErrorIdStrings.RemoteDebuggerError, item.TypeNames[0], errorMessage)); + return true; + } + + return false; + } + // // Event handlers // @@ -2785,6 +2909,11 @@ namespace System.Management.Automation { return (T)item.BaseObject; } + + if (TryGetRemoteDebuggerException(item, out Exception ex)) + { + throw ex; + } } return default(T); diff --git a/src/System.Management.Automation/engine/remoting/server/ServerPowerShellDriver.cs b/src/System.Management.Automation/engine/remoting/server/ServerPowerShellDriver.cs index 8096a0e3f6..ce5f69ee6a 100644 --- a/src/System.Management.Automation/engine/remoting/server/ServerPowerShellDriver.cs +++ b/src/System.Management.Automation/engine/remoting/server/ServerPowerShellDriver.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; using System.Management.Automation.Internal; using System.Management.Automation.Remoting; using System.Management.Automation.Runspaces; @@ -289,7 +290,8 @@ namespace System.Management.Automation /// commands that sets debugger state but doesn't run any command /// on the server runspace. /// - internal void RunNoOpCommand() + /// The output from preprocessing that we want to send to the client. + internal void RunNoOpCommand(IReadOnlyCollection output) { if (LocalPowerShell != null) { @@ -300,6 +302,14 @@ namespace System.Management.Automation new PSInvocationStateInfo( PSInvocationState.Running, null)); + foreach (var item in output) + { + if (item != null) + { + _localPowerShellOutput.Add(PSObject.AsPSObject(item)); + } + } + LocalPowerShell.SetStateChanged( new PSInvocationStateInfo( PSInvocationState.Completed, null)); diff --git a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs index 217d6db64e..22303acee8 100644 --- a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs +++ b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; @@ -747,36 +748,52 @@ namespace System.Management.Automation { DebuggerCommandArgument commandArgument; bool terminateImmediate = false; - var result = PreProcessDebuggerCommand(powershell.Commands, _serverRemoteDebugger.IsActive, _serverRemoteDebugger.IsRemote, out commandArgument); + Collection preProcessOutput = new Collection(); - switch (result) + try { - case PreProcessCommandResult.SetDebuggerAction: - // Run this directly on the debugger and terminate the remote command. - _serverRemoteDebugger.SetDebuggerAction(commandArgument.ResumeAction.Value); - terminateImmediate = true; - break; + var result = PreProcessDebuggerCommand(powershell.Commands, _serverRemoteDebugger, preProcessOutput, out commandArgument); - case PreProcessCommandResult.SetDebugMode: - // Set debug mode directly and terminate remote command. - _serverRemoteDebugger.SetDebugMode(commandArgument.Mode.Value); - terminateImmediate = true; - break; + switch (result) + { + case PreProcessCommandResult.SetDebuggerAction: + // Run this directly on the debugger and terminate the remote command. + _serverRemoteDebugger.SetDebuggerAction(commandArgument.ResumeAction.Value); + terminateImmediate = true; + break; - case PreProcessCommandResult.SetDebuggerStepMode: - // Enable debugger and set to step action, then terminate remote command. - _serverRemoteDebugger.SetDebuggerStepMode(commandArgument.DebuggerStepEnabled.Value); - terminateImmediate = true; - break; + case PreProcessCommandResult.SetDebugMode: + // Set debug mode directly and terminate remote command. + _serverRemoteDebugger.SetDebugMode(commandArgument.Mode.Value); + terminateImmediate = true; + break; - case PreProcessCommandResult.SetPreserveUnhandledBreakpointMode: - _serverRemoteDebugger.UnhandledBreakpointMode = commandArgument.UnhandledBreakpointMode.Value; - terminateImmediate = true; - break; + case PreProcessCommandResult.SetDebuggerStepMode: + // Enable debugger and set to step action, then terminate remote command. + _serverRemoteDebugger.SetDebuggerStepMode(commandArgument.DebuggerStepEnabled.Value); + terminateImmediate = true; + break; - case PreProcessCommandResult.ValidNotProcessed: - terminateImmediate = true; - break; + case PreProcessCommandResult.SetPreserveUnhandledBreakpointMode: + _serverRemoteDebugger.UnhandledBreakpointMode = commandArgument.UnhandledBreakpointMode.Value; + terminateImmediate = true; + break; + + case PreProcessCommandResult.ValidNotProcessed: + terminateImmediate = true; + break; + + case PreProcessCommandResult.BreakpointManagement: + terminateImmediate = true; + break; + } + } + catch (Exception ex) + { + terminateImmediate = true; + + preProcessOutput.Add( + PSObject.AsPSObject(ex)); } // If we don't want to run or queue a command to run in the server session then @@ -796,7 +813,7 @@ namespace System.Management.Automation addToHistory, null); - noOpDriver.RunNoOpCommand(); + noOpDriver.RunNoOpCommand(preProcessOutput); return; } } @@ -1237,6 +1254,11 @@ namespace System.Management.Automation /// SetPreserveUnhandledBreakpointMode. /// SetPreserveUnhandledBreakpointMode, + + /// + /// The PreProcessCommandResult used for managing breakpoints. + /// + BreakpointManagement, }; private class DebuggerCommandArgument @@ -1254,21 +1276,21 @@ namespace System.Management.Automation /// Pre-processor for debugger commands. /// Parses special debugger commands and converts to equivalent script for remote execution as needed. /// - /// PSCommand. - /// True if debugger is active. - /// True if active debugger is pushed and is a remote debugger. + /// The PSCommand. + /// The debugger that can be used to invoke debug operations via API. + /// A Collection that can be used to send output to the client. /// Command argument. /// PreProcessCommandResult type if preprocessing occurred. private static PreProcessCommandResult PreProcessDebuggerCommand( PSCommand commands, - bool isDebuggerActive, - bool isDebuggerRemote, + ServerRemoteDebugger serverRemoteDebugger, + Collection preProcessOutput, out DebuggerCommandArgument commandArgument) { commandArgument = new DebuggerCommandArgument(); PreProcessCommandResult result = PreProcessCommandResult.None; - if ((commands.Commands.Count == 0) || (commands.Commands[0].IsScript)) + if (commands.Commands.Count == 0 || commands.Commands[0].IsScript) { return result; } @@ -1282,7 +1304,7 @@ namespace System.Management.Automation // Returns DebuggerStopEventArgs object. // Evaluate this command only if the debugger is activated. - if (!isDebuggerActive) + if (!serverRemoteDebugger.IsActive) { return PreProcessCommandResult.ValidNotProcessed; } @@ -1296,7 +1318,7 @@ namespace System.Management.Automation // Returns void. // Evaluate this command only if the debugger is activated. - if (!isDebuggerActive) + if (!serverRemoteDebugger.IsActive) { return PreProcessCommandResult.ValidNotProcessed; } @@ -1315,7 +1337,10 @@ namespace System.Management.Automation { resumeAction = (DebuggerResumeAction)resumeObject.BaseObject; } - catch (InvalidCastException) { } + catch (InvalidCastException) + { + // Do nothing. + } } commandArgument.ResumeAction = resumeAction ?? throw new PSArgumentException("ResumeAction"); @@ -1340,7 +1365,10 @@ namespace System.Management.Automation { mode = (DebugModes)modeObject.BaseObject; } - catch (InvalidCastException) { } + catch (InvalidCastException) + { + // Do nothing. + } } commandArgument.Mode = mode ?? throw new PSArgumentException("Mode"); @@ -1380,7 +1408,10 @@ namespace System.Management.Automation { mode = (UnhandledBreakpointProcessingMode)modeObject.BaseObject; } - catch (InvalidCastException) { } + catch (InvalidCastException) + { + // Do nothing. + } } commandArgument.UnhandledBreakpointMode = mode ?? throw new PSArgumentException("Mode"); @@ -1392,126 +1423,121 @@ namespace System.Management.Automation // Input parameters: // [-Id ] // Returns Breakpoint object(s). - string script = null; - - if (command.Parameters?.Count > 0) + TryGetParameter(command, "RunspaceId", out int? runspaceId); + if (TryGetParameter(command, "Id", out int breakpointId)) { - int breakpointId = CheckBreakpointIdParameter(command); - script = $"$host.Runspace.Debugger.GetBreakpoint({breakpointId})"; + preProcessOutput.Add(serverRemoteDebugger.GetBreakpoint(breakpointId, runspaceId)); } else { - script = $"$host.Runspace.Debugger.GetBreakpoints()"; + foreach (Breakpoint breakpoint in serverRemoteDebugger.GetBreakpoints(runspaceId)) + { + preProcessOutput.Add(breakpoint); + } } - ReplaceVirtualCommandWithScript(commands, script); + result = PreProcessCommandResult.BreakpointManagement; } else if (commandText.Equals(RemoteDebuggingCommands.SetBreakpoint, StringComparison.OrdinalIgnoreCase)) { // __Set-PSBreakpoint private virtual command. // Input parameters: - // [-Script] [-Line] [[-Column] ] [-Action ] - // [[-Script] ] -Command [-Action ] - // [[-Script] ] -Variable [-Action ] [-Mode ] - // Returns Breakpoint object. - if (command.Parameters == null || command.Parameters.Count == 0) + // -Breakpoint or -BreakpointList > + // [-RunspaceId ] + // Returns Breakpoint object(s). + TryGetParameter(command, "Breakpoint", out Breakpoint breakpoint); + TryGetParameter(command, "BreakpointList", out ArrayList breakpoints); + if (breakpoint == null && breakpoints == null) { - throw new PSArgumentException("You must provide at least one parameter."); + throw new PSArgumentException(DebuggerStrings.BreakpointOrBreakpointListNotSpecified); } - string breakpointType = null; - foreach (var commandParameter in command.Parameters) - { - if (!commandParameter.Name.Equals("Script", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Line", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Column", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Action", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Command", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Variable", StringComparison.OrdinalIgnoreCase) && - !commandParameter.Name.Equals("Mode", StringComparison.OrdinalIgnoreCase)) - { - throw new PSArgumentException(commandParameter.Name); - } - - if (commandParameter.Name.Equals("Line", StringComparison.OrdinalIgnoreCase) || - commandParameter.Name.Equals("Column", StringComparison.OrdinalIgnoreCase)) - { - if (breakpointType != null && breakpointType != "Line") - { - throw new PSArgumentException(commandParameter.Name); - } - - breakpointType = "Line"; - } - else if (commandParameter.Name.Equals("Command", StringComparison.OrdinalIgnoreCase)) - { - if (breakpointType != null) - { - throw new PSArgumentException(commandParameter.Name); - } - - breakpointType = "Command"; - } - else if (commandParameter.Name.Equals("Variable", StringComparison.OrdinalIgnoreCase)) - { - if (breakpointType != null) - { - throw new PSArgumentException(commandParameter.Name); - } - - breakpointType = "Variable"; - } - } + TryGetParameter(command, "RunspaceId", out int? runspaceId); commands.Clear(); - commands.AddCommand("Set-PSBreakpoint"); - foreach (var commandParameter in command.Parameters) - { - var parameterValue = commandParameter.Name.Equals("Action", StringComparison.OrdinalIgnoreCase) - ? ScriptBlock.Create(commandParameter.Value as string) - : commandParameter.Value; - commands.AddParameter(commandParameter.Name, parameterValue); + // Any collection comes through remoting as an ArrayList of Objects so we convert each object + // into a breakpoint and add it to the list. + var bps = new List(); + if (breakpoints != null) + { + foreach (object obj in breakpoints) + { + if (!LanguagePrimitives.TryConvertTo(obj, out Breakpoint bp)) + { + throw new PSArgumentException(DebuggerStrings.BreakpointListContainedANonBreakpoint); + } + + bps.Add(bp); + } } + else + { + bps.Add(breakpoint); + } + + serverRemoteDebugger.SetBreakpoints(bps, runspaceId); + + foreach (var bp in bps) + { + preProcessOutput.Add(bp); + } + + result = PreProcessCommandResult.BreakpointManagement; } else if (commandText.Equals(RemoteDebuggingCommands.RemoveBreakpoint, StringComparison.OrdinalIgnoreCase)) { // __Remove-PSBreakpoint private virtual command. // Input parameters: // -Id + // [-RunspaceId ] // Returns bool. + int breakpointId = GetParameter(command, "Id"); + TryGetParameter(command, "RunspaceId", out int? runspaceId); - int breakpointId = CheckBreakpointIdParameter(command); + Breakpoint breakpoint = serverRemoteDebugger.GetBreakpoint(breakpointId, runspaceId); + preProcessOutput.Add( + breakpoint == null + ? false + : serverRemoteDebugger.RemoveBreakpoint(breakpoint, runspaceId)); - string script = $"$bp = $host.Runspace.Debugger.GetBreakpoint({breakpointId}); $bp -ne $null -and $host.Runspace.Debugger.RemoveBreakpoint($bp)"; - - ReplaceVirtualCommandWithScript(commands, script); + result = PreProcessCommandResult.BreakpointManagement; } else if (commandText.Equals(RemoteDebuggingCommands.EnableBreakpoint, StringComparison.OrdinalIgnoreCase)) { // __Enable-PSBreakpoint private virtual command. // Input parameters: // -Id - // Returns Breakpoint. + // [-RunspaceId ] + // Returns Breakpoint object. + int breakpointId = GetParameter(command, "Id"); + TryGetParameter(command, "RunspaceId", out int? runspaceId); - int breakpointId = CheckBreakpointIdParameter(command); - - string script = $"$bp = $host.Runspace.Debugger.GetBreakpoint({breakpointId}); if ($bp -ne $null) {{$host.Runspace.Debugger.EnableBreakpoint($bp)}}"; - - ReplaceVirtualCommandWithScript(commands, script); + Breakpoint bp = serverRemoteDebugger.GetBreakpoint(breakpointId, runspaceId); + if (bp != null) + { + preProcessOutput.Add(serverRemoteDebugger.EnableBreakpoint(bp, runspaceId)); + } + + result = PreProcessCommandResult.BreakpointManagement; } else if (commandText.Equals(RemoteDebuggingCommands.DisableBreakpoint, StringComparison.OrdinalIgnoreCase)) { // __Disable-PSBreakpoint private virtual command. // Input parameters: // -Id - // Returns Breakpoint. + // [-RunspaceId ] + // Returns Breakpoint object. + int breakpointId = GetParameter(command, "Id"); + TryGetParameter(command, "RunspaceId", out int? runspaceId); - int breakpointId = CheckBreakpointIdParameter(command); - - string script = $"$bp = $host.Runspace.Debugger.GetBreakpoint({breakpointId}); if ($bp -ne $null) {{$host.Runspace.Debugger.DisableBreakpoint($bp)}}"; - - ReplaceVirtualCommandWithScript(commands, script); + Breakpoint bp = serverRemoteDebugger.GetBreakpoint(breakpointId, runspaceId); + if (bp != null) + { + preProcessOutput.Add(serverRemoteDebugger.DisableBreakpoint(bp, runspaceId)); + } + + result = PreProcessCommandResult.BreakpointManagement; } return result; @@ -1527,22 +1553,39 @@ namespace System.Management.Automation .AddParameter("NoNewScope", true); } - private static int CheckBreakpointIdParameter(Command command) + private static T GetParameter(Command command, string parameterName) { - if (command.Parameters == null || - command.Parameters.Count == 0 || - !command.Parameters[0].Name.Equals("Id", StringComparison.OrdinalIgnoreCase)) + if (command.Parameters?.Count == 0) { - throw new PSArgumentException("Id"); + throw new PSArgumentException(parameterName); } - int? breakpointId = command.Parameters[0].Value as int?; - if (breakpointId == null) + foreach (CommandParameter param in command.Parameters) { - throw new PSArgumentException("Id"); + if (string.Equals(param.Name, parameterName, StringComparison.OrdinalIgnoreCase)) + { + return LanguagePrimitives.ConvertTo(param.Value); + } } - return breakpointId.Value; + throw new PSArgumentException(parameterName); + } + + private static bool TryGetParameter(Command command, string parameterName, out T value) + { + try + { + value = GetParameter(command, parameterName); + return true; + } + catch (Exception ex) when( + ex is PSArgumentException || + ex is InvalidCastException || + ex is PSInvalidCastException) + { + value = default(T); + return false; + } } #endregion @@ -1846,32 +1889,88 @@ namespace System.Management.Automation /// Adds the provided set of breakpoints to the debugger. /// /// List of breakpoints. - public override void SetBreakpoints(IEnumerable breakpoints) => - _wrappedDebugger.Value.SetBreakpoints(breakpoints); + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + public override void SetBreakpoints(IEnumerable breakpoints, int? runspaceId = null) => + _wrappedDebugger.Value.SetBreakpoints(breakpoints, runspaceId); - public override Breakpoint GetBreakpoint(int id) => - _wrappedDebugger.Value.GetBreakpoint(id); + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The breakpoint with the specified id. + public override Breakpoint GetBreakpoint(int id, int? runspaceId = null) => + _wrappedDebugger.Value.GetBreakpoint(id, runspaceId); - public override List GetBreakpoints() => - _wrappedDebugger.Value.GetBreakpoints(); + /// + /// Returns breakpoints on a runspace. + /// + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// A list of breakpoints in a runspace. + public override List GetBreakpoints(int? runspaceId = null) => + _wrappedDebugger.Value.GetBreakpoints(runspaceId); - public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null) => - _wrappedDebugger.Value.SetCommandBreakpoint(command, action, path); + /// + /// Sets a command breakpoint in the debugger. + /// + /// The name of the command that will trigger the breakpoint. This value is required and may not be null. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the command is invoked. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The command breakpoint that was set. + public override CommandBreakpoint SetCommandBreakpoint(string command, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.Value.SetCommandBreakpoint(command, action, path, runspaceId); - public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null) => - _wrappedDebugger.Value.SetLineBreakpoint(path, line, column, action); + /// + /// Sets a line breakpoint in the debugger. + /// + /// The path to the script file where the breakpoint may be hit. This value is required and may not be null. + /// The line in the script file where the breakpoint may be hit. This value is required and must be greater than or equal to 1. + /// The column in the script file where the breakpoint may be hit. If 0, the breakpoint will trigger on any statement on the line. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The line breakpoint that was set. + public override LineBreakpoint SetLineBreakpoint(string path, int line, int column = 0, ScriptBlock action = null, int? runspaceId = null) => + _wrappedDebugger.Value.SetLineBreakpoint(path, line, column, action, runspaceId); - public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null) => - _wrappedDebugger.Value.SetVariableBreakpoint(variableName, accessMode, action, path); + /// + /// Sets a variable breakpoint in the debugger. + /// + /// The name of the variable that will trigger the breakpoint. This value is required and may not be null. + /// The variable access mode that will trigger the breakpoint. By default variable breakpoints will trigger only when the variable is updated. + /// The action to take when the breakpoint is hit. If null, PowerShell will break into the debugger when the breakpoint is hit. + /// The path to the script file where the breakpoint may be hit. If null, the breakpoint may be hit anywhere the variable is accessed using the specified access mode. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The variable breakpoint that was set. + public override VariableBreakpoint SetVariableBreakpoint(string variableName, VariableAccessMode accessMode = VariableAccessMode.Write, ScriptBlock action = null, string path = null, int? runspaceId = null) => + _wrappedDebugger.Value.SetVariableBreakpoint(variableName, accessMode, action, path, runspaceId); - public override bool RemoveBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.Value.RemoveBreakpoint(breakpoint); + /// + /// Removes a breakpoint from the debugger. + /// + /// The breakpoint to remove from the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// True if the breakpoint was removed from the debugger; false otherwise. + public override bool RemoveBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.Value.RemoveBreakpoint(breakpoint, runspaceId); - public override Breakpoint EnableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.Value.EnableBreakpoint(breakpoint); + /// + /// Enables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint EnableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.Value.EnableBreakpoint(breakpoint, runspaceId); - public override Breakpoint DisableBreakpoint(Breakpoint breakpoint) => - _wrappedDebugger.Value.DisableBreakpoint(breakpoint); + /// + /// Disables a breakpoint in the debugger. + /// + /// The breakpoint to enable in the debugger. This value is required and may not be null. + /// The runspace id of the runspace you want to interact with. Defaults to null (current runspace). + /// The updated breakpoint if it was found; null if the breakpoint was not found in the debugger. + public override Breakpoint DisableBreakpoint(Breakpoint breakpoint, int? runspaceId = null) => + _wrappedDebugger.Value.DisableBreakpoint(breakpoint, runspaceId); /// /// Exits debugger mode with the provided resume action. diff --git a/src/System.Management.Automation/resources/DebuggerStrings.resx b/src/System.Management.Automation/resources/DebuggerStrings.resx index 79b0cf42a4..466ee067ce 100644 --- a/src/System.Management.Automation/resources/DebuggerStrings.resx +++ b/src/System.Management.Automation/resources/DebuggerStrings.resx @@ -249,4 +249,16 @@ The current session does not support debugging; operation will continue. The debugger detach command is not applicable. The detach command only applies when debugging jobs and runspaces with the Debug-Job or Debug-Runspace cmdlets. + + Invalid runspace id: {0} + + + Unable to get Runspace. + + + Breakpoint or BreakpointList must be specified. + + + The BreakpointList contained an item that was not a breakpoint. + diff --git a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx index 4cfd7203f6..3f9e9ad118 100644 --- a/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx +++ b/src/System.Management.Automation/resources/RemotingErrorIdStrings.resx @@ -1693,4 +1693,7 @@ All WinRM sessions connected to PowerShell session configurations, such as Micro Enter-PSHostProcess cmdlet is disabled because an application control policy such as 'AppLocker' or 'Windows Defender Application Control' is in enforcement. + + Remote debugger exception: {0}, error message: {1} + diff --git a/test/powershell/SDK/Breakpoint.Tests.ps1 b/test/powershell/SDK/Breakpoint.Tests.ps1 index b81148d8fa..7c3b886866 100644 --- a/test/powershell/SDK/Breakpoint.Tests.ps1 +++ b/test/powershell/SDK/Breakpoint.Tests.ps1 @@ -34,6 +34,12 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { Context 'Managing breakpoints in the host runspace via the SDK' { + AfterAll { + foreach ($bp in $host.Runspace.Debugger.GetBreakpoints()) { + $host.Runspace.Debugger.RemoveBreakpoint($bp) | Should -BeTrue + } + } + It 'Can set command breakpoints' { $host.Runspace.Debugger.SetCommandBreakpoint('Test-ThisCommandDoesNotExist') | Should -BeOfType [System.Management.Automation.CommandBreakpoint] } @@ -52,14 +58,14 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { It 'Can disable breakpoints' { foreach ($bp in $host.Runspace.Debugger.GetBreakpoints()) { - $bp = $host.Runspace.Debugger.DisableBreakpoint($bp) + $bp = $host.Runspace.Debugger.DisableBreakpoint($bp) $bp.Enabled | Should -BeFalse } } It 'Can enable breakpoints' { foreach ($bp in $host.Runspace.Debugger.GetBreakpoints()) { - $bp = $host.Runspace.Debugger.EnableBreakpoint($bp) + $bp = $host.Runspace.Debugger.EnableBreakpoint($bp) $bp.Enabled | Should -BeTrue } } @@ -73,10 +79,27 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { It 'Returns an empty collection when there are no breakpoints' { $host.Runspace.Debugger.GetBreakpoints() | Should -HaveCount 0 } + + It 'Can set multiple breakpoints' { + $breakpoints = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]] @( + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 1) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 2) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 3) + ) + + $host.Runspace.Debugger.SetBreakpoints($breakpoints) + $host.Runspace.Debugger.GetBreakpoints() | Should -HaveCount 3 + } } Context 'Managing breakpoints in a remote runspace via the SDK' { + AfterAll { + foreach ($bp in $jobRunspace.Debugger.GetBreakpoints()) { + $jobRunspace.Debugger.RemoveBreakpoint($bp) | Should -BeTrue + } + } + It 'Can set command breakpoints' { $jobRunspace.Debugger.SetCommandBreakpoint('Write-Verbose', { break }) | Should -BeOfType [System.Management.Automation.CommandBreakpoint] } @@ -96,14 +119,14 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { It 'Can disable breakpoints' { foreach ($bp in $jobRunspace.Debugger.GetBreakpoints()) { - $bp = $jobRunspace.Debugger.DisableBreakpoint($bp) + $bp = $jobRunspace.Debugger.DisableBreakpoint($bp) $bp.Enabled | Should -BeFalse } } It 'Can enable breakpoints' { foreach ($bp in $jobRunspace.Debugger.GetBreakpoints()) { - $bp = $jobRunspace.Debugger.EnableBreakpoint($bp) + $bp = $jobRunspace.Debugger.EnableBreakpoint($bp) $bp.Enabled | Should -BeTrue } } @@ -125,13 +148,23 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { It 'Returns an empty collection when there are no breakpoints' { $jobRunspace.Debugger.GetBreakpoints() | Should -HaveCount 0 } + + It 'Can set multiple breakpoints' { + $breakpoints = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]] @( + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 1) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 2) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 3) + ) + + $jobRunspace.Debugger.SetBreakpoints($breakpoints) + $jobRunspace.Debugger.GetBreakpoints() | Should -HaveCount 3 + } } - Context 'Handling empty collections and errors while managing breakpoints in the host runspace via the SDK' { + Context 'Handling empty collections and errors while managing breakpoints in the host runspace via the SDK' { BeforeAll { - $bp = $host.Runspace.Debugger.SetCommandBreakpoint('Test-ThisCommandDoesNotExist') - $host.Runspace.Debugger.RemoveBreakpoint($bp) > $null + $bp = [System.Management.Automation.CommandBreakpoint]::new($TestDrive, $null, 'Test-ThisCommandDoesNotExist') } It 'Returns false when trying to disable a breakpoint that does not exist' { @@ -166,4 +199,71 @@ Describe 'Breakpoint SDK Unit Tests' -Tags 'CI' { $jobRunspace.Debugger.RemoveBreakpoint($bp) | Should -BeFalse } } + + Context 'Manage breakpoints in another runspace' { + BeforeAll { + $runspace = [runspacefactory]::CreateRunspace() + $runspace.Open() + } + + AfterAll { + $runspace.Close() + $runspace.Dispose() + } + + It 'Can set command breakpoints' { + $host.Runspace.Debugger.SetCommandBreakpoint('Test-ThisCommandDoesNotExist', $null, $null, $runspace.Id) | Should -BeOfType [System.Management.Automation.CommandBreakpoint] + } + + It 'Can set variable breakpoints' { + $host.Runspace.Debugger.SetVariableBreakpoint('DebugPreference', 'ReadWrite', { continue }, $null, $runspace.Id) | Should -BeOfType [System.Management.Automation.VariableBreakpoint] + } + + It 'Can set line breakpoints' { + $host.Runspace.Debugger.SetLineBreakpoint($PSCommandPath, 1, 1, { continue }, $runspace.Id) | Should -BeOfType [System.Management.Automation.LineBreakpoint] + } + + It 'Can get breakpoints' { + $host.Runspace.Debugger.GetBreakpoints($runspace.Id) | Should -HaveCount 3 + } + + It 'Can disable breakpoints' { + foreach ($bp in $host.Runspace.Debugger.GetBreakpoints($runspace.Id)) { + $bp = $host.Runspace.Debugger.DisableBreakpoint($bp, $runspace.Id) + $bp.Enabled | Should -BeFalse + } + } + + It 'Can enable breakpoints' { + foreach ($bp in $host.Runspace.Debugger.GetBreakpoints($runspace.Id)) { + $bp = $host.Runspace.Debugger.EnableBreakpoint($bp, $runspace.Id) + $bp.Enabled | Should -BeTrue + } + } + + It 'Doesn''t manipulate any breakpoints in the default runspace' { + $host.Runspace.Debugger.GetBreakpoints() | Should -BeNullOrEmpty + } + + It 'Can remove breakpoints' { + foreach ($bp in $host.Runspace.Debugger.GetBreakpoints($runspace.Id)) { + $host.Runspace.Debugger.RemoveBreakpoint($bp, $runspace.Id) | Should -BeTrue + } + } + + It 'Returns an empty collection when there are no breakpoints' { + $host.Runspace.Debugger.GetBreakpoints($runspace.Id) | Should -HaveCount 0 + } + + It 'Can set multiple breakpoints' { + $breakpoints = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]] @( + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 1) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 2) + [System.Management.Automation.LineBreakpoint]::new("/Path/to/foo.ps1", 3) + ) + + $host.Runspace.Debugger.SetBreakpoints($breakpoints, $runspace.Id) + $host.Runspace.Debugger.GetBreakpoints($runspace.Id) | Should -HaveCount 3 + } + } }