From 13fd3af8106dbfd5d65e697036af5999255badc7 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 13 Apr 2019 19:14:53 -0700 Subject: [PATCH] New `New-PSBreakpoint` cmdlet & new `-Breakpoint` parameter for `Debug-Runspace` (#8923) This PR does 4 things: * Adds a new cmdlet `New-PSBreakpoint` which creates new `Breakpoint` objects and writes them to the pipeline * Adds a `-Breakpoint` parameter to `Debug-Runspace` which will receive `Breakpoint` objects * Makes the constructors for `*Breakpoint` public for use with the API * Makes `Debugger.GetBreakpoint(string id)` and `Debugger.GetBreakpoints()` public since `SetBreakpoints` is public Note: `New-PSBreakpoint` and `Set-PSBreakpoint` (which already exists) are similar... but `Set-PSBreakpoint` also sets the breakpoints in the _current_ runspace. This is not ideal if we want to set breakpoints in a _different runspace than the current one_. ## PR Context The "Attach to process" debugging experience in the PowerShell extension for VSCode is _ok_ but it's not great. The reason it's not great is due to the `BreakAll` feature of PowerShell debugging which, when you run `Debug-Runspace`, will break at the first piece of code that gets run. This is not ideal when you "Attach to process" _and then_ run your code in the other runspace. Today, the experience drops you in `PSReadLine`'s psm1 if PSRL is available or in the vscode PowerShell helper psm1. It's unexpected for the user and not ideal. This PR will allow the extension to pass in the breakpoints that need to be set initially with `BreakAll` turned off for none of this silly behavior. ### Silly behavior example If you want a repro, try this: PowerShell instance 1: ``` Enter-PSHostProcess -Id $otherprocesspid Debug-Runspace 1 ``` PowerShell instance 2: ``` ./runfoo.ps1 ``` Note that you end up NOT `runfoo.ps1` --- .../commands/utility/DebugRunspaceCommand.cs | 21 +- .../EnableDisableRunspaceDebugCommand.cs | 105 ++++++---- .../commands/utility/New-PSBreakpoint.cs | 101 +++++++++ .../utility/PSBreakpointCreationBase.cs | 122 +++++++++++ .../commands/utility/Set-PSBreakpoint.cs | 123 +---------- .../Microsoft.PowerShell.Utility.psd1 | 12 +- .../Microsoft.PowerShell.Utility.psd1 | 2 +- .../engine/InitialSessionState.cs | 1 + .../engine/debugger/Breakpoint.cs | 113 +++++++++- .../engine/debugger/debugger.cs | 171 +++++++++------ .../engine/remoting/client/Job.cs | 22 ++ .../engine/remoting/client/remoterunspace.cs | 22 ++ .../server/ServerRunspacePoolDriver.cs | 32 +++ .../Enable-RunspaceDebug.Tests.ps1 | 77 +++++++ .../New-PSBreakpoint.Tests.ps1 | 195 ++++++++++++++++++ .../engine/Basic/DefaultCommands.Tests.ps1 | 1 + test/tools/TestMetadata.json | 1 + test/xUnit/csharp/test_Runspace.cs | 27 +++ 18 files changed, 914 insertions(+), 234 deletions(-) create mode 100644 src/Microsoft.PowerShell.Commands.Utility/commands/utility/New-PSBreakpoint.cs create mode 100644 src/Microsoft.PowerShell.Commands.Utility/commands/utility/PSBreakpointCreationBase.cs create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Utility/Enable-RunspaceDebug.Tests.ps1 create mode 100644 test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1 diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/DebugRunspaceCommand.cs index ad9414b738..02e6fcad67 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 @@ namespace Microsoft.PowerShell.Commands 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 @@ namespace Microsoft.PowerShell.Commands _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 @@ namespace Microsoft.PowerShell.Commands { 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 92dcae2d05..5c1b61f360 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 @@ namespace Microsoft.PowerShell.Commands 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 @@ namespace Microsoft.PowerShell.Commands 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)); + + continue; + } + + System.Management.Automation.Debugger debugger = GetDebuggerFromRunspace(runspace); + if (debugger == null) + { + continue; + } + + // Enable debugging by preserving debug stop events. + debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Wait; + + if (this.MyInvocation.BoundParameters.ContainsKey(nameof(BreakAll))) + { + if (BreakAll) { - WriteError( - new ErrorRecord(new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, Debugger.RunspaceOptionInvalidRunspaceState, runspace.Name)), - "SetRunspaceDebugOptionCommandInvalidRunspaceState", - ErrorCategory.InvalidOperation, - this) - ); - - continue; - } - - System.Management.Automation.Debugger debugger = GetDebuggerFromRunspace(runspace); - if (debugger == null) - { - continue; - } - - // Enable debugging by preserving debug stop events. - debugger.UnhandledBreakpointMode = UnhandledBreakpointProcessingMode.Wait; - - if (this.MyInvocation.BoundParameters.ContainsKey(nameof(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 0000000000..b5837cc0da --- /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 0000000000..43e3e93839 --- /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 1a68ee4ce3..23773fd447 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 @@ namespace Microsoft.PowerShell.Commands 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 @@ namespace Microsoft.PowerShell.Commands // // 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 @@ namespace Microsoft.PowerShell.Commands // 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 @@ namespace Microsoft.PowerShell.Commands 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 863b38f749..71edf2b703 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 2db43e6853..152f883268 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 55e44aef4f..40cd830572 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -4553,6 +4553,7 @@ end { 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 07c0f26ec0..f7372a2752 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.IO; using System.Linq; using System.Management.Automation.Internal; using System.Management.Automation.Language; +using System.Threading; namespace System.Management.Automation { @@ -58,16 +59,36 @@ namespace System.Management.Automation #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 @@ namespace System.Management.Automation /// 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 @@ namespace System.Management.Automation /// Read, /// - /// Break on write access only (default) + /// Break on write access only (default). /// Write, /// @@ -238,14 +279,34 @@ namespace System.Management.Automation /// 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 @@ namespace System.Management.Automation /// 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation 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 b78c8b66aa..1d2c1624d0 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -626,6 +626,23 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation { _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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation private Breakpoint AddCommandBreakpoint(CommandBreakpoint breakpoint) { AddBreakpointCommon(breakpoint); - _commandBreakpoints.Add(breakpoint); + _commandBreakpoints[breakpoint.Id] = breakpoint; return breakpoint; } @@ -1116,7 +1143,7 @@ namespace System.Management.Automation private Breakpoint AddLineBreakpoint(LineBreakpoint breakpoint) { AddBreakpointCommon(breakpoint); - _pendingBreakpoints.Add(breakpoint); + _pendingBreakpoints[breakpoint.Id] = breakpoint; return breakpoint; } @@ -1161,14 +1188,13 @@ namespace System.Management.Automation { 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 @@ namespace System.Management.Automation // 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation } 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 @@ namespace System.Management.Automation { 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation /// /// 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation } 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 @@ namespace System.Management.Automation breakpoint.SequencePoints = null; breakpoint.SequencePointIndex = -1; breakpoint.BreakpointBitArray = null; - _pendingBreakpoints.Add(breakpoint); + _pendingBreakpoints[breakpoint.Id] = breakpoint; } boundBreakpoints.Clear(); @@ -1946,7 +1972,7 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation // 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 @@ namespace System.Management.Automation { if (_idToBreakpoint.ContainsKey(breakpoint.Id)) { continue; } - LineBreakpoint lineBp = breakpoint as LineBreakpoint; - if (lineBp != null) + switch (breakpoint) { - AddLineBreakpoint(lineBp); - continue; - } - - CommandBreakpoint cmdBp = breakpoint as CommandBreakpoint; - if (cmdBp != null) - { - 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 @@ namespace System.Management.Automation #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 @@ namespace System.Management.Automation AddToRunningRunspaceList(new PSStandaloneMonitorRunspaceInfo(runspace)); - if (!runspace.Debugger.InBreakpoint) + if (!runspace.Debugger.InBreakpoint && !disableBreakAll) { EnableDebuggerStepping(EnableNestedType.NestedRunspace); } @@ -3988,6 +4015,28 @@ namespace System.Management.Automation 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 39a5e35ff7..78397a2d90 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 @@ namespace System.Management.Automation 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 b390e3f9b6..9d673b5a24 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 @@ namespace System.Management.Automation } } + /// + /// 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 5ed66313f7..29ecb40945 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 @@ namespace System.Management.Automation 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 @@ namespace System.Management.Automation _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 0000000000..176d980f55 --- /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 0000000000..544cdff290 --- /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 b33e243606..750ecfbd51 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 c49ab1758c..bc17294897 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 7bf0bc3f85..38a6dfd1bf 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 @@ namespace PSTests.Sequential 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(); + } + } + } } }