diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Disable-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Disable-PSBreakpoint.cs index c54c9499761..1bdd5a5aea8 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Disable-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Disable-PSBreakpoint.cs @@ -8,41 +8,36 @@ namespace Microsoft.PowerShell.Commands /// /// This class implements Disable-PSBreakpoint. /// - [Cmdlet(VerbsLifecycle.Disable, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = "Breakpoint", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096498")] + [Cmdlet(VerbsLifecycle.Disable, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = BreakpointParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096498")] [OutputType(typeof(Breakpoint))] - public class DisablePSBreakpointCommand : PSBreakpointCommandBase + public class DisablePSBreakpointCommand : PSBreakpointUpdaterCommandBase { + #region parameters + /// /// Gets or sets the parameter -passThru which states whether the /// command should place the breakpoints it processes in the pipeline. /// [Parameter] - public SwitchParameter PassThru - { - get - { - return _passThru; - } + public SwitchParameter PassThru { get; set; } - set - { - _passThru = value; - } - } + #endregion parameters - private bool _passThru; + #region overrides /// /// Disables the given breakpoint. /// protected override void ProcessBreakpoint(Breakpoint breakpoint) { - this.Context.Debugger.DisableBreakpoint(breakpoint); + breakpoint = Runspace.Debugger.DisableBreakpoint(breakpoint); - if (_passThru) + if (PassThru) { - WriteObject(breakpoint); + base.ProcessBreakpoint(breakpoint); } } + + #endregion overrides } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Enable-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Enable-PSBreakpoint.cs index d06073c4214..0822e146ae6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Enable-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Enable-PSBreakpoint.cs @@ -1,158 +1,43 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Diagnostics; using System.Management.Automation; -using System.Management.Automation.Internal; namespace Microsoft.PowerShell.Commands { - /// - /// Base class for Enable/Disable/Remove-PSBreakpoint. - /// - public abstract class PSBreakpointCommandBase : PSCmdlet - { - /// - /// The breakpoint to enable. - /// - [Parameter(ParameterSetName = "Breakpoint", ValueFromPipeline = true, Position = 0, Mandatory = true)] - [ValidateNotNull] - public Breakpoint[] Breakpoint - { - get - { - return _breakpoints; - } - - set - { - _breakpoints = value; - } - } - - private Breakpoint[] _breakpoints; - - /// - /// The Id of the breakpoint to enable. - /// - [Parameter(ParameterSetName = "Id", ValueFromPipelineByPropertyName = true, Position = 0, Mandatory = true)] - [ValidateNotNull] - public int[] Id - { - get - { - return _ids; - } - - set - { - _ids = value; - } - } - - private int[] _ids; - - /// - /// Gathers the list of breakpoints to process and calls ProcessBreakpoints. - /// - protected override void ProcessRecord() - { - if (ParameterSetName.Equals("Breakpoint", StringComparison.OrdinalIgnoreCase)) - { - foreach (Breakpoint breakpoint in _breakpoints) - { - if (ShouldProcessInternal(breakpoint.ToString())) - { - ProcessBreakpoint(breakpoint); - } - } - } - else - { - Debug.Assert(ParameterSetName.Equals("Id", StringComparison.OrdinalIgnoreCase)); - - foreach (int i in _ids) - { - Breakpoint breakpoint = this.Context.Debugger.GetBreakpoint(i); - - if (breakpoint == null) - { - WriteError( - new ErrorRecord( - new ArgumentException(StringUtil.Format(Debugger.BreakpointIdNotFound, i)), - "PSBreakpoint:BreakpointIdNotFound", - ErrorCategory.InvalidArgument, - null)); - continue; - } - - if (ShouldProcessInternal(breakpoint.ToString())) - { - ProcessBreakpoint(breakpoint); - } - } - } - } - - /// - /// Process the given breakpoint. - /// - protected abstract void ProcessBreakpoint(Breakpoint breakpoint); - - private bool ShouldProcessInternal(string target) - { - // ShouldProcess should be called only if the WhatIf or Confirm parameters are passed in explicitly. - // It should *not* be called if we are in a nested debug prompt and the current running command was - // run with -WhatIf or -Confirm, because this prevents the user from adding/removing breakpoints inside - // a debugger stop. - if (this.MyInvocation.BoundParameters.ContainsKey("WhatIf") || this.MyInvocation.BoundParameters.ContainsKey("Confirm")) - { - return ShouldProcess(target); - } - - return true; - } - } - /// /// This class implements Enable-PSBreakpoint. /// - [Cmdlet(VerbsLifecycle.Enable, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = "Id", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096700")] + [Cmdlet(VerbsLifecycle.Enable, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = BreakpointParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096700")] [OutputType(typeof(Breakpoint))] - public class EnablePSBreakpointCommand : PSBreakpointCommandBase + public class EnablePSBreakpointCommand : PSBreakpointUpdaterCommandBase { + #region parameters + /// /// Gets or sets the parameter -passThru which states whether the /// command should place the breakpoints it processes in the pipeline. /// [Parameter] - public SwitchParameter PassThru - { - get - { - return _passThru; - } + public SwitchParameter PassThru { get; set; } - set - { - _passThru = value; - } - } + #endregion parameters - private bool _passThru; + #region overrides /// /// Enables the given breakpoint. /// protected override void ProcessBreakpoint(Breakpoint breakpoint) { - this.Context.Debugger.EnableBreakpoint(breakpoint); + breakpoint = Runspace.Debugger.EnableBreakpoint(breakpoint); - if (_passThru) + if (PassThru) { - WriteObject(breakpoint); + base.ProcessBreakpoint(breakpoint); } } + + #endregion overrides } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Get-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Get-PSBreakpoint.cs index b836fafc9f9..4f8903facfd 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Get-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Get-PSBreakpoint.cs @@ -27,152 +27,97 @@ public enum BreakpointType /// /// This class implements Get-PSBreakpoint. /// - [Cmdlet(VerbsCommon.Get, "PSBreakpoint", DefaultParameterSetName = "Script", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097108")] - [OutputType(typeof(Breakpoint))] - public class GetPSBreakpointCommand : PSCmdlet + [Cmdlet(VerbsCommon.Get, "PSBreakpoint", DefaultParameterSetName = LineParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097108")] + [OutputType(typeof(CommandBreakpoint), ParameterSetName = new[] { CommandParameterSetName })] + [OutputType(typeof(LineBreakpoint), ParameterSetName = new[] { LineParameterSetName })] + [OutputType(typeof(VariableBreakpoint), ParameterSetName = new[] { VariableParameterSetName })] + [OutputType(typeof(Breakpoint), ParameterSetName = new[] { TypeParameterSetName, IdParameterSetName })] + public class GetPSBreakpointCommand : PSBreakpointAccessorCommandBase { + #region strings + + internal const string TypeParameterSetName = "Type"; + internal const string IdParameterSetName = "Id"; + + #endregion strings + #region parameters /// /// Scripts of the breakpoints to output. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's OK to use arrays for cmdlet parameters")] - [Parameter(ParameterSetName = "Script", Position = 0, ValueFromPipeline = true)] - [Parameter(ParameterSetName = "Variable")] - [Parameter(ParameterSetName = "Command")] - [Parameter(ParameterSetName = "Type")] + [Parameter(ParameterSetName = LineParameterSetName, Position = 0, ValueFromPipeline = true)] + [Parameter(ParameterSetName = CommandParameterSetName)] + [Parameter(ParameterSetName = VariableParameterSetName)] + [Parameter(ParameterSetName = TypeParameterSetName)] [ValidateNotNullOrEmpty()] - public string[] Script - { - get - { - return _script; - } - - set - { - _script = value; - } - } - - private string[] _script; + public string[] Script { get; set; } /// /// IDs of the breakpoints to output. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's OK to use arrays for cmdlet parameters")] - [Parameter(ParameterSetName = "Id", Mandatory = true, Position = 0, ValueFromPipeline = true)] + [Parameter(ParameterSetName = IdParameterSetName, Mandatory = true, Position = 0, ValueFromPipeline = true)] [ValidateNotNull] - public int[] Id - { - get - { - return _id; - } - - set - { - _id = value; - } - } - - private int[] _id; + public int[] Id { get; set; } /// /// Variables of the breakpoints to output. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's OK to use arrays for cmdlet parameters")] - [Parameter(ParameterSetName = "Variable", Mandatory = true)] + [Parameter(ParameterSetName = VariableParameterSetName, Mandatory = true)] [ValidateNotNull] - public string[] Variable - { - get - { - return _variable; - } - - set - { - _variable = value; - } - } - - private string[] _variable; + public string[] Variable { get; set; } /// /// Commands of the breakpoints to output. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's OK to use arrays for cmdlet parameters")] - [Parameter(ParameterSetName = "Command", Mandatory = true)] + [Parameter(ParameterSetName = CommandParameterSetName, Mandatory = true)] [ValidateNotNull] - public string[] Command - { - get - { - return _command; - } - - set - { - _command = value; - } - } - - private string[] _command; + public string[] Command { get; set; } /// /// Commands of the breakpoints to output. /// [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's OK to use arrays for cmdlet parameters")] [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is OK for a cmdlet parameter")] - [Parameter(ParameterSetName = "Type", Mandatory = true, Position = 0, ValueFromPipeline = true)] + [Parameter(ParameterSetName = TypeParameterSetName, Mandatory = true, Position = 0, ValueFromPipeline = true)] [ValidateNotNull] - public BreakpointType[] Type - { - get - { - return _type; - } - - set - { - _type = value; - } - } - - private BreakpointType[] _type; + public BreakpointType[] Type { get; set; } #endregion parameters + #region overrides + /// /// Remove breakpoints. /// protected override void ProcessRecord() { - List breakpoints = Context.Debugger.GetBreakpoints(); + List breakpoints = Runspace.Debugger.GetBreakpoints(); - // // Filter by parameter set - // - if (this.ParameterSetName.Equals("Script", StringComparison.OrdinalIgnoreCase)) + if (ParameterSetName.Equals(LineParameterSetName, StringComparison.OrdinalIgnoreCase)) { // no filter } - else if (this.ParameterSetName.Equals("Id", StringComparison.OrdinalIgnoreCase)) + else if (ParameterSetName.Equals(IdParameterSetName, StringComparison.OrdinalIgnoreCase)) { breakpoints = Filter( breakpoints, - _id, + Id, delegate (Breakpoint breakpoint, int id) { return breakpoint.Id == id; } ); } - else if (this.ParameterSetName.Equals("Command", StringComparison.OrdinalIgnoreCase)) + else if (ParameterSetName.Equals(CommandParameterSetName, StringComparison.OrdinalIgnoreCase)) { breakpoints = Filter( breakpoints, - _command, + Command, delegate (Breakpoint breakpoint, string command) { CommandBreakpoint commandBreakpoint = breakpoint as CommandBreakpoint; @@ -185,11 +130,11 @@ protected override void ProcessRecord() return commandBreakpoint.Command.Equals(command, StringComparison.OrdinalIgnoreCase); }); } - else if (this.ParameterSetName.Equals("Variable", StringComparison.OrdinalIgnoreCase)) + else if (ParameterSetName.Equals(VariableParameterSetName, StringComparison.OrdinalIgnoreCase)) { breakpoints = Filter( breakpoints, - _variable, + Variable, delegate (Breakpoint breakpoint, string variable) { VariableBreakpoint variableBreakpoint = breakpoint as VariableBreakpoint; @@ -202,11 +147,11 @@ protected override void ProcessRecord() return variableBreakpoint.Variable.Equals(variable, StringComparison.OrdinalIgnoreCase); }); } - else if (this.ParameterSetName.Equals("Type", StringComparison.OrdinalIgnoreCase)) + else if (ParameterSetName.Equals(TypeParameterSetName, StringComparison.OrdinalIgnoreCase)) { breakpoints = Filter( breakpoints, - _type, + Type, delegate (Breakpoint breakpoint, BreakpointType type) { switch (type) @@ -244,14 +189,12 @@ protected override void ProcessRecord() Diagnostics.Assert(false, "Invalid parameter set: {0}", this.ParameterSetName); } - // // Filter by script - // - if (_script != null) + if (Script != null) { breakpoints = Filter( breakpoints, - _script, + Script, delegate (Breakpoint breakpoint, string script) { if (breakpoint.Script == null) @@ -267,15 +210,17 @@ protected override void ProcessRecord() }); } - // // Output results - // foreach (Breakpoint b in breakpoints) { - WriteObject(b); + ProcessBreakpoint(b); } } + #endregion overrides + + #region private methods + /// /// Gives the criteria to filter breakpoints. /// @@ -303,5 +248,7 @@ private List Filter(List input, T[] filter, FilterSel return output; } + + #endregion private methods } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointAccessorCommandBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointAccessorCommandBase.cs new file mode 100644 index 00000000000..88b3cde9acf --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointAccessorCommandBase.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Base class for Get/Set-PSBreakpoint. + /// + public abstract class PSBreakpointAccessorCommandBase : PSBreakpointCommandBase + { + #region strings + + internal const string CommandParameterSetName = "Command"; + internal const string LineParameterSetName = "Line"; + internal const string VariableParameterSetName = "Variable"; + + #endregion strings + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCommandBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCommandBase.cs new file mode 100644 index 00000000000..bc01f341861 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCommandBase.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Base class for PSBreakpoint cmdlets. + /// + public abstract class PSBreakpointCommandBase : PSCmdlet + { + #region parameters + + /// + /// Gets or sets the runspace where the breakpoints will be used. + /// + [Experimental("Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace", ExperimentAction.Show)] + [Parameter] + [ValidateNotNull] + [Runspace] + public virtual Runspace Runspace { get; set; } + + #endregion parameters + + #region overrides + + /// + /// Identifies the default runspace. + /// + protected override void BeginProcessing() + { + if (Runspace == null) + { + Runspace = Context.CurrentRunspace; + } + } + + #endregion overrides + + #region protected methods + + /// + /// Write the given breakpoint out to the pipeline, decorated with the runspace instance id if appropriate. + /// + /// The breakpoint to write to the pipeline. + protected virtual void ProcessBreakpoint(Breakpoint breakpoint) + { + if (Runspace != Context.CurrentRunspace) + { + var pso = new PSObject(breakpoint); + pso.Properties.Add(new PSNoteProperty(RemotingConstants.RunspaceIdNoteProperty, Runspace.InstanceId)); + WriteObject(pso); + } + else + { + WriteObject(breakpoint); + } + } + + #endregion protected methods + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs deleted file mode 100644 index cb0e9ea2435..00000000000 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Management.Automation; -using System.Management.Automation.Internal; - -namespace Microsoft.PowerShell.Commands -{ - /// - /// Base class for Set/New-PSBreakpoint. - /// - public class PSBreakpointCreationBase : PSCmdlet - { - internal const string CommandParameterSetName = "Command"; - internal const string LineParameterSetName = "Line"; - internal const string VariableParameterSetName = "Variable"; - - #region parameters - - /// - /// The action to take when hitting this breakpoint. - /// - [Parameter(ParameterSetName = CommandParameterSetName)] - [Parameter(ParameterSetName = LineParameterSetName)] - [Parameter(ParameterSetName = VariableParameterSetName)] - public ScriptBlock Action { get; set; } - - /// - /// The column to set the breakpoint on. - /// - [Parameter(Position = 2, ParameterSetName = LineParameterSetName)] - [ValidateRange(1, int.MaxValue)] - public int Column { get; set; } - - /// - /// The command(s) to set the breakpoint on. - /// - [Alias("C")] - [Parameter(ParameterSetName = CommandParameterSetName, Mandatory = true)] - public string[] Command { get; set; } - - /// - /// The line to set the breakpoint on. - /// - [Parameter(Position = 1, ParameterSetName = LineParameterSetName, Mandatory = true)] - public int[] Line { get; set; } - - /// - /// The script to set the breakpoint on. - /// - [Parameter(ParameterSetName = CommandParameterSetName, Position = 0)] - [Parameter(ParameterSetName = LineParameterSetName, Mandatory = true, Position = 0)] - [Parameter(ParameterSetName = VariableParameterSetName, Position = 0)] - [ValidateNotNull] - public string[] Script { get; set; } - - /// - /// The variables to set the breakpoint(s) on. - /// - [Alias("V")] - [Parameter(ParameterSetName = VariableParameterSetName, Mandatory = true)] - public string[] Variable { get; set; } - - /// - /// The access type for variable breakpoints to break on. - /// - [Parameter(ParameterSetName = VariableParameterSetName)] - public VariableAccessMode Mode { get; set; } = VariableAccessMode.Write; - - #endregion parameters - - internal Collection ResolveScriptPaths() - { - Collection scripts = new Collection(); - - if (Script != null) - { - foreach (string script in Script) - { - Collection scriptPaths = SessionState.Path.GetResolvedPSPathFromPSPath(script); - - for (int i = 0; i < scriptPaths.Count; i++) - { - string providerPath = scriptPaths[i].ProviderPath; - - if (!File.Exists(providerPath)) - { - WriteError( - new ErrorRecord( - new ArgumentException(StringUtil.Format(Debugger.FileDoesNotExist, providerPath)), - "NewPSBreakpoint:FileDoesNotExist", - ErrorCategory.InvalidArgument, - null)); - - continue; - } - - string extension = Path.GetExtension(providerPath); - - if (!extension.Equals(".ps1", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".psm1", StringComparison.OrdinalIgnoreCase)) - { - WriteError( - new ErrorRecord( - new ArgumentException(StringUtil.Format(Debugger.WrongExtension, providerPath)), - "NewPSBreakpoint:WrongExtension", - ErrorCategory.InvalidArgument, - null)); - continue; - } - - scripts.Add(Path.GetFullPath(providerPath)); - } - } - } - - return scripts; - } - } -} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointUpdaterCommandBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointUpdaterCommandBase.cs new file mode 100644 index 00000000000..0b1452b0709 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointUpdaterCommandBase.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management.Automation; +using System.Management.Automation.Internal; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Base class for Enable/Disable/Remove-PSBreakpoint. + /// + public abstract class PSBreakpointUpdaterCommandBase : PSBreakpointCommandBase + { + #region strings + + internal const string BreakpointParameterSetName = "Breakpoint"; + internal const string IdParameterSetName = "Id"; + + #endregion strings + + #region parameters + + /// + /// Gets or sets the breakpoint to enable. + /// + [Parameter(ParameterSetName = BreakpointParameterSetName, ValueFromPipeline = true, Position = 0, Mandatory = true)] + [ValidateNotNull] + public Breakpoint[] Breakpoint { get; set; } + + /// + /// Gets or sets the Id of the breakpoint to enable. + /// + [Parameter(ParameterSetName = IdParameterSetName, ValueFromPipelineByPropertyName = true, Position = 0, Mandatory = true)] + [ValidateNotNull] + public int[] Id { get; set; } + + /// + /// Gets or sets the runspace where the breakpoints will be used. + /// + [Parameter(ParameterSetName = IdParameterSetName, ValueFromPipelineByPropertyName = true)] + [Alias("RunspaceId")] + [Runspace] + public override Runspace Runspace { get; set; } + + #endregion parameters + + #region overrides + + /// + /// Gathers the list of breakpoints to process and calls ProcessBreakpoints. + /// + protected override void ProcessRecord() + { + if (ParameterSetName.Equals(BreakpointParameterSetName, StringComparison.OrdinalIgnoreCase)) + { + foreach (Breakpoint breakpoint in Breakpoint) + { + if (ShouldProcessInternal(breakpoint.ToString()) && + TryGetRunspace(breakpoint)) + { + ProcessBreakpoint(breakpoint); + } + } + } + else + { + Debug.Assert( + ParameterSetName.Equals(IdParameterSetName, StringComparison.OrdinalIgnoreCase), + $"There should be no other parameter sets besides '{BreakpointParameterSetName}' and '{IdParameterSetName}'."); + + foreach (int id in Id) + { + Breakpoint breakpoint; + if (TryGetBreakpoint(id, out breakpoint) && + ShouldProcessInternal(breakpoint.ToString())) + { + ProcessBreakpoint(breakpoint); + } + } + } + } + + #endregion overrides + + #region private data + + private readonly Dictionary runspaces = new Dictionary(); + + #endregion private data + + #region private methods + + private bool TryGetRunspace(Breakpoint breakpoint) + { + // Breakpoints retrieved from another runspace will have a RunspaceId note property of type Guid on them. + var pso = new PSObject(breakpoint); + var runspaceInstanceIdProperty = pso.Properties[RemotingConstants.RunspaceIdNoteProperty]; + if (runspaceInstanceIdProperty == null) + { + Runspace = Context.CurrentRunspace; + return true; + } + + Debug.Assert(runspaceInstanceIdProperty.TypeNameOfValue.Equals("System.Guid", StringComparison.OrdinalIgnoreCase), "Instance ids must be GUIDs."); + + var runspaceInstanceId = (Guid)runspaceInstanceIdProperty.Value; + if (runspaces.ContainsKey(runspaceInstanceId)) + { + Runspace = runspaces[runspaceInstanceId]; + return true; + } + + var matchingRunspaces = GetRunspaceUtils.GetRunspacesByInstanceId(new[] { runspaceInstanceId }); + if (matchingRunspaces.Count != 1) + { + WriteError( + new ErrorRecord( + new ArgumentException(StringUtil.Format(Debugger.RunspaceInstanceIdNotFound, runspaceInstanceId)), + "PSBreakpoint:RunspaceInstanceIdNotFound", + ErrorCategory.InvalidArgument, + null)); + return false; + } + + Runspace = runspaces[runspaceInstanceId] = matchingRunspaces[0]; + return true; + } + + private bool TryGetBreakpoint(int id, out Breakpoint breakpoint) + { + breakpoint = Runspace.Debugger.GetBreakpoint(id); + + if (breakpoint == null) + { + WriteError( + new ErrorRecord( + new ArgumentException(StringUtil.Format(Debugger.BreakpointIdNotFound, id)), + "PSBreakpoint:BreakpointIdNotFound", + ErrorCategory.InvalidArgument, + null)); + return false; + } + + return true; + } + + private bool ShouldProcessInternal(string target) + { + // ShouldProcess should be called only if the WhatIf or Confirm parameters are passed in explicitly. + // It should *not* be called if we are in a nested debug prompt and the current running command was + // run with -WhatIf or -Confirm, because this prevents the user from adding/removing breakpoints inside + // a debugger stop. + if (MyInvocation.BoundParameters.ContainsKey("WhatIf") || + MyInvocation.BoundParameters.ContainsKey("Confirm")) + { + return ShouldProcess(target); + } + + return true; + } + + #endregion private methods + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Remove-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Remove-PSBreakpoint.cs index 458b471b322..de324b33fb8 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Remove-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Remove-PSBreakpoint.cs @@ -8,16 +8,20 @@ namespace Microsoft.PowerShell.Commands /// /// This class implements Remove-PSBreakpoint. /// - [Cmdlet(VerbsCommon.Remove, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = "Breakpoint", + [Cmdlet(VerbsCommon.Remove, "PSBreakpoint", SupportsShouldProcess = true, DefaultParameterSetName = BreakpointParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2097134")] - public class RemovePSBreakpointCommand : PSBreakpointCommandBase + public class RemovePSBreakpointCommand : PSBreakpointUpdaterCommandBase { + #region overrides + /// /// Removes the given breakpoint. /// protected override void ProcessBreakpoint(Breakpoint breakpoint) { - this.Context.Debugger.RemoveBreakpoint(breakpoint); + Runspace.Debugger.RemoveBreakpoint(breakpoint); } + + #endregion overrides } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/RunspaceAttribute.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/RunspaceAttribute.cs new file mode 100644 index 00000000000..0967c9a4985 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/RunspaceAttribute.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma warning disable 1634, 1691 +#pragma warning disable 56506 + +using Microsoft.PowerShell.Commands; + +namespace System.Management.Automation.Runspaces +{ + /// + /// Defines the attribute used to designate a cmdlet parameter as one that + /// should accept runspaces. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public sealed class RunspaceAttribute : ArgumentTransformationAttribute + { + /// + /// Transforms the input data to a Runspace. + /// + /// + /// The engine APIs for the context under which the transformation is being + /// made. + /// + /// + /// If a string, the transformation uses the input as the runspace name. + /// If an int, the transformation uses the input as the runspace ID. + /// If a guid, the transformation uses the input as the runspace GUID. + /// If already a Runspace, the transform does nothing. + /// + /// A runspace object representing the inputData. + public override object Transform(EngineIntrinsics engineIntrinsics, object inputData) + { + if (engineIntrinsics?.Host?.UI == null) + { + throw PSTraceSource.NewArgumentNullException("engineIntrinsics"); + } + + if (inputData == null) + { + return null; + } + + // Try to coerce the input as a runspace + Runspace runspace = LanguagePrimitives.FromObjectAs(inputData); + if (runspace != null) + { + return runspace; + } + + // Try to coerce the runspace if the user provided a string, int, or guid + switch (inputData) + { + case string name: + var runspacesByName = GetRunspaceUtils.GetRunspacesByName(new[] { name }); + if (runspacesByName.Count == 1) + { + return runspacesByName[0]; + } + + break; + + case int id: + var runspacesById = GetRunspaceUtils.GetRunspacesById(new[] { id }); + if (runspacesById.Count == 1) + { + return runspacesById[0]; + } + + break; + + case Guid guid: + var runspacesByGuid = GetRunspaceUtils.GetRunspacesByInstanceId(new[] { guid }); + if (runspacesByGuid.Count == 1) + { + return runspacesByGuid[0]; + } + + break; + + default: + // Non-convertible type + break; + } + + // If we couldn't get a single runspace, return the inputData + return inputData; + } + + /// + /// Gets a flag indicating whether or not null optional parameters are transformed. + /// + public override bool TransformNullOptionalParameters { get { return false; } } + } +} + +#pragma warning restore 56506 diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs index cb4b14a6619..e628da48244 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs @@ -7,6 +7,7 @@ using System.IO; using System.Management.Automation; using System.Management.Automation.Internal; +using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.Commands { @@ -14,14 +15,75 @@ namespace Microsoft.PowerShell.Commands /// This class implements Set-PSBreakpoint command. /// [Cmdlet(VerbsCommon.Set, "PSBreakpoint", DefaultParameterSetName = LineParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2096623")] - [OutputType(typeof(VariableBreakpoint), typeof(CommandBreakpoint), typeof(LineBreakpoint))] - public class SetPSBreakpointCommand : PSBreakpointCreationBase + [OutputType(typeof(CommandBreakpoint), ParameterSetName = new string[] { CommandParameterSetName })] + [OutputType(typeof(LineBreakpoint), ParameterSetName = new string[] { LineParameterSetName })] + [OutputType(typeof(VariableBreakpoint), ParameterSetName = new string[] { VariableParameterSetName })] + public class SetPSBreakpointCommand : PSBreakpointAccessorCommandBase { + #region parameters + + /// + /// Gets or sets the action to take when hitting this breakpoint. + /// + [Parameter(ParameterSetName = CommandParameterSetName)] + [Parameter(ParameterSetName = LineParameterSetName)] + [Parameter(ParameterSetName = VariableParameterSetName)] + public ScriptBlock Action { get; set; } + + /// + /// Gets or sets the column to set the breakpoint on. + /// + [Parameter(Position = 2, ParameterSetName = LineParameterSetName)] + [ValidateRange(1, int.MaxValue)] + public int Column { get; set; } + + /// + /// Gets or sets the command(s) to set the breakpoint on. + /// + [Alias("C")] + [Parameter(ParameterSetName = CommandParameterSetName, Mandatory = true)] + public string[] Command { get; set; } + + /// + /// Gets or sets the line to set the breakpoint on. + /// + [Parameter(Position = 1, ParameterSetName = LineParameterSetName, Mandatory = true)] + public int[] Line { get; set; } + + /// + /// Gets or sets the script to set the breakpoint on. + /// + [Parameter(ParameterSetName = CommandParameterSetName, Position = 0)] + [Parameter(ParameterSetName = LineParameterSetName, Mandatory = true, Position = 0)] + [Parameter(ParameterSetName = VariableParameterSetName, Position = 0)] + [ValidateNotNull] + public string[] Script { get; set; } + + /// + /// Gets or sets the variables to set the breakpoint(s) on. + /// + [Alias("V")] + [Parameter(ParameterSetName = VariableParameterSetName, Mandatory = true)] + public string[] Variable { get; set; } + + /// + /// Gets or sets the access type for variable breakpoints to break on. + /// + [Parameter(ParameterSetName = VariableParameterSetName)] + public VariableAccessMode Mode { get; set; } = VariableAccessMode.Write; + + #endregion parameters + + #region overrides + /// /// Verifies that debugging is supported. /// protected override void BeginProcessing() { + // Call the base method to ensure Runspace is initialized properly. + base.BeginProcessing(); + // Check whether we are executing on a remote session and if so // whether the RemoteScript debug option is selected. if (this.Context.InternalHost.ExternalHost is System.Management.Automation.Remoting.ServerRemoteHost && @@ -61,11 +123,49 @@ protected override void BeginProcessing() protected override void ProcessRecord() { // If there is a script, resolve its path - Collection scripts = ResolveScriptPaths(); + Collection scripts = new Collection(); + + if (Script != null) + { + foreach (string script in Script) + { + Collection scriptPaths = SessionState.Path.GetResolvedPSPathFromPSPath(script); + + for (int i = 0; i < scriptPaths.Count; i++) + { + string providerPath = scriptPaths[i].ProviderPath; + + if (!File.Exists(providerPath)) + { + WriteError( + new ErrorRecord( + new ArgumentException(StringUtil.Format(Debugger.FileDoesNotExist, providerPath)), + "NewPSBreakpoint:FileDoesNotExist", + ErrorCategory.InvalidArgument, + null)); + + continue; + } + + string extension = Path.GetExtension(providerPath); + + if (!extension.Equals(".ps1", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".psm1", StringComparison.OrdinalIgnoreCase)) + { + WriteError( + new ErrorRecord( + new ArgumentException(StringUtil.Format(Debugger.WrongExtension, providerPath)), + "NewPSBreakpoint:WrongExtension", + ErrorCategory.InvalidArgument, + null)); + continue; + } + + scripts.Add(Path.GetFullPath(providerPath)); + } + } + } - // // If it is a command breakpoint... - // if (ParameterSetName.Equals(CommandParameterSetName, StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < Command.Length; i++) @@ -74,20 +174,18 @@ protected override void ProcessRecord() { foreach (string path in scripts) { - WriteObject( - Context.Debugger.SetCommandBreakpoint(Command[i], Action, path)); + ProcessBreakpoint( + Runspace.Debugger.SetCommandBreakpoint(Command[i], Action, path)); } } else { - WriteObject( - Context.Debugger.SetCommandBreakpoint(Command[i], Action, path: null)); + ProcessBreakpoint( + Runspace.Debugger.SetCommandBreakpoint(Command[i], Action, path: null)); } } } - // // If it is a variable breakpoint... - // else if (ParameterSetName.Equals(VariableParameterSetName, StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < Variable.Length; i++) @@ -96,20 +194,18 @@ protected override void ProcessRecord() { foreach (string path in scripts) { - WriteObject( - Context.Debugger.SetVariableBreakpoint(Variable[i], Mode, Action, path)); + ProcessBreakpoint( + Runspace.Debugger.SetVariableBreakpoint(Variable[i], Mode, Action, path)); } } else { - WriteObject( - Context.Debugger.SetVariableBreakpoint(Variable[i], Mode, Action, path: null)); + ProcessBreakpoint( + Runspace.Debugger.SetVariableBreakpoint(Variable[i], Mode, Action, path: null)); } } } - // // Else it is the default parameter set (Line breakpoint)... - // else { Debug.Assert(ParameterSetName.Equals(LineParameterSetName, StringComparison.OrdinalIgnoreCase)); @@ -130,11 +226,13 @@ protected override void ProcessRecord() foreach (string path in scripts) { - WriteObject( - Context.Debugger.SetLineBreakpoint(path, Line[i], Column, Action)); + ProcessBreakpoint( + Runspace.Debugger.SetLineBreakpoint(path, Line[i], Column, Action)); } } } } + + #endregion overrides } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/Debugger.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/Debugger.resx index 1c36e4c8af0..043fa4a9320 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/Debugger.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/Debugger.resx @@ -174,4 +174,7 @@ Wait-Debugger called on line {0} in {1}. + + A breakpoint associated with another runspace cannot be updated because there is no runspace with instance ID '{0}'. + diff --git a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index 33823700f7f..1ef5f3328e0 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -36,7 +36,7 @@ PrivateData = @{ ExperimentalFeatures = @( @{ Name = 'Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace' - Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger.' + Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger. Enables -Runspace parameter on *-PSBreakpoint cmdlets to support management of breakpoints in another runspace.' } ) } diff --git a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 index cd12b64d034..3b401f039e9 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -35,7 +35,7 @@ PrivateData = @{ ExperimentalFeatures = @( @{ Name = 'Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace' - Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger.' + Description = 'Enables -BreakAll parameter on Debug-Runspace and Debug-Job cmdlets to allow users to decide if they want PowerShell to break immediately in the current location when they attach a debugger. Enables -Runspace parameter on *-PSBreakpoint cmdlets to support management of breakpoints in another runspace.' } ) } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Disable-PSBreakpoint.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Disable-PSBreakpoint.Tests.ps1 new file mode 100644 index 00000000000..ab47ac094f6 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Disable-PSBreakpoint.Tests.ps1 @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe 'Disable-PSBreakpoint' -Tags 'CI' { + + BeforeEach { + # Set some breakpoints + $lineBp = Set-PSBreakpoint -Line ([int]::MaxValue) -Script $PSCommandPath + $cmdBp = Set-PSBreakpoint -Command Test-ThisIsNotReallyACommand + $varBp = Set-PSBreakpoint -Variable thisIsNotReallyAVariable + } + + AfterEach { + # Clean up after ourselves + Get-PSBreakpoint | Remove-PSBreakpoint + } + + It 'Should disable breakpoints using pipeline input by value' { + foreach ($bp in $lineBp, $cmdBp, $varBp) { + $bp = $bp | Disable-PSBreakpoint -PassThru + $bp.Enabled | Should -Not -BeTrue + } + } + + It 'Should disable breakpoints using pipeline input by property name' { + foreach ($bp in $lineBp, $cmdBp, $varBp) { + $bp = [pscustomobject]@{ Id = $bp.Id } | Disable-PSBreakpoint -PassThru + $bp.Enabled | Should -Not -BeTrue + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-PSBreakpoint.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-PSBreakpoint.Tests.ps1 new file mode 100644 index 00000000000..b0550cd0773 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-PSBreakpoint.Tests.ps1 @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe 'Enable-PSBreakpoint' -Tags 'CI' { + + BeforeEach { + # Set some breakpoints + $lineBp = Set-PSBreakpoint -Line ([int]::MaxValue) -Script $PSCommandPath | Disable-PSBreakpoint -PassThru + $cmdBp = Set-PSBreakpoint -Command Test-ThisIsNotReallyACommand | Disable-PSBreakpoint -PassThru + $varBp = Set-PSBreakpoint -Variable thisIsNotReallyAVariable | Disable-PSBreakpoint -PassThru + } + + AfterEach { + # Clean up after ourselves + Get-PSBreakpoint | Remove-PSBreakpoint + } + + It 'Should enable breakpoints using pipeline input by value' { + foreach ($bp in $lineBp, $cmdBp, $varBp) { + $bp = $bp | Enable-PSBreakpoint -PassThru + $bp.Enabled | Should -BeTrue + } + } + + It 'Should enable breakpoints using pipeline input by property name' { + foreach ($bp in $lineBp, $cmdBp, $varBp) { + $bp = [pscustomobject]@{ Id = $bp.Id } | Enable-PSBreakpoint -PassThru + $bp.Enabled | Should -BeTrue + } + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Remove-PSBreakpoint.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Remove-PSBreakpoint.Tests.ps1 index 549bb97851d..d1b04339167 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Remove-PSBreakpoint.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Remove-PSBreakpoint.Tests.ps1 @@ -1,65 +1,87 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + Describe "Remove-PSBreakpoint" -Tags "CI" { - # Set up test script - $testScript = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath assets) -ChildPath psbreakpointtestscript.ps1 + + BeforeAll { + # Set up test script + $testScript = Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath assets) -ChildPath psbreakpointtestscript.ps1 - $script = "`$var = 1 + $script = "`$var = 1 `$var2 = Get-Command # this is a comment Get-Date " - $script > $testScript + $script > $testScript + } BeforeEach { - # set some breakpoints - $line = Set-PSBreakpoint -Line 1,2,3 -Script $testScript - $command = Set-PSBreakpoint -Command "Get-Date" -Script $testScript - $variable = Set-PSBreakpoint -Variable var2 -Script $testScript + # set some breakpoints + $line = Set-PSBreakpoint -Line 1, 2, 3 -Script $testScript + $command = Set-PSBreakpoint -Command "Get-Date" -Script $testScript + $variable = Set-PSBreakpoint -Variable var2 -Script $testScript } Context "Basic Removal Methods Tests" { - It "Should be able to remove a breakpoint by breakpoint Id" { - $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length - $BreakID = $(Get-PSBreakpoint).Id[0] - Remove-PSBreakpoint -Id $BreakID + It "Should be able to remove a breakpoint by breakpoint Id" { + $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length + $BreakID = $(Get-PSBreakpoint).Id[0] + Remove-PSBreakpoint -Id $BreakID - $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints -1) - } + $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints - 1) + } - It "Should be able to remove a breakpoint by variable" { - $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length - Remove-PSBreakpoint -Breakpoint $variable + It "Should be able to remove a breakpoint by variable" { + $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length + Remove-PSBreakpoint -Breakpoint $variable - $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints -1) - } + $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints - 1) + } - It "Should be able to remove a breakpoint by command" { - $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length - Remove-PSBreakpoint -Breakpoint $command + It "Should be able to remove a breakpoint by command" { + $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length + Remove-PSBreakpoint -Breakpoint $command - $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints -1) - } + $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints - 1) + } - It "Should be able to pipe breakpoint objects to Remove-PSBreakpoint" { - $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length - $variable | Remove-PSBreakpoint + It "Should be able to pipe breakpoint objects to Remove-PSBreakpoint" { + $NumberOfBreakpoints = $(Get-PSBreakpoint).Id.length + $variable | Remove-PSBreakpoint - $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints -1) - } + $(Get-PSBreakpoint).Id.length | Should -Be ($NumberOfBreakpoints - 1) + } } It "Should Remove all breakpoints" { - $(Get-PSBreakpoint).Id.Length | Should -Not -BeNullOrEmpty + $(Get-PSBreakpoint).Id.Length | Should -Not -BeNullOrEmpty - Get-PSBreakpoint | Remove-PSBreakpoint + Get-PSBreakpoint | Remove-PSBreakpoint - $(Get-PSBreakpoint).Id.Length | Should -Be 0 + $(Get-PSBreakpoint).Id.Length | Should -Be 0 } - #Clean up after ourselves + It 'Should remove breakpoints using pipeline input by value' { + foreach ($bp in $line, $command, $variable) { + $bp | Remove-PSBreakpoint + } - Remove-Item $testScript + Get-PSBreakpoint | Should -HaveCount 0 + } + + It 'Should remove breakpoints using pipeline input by property name' { + foreach ($bp in $line, $command, $variable) { + [pscustomobject]@{ Id = $bp.Id } | Remove-PSBreakpoint + } + + Get-PSBreakpoint | Should -HaveCount 0 + } + + AfterAll { + #Clean up after ourselves + + Remove-Item $testScript + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1 new file mode 100644 index 00000000000..fe131f99117 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1 @@ -0,0 +1,207 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$FeatureEnabled = $EnabledExperimentalFeatures.Contains('Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace') + +Describe 'Runspace Breakpoint Unit Tests - Feature-Enabled' -Tags 'CI' { + + BeforeAll { + if (!$FeatureEnabled) { + Write-Verbose 'Test series skipped. This series of tests requires the experimental feature ''Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace'' to be enabled.' -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues['it:skip'] = $true + return + } + + # Start a job; this will create a runspace in which we can manage breakpoints + $job = Start-Job -ScriptBlock { + Set-PSBreakpoint -Command Start-Sleep + 1..240 | ForEach-Object { + Start-Sleep -Milliseconds 250 + $_ + Write-Error 'boo' + Write-Verbose 'Verbose' -Verbose + $DebugPreference = 'Continue' + Write-Debug 'Debug' + Write-Warning 'Warning' + } + } + + # Wait for the child job that gets created to hit the breakpoint. This is the + # only safe way to know that the job has actually entered a running state and + # that the remote runspace is listening for requests. + Wait-UntilTrue { $job.ChildJobs.Count -gt 0 -and $job.ChildJobs[0].State -eq 'AtBreakpoint' } -TimeoutInMilliseconds 10000 -IntervalInMilliseconds 250 + + # Get the runspace for the running job + $jobRunspace = $job.ChildJobs[0].Runspace + } + + AfterAll { + if (!$FeatureEnabled) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + return + } + + # Remove the running job forcibly (whether it has finished or not) + Remove-Job -Job $job -Force + } + + # Test the transformation attribute independently of PSBreakpoint cmdlet tests so that + # those tests can focus on scenarios expected to work. + Context 'Can transform a runspace name, id, or instanceid into a runspace' { + + function Test-RunspaceTransform { + param( + [ValidateNotNull()] + [Runspace] + [System.Management.Automation.Runspaces.Runspace()] + $Runspace + ) + $Runspace + } + + It 'Transforms a valid runspace name into a runspace' { + Test-RunspaceTransform -Runspace $host.Runspace.Name | Should -Be $host.Runspace + } + + It 'Transforms a valid runspace ID into a runspace' { + Test-RunspaceTransform -Runspace $host.Runspace.Id | Should -Be $host.Runspace + } + + It 'Transforms a valid runspace instance ID into a runspace' { + Test-RunspaceTransform -Runspace $host.Runspace.InstanceId | Should -Be $host.Runspace + } + + It 'Reports an argument transformation error when given invalid input' { + $e = { Test-RunspaceTransform -Runspace 'This is not a runspace name' } | Should -Throw -PassThru + $e.Exception.GetType().Name | Should -Be 'ParameterBindingArgumentTransformationException' + } + + It 'Passes through $null without transforming it' { + $e = { Test-RunspaceTransform -Runspace $null } | Should -Throw -PassThru + $e.Exception.GetType().Name | Should -Be 'ParameterBindingValidationException' + } + } + + Context 'Managing breakpoints in the host runspace' { + + It 'Can set breakpoints' { + Set-PSBreakpoint -Command Test-ThisCommandDoesNotExist -Runspace $host.Runspace | Should -BeOfType [System.Management.Automation.CommandBreakpoint] + } + + It 'Can get breakpoints, and the result breakpoints do not show the runspace id because they are local' { + foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) { + Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null + } + } + + It 'Can disable breakpoints in a pipeline' { + foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace | Disable-PSBreakpoint -PassThru) { + $bp.Enabled | Should -BeFalse + # This ensures we're working in the right runspace + Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null + } + } + + It 'Can enable breakpoints in a pipeline' { + foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace | Enable-PSBreakpoint -PassThru) { + $bp.Enabled | Should -BeTrue + # This ensures we're working in the right runspace + Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null + } + } + + It 'Can disable breakpoints by id' { + foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) { + $bp = Disable-PSBreakpoint -Id $bp.Id -Runspace $host.Runspace -PassThru + $bp.Enabled | Should -BeFalse + # This ensures we're working in the right runspace + Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null + } + } + + It 'Can enable breakpoints by id' { + foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) { + $bp = Enable-PSBreakpoint -Id $bp.Id -Runspace $host.Runspace -PassThru + $bp.Enabled | Should -BeTrue + # This ensures we're working in the right runspace + Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null + } + } + + It 'Can remove breakpoints' { + Get-PSBreakpoint -Runspace $host.Runspace | Remove-PSBreakpoint + Get-PSBreakpoint -Runspace $host.Runspace | Should -BeNull + } + } + + Context 'Managing breakpoints in a remote runspace' { + + AfterAll { + # Get rid of any breakpoints that were created in the default runspace. + # This is necessary due to a known bug that causes breakpoints with the + # same id to be created or updated in the default runspace. + Get-PSBreakpoint | Remove-PSBreakpoint + } + + It 'Can set breakpoints' { + Set-PSBreakpoint -Command Write-Verbose -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.CommandBreakpoint] + Set-PSBreakpoint -Variable DebugPreference -Mode ReadWrite -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.VariableBreakpoint] + Set-PSBreakpoint -Script $PSCommandPath -Line 1 -Column 1 -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.LineBreakpoint] + } + + It 'Can get breakpoints, and the result breakpoints show the remote runspace id' { + foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) { + $bp.RunspaceId | Should -Be $jobRunspace.InstanceId + } + } + + It 'Breakpoints are triggered by the remote debugger' { + $startTime = [DateTime]::UtcNow + $maxTimeToWait = [TimeSpan]'00:00:20' + while ($job.State -ne 'AtBreakpoint' -and ([DateTime]::UtcNow - $startTime) -lt $maxTimeToWait) { + Start-Sleep -Milliseconds 100 # Give the job a bit of time to hit a breakpoint + } + $job.State | Should -Be 'AtBreakpoint' + } + + It 'Can disable breakpoints in a pipeline' { + foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace | Disable-PSBreakpoint -PassThru) { + $bp.Enabled | Should -BeFalse + # This ensures we're working in the right runspace + $bp.RunspaceId | Should -Be $jobRunspace.InstanceId + } + } + + It 'Can enable breakpoints in a pipeline' { + foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace | Enable-PSBreakpoint -PassThru) { + $bp.Enabled | Should -BeTrue + # This ensures we're working in the right runspace + $bp.RunspaceId | Should -Be $jobRunspace.InstanceId + } + } + + It 'Can disable breakpoints by id' { + foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) { + $bp = Disable-PSBreakpoint -Id $bp.Id -Runspace $jobRunspace -PassThru + $bp.Enabled | Should -BeFalse + # This ensures we're working in the right runspace + $bp.RunspaceId | Should -Be $jobRunspace.InstanceId + } + } + + It 'Can enable breakpoints in a pipeline' { + foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) { + $bp = Enable-PSBreakpoint -Id $bp.Id -Runspace $jobRunspace -PassThru + $bp.Enabled | Should -BeTrue + # This ensures we're working in the right runspace + $bp.RunspaceId | Should -Be $jobRunspace.InstanceId + } + } + + It 'Can remove breakpoints' { + Get-PSBreakpoint -Runspace $jobRunspace | Remove-PSBreakpoint + Get-PSBreakpoint -Runspace $jobRunspace | Should -BeNull + } + } +} diff --git a/test/tools/TestMetadata.json b/test/tools/TestMetadata.json index fede31d68ed..db65e6ec212 100644 --- a/test/tools/TestMetadata.json +++ b/test/tools/TestMetadata.json @@ -5,6 +5,7 @@ "test/powershell/Language/Operators/NullConditional.Tests.ps1", "test/powershell/Language/Parser/Parsing.Tests.ps1", "test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1" ], - "PSCultureInvariantReplaceOperator": [ "test/powershell/Language/Operators/ReplaceOperator.Tests.ps1" ] + "PSCultureInvariantReplaceOperator": [ "test/powershell/Language/Operators/ReplaceOperator.Tests.ps1" ], + "Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1" ] } }