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`
This commit is contained in:
Tyler James Leonhardt 2019-04-13 19:14:53 -07:00 committed by Travis Plunk
parent d67ee7aee3
commit 13fd3af810
18 changed files with 914 additions and 234 deletions

View File

@ -100,6 +100,21 @@ namespace Microsoft.PowerShell.Commands
set;
}
/// <summary>
/// The optional breakpoint objects to use for debugging.
/// </summary>
[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)

View File

@ -350,6 +350,22 @@ namespace Microsoft.PowerShell.Commands
set;
}
/// <summary>
/// The optional breakpoint objects to use for debugging.
/// </summary>
[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<Runspace> results = GetRunspaces();
foreach (var runspace in results)
{
IReadOnlyList<Runspace> 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);
}
}
}

View File

@ -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
{
/// <summary>
/// This class implements New-PSBreakpoint command.
/// </summary>
[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
{
/// <summary>
/// Create a new breakpoint.
/// </summary>
protected override void ProcessRecord()
{
// If there is a script, resolve its path
Collection<string> 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));
}
}
}
}
}
}
}

View File

@ -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
{
/// <summary>
/// Base class for Set/New-PSBreakpoint.
/// </summary>
public class PSBreakpointCreationBase : PSCmdlet
{
internal const string CommandParameterSetName = "Command";
internal const string LineParameterSetName = "Line";
internal const string VariableParameterSetName = "Variable";
#region parameters
/// <summary>
/// The action to take when hitting this breakpoint.
/// </summary>
[Parameter(ParameterSetName = CommandParameterSetName)]
[Parameter(ParameterSetName = LineParameterSetName)]
[Parameter(ParameterSetName = VariableParameterSetName)]
public ScriptBlock Action { get; set; }
/// <summary>
/// The column to set the breakpoint on.
/// </summary>
[Parameter(Position = 2, ParameterSetName = LineParameterSetName)]
[ValidateRange(1, int.MaxValue)]
public int Column { get; set; }
/// <summary>
/// The command(s) to set the breakpoint on.
/// </summary>
[Alias("C")]
[Parameter(ParameterSetName = CommandParameterSetName, Mandatory = true)]
public string[] Command { get; set; }
/// <summary>
/// The line to set the breakpoint on.
/// </summary>
[Parameter(Position = 1, ParameterSetName = LineParameterSetName, Mandatory = true)]
public int[] Line { get; set; }
/// <summary>
/// The script to set the breakpoint on.
/// </summary>
[Parameter(ParameterSetName = CommandParameterSetName, Position = 0)]
[Parameter(ParameterSetName = LineParameterSetName, Mandatory = true, Position = 0)]
[Parameter(ParameterSetName = VariableParameterSetName, Position = 0)]
[ValidateNotNull]
public string[] Script { get; set; }
/// <summary>
/// The variables to set the breakpoint(s) on.
/// </summary>
[Alias("V")]
[Parameter(ParameterSetName = VariableParameterSetName, Mandatory = true)]
public string[] Variable { get; set; }
/// <summary>
/// The access type for variable breakpoints to break on.
/// </summary>
[Parameter(ParameterSetName = VariableParameterSetName)]
public VariableAccessMode Mode { get; set; } = VariableAccessMode.Write;
#endregion parameters
internal Collection<string> ResolveScriptPaths()
{
Collection<string> scripts = new Collection<string>();
if (Script != null)
{
foreach (string script in Script)
{
Collection<PathInfo> 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;
}
}
}

View File

@ -13,79 +13,10 @@ namespace Microsoft.PowerShell.Commands
/// <summary>
/// This class implements Set-PSBreakpoint command.
/// </summary>
[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
/// <summary>
/// The action to take when hitting this breakpoint.
/// </summary>
[Parameter(ParameterSetName = "Command")]
[Parameter(ParameterSetName = "Line")]
[Parameter(ParameterSetName = "Variable")]
public ScriptBlock Action { get; set; } = null;
/// <summary>
/// The column to set the breakpoint on.
/// </summary>
[Parameter(Position = 2, ParameterSetName = "Line")]
[ValidateRange(1, int.MaxValue)]
public int Column
{
get
{
return _column ?? 0;
}
set
{
_column = value;
}
}
private int? _column = null;
/// <summary>
/// The command(s) to set the breakpoint on.
/// </summary>
[Alias("C")]
[Parameter(ParameterSetName = "Command", Mandatory = true)]
[ValidateNotNull]
public string[] Command { get; set; } = null;
/// <summary>
/// The line to set the breakpoint on.
/// </summary>
[Parameter(Position = 1, ParameterSetName = "Line", Mandatory = true)]
[ValidateNotNull]
public int[] Line { get; set; } = null;
/// <summary>
/// The script to set the breakpoint on.
/// </summary>
[Parameter(ParameterSetName = "Command", Position = 0)]
[Parameter(ParameterSetName = "Line", Mandatory = true, Position = 0)]
[Parameter(ParameterSetName = "Variable", Position = 0)]
[ValidateNotNull]
public string[] Script { get; set; } = null;
/// <summary>
/// The variables to set the breakpoint(s) on.
/// </summary>
[Alias("V")]
[Parameter(ParameterSetName = "Variable", Mandatory = true)]
[ValidateNotNull]
public string[] Variable { get; set; } = null;
/// <summary>
/// </summary>
[Parameter(ParameterSetName = "Variable")]
public VariableAccessMode Mode { get; set; } = VariableAccessMode.Write;
#endregion parameters
/// <summary>
/// Verifies that debugging is supported.
/// </summary>
@ -130,52 +61,12 @@ namespace Microsoft.PowerShell.Commands
protected override void ProcessRecord()
{
// If there is a script, resolve its path
Collection<string> scripts = new Collection<string>();
if (Script != null)
{
foreach (string script in Script)
{
Collection<PathInfo> 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<string> 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));

View File

@ -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."
}
)
}
}
}

View File

@ -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',

View File

@ -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),

View File

@ -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)
/// <summary>
/// Creates a new instance of a <see cref="Breakpoint"/>
/// </summary>
protected Breakpoint(string script)
: this(script, null)
{}
/// <summary>
/// Creates a new instance of a <see cref="Breakpoint"/>
/// </summary>
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)
/// <summary>
/// Creates a new instance of a <see cref="Breakpoint"/>
/// </summary>
protected Breakpoint(string script, int id)
: this(script, null, id)
{}
/// <summary>
/// Creates a new instance of a <see cref="Breakpoint"/>
/// </summary>
protected Breakpoint(string script, ScriptBlock action, int id)
{
Enabled = true;
Script = script;
@ -135,14 +156,34 @@ namespace System.Management.Automation
/// </summary>
public class CommandBreakpoint : Breakpoint
{
internal CommandBreakpoint(string script, WildcardPattern command, string commandString, ScriptBlock action)
/// <summary>
/// Creates a new instance of a <see cref="CommandBreakpoint"/>
/// </summary>
public CommandBreakpoint(string script, WildcardPattern command, string commandString)
: this(script, command, commandString, null)
{}
/// <summary>
/// Creates a new instance of a <see cref="CommandBreakpoint"/>
/// </summary>
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)
/// <summary>
/// Creates a new instance of a <see cref="CommandBreakpoint"/>
/// </summary>
public CommandBreakpoint(string script, WildcardPattern command, string commandString, int id)
: this(script, command, commandString, null, id)
{}
/// <summary>
/// Creates a new instance of a <see cref="CommandBreakpoint"/>
/// </summary>
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
/// </summary>
Read,
/// <summary>
/// Break on write access only (default)
/// Break on write access only (default).
/// </summary>
Write,
/// <summary>
@ -238,14 +279,34 @@ namespace System.Management.Automation
/// </summary>
public class VariableBreakpoint : Breakpoint
{
internal VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, ScriptBlock action)
/// <summary>
/// Creates a new instance of a <see cref="VariableBreakpoint"/>.
/// </summary>
public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode)
: this(script, variable, accessMode, null)
{}
/// <summary>
/// Creates a new instance of a <see cref="VariableBreakpoint"/>.
/// </summary>
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)
/// <summary>
/// Creates a new instance of a <see cref="VariableBreakpoint"/>.
/// </summary>
public VariableBreakpoint(string script, string variable, VariableAccessMode accessMode, int id)
: this(script, variable, accessMode, null, id)
{}
/// <summary>
/// Creates a new instance of a <see cref="VariableBreakpoint"/>.
/// </summary>
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
/// </summary>
public class LineBreakpoint : Breakpoint
{
internal LineBreakpoint(string script, int line, ScriptBlock action)
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
public LineBreakpoint(string script, int line)
: this(script, line, null)
{}
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
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)
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
public LineBreakpoint(string script, int line, int column)
: this(script, line, column, null)
{}
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
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)
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
public LineBreakpoint(string script, int line, int column, int id)
: this(script, line, column, null, id)
{}
/// <summary>
/// Creates a new instance of a <see cref="LineBreakpoint"/>
/// </summary>
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.");

View File

@ -626,6 +626,23 @@ namespace System.Management.Automation
throw new PSNotImplementedException();
}
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
public virtual Breakpoint GetBreakpoint(int id)
{
throw new PSNotImplementedException();
}
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
public virtual List<Breakpoint> GetBreakpoints()
{
throw new PSNotImplementedException();
}
/// <summary>
/// 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();
}
/// <summary>
/// Sets up debugger to debug provided Runspace in a nested debug session.
/// </summary>
/// <param name="runspace">Runspace to debug.</param>
/// <param name="disableBreakAll"></param>
internal virtual void DebugRunspace(Runspace runspace, bool disableBreakAll)
{
throw new PSNotImplementedException();
}
/// <summary>
/// Removes the provided Runspace from the nested "active" debugger state.
/// </summary>
@ -822,11 +849,11 @@ namespace System.Management.Automation
{
_context = context;
_inBreakpoint = false;
_idToBreakpoint = new Dictionary<int, Breakpoint>();
_pendingBreakpoints = new List<LineBreakpoint>();
_boundBreakpoints = new Dictionary<string, Tuple<WeakReference, List<LineBreakpoint>>>(StringComparer.OrdinalIgnoreCase);
_commandBreakpoints = new List<CommandBreakpoint>();
_variableBreakpoints = new Dictionary<string, List<VariableBreakpoint>>(StringComparer.OrdinalIgnoreCase);
_idToBreakpoint = new ConcurrentDictionary<int, Breakpoint>();
_pendingBreakpoints = new ConcurrentDictionary<int, LineBreakpoint>();
_boundBreakpoints = new ConcurrentDictionary<string, Tuple<WeakReference, ConcurrentDictionary<int, LineBreakpoint>>>(StringComparer.OrdinalIgnoreCase);
_commandBreakpoints = new ConcurrentDictionary<int, CommandBreakpoint>();
_variableBreakpoints = new ConcurrentDictionary<string, ConcurrentDictionary<int, VariableBreakpoint>>(StringComparer.OrdinalIgnoreCase);
_steppingMode = SteppingMode.None;
_callStack = new CallStackList { _callStackList = new List<CallStackInfo>() };
@ -1061,10 +1088,10 @@ namespace System.Management.Automation
internal void RegisterScriptFile(string path, string scriptContents)
{
Tuple<WeakReference, List<LineBreakpoint>> boundBreakpoints;
Tuple<WeakReference, ConcurrentDictionary<int, LineBreakpoint>> boundBreakpoints;
if (!_boundBreakpoints.TryGetValue(path, out boundBreakpoints))
{
_boundBreakpoints.Add(path, Tuple.Create(new WeakReference(scriptContents), new List<LineBreakpoint>()));
_boundBreakpoints[path] = Tuple.Create(new WeakReference(scriptContents), new ConcurrentDictionary<int, LineBreakpoint>());
}
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<LineBreakpoint>());
UnbindBoundBreakpoints(boundBreakpoints.Item2.Values.ToList());
_boundBreakpoints[path] = Tuple.Create(new WeakReference(scriptContents), new ConcurrentDictionary<int, LineBreakpoint>());
}
}
}
@ -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<VariableBreakpoint> breakpoints;
if (!_variableBreakpoints.TryGetValue(breakpoint.Variable, out breakpoints))
if (!_variableBreakpoints.TryGetValue(breakpoint.Variable, out ConcurrentDictionary<int, VariableBreakpoint> breakpoints))
{
breakpoints = new List<VariableBreakpoint>();
_variableBreakpoints.Add(breakpoint.Variable, breakpoints);
breakpoints = new ConcurrentDictionary<int, VariableBreakpoint>();
_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<WeakReference, List<LineBreakpoint>> value;
Tuple<WeakReference, ConcurrentDictionary<int, LineBreakpoint>> 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<Breakpoint> breakpoints =
_commandBreakpoints.Where(bp => bp.Enabled && bp.Trigger(invocationInfo)).ToList<Breakpoint>();
_commandBreakpoints.Values.Where(bp => bp.Enabled && bp.Trigger(invocationInfo)).ToList<Breakpoint>();
bool checkLineBp = true;
if (breakpoints.Any())
@ -1308,7 +1334,7 @@ namespace System.Management.Automation
{
SetInternalDebugMode(InternalDebugMode.Disabled);
List<VariableBreakpoint> breakpoints;
ConcurrentDictionary<int, VariableBreakpoint> 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
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
internal Breakpoint GetBreakpoint(int id)
/// <param name="id">Id of the breakpoint you want.</param>
public override Breakpoint GetBreakpoint(int id)
{
Breakpoint breakpoint;
_idToBreakpoint.TryGetValue(id, out breakpoint);
_idToBreakpoint.TryGetValue(id, out Breakpoint breakpoint);
return breakpoint;
}
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
internal List<Breakpoint> GetBreakpoints()
public override List<Breakpoint> 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<LineBreakpoint> _pendingBreakpoints;
private readonly Dictionary<string, Tuple<WeakReference, List<LineBreakpoint>>> _boundBreakpoints;
private readonly List<CommandBreakpoint> _commandBreakpoints;
private readonly Dictionary<string, List<VariableBreakpoint>> _variableBreakpoints;
private readonly Dictionary<int, Breakpoint> _idToBreakpoint;
private ConcurrentDictionary<int, LineBreakpoint> _pendingBreakpoints;
private readonly ConcurrentDictionary<string, Tuple<WeakReference, ConcurrentDictionary<int, LineBreakpoint>>> _boundBreakpoints;
private readonly ConcurrentDictionary<int, CommandBreakpoint> _commandBreakpoints;
private readonly ConcurrentDictionary<string, ConcurrentDictionary<int, VariableBreakpoint>> _variableBreakpoints;
private readonly ConcurrentDictionary<int, Breakpoint> _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<LineBreakpoint>();
var newPendingBreakpoints = new Dictionary<int, LineBreakpoint>();
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<int, LineBreakpoint>(newPendingBreakpoints);
}
private void StopOnSequencePoint(FunctionContext functionContext, List<Breakpoint> 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);
}
/// <summary>
/// Sets up debugger to debug provided Runspace in a nested debug session.
/// </summary>
/// <param name="runspace">Runspace to debug.</param>
internal override void DebugRunspace(Runspace runspace)
/// <param name="disableBreakAll">When specified, it will not turn on BreakAll.</param>
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);
}
/// <summary>
/// Adds the provided set of breakpoints to the debugger.
/// </summary>
/// <param name="breakpoints">Breakpoints.</param>
public override void SetBreakpoints(IEnumerable<Breakpoint> breakpoints)
{
_wrappedDebugger.SetBreakpoints(breakpoints);
}
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
public override Breakpoint GetBreakpoint(int id) =>
_wrappedDebugger.GetBreakpoint(id);
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
public override List<Breakpoint> GetBreakpoints() =>
_wrappedDebugger.GetBreakpoints();
/// <summary>
/// SetDebuggerAction.
/// </summary>

View File

@ -3907,6 +3907,28 @@ namespace System.Management.Automation
return _wrappedDebugger.ProcessCommand(command, output);
}
/// <summary>
/// Adds the provided set of breakpoints to the debugger.
/// </summary>
/// <param name="breakpoints">Breakpoints.</param>
public override void SetBreakpoints(IEnumerable<Breakpoint> breakpoints)
{
_wrappedDebugger.SetBreakpoints(breakpoints);
}
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
public override Breakpoint GetBreakpoint(int id) =>
_wrappedDebugger.GetBreakpoint(id);
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
public override List<Breakpoint> GetBreakpoints() =>
_wrappedDebugger.GetBreakpoints();
/// <summary>
/// Sets the debugger resume action.
/// </summary>

View File

@ -2007,6 +2007,28 @@ namespace System.Management.Automation
}
}
/// <summary>
/// Adds the provided set of breakpoints to the debugger.
/// </summary>
/// <param name="breakpoints">Breakpoints.</param>
public override void SetBreakpoints(IEnumerable<Breakpoint> breakpoints)
{
_runspace.Debugger?.SetBreakpoints(breakpoints);
}
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
public override Breakpoint GetBreakpoint(int id) =>
_runspace.Debugger?.GetBreakpoint(id);
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
public override List<Breakpoint> GetBreakpoints() =>
_runspace.Debugger?.GetBreakpoints();
/// <summary>
/// SetDebuggerAction.
/// </summary>

View File

@ -1767,6 +1767,28 @@ namespace System.Management.Automation
get { return _inDebugMode; }
}
/// <summary>
/// Adds the provided set of breakpoints to the debugger.
/// </summary>
/// <param name="breakpoints">Breakpoints.</param>
public override void SetBreakpoints(IEnumerable<Breakpoint> breakpoints)
{
_wrappedDebugger.Value.SetBreakpoints(breakpoints);
}
/// <summary>
/// Get a breakpoint by id, primarily for Enable/Disable/Remove-PSBreakpoint cmdlets.
/// </summary>
/// <param name="id">Id of the breakpoint you want.</param>
public override Breakpoint GetBreakpoint(int id) =>
_wrappedDebugger.Value.GetBreakpoint(id);
/// <summary>
/// Returns breakpoints primarily for the Get-PSBreakpoint cmdlet.
/// </summary>
public override List<Breakpoint> GetBreakpoints() =>
_wrappedDebugger.Value.GetBreakpoints();
/// <summary>
/// Exits debugger mode with the provided resume action.
/// </summary>
@ -1921,6 +1943,16 @@ namespace System.Management.Automation
_wrappedDebugger.Value.DebugRunspace(runspace);
}
/// <summary>
/// Sets up debugger to debug provided Runspace in a nested debug session.
/// </summary>
/// <param name="runspace">Runspace to debug.</param>
/// <param name="disableBreakAll"></param>
internal override void DebugRunspace(Runspace runspace, bool disableBreakAll)
{
_wrappedDebugger.Value.DebugRunspace(runspace, disableBreakAll);
}
/// <summary>
/// Removes the provided Runspace from the nested "active" debugger state.
/// </summary>

View File

@ -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 - <Name>" -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)
}
}

View File

@ -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
}
}

View File

@ -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", "", ""

View File

@ -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" ]
}
}

View File

@ -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<Breakpoint> actualBreakpoints = runspace.Debugger.GetBreakpoints();
Assert.Equal(expectedBreakpoints.Length, actualBreakpoints.Count);
Assert.Equal(expectedBreakpoints, actualBreakpoints);
}
finally
{
runspace.Close();
}
}
}
}
}