diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs index 9282a39df02..6ad1f929c31 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs @@ -100,6 +100,21 @@ public Guid InstanceId set; } + /// + /// The optional breakpoint objects to use for debugging. + /// + [Experimental("Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints", ExperimentAction.Show)] + [Parameter(Position = 1, + ParameterSetName = DebugRunspaceCommand.InstanceIdParameterSet)] + [Parameter(ParameterSetName = DebugRunspaceCommand.RunspaceParameterSet)] + [Parameter(ParameterSetName = DebugRunspaceCommand.IdParameterSet)] + [Parameter(ParameterSetName = DebugRunspaceCommand.NameParameterSet)] + public Breakpoint[] Breakpoint + { + get; + set; + } + #endregion #region Overrides @@ -260,7 +275,7 @@ private void WaitAndReceiveRunspaceOutput() _debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); // Set up host script debugger to debug the runspace. - _debugger.DebugRunspace(_runspace); + _debugger.DebugRunspace(_runspace, disableBreakAll: Breakpoint?.Length > 0); while (_debugging) { @@ -517,6 +532,10 @@ private void PrepareRunspace(Runspace runspace) { SetLocalMode(runspace.Debugger, true); EnableHostDebugger(runspace, false); + if (Breakpoint?.Length > 0) + { + runspace.Debugger?.SetBreakpoints(Breakpoint); + } } private void RestoreRunspace(Runspace runspace) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/EnableDisableRunspaceDebugCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/EnableDisableRunspaceDebugCommand.cs index 92dcae2d056..5c1b61f3601 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/EnableDisableRunspaceDebugCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/EnableDisableRunspaceDebugCommand.cs @@ -350,6 +350,22 @@ public SwitchParameter BreakAll set; } + /// + /// The optional breakpoint objects to use for debugging. + /// + [Experimental("Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints", ExperimentAction.Show)] + [Parameter(Position = 1, + ParameterSetName = CommonRunspaceCommandBase.RunspaceParameterSet)] + [Parameter(Position = 1, + ParameterSetName = CommonRunspaceCommandBase.RunspaceNameParameterSet)] + [Parameter(Position = 1, + ParameterSetName = CommonRunspaceCommandBase.RunspaceIdParameterSet)] + public Breakpoint[] Breakpoint + { + get; + set; + } + #endregion #region Overrides @@ -362,58 +378,61 @@ protected override void ProcessRecord() if (this.ParameterSetName.Equals(CommonRunspaceCommandBase.ProcessNameParameterSet)) { SetDebugPreferenceHelper(ProcessName, AppDomainName, true, "EnableRunspaceDebugCommandPersistDebugPreferenceFailure"); + return; } - else - { - IReadOnlyList results = GetRunspaces(); - foreach (var runspace in results) + IReadOnlyList results = GetRunspaces(); + + foreach (var runspace in results) + { + if (runspace.RunspaceStateInfo.State != RunspaceState.Opened) { - if (runspace.RunspaceStateInfo.State != RunspaceState.Opened) - { - WriteError( - new ErrorRecord(new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, Debugger.RunspaceOptionInvalidRunspaceState, runspace.Name)), - "SetRunspaceDebugOptionCommandInvalidRunspaceState", - ErrorCategory.InvalidOperation, - this) - ); + WriteError( + new ErrorRecord(new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, Debugger.RunspaceOptionInvalidRunspaceState, runspace.Name)), + "SetRunspaceDebugOptionCommandInvalidRunspaceState", + ErrorCategory.InvalidOperation, + this)); - continue; - } + continue; + } - System.Management.Automation.Debugger debugger = GetDebuggerFromRunspace(runspace); - if (debugger == null) - { - continue; - } + System.Management.Automation.Debugger debugger = GetDebuggerFromRunspace(runspace); + if (debugger == null) + { + continue; + } - // Enable debugging by preserving debug stop events. - debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Wait; + // Enable debugging by preserving debug stop events. + debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Wait; - if (this.MyInvocation.BoundParameters.ContainsKey(nameof(BreakAll))) + if (this.MyInvocation.BoundParameters.ContainsKey(nameof(BreakAll))) + { + if (BreakAll) { - if (BreakAll) + try { - try - { - debugger.SetDebuggerStepMode(true); - } - catch (PSInvalidOperationException e) - { - WriteError( - new ErrorRecord( - e, - "SetRunspaceDebugOptionCommandCannotEnableDebuggerStepping", - ErrorCategory.InvalidOperation, - this) - ); - } + debugger.SetDebuggerStepMode(true); } - else + catch (PSInvalidOperationException e) { - debugger.SetDebuggerStepMode(false); + WriteError( + new ErrorRecord( + e, + "SetRunspaceDebugOptionCommandCannotEnableDebuggerStepping", + ErrorCategory.InvalidOperation, + this)); } } + else + { + debugger.SetDebuggerStepMode(false); + } + } + + // If any breakpoints were provided, set those in the debugger. + if (Breakpoint?.Length > 0) + { + debugger.SetBreakpoints(Breakpoint); } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/New-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/New-PSBreakpoint.cs new file mode 100644 index 00000000000..b5837cc0da3 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/New-PSBreakpoint.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Internal; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// This class implements New-PSBreakpoint command. + /// + [Experimental("Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints", ExperimentAction.Show)] + [Cmdlet(VerbsCommon.New, "PSBreakpoint", DefaultParameterSetName = LineParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=113449")] + [OutputType(typeof(VariableBreakpoint), typeof(CommandBreakpoint), typeof(LineBreakpoint))] + public class NewPSBreakpointCommand : PSBreakpointCreationBase + { + /// + /// Create a new breakpoint. + /// + protected override void ProcessRecord() + { + // If there is a script, resolve its path + Collection scripts = ResolveScriptPaths(); + + // If it is a command breakpoint... + if (ParameterSetName.Equals(CommandParameterSetName, StringComparison.OrdinalIgnoreCase)) + { + for (int i = 0; i < Command.Length; i++) + { + if (scripts.Count > 0) + { + foreach (string path in scripts) + { + WildcardPattern pattern = WildcardPattern.Get(Command[i], WildcardOptions.Compiled | WildcardOptions.IgnoreCase); + WriteObject(new CommandBreakpoint(path, pattern, Command[i], Action)); + } + } + else + { + WildcardPattern pattern = WildcardPattern.Get(Command[i], WildcardOptions.Compiled | WildcardOptions.IgnoreCase); + WriteObject(new CommandBreakpoint(null, pattern, Command[i], Action)); + } + } + } + else if (ParameterSetName.Equals(VariableParameterSetName, StringComparison.OrdinalIgnoreCase)) + { + // If it is a variable breakpoint... + for (int i = 0; i < Variable.Length; i++) + { + if (scripts.Count > 0) + { + foreach (string path in scripts) + { + WriteObject(new VariableBreakpoint(path, Variable[i], Mode, Action)); + } + } + else + { + WriteObject(new VariableBreakpoint(null, Variable[i], Mode, Action)); + } + } + } + else + { + // Else it is the default parameter set (Line breakpoint)... + Debug.Assert(ParameterSetName.Equals(LineParameterSetName, StringComparison.OrdinalIgnoreCase)); + + for (int i = 0; i < Line.Length; i++) + { + if (Line[i] < 1) + { + WriteError( + new ErrorRecord( + new ArgumentException(Debugger.LineLessThanOne), + "NewPSBreakpoint:LineLessThanOne", + ErrorCategory.InvalidArgument, + null)); + + continue; + } + + foreach (string path in scripts) + { + if (Column != 0) + { + WriteObject(new LineBreakpoint(path, Line[i], Column, Action)); + } + else + { + WriteObject(new LineBreakpoint(path, Line[i], Action)); + } + } + } + } + } + } +} diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs new file mode 100644 index 00000000000..43e3e93839d --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// 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/Set-PSBreakpoint.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs index 1a68ee4ce31..23773fd4477 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Set-PSBreakpoint.cs @@ -13,79 +13,10 @@ namespace Microsoft.PowerShell.Commands /// /// This class implements Set-PSBreakpoint command. /// - [Cmdlet(VerbsCommon.Set, "PSBreakpoint", DefaultParameterSetName = "Line", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=113449")] + [Cmdlet(VerbsCommon.Set, "PSBreakpoint", DefaultParameterSetName = LineParameterSetName, HelpUri = "https://go.microsoft.com/fwlink/?LinkID=113449")] [OutputType(typeof(VariableBreakpoint), typeof(CommandBreakpoint), typeof(LineBreakpoint))] - public class SetPSBreakpointCommand : PSCmdlet + public class SetPSBreakpointCommand : PSBreakpointCreationBase { - #region parameters - - /// - /// The action to take when hitting this breakpoint. - /// - [Parameter(ParameterSetName = "Command")] - [Parameter(ParameterSetName = "Line")] - [Parameter(ParameterSetName = "Variable")] - public ScriptBlock Action { get; set; } = null; - - /// - /// The column to set the breakpoint on. - /// - [Parameter(Position = 2, ParameterSetName = "Line")] - [ValidateRange(1, int.MaxValue)] - public int Column - { - get - { - return _column ?? 0; - } - - set - { - _column = value; - } - } - - private int? _column = null; - - /// - /// The command(s) to set the breakpoint on. - /// - [Alias("C")] - [Parameter(ParameterSetName = "Command", Mandatory = true)] - [ValidateNotNull] - public string[] Command { get; set; } = null; - - /// - /// The line to set the breakpoint on. - /// - [Parameter(Position = 1, ParameterSetName = "Line", Mandatory = true)] - [ValidateNotNull] - public int[] Line { get; set; } = null; - - /// - /// The script to set the breakpoint on. - /// - [Parameter(ParameterSetName = "Command", Position = 0)] - [Parameter(ParameterSetName = "Line", Mandatory = true, Position = 0)] - [Parameter(ParameterSetName = "Variable", Position = 0)] - [ValidateNotNull] - public string[] Script { get; set; } = null; - - /// - /// The variables to set the breakpoint(s) on. - /// - [Alias("V")] - [Parameter(ParameterSetName = "Variable", Mandatory = true)] - [ValidateNotNull] - public string[] Variable { get; set; } = null; - - /// - /// - [Parameter(ParameterSetName = "Variable")] - public VariableAccessMode Mode { get; set; } = VariableAccessMode.Write; - - #endregion parameters - /// /// Verifies that debugging is supported. /// @@ -130,52 +61,12 @@ protected override void BeginProcessing() protected override void ProcessRecord() { // If there is a script, resolve its path - 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)), - "SetPSBreakpoint: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)), - "SetPSBreakpoint:WrongExtension", - ErrorCategory.InvalidArgument, - null)); - continue; - } - - scripts.Add(Path.GetFullPath(providerPath)); - } - } - } + Collection scripts = ResolveScriptPaths(); // // If it is a command breakpoint... // - if (ParameterSetName.Equals("Command", StringComparison.OrdinalIgnoreCase)) + if (ParameterSetName.Equals(CommandParameterSetName, StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < Command.Length; i++) { @@ -197,7 +88,7 @@ protected override void ProcessRecord() // // If it is a variable breakpoint... // - else if (ParameterSetName.Equals("Variable", StringComparison.OrdinalIgnoreCase)) + else if (ParameterSetName.Equals(VariableParameterSetName, StringComparison.OrdinalIgnoreCase)) { for (int i = 0; i < Variable.Length; i++) { @@ -221,7 +112,7 @@ protected override void ProcessRecord() // else { - Debug.Assert(ParameterSetName.Equals("Line", StringComparison.OrdinalIgnoreCase)); + Debug.Assert(ParameterSetName.Equals(LineParameterSetName, StringComparison.OrdinalIgnoreCase)); for (int i = 0; i < Line.Length; i++) { @@ -239,7 +130,7 @@ protected override void ProcessRecord() foreach (string path in scripts) { - if (_column != null) + if (Column != 0) { WriteObject( Context.Debugger.NewStatementBreakpoint(path, Line[i], Column, Action)); 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 863b38f7498..71edf2b7031 100644 --- a/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Unix/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -18,7 +18,7 @@ CmdletsToExport = @( 'Set-MarkdownOption', 'Add-Member', 'Get-Member', 'Compare-Object', 'Group-Object', 'Measure-Object', 'New-Object', 'Select-Object', 'Sort-Object', 'Tee-Object', 'Register-ObjectEvent', 'Write-Output', 'Import-PowerShellDataFile', 'Write-Progress', 'Disable-PSBreakpoint', 'Enable-PSBreakpoint', - 'Get-PSBreakpoint', 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', + 'Get-PSBreakpoint', 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'New-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', 'Disable-RunspaceDebug', 'Enable-RunspaceDebug', 'Get-RunspaceDebug', 'Start-Sleep', 'Join-String', 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', @@ -31,4 +31,14 @@ FunctionsToExport = @() AliasesToExport = @('fhx') NestedModules = @("Microsoft.PowerShell.Commands.Utility.dll") HelpInfoURI = 'https://go.microsoft.com/fwlink/?linkid=855960' +PrivateData = @{ + PSData = @{ + ExperimentalFeatures = @( + @{ + Name = 'Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints' + Description = "Enables the New-PSBreakpoint cmdlet and the -Breakpoint parameter on Debug-Runspace to set breakpoints in another Runspace upfront." + } + ) + } +} } 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 2db43e68531..152f8832688 100644 --- a/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 +++ b/src/Modules/Windows/Microsoft.PowerShell.Utility/Microsoft.PowerShell.Utility.psd1 @@ -17,7 +17,7 @@ CmdletsToExport = @( 'Show-Markdown', 'Get-MarkdownOption', 'Set-MarkdownOption', 'Add-Member', 'Get-Member', 'Compare-Object', 'Group-Object', 'Measure-Object', 'New-Object', 'Select-Object', 'Sort-Object', 'Tee-Object', 'Register-ObjectEvent', 'Write-Output', 'Import-PowerShellDataFile', 'Write-Progress', 'Disable-PSBreakpoint', 'Enable-PSBreakpoint', 'Get-PSBreakpoint', - 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', + 'Remove-PSBreakpoint', 'Set-PSBreakpoint', 'New-PSBreakpoint', 'Get-PSCallStack', 'Export-PSSession', 'Import-PSSession', 'Get-Random', 'Invoke-RestMethod', 'Debug-Runspace', 'Get-Runspace', 'Disable-RunspaceDebug', 'Enable-RunspaceDebug', 'Get-RunspaceDebug', 'ConvertFrom-SddlString', 'Start-Sleep', 'Join-String', 'Out-String', 'Select-String', 'ConvertFrom-StringData', 'Format-Table', 'New-TemporaryFile', 'New-TimeSpan', 'Get-TraceSource', 'Set-TraceSource', diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index 68860b90e23..cf2a32295ef 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -4532,6 +4532,7 @@ internal static SessionStateAliasEntry[] BuiltInAliases new SessionStateAliasEntry("mi", "Move-Item", string.Empty, ReadOnly), new SessionStateAliasEntry("mp", "Move-ItemProperty", string.Empty, ReadOnly), new SessionStateAliasEntry("nal", "New-Alias", string.Empty, ReadOnly), + new SessionStateAliasEntry("nbp", "New-PSBreakpoint", string.Empty, ReadOnly), new SessionStateAliasEntry("ndr", "New-PSDrive", string.Empty, ReadOnly), new SessionStateAliasEntry("ni", "New-Item", string.Empty, ReadOnly), new SessionStateAliasEntry("nv", "New-Variable", string.Empty, ReadOnly), diff --git a/src/System.Management.Automation/engine/debugger/Breakpoint.cs b/src/System.Management.Automation/engine/debugger/Breakpoint.cs index 07c0f26ec01..f7372a27520 100644 --- a/src/System.Management.Automation/engine/debugger/Breakpoint.cs +++ b/src/System.Management.Automation/engine/debugger/Breakpoint.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Management.Automation.Internal; using System.Management.Automation.Language; +using System.Threading; namespace System.Management.Automation { @@ -58,16 +59,36 @@ internal bool IsScriptBreakpoint #region constructors - internal Breakpoint(string script, ScriptBlock action) + /// + /// Creates a new instance of a + /// + protected Breakpoint(string script) + : this(script, null) + {} + + /// + /// Creates a new instance of a + /// + protected Breakpoint(string script, ScriptBlock action) { Enabled = true; Script = script; - Id = s_lastID++; + Id = Interlocked.Increment(ref s_lastID); Action = action; HitCount = 0; } - internal Breakpoint(string script, ScriptBlock action, int id) + /// + /// Creates a new instance of a + /// + protected Breakpoint(string script, int id) + : this(script, null, id) + {} + + /// + /// Creates a new instance of a + /// + protected Breakpoint(string script, ScriptBlock action, int id) { Enabled = true; Script = script; @@ -135,14 +156,34 @@ internal enum BreakpointAction /// public class CommandBreakpoint : Breakpoint { - internal CommandBreakpoint(string script, WildcardPattern command, string commandString, ScriptBlock action) + /// + /// Creates a new instance of a + /// + public CommandBreakpoint(string script, WildcardPattern command, string commandString) + : this(script, command, commandString, null) + {} + + /// + /// Creates a new instance of a + /// + public CommandBreakpoint(string script, WildcardPattern command, string commandString, ScriptBlock action) : base(script, action) { CommandPattern = command; Command = commandString; } - internal CommandBreakpoint(string script, WildcardPattern command, string commandString, ScriptBlock action, int id) + /// + /// Creates a new instance of a + /// + public CommandBreakpoint(string script, WildcardPattern command, string commandString, int id) + : this(script, command, commandString, null, id) + {} + + /// + /// Creates a new instance of a + /// + public CommandBreakpoint(string script, WildcardPattern command, string commandString, ScriptBlock action, int id) : base(script, action, id) { CommandPattern = command; @@ -224,7 +265,7 @@ public enum VariableAccessMode /// Read, /// - /// Break on write access only (default) + /// Break on write access only (default). /// Write, /// @@ -238,14 +279,34 @@ public enum VariableAccessMode /// public class VariableBreakpoint : Breakpoint { - internal VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, ScriptBlock action) + /// + /// Creates a new instance of a . + /// + public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode) + : this(script, variable, accessMode, null) + {} + + /// + /// Creates a new instance of a . + /// + public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, ScriptBlock action) : base(script, action) { Variable = variable; AccessMode = accessMode; } - internal VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, ScriptBlock action, int id) + /// + /// Creates a new instance of a . + /// + public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, int id) + : this(script, variable, accessMode, null, id) + {} + + /// + /// Creates a new instance of a . + /// + public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, ScriptBlock action, int id) : base(script, action, id) { Variable = variable; @@ -300,7 +361,17 @@ internal override void RemoveSelf(ScriptDebugger debugger) /// public class LineBreakpoint : Breakpoint { - internal LineBreakpoint(string script, int line, ScriptBlock action) + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line) + : this(script, line, null) + {} + + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line, ScriptBlock action) : base(script, action) { Diagnostics.Assert(!string.IsNullOrEmpty(script), "Caller to verify script parameter is not null or empty."); @@ -309,7 +380,17 @@ internal LineBreakpoint(string script, int line, ScriptBlock action) SequencePointIndex = -1; } - internal LineBreakpoint(string script, int line, int column, ScriptBlock action) + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line, int column) + : this(script, line, column, null) + {} + + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line, int column, ScriptBlock action) : base(script, action) { Diagnostics.Assert(!string.IsNullOrEmpty(script), "Caller to verify script parameter is not null or empty."); @@ -318,7 +399,17 @@ internal LineBreakpoint(string script, int line, int column, ScriptBlock action) SequencePointIndex = -1; } - internal LineBreakpoint(string script, int line, int column, ScriptBlock action, int id) + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line, int column, int id) + : this(script, line, column, null, id) + {} + + /// + /// Creates a new instance of a + /// + public LineBreakpoint(string script, int line, int column, ScriptBlock action, int id) : base(script, action, id) { Diagnostics.Assert(!string.IsNullOrEmpty(script), "Caller to verify script parameter is not null or empty."); diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index b78c8b66aaf..1d2c1624d0f 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -626,6 +626,23 @@ public virtual void SetBreakpoints(IEnumerable breakpoints) throw new PSNotImplementedException(); } + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + public virtual Breakpoint GetBreakpoint(int id) + { + throw new PSNotImplementedException(); + } + + /// + /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. + /// + public virtual List GetBreakpoints() + { + throw new PSNotImplementedException(); + } + /// /// Resets the command processor source information so that it is /// updated with latest information on the next debug stop. @@ -742,6 +759,16 @@ internal virtual void DebugRunspace(Runspace runspace) throw new PSNotImplementedException(); } + /// + /// Sets up debugger to debug provided Runspace in a nested debug session. + /// + /// Runspace to debug. + /// + internal virtual void DebugRunspace(Runspace runspace, bool disableBreakAll) + { + throw new PSNotImplementedException(); + } + /// /// Removes the provided Runspace from the nested "active" debugger state. /// @@ -822,11 +849,11 @@ internal ScriptDebugger(ExecutionContext context) { _context = context; _inBreakpoint = false; - _idToBreakpoint = new Dictionary(); - _pendingBreakpoints = new List(); - _boundBreakpoints = new Dictionary>>(StringComparer.OrdinalIgnoreCase); - _commandBreakpoints = new List(); - _variableBreakpoints = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _idToBreakpoint = new ConcurrentDictionary(); + _pendingBreakpoints = new ConcurrentDictionary(); + _boundBreakpoints = new ConcurrentDictionary>>(StringComparer.OrdinalIgnoreCase); + _commandBreakpoints = new ConcurrentDictionary(); + _variableBreakpoints = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); _steppingMode = SteppingMode.None; _callStack = new CallStackList { _callStackList = new List() }; @@ -1061,10 +1088,10 @@ internal void RegisterScriptFile(ExternalScriptInfo scriptCommandInfo) internal void RegisterScriptFile(string path, string scriptContents) { - Tuple> boundBreakpoints; + Tuple> boundBreakpoints; if (!_boundBreakpoints.TryGetValue(path, out boundBreakpoints)) { - _boundBreakpoints.Add(path, Tuple.Create(new WeakReference(scriptContents), new List())); + _boundBreakpoints[path] = Tuple.Create(new WeakReference(scriptContents), new ConcurrentDictionary()); } else { @@ -1073,8 +1100,8 @@ internal void RegisterScriptFile(string path, string scriptContents) boundBreakpoints.Item1.TryGetTarget(out oldScriptContents); if (oldScriptContents == null || !oldScriptContents.Equals(scriptContents, StringComparison.Ordinal)) { - UnbindBoundBreakpoints(boundBreakpoints.Item2); - _boundBreakpoints[path] = Tuple.Create(new WeakReference(scriptContents), new List()); + UnbindBoundBreakpoints(boundBreakpoints.Item2.Values.ToList()); + _boundBreakpoints[path] = Tuple.Create(new WeakReference(scriptContents), new ConcurrentDictionary()); } } } @@ -1097,7 +1124,7 @@ internal void AddBreakpointCommon(Breakpoint breakpoint) private Breakpoint AddCommandBreakpoint(CommandBreakpoint breakpoint) { AddBreakpointCommon(breakpoint); - _commandBreakpoints.Add(breakpoint); + _commandBreakpoints[breakpoint.Id] = breakpoint; return breakpoint; } @@ -1116,7 +1143,7 @@ internal Breakpoint NewCommandBreakpoint(string command, ScriptBlock action) private Breakpoint AddLineBreakpoint(LineBreakpoint breakpoint) { AddBreakpointCommon(breakpoint); - _pendingBreakpoints.Add(breakpoint); + _pendingBreakpoints[breakpoint.Id] = breakpoint; return breakpoint; } @@ -1161,14 +1188,13 @@ internal VariableBreakpoint AddVariableBreakpoint(VariableBreakpoint breakpoint) { AddBreakpointCommon(breakpoint); - List breakpoints; - if (!_variableBreakpoints.TryGetValue(breakpoint.Variable, out breakpoints)) + if (!_variableBreakpoints.TryGetValue(breakpoint.Variable, out ConcurrentDictionary breakpoints)) { - breakpoints = new List(); - _variableBreakpoints.Add(breakpoint.Variable, breakpoints); + breakpoints = new ConcurrentDictionary(); + _variableBreakpoints[breakpoint.Variable] = breakpoints; } - breakpoints.Add(breakpoint); + breakpoints[breakpoint.Id] = breakpoint; return breakpoint; } @@ -1198,7 +1224,7 @@ private void OnBreakpointUpdated(BreakpointUpdatedEventArgs e) // This is the implementation of the Remove-PSBreakpoint cmdlet. internal void RemoveBreakpoint(Breakpoint breakpoint) { - _idToBreakpoint.Remove(breakpoint.Id); + _idToBreakpoint.Remove(breakpoint.Id, out _); breakpoint.RemoveSelf(this); @@ -1213,22 +1239,22 @@ internal void RemoveBreakpoint(Breakpoint breakpoint) internal void RemoveVariableBreakpoint(VariableBreakpoint breakpoint) { - _variableBreakpoints[breakpoint.Variable].Remove(breakpoint); + _variableBreakpoints[breakpoint.Variable].Remove(breakpoint.Id, out _); } internal void RemoveCommandBreakpoint(CommandBreakpoint breakpoint) { - _commandBreakpoints.Remove(breakpoint); + _commandBreakpoints.Remove(breakpoint.Id, out _); } internal void RemoveLineBreakpoint(LineBreakpoint breakpoint) { - _pendingBreakpoints.Remove(breakpoint); + _pendingBreakpoints.Remove(breakpoint.Id, out _); - Tuple> value; + Tuple> value; if (_boundBreakpoints.TryGetValue(breakpoint.Script, out value)) { - value.Item2.Remove(breakpoint); + value.Item2.Remove(breakpoint.Id, out _); } } @@ -1255,7 +1281,7 @@ internal bool CheckCommand(InvocationInfo invocationInfo) } List breakpoints = - _commandBreakpoints.Where(bp => bp.Enabled && bp.Trigger(invocationInfo)).ToList(); + _commandBreakpoints.Values.Where(bp => bp.Enabled && bp.Trigger(invocationInfo)).ToList(); bool checkLineBp = true; if (breakpoints.Any()) @@ -1308,7 +1334,7 @@ private List GetVariableBreakpointsToTrigger(string variable { SetInternalDebugMode(InternalDebugMode.Disabled); - List breakpoints; + ConcurrentDictionary breakpoints; if (!_variableBreakpoints.TryGetValue(variableName, out breakpoints)) { // $PSItem is an alias for $_. We don't use PSItem internally, but a user might @@ -1324,7 +1350,7 @@ private List GetVariableBreakpointsToTrigger(string variable var callStackInfo = _callStack.Last(); var currentScriptFile = (callStackInfo != null) ? callStackInfo.File : null; - return breakpoints.Where(bp => bp.Trigger(currentScriptFile, read: read)).ToList(); + return breakpoints.Values.Where(bp => bp.Trigger(currentScriptFile, read: read)).ToList(); } finally { @@ -1342,17 +1368,17 @@ internal void TriggerVariableBreakpoints(List breakpoints) /// /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. /// - internal Breakpoint GetBreakpoint(int id) + /// Id of the breakpoint you want. + public override Breakpoint GetBreakpoint(int id) { - Breakpoint breakpoint; - _idToBreakpoint.TryGetValue(id, out breakpoint); + _idToBreakpoint.TryGetValue(id, out Breakpoint breakpoint); return breakpoint; } /// /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. /// - internal List GetBreakpoints() + public override List GetBreakpoints() { return (from bp in _idToBreakpoint.Values orderby bp.Id select bp).ToList(); } @@ -1499,7 +1525,7 @@ private void UpdateBreakpoints(FunctionContext functionContext) if (string.IsNullOrEmpty(functionContext._file)) { return; } bool havePendingBreakpoint = false; - foreach (var item in _pendingBreakpoints) + foreach ((int breakpointId, LineBreakpoint item) in _pendingBreakpoints) { if (item.IsScriptBreakpoint && item.Script.Equals(functionContext._file, StringComparison.OrdinalIgnoreCase)) { @@ -1618,11 +1644,11 @@ internal void Clear() } private readonly ExecutionContext _context; - private List _pendingBreakpoints; - private readonly Dictionary>> _boundBreakpoints; - private readonly List _commandBreakpoints; - private readonly Dictionary> _variableBreakpoints; - private readonly Dictionary _idToBreakpoint; + private ConcurrentDictionary _pendingBreakpoints; + private readonly ConcurrentDictionary>> _boundBreakpoints; + private readonly ConcurrentDictionary _commandBreakpoints; + private readonly ConcurrentDictionary> _variableBreakpoints; + private readonly ConcurrentDictionary _idToBreakpoint; private SteppingMode _steppingMode; private CallStackInfo _overOrOutFrame; private CallStackList _callStack; @@ -1935,7 +1961,7 @@ private void UnbindBoundBreakpoints(List boundBreakpoints) breakpoint.SequencePoints = null; breakpoint.SequencePointIndex = -1; breakpoint.BreakpointBitArray = null; - _pendingBreakpoints.Add(breakpoint); + _pendingBreakpoints[breakpoint.Id] = breakpoint; } boundBreakpoints.Clear(); @@ -1946,7 +1972,7 @@ private void SetPendingBreakpoints(FunctionContext functionContext) if (!_pendingBreakpoints.Any()) return; - var newPendingBreakpoints = new List(); + var newPendingBreakpoints = new Dictionary(); var currentScriptFile = functionContext._file; // If we're not in a file, we can't have any line breakpoints. @@ -1967,7 +1993,7 @@ private void SetPendingBreakpoints(FunctionContext functionContext) Diagnostics.Assert(tuple.Item1 == functionContext._boundBreakpoints, "What's up?"); - foreach (var breakpoint in _pendingBreakpoints) + foreach ((int breakpointId, LineBreakpoint breakpoint) in _pendingBreakpoints) { bool bound = false; if (breakpoint.TrySetBreakpoint(currentScriptFile, functionContext)) @@ -1983,17 +2009,16 @@ private void SetPendingBreakpoints(FunctionContext functionContext) // We need to keep track of any breakpoints that are bound in each script because they may // need to be rebound if the script changes. var boundBreakpoints = _boundBreakpoints[currentScriptFile].Item2; - Diagnostics.Assert(boundBreakpoints.IndexOf(breakpoint) < 0, "Don't add more than once."); - boundBreakpoints.Add(breakpoint); + boundBreakpoints[breakpoint.Id] = breakpoint; } if (!bound) { - newPendingBreakpoints.Add(breakpoint); + newPendingBreakpoints.Add(breakpoint.Id, breakpoint); } } - _pendingBreakpoints = newPendingBreakpoints; + _pendingBreakpoints = new ConcurrentDictionary(newPendingBreakpoints); } private void StopOnSequencePoint(FunctionContext functionContext, List breakpoints) @@ -2355,24 +2380,20 @@ public override void SetBreakpoints(IEnumerable breakpoints) { if (_idToBreakpoint.ContainsKey(breakpoint.Id)) { continue; } - LineBreakpoint lineBp = breakpoint as LineBreakpoint; - if (lineBp != null) - { - AddLineBreakpoint(lineBp); - continue; - } - - CommandBreakpoint cmdBp = breakpoint as CommandBreakpoint; - if (cmdBp != null) + switch (breakpoint) { - AddCommandBreakpoint(cmdBp); - continue; - } - - VariableBreakpoint variableBp = breakpoint as VariableBreakpoint; - if (variableBp != null) - { - AddVariableBreakpoint(variableBp); + case LineBreakpoint lineBp: + AddLineBreakpoint(lineBp); + continue; + case CommandBreakpoint cmdBp: + AddCommandBreakpoint(cmdBp); + continue; + case VariableBreakpoint variableBp: + AddVariableBreakpoint(variableBp); + continue; + default: + // Unreachable default block + break; } } } @@ -2657,11 +2678,17 @@ internal static void SetDebugJobAsync(IJobDebugger debuggableJob, bool isAsync) #region Runspace Debugging + internal override void DebugRunspace(Runspace runspace) + { + DebugRunspace(runspace, disableBreakAll:false); + } + /// /// Sets up debugger to debug provided Runspace in a nested debug session. /// /// Runspace to debug. - internal override void DebugRunspace(Runspace runspace) + /// When specified, it will not turn on BreakAll. + internal override void DebugRunspace(Runspace runspace, bool disableBreakAll) { if (runspace == null) { @@ -2696,7 +2723,7 @@ internal override void DebugRunspace(Runspace runspace) AddToRunningRunspaceList(new PSStandaloneMonitorRunspaceInfo(runspace)); - if (!runspace.Debugger.InBreakpoint) + if (!runspace.Debugger.InBreakpoint && !disableBreakAll) { EnableDebuggerStepping(EnableNestedType.NestedRunspace); } @@ -3988,6 +4015,28 @@ public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataC return _wrappedDebugger.ProcessCommand(command, output); } + /// + /// Adds the provided set of breakpoints to the debugger. + /// + /// Breakpoints. + public override void SetBreakpoints(IEnumerable breakpoints) + { + _wrappedDebugger.SetBreakpoints(breakpoints); + } + + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + public override Breakpoint GetBreakpoint(int id) => + _wrappedDebugger.GetBreakpoint(id); + + /// + /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. + /// + public override List GetBreakpoints() => + _wrappedDebugger.GetBreakpoints(); + /// /// SetDebuggerAction. /// diff --git a/src/System.Management.Automation/engine/remoting/client/Job.cs b/src/System.Management.Automation/engine/remoting/client/Job.cs index 39a5e35ff74..78397a2d906 100644 --- a/src/System.Management.Automation/engine/remoting/client/Job.cs +++ b/src/System.Management.Automation/engine/remoting/client/Job.cs @@ -3907,6 +3907,28 @@ public override DebuggerCommandResults ProcessCommand(PSCommand command, PSDataC return _wrappedDebugger.ProcessCommand(command, output); } + /// + /// Adds the provided set of breakpoints to the debugger. + /// + /// Breakpoints. + public override void SetBreakpoints(IEnumerable breakpoints) + { + _wrappedDebugger.SetBreakpoints(breakpoints); + } + + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + public override Breakpoint GetBreakpoint(int id) => + _wrappedDebugger.GetBreakpoint(id); + + /// + /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. + /// + public override List GetBreakpoints() => + _wrappedDebugger.GetBreakpoints(); + /// /// 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 b390e3f9b69..9d673b5a24c 100644 --- a/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs +++ b/src/System.Management.Automation/engine/remoting/client/remoterunspace.cs @@ -2007,6 +2007,28 @@ public override void StopProcessCommand() } } + /// + /// Adds the provided set of breakpoints to the debugger. + /// + /// Breakpoints. + public override void SetBreakpoints(IEnumerable breakpoints) + { + _runspace.Debugger?.SetBreakpoints(breakpoints); + } + + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + public override Breakpoint GetBreakpoint(int id) => + _runspace.Debugger?.GetBreakpoint(id); + + /// + /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. + /// + public override List GetBreakpoints() => + _runspace.Debugger?.GetBreakpoints(); + /// /// SetDebuggerAction. /// diff --git a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs index 5ed66313f72..29ecb409455 100644 --- a/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs +++ b/src/System.Management.Automation/engine/remoting/server/ServerRunspacePoolDriver.cs @@ -1767,6 +1767,28 @@ public override bool InBreakpoint get { return _inDebugMode; } } + /// + /// Adds the provided set of breakpoints to the debugger. + /// + /// Breakpoints. + public override void SetBreakpoints(IEnumerable breakpoints) + { + _wrappedDebugger.Value.SetBreakpoints(breakpoints); + } + + /// + /// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets. + /// + /// Id of the breakpoint you want. + public override Breakpoint GetBreakpoint(int id) => + _wrappedDebugger.Value.GetBreakpoint(id); + + /// + /// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet. + /// + public override List GetBreakpoints() => + _wrappedDebugger.Value.GetBreakpoints(); + /// /// Exits debugger mode with the provided resume action. /// @@ -1921,6 +1943,16 @@ internal override void DebugRunspace(Runspace runspace) _wrappedDebugger.Value.DebugRunspace(runspace); } + /// + /// Sets up debugger to debug provided Runspace in a nested debug session. + /// + /// Runspace to debug. + /// + internal override void DebugRunspace(Runspace runspace, bool disableBreakAll) + { + _wrappedDebugger.Value.DebugRunspace(runspace, disableBreakAll); + } + /// /// Removes the provided Runspace from the nested "active" debugger state. /// diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-RunspaceDebug.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-RunspaceDebug.Tests.ps1 new file mode 100644 index 00000000000..176d980f551 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-RunspaceDebug.Tests.ps1 @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$FeatureEnabled = $EnabledExperimentalFeatures.Contains('Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints') + +Describe "`Enable-RunspaceDebug -Breakpoint` Unit Tests - Feature-Enabled" -Tags "CI" { + + BeforeAll { + if (!$FeatureEnabled) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + return + } + + #Set up script file 1 + $scriptFileName1 = Join-Path $TestDrive -ChildPath breakpointTestScript.ps1 + + $contents = @" +function Hello +{ + `$greeting = 'Hello, world!' + write-host `$greeting +} + +function Goodbye +{ + `$message = 'Good bye, cruel world!' + write-host `$message +} + +Hello +Goodbye +"@ + + $contents > $scriptFileName1 + + # The breakpoints are created here because when the tests are run with the experimental feature off, + # this command does not exist and the Pester tests fail to work + $breakpointArr = @( + New-PSBreakpoint -Line 12 $scriptFileName1 + New-PSBreakpoint -Line 13 $scriptFileName1 + ) + + $iss = [initialsessionstate]::CreateDefault2(); + $testRunspace1 = [runspacefactory]::CreateRunspace($iss) + $testRunspace1.Name = "TestRunspaceDebuggerReset" + $testRunspace1.Open() + } + + AfterAll { + if (!$FeatureEnabled) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + return + } + + # Clean up + $testRunspace1.Dispose() + } + + It "Can set breakpoints in the runspace - " -TestCases @( + @{ + Name = "Current runspace" + Runspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace + Breakpoints = $breakpointArr + }, + @{ + Name = $testRunspace1.Name + Runspace = $testRunspace1 + Breakpoints = $breakpointArr + } + ) { + param($Runspace, $Breakpoints) + Enable-RunspaceDebug -Breakpoint $Breakpoints -Runspace $Runspace + $Runspace.Debugger.GetBreakpoints() | Should -Be @($Breakpoints) + } +} diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1 new file mode 100644 index 00000000000..544cdff2902 --- /dev/null +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1 @@ -0,0 +1,195 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$FeatureEnabled = $EnabledExperimentalFeatures.Contains('Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints') + +Describe "New-PSBreakpoint Unit Tests - Feature-Enabled" -Tags "CI" { + + BeforeAll { + if (!$FeatureEnabled) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + return + } + + #Set up script file 1 + $scriptFileName1 = Join-Path $TestDrive -ChildPath breakpointTestScript.ps1 + + $contents = @" +function Hello +{ + `$greeting = 'Hello, world!' + write-host `$greeting +} + +function Goodbye +{ + `$message = 'Good bye, cruel world!' + write-host `$message +} + +Hello +Goodbye + +# The following 2 statements produce null tokens (needed to verify 105473) +# +`$table = @{} + +return +"@ + + $contents > $scriptFileName1 + + # Set up script file 2 + $scriptFileName2 = Join-Path -Path $TestDrive -ChildPath psbreakpointtestscript.ps1 + + "`$var = 1 " > $scriptFileName2 + } + + AfterAll { + if (!$FeatureEnabled) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + return + } + } + + It "Should be able to set psbreakpoints for -Line" { + $brk = New-PSBreakpoint -Line 13 -Script $scriptFileName1 + $brk.Line | Should -Be 13 + } + + It "Should be able to set psbreakpoints for -Line and -column" { + $brk = New-PSBreakpoint -line 13 -column 1 -script $scriptFileName1 + $brk.Line | Should -Be 13 + $brk.Column | Should -Be 1 + } + + It "Should be able to set psbreakpoints for -Line and -action" { + $brk = New-PSBreakpoint -line 13 -action {{ break; }} -script $scriptFileName1 + $brk.Line | Should -Be 13 + $brk.Action | Should -Match "break" + } + + It "Should be able to set psbreakpoints for -Line, -column and -action" { + $brk = New-PSBreakpoint -line 13 -column 1 -action {{ break; }} -script $scriptFileName1 + $brk.Line | Should -Be 13 + $brk.Column | Should -Be 1 + $brk.Action | Should -Match "break" + } + + It "-script and -line can take multiple items" { + $brk = New-PSBreakpoint -line 11,12,13 -column 1 -script $scriptFileName1,$scriptFileName1 + $brk.Line | Should -BeIn 11,12,13 + $brk.Column | Should -BeIn 1 + } + + It "-script and -line are positional" { + $brk = New-PSBreakpoint $scriptFileName1 13 + $brk.Line | Should -Be 13 + } + + It "-script, -line and -column are positional" { + $brk = New-PSBreakpoint $scriptFileName1 13 1 + $brk.Line | Should -Be 13 + $brk.Column | Should -Be 1 + } + + It "Should throw Exception when missing mandatory parameter -line" -Pending { + $output = pwsh -noninteractive -command "nbp -column 1 -script $scriptFileName1" + [system.string]::Join(" ", $output) | Should -Match "MissingMandatoryParameter,Microsoft.PowerShell.Commands.NewPSBreakpointCommand" + } + + It "Should throw Exception when missing mandatory parameter" -Pending { + $output = pwsh -noprofile -noninteractive -command "nbp -line 1" + [system.string]::Join(" ", $output) | Should -Match "MissingMandatoryParameter,Microsoft.PowerShell.Commands.NewPSBreakpointCommand" + } + + It "Should be able to set psbreakpoints for -command" { + $brk = New-PSBreakpoint -command "write-host" + $brk.Command | Should -BeExactly "write-host" + } + + It "Should be able to set psbreakpoints for -command, -script" { + $brk = New-PSBreakpoint -command "write-host" -script $scriptFileName1 + $brk.Command | Should -BeExactly "write-host" + } + + It "Should be able to set psbreakpoints for -command, -action and -script" { + $brk = New-PSBreakpoint -command "write-host" -action {{ break; }} -script $scriptFileName1 + $brk.Action | Should -Match "break" + } + + It "-Command can take multiple items" { + $brk = New-PSBreakpoint -command write-host,Hello + $brk.Command | Should -Be write-host,Hello + } + + It "-Script is positional" { + $brk = New-PSBreakpoint -command "Hello" $scriptFileName1 + $brk.Command | Should -BeExactly "Hello" + + $brk = New-PSBreakpoint $scriptFileName1 -command "Hello" + $brk.Command | Should -BeExactly "Hello" + } + + It "Should be able to set breakpoints on functions" { + $brk = New-PSBreakpoint -command Hello,Goodbye -script $scriptFileName1 + $brk.Command | Should -Be Hello,Goodbye + } + + It "Should be throw Exception when Column number less than 1" { + { New-PSBreakpoint -line 1 -column -1 -script $scriptFileName1 } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.NewPSBreakpointCommand" + } + + It "Should be throw Exception when Line number less than 1" { + $ErrorActionPreference = "Stop" + { New-PSBreakpoint -line -1 -script $scriptFileName1 } | Should -Throw -ErrorId "NewPSBreakpoint:LineLessThanOne,Microsoft.PowerShell.Commands.NewPSBreakpointCommand" + $ErrorActionPreference = "SilentlyContinue" + } + + It "Fail to set psbreakpoints when script is a file of wrong type" { + $tempFile = [System.IO.Path]::GetTempFileName() + $ErrorActionPreference = "Stop" + { + New-PSBreakpoint -Script $tempFile -Line 1 + } | Should -Throw + $ErrorActionPreference = "SilentlyContinue" + Remove-Item $tempFile -Force + } + + It "Fail to set psbreakpoints when script file does not exist" { + $ErrorActionPreference = "Stop" + ${script.ps1} = 10 + { + New-PSBreakpoint -Script variable:\script.ps1 -Line 1 + } | Should -Throw + $ErrorActionPreference = "SilentlyContinue" + } + + It "Should be able to set a psbreakpoint on a line" { + $lineNumber = 1 + $brk = New-PSBreakpoint -Line $lineNumber -Script $scriptFileName2 + $brk.Line | Should -Be $lineNumber + } + + It "Should throw when a string is entered for a line number" { + { + $lineNumber = "one" + New-PSBreakpoint -Line $lineNumber -Script $scriptFileName2 + + } | Should -Throw + } + + It "Should be able to set a psbreakpoint on a Command" { + $command = "theCommand" + $brk = New-PSBreakpoint -Command $command -Script $scriptFileName2 + $brk.Command | Should -Be $command + } + + It "Should be able to set a psbreakpoint on a variable" { + $var = "theVariable" + $brk = New-PSBreakpoint -Command $var -Script $scriptFileName2 + $brk.Command | Should -Be $var + } +} diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index b33e243606d..750ecfbd518 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -105,6 +105,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Alias", "move", "Move-Item", $($FullCLR -or $CoreWindows -or $CoreUnix), "", "AllScope", "" "Alias", "mp", "Move-ItemProperty", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", "" "Alias", "mv", "Move-Item", $($FullCLR -or $CoreWindows ), "", "", "" +"Alias", "nbp", "New-PSBreakpoint", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", "" "Alias", "nal", "New-Alias", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", "" "Alias", "ndr", "New-PSDrive", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", "" "Alias", "ni", "New-Item", $($FullCLR -or $CoreWindows -or $CoreUnix), "ReadOnly", "", "" diff --git a/test/tools/TestMetadata.json b/test/tools/TestMetadata.json index c49ab1758c6..bc172948978 100644 --- a/test/tools/TestMetadata.json +++ b/test/tools/TestMetadata.json @@ -1,5 +1,6 @@ { "ExperimentalFeatures": { + "Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints": ["test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1"], "ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ] } } diff --git a/test/xUnit/csharp/test_Runspace.cs b/test/xUnit/csharp/test_Runspace.cs index 7bf0bc3f85f..38a6dfd1bf3 100644 --- a/test/xUnit/csharp/test_Runspace.cs +++ b/test/xUnit/csharp/test_Runspace.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Management.Automation; using System.Management.Automation.Runspaces; using Xunit; @@ -100,5 +101,31 @@ public void TestRunspaceWithPowerShellAndInitialSessionState() runspace.Close(); } } + + [Fact] + public void TestRunspaceSetBreakpoints() + { + using (var runspace = RunspaceFactory.CreateRunspace()) + { + var expectedBreakpoints = new Breakpoint[] { + new LineBreakpoint(@"./path/to/some/file.ps1", 1), + new CommandBreakpoint(@"./path/to/some/file.ps1", new WildcardPattern("Write-Host"), "Write-Host"), + }; + + runspace.Open(); + + try + { + runspace.Debugger.SetBreakpoints(expectedBreakpoints); + List actualBreakpoints = runspace.Debugger.GetBreakpoints(); + Assert.Equal(expectedBreakpoints.Length, actualBreakpoints.Count); + Assert.Equal(expectedBreakpoints, actualBreakpoints); + } + finally + { + runspace.Close(); + } + } + } } }