diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
index 007b75d49..578cf1f70 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
@@ -18,6 +18,154 @@ namespace Microsoft.PowerShell.EditorServices.Services
{
internal class BreakpointService
{
+ private const string _getPSBreakpointLegacy = @"
+ [CmdletBinding()]
+ param (
+ [Parameter()]
+ [string]
+ $Script,
+
+ [Parameter()]
+ [int]
+ $RunspaceId = [Runspace]::DefaultRunspace.Id
+ )
+
+ $runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) {
+ Get-Runspace -Id $RunspaceId
+ $null = $PSBoundParameters.Remove('RunspaceId')
+ }
+ else {
+ [Runspace]::DefaultRunspace
+ }
+
+ $debugger = $runspace.Debugger
+ $getBreakpointsMeth = $debugger.GetType().GetMethod(
+ 'GetBreakpoints',
+ [System.Reflection.BindingFlags]'NonPublic, Public, Instance',
+ $null,
+ [type[]]@(),
+ $null)
+
+ $runspaceIdProp = [System.Management.Automation.PSNoteProperty]::new(
+ 'RunspaceId',
+ $runspaceId)
+
+ @(
+ if (-not $getBreakpointsMeth) {
+ if ($RunspaceId -ne [Runspace]::DefaultRunspace.Id) {
+ throw 'Failed to find GetBreakpoints method on Debugger.'
+ }
+
+ Microsoft.PowerShell.Utility\Get-PSBreakpoint @PSBoundParameters
+ }
+ else {
+ $getBreakpointsMeth.Invoke($debugger, @()) | Where-Object {
+ if ($Script) {
+ $_.Script -eq $Script
+ }
+ else {
+ $true
+ }
+ }
+ }
+ ) | ForEach-Object {
+ $_.PSObject.Properties.Add($runspaceIdProp)
+ $_
+ }
+ ";
+
+ private const string _removePSBreakpointLegacy = @"
+ [CmdletBinding(DefaultParameterSetName = 'Breakpoint')]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Breakpoint')]
+ [System.Management.Automation.Breakpoint[]]
+ $Breakpoint,
+
+ [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Id')]
+ [int[]]
+ $Id,
+
+ [Parameter(ParameterSetName = 'Id')]
+ [int]
+ $RunspaceId = [Runspace]::DefaultRunspace.Id
+ )
+
+ begin {
+ $removeBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod(
+ 'RemoveBreakpoint',
+ [System.Reflection.BindingFlags]'NonPublic, Public, Instance',
+ $null,
+ [type[]]@([System.Management.Automation.Breakpoint]),
+ $null)
+ $getBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod(
+ 'GetBreakpoint',
+ [System.Reflection.BindingFlags]'NonPublic, Public, Instance',
+ $null,
+ [type[]]@([int]),
+ $null)
+
+ $breakpointCollection = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]]::new()
+ }
+
+ process {
+ if ($PSCmdlet.ParameterSetName -eq 'Id') {
+ $runspace = Get-Runspace -Id $RunspaceId
+ $runspaceProp = [System.Management.Automation.PSNoteProperty]::new(
+ 'Runspace',
+ $Runspace)
+
+ $breakpoints = if ($getBreakpointMeth) {
+ foreach ($breakpointId in $Id) {
+ $getBreakpointMeth.Invoke($runspace.Debugger, @($breakpointId))
+ }
+ }
+ elseif ($runspace -eq [Runspace]::DefaultRunspace) {
+ Microsoft.PowerShell.Utility\Get-PSBreakpoint -Id $Id
+ }
+ else {
+ throw 'Failed to find GetBreakpoint method on Debugger.'
+ }
+
+ $breakpoints | ForEach-Object {
+ $_.PSObject.Properties.Add($runspaceProp)
+ $breakpointCollection.Add($_)
+ }
+ }
+ else {
+ foreach ($b in $Breakpoint) {
+ # RunspaceId may be set by _getPSBreakpointLegacy when
+ # targeting a breakpoint in a specific runspace.
+ $runspace = if ($b.PSObject.Properties.Match('RunspaceId')) {
+ Get-Runspace -Id $b.RunspaceId
+ }
+ else {
+ [Runspace]::DefaultRunspace
+ }
+
+ $b.PSObject.Properties.Add(
+ [System.Management.Automation.PSNoteProperty]::new('Runspace', $runspace))
+ $breakpointCollection.Add($b)
+ }
+ }
+ }
+
+ end {
+ foreach ($b in $breakpointCollection) {
+ if ($removeBreakpointMeth) {
+ $removeBreakpointMeth.Invoke($b.Runspace.Debugger, @($b))
+ }
+ elseif ($b.Runspace -eq [Runspace]::DefaultRunspace) {
+ # If we don't have the method, we can only remove breakpoints
+ # from the default runspace using Remove-PSBreakpoint.
+ $b | Microsoft.PowerShell.Utility\Remove-PSBreakpoint
+ }
+ else {
+ throw 'Failed to find RemoveBreakpoint method on Debugger.'
+ }
+ }
+ }
+ ";
+
///
/// Code used on WinPS 5.1 to set breakpoints without Script path validation.
/// It uses reflection because the APIs were not public until 7.0 but just in
@@ -45,7 +193,11 @@ internal class BreakpointService
[Parameter(ParameterSetName = 'Command', Mandatory = $true)]
[string]
- $Command
+ $Command,
+
+ [Parameter()]
+ [int]
+ $RunspaceId
)
if ($Script) {
@@ -65,6 +217,9 @@ internal class BreakpointService
$null)
if (-not $cmdCtor) {
+ if ($PSBoundParameters.ContainsKey('RunspaceId')) {
+ throw 'Failed to find constructor for CommandBreakpoint.'
+ }
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
return
}
@@ -82,6 +237,9 @@ internal class BreakpointService
$null)
if (-not $lineCtor) {
+ if ($PSBoundParameters.ContainsKey('RunspaceId')) {
+ throw 'Failed to find constructor for LineBreakpoint.'
+ }
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
return
}
@@ -89,8 +247,14 @@ internal class BreakpointService
$b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
}
- [Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
- [System.Management.Automation.Breakpoint[]]@($b))
+ $runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) {
+ Get-Runspace -Id $RunspaceId
+ }
+ else {
+ [Runspace]::DefaultRunspace
+ }
+
+ $runspace.Debugger.SetBreakpoints([System.Management.Automation.Breakpoint[]]@($b))
$b
";
@@ -128,10 +292,14 @@ public async Task> GetBreakpointsAsync()
}
// Legacy behavior
- PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint");
+ PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true);
+ if (_debugStateService.RunspaceId is not null)
+ {
+ psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
+ }
return await _executionService
- .ExecutePSCommandAsync(psCommand, CancellationToken.None)
- .ConfigureAwait(false);
+ .ExecutePSCommandAsync(psCommand, CancellationToken.None)
+ .ConfigureAwait(false);
}
public async Task> SetBreakpointsAsync(IReadOnlyList breakpoints)
@@ -195,7 +363,7 @@ public async Task> SetBreakpointsAsync(IReadOnl
// path which may or may not exist.
psCommand
.AddScript(_setPSBreakpointLegacy, useLocalScope: true)
- .AddParameter("Script", breakpoint.Source)
+ .AddParameter("Script", breakpoint.MappedSource ?? breakpoint.Source)
.AddParameter("Line", breakpoint.LineNumber);
// Check if the user has specified the column number for the breakpoint.
@@ -211,6 +379,11 @@ public async Task> SetBreakpointsAsync(IReadOnl
{
psCommand.AddParameter("Action", actionScriptBlock);
}
+
+ if (_debugStateService.RunspaceId is not null)
+ {
+ psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
+ }
}
// If no PSCommand was created then there are no breakpoints to set.
@@ -219,7 +392,16 @@ public async Task> SetBreakpointsAsync(IReadOnl
IEnumerable setBreakpoints = await _executionService
.ExecutePSCommandAsync(psCommand, CancellationToken.None)
.ConfigureAwait(false);
- configuredBreakpoints.AddRange(setBreakpoints.Select((breakpoint) => BreakpointDetails.Create(breakpoint)));
+
+ int bpIdx = 0;
+ foreach (Breakpoint setBp in setBreakpoints)
+ {
+ BreakpointDetails setBreakpoint = BreakpointDetails.Create(
+ setBp,
+ sourceBreakpoint: breakpoints[bpIdx]);
+ configuredBreakpoints.Add(setBreakpoint);
+ bpIdx++;
+ }
}
return configuredBreakpoints;
}
@@ -326,14 +508,17 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null)
}
// Legacy behavior
- PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint");
-
+ PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true);
+ if (_debugStateService.RunspaceId is not null)
+ {
+ psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
+ }
if (!string.IsNullOrEmpty(scriptPath))
{
psCommand.AddParameter("Script", scriptPath);
}
- psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint");
+ psCommand.AddScript(_removePSBreakpointLegacy, useLocalScope: true);
await _executionService.ExecutePSCommandAsync
public string Source { get; private set; }
+ ///
+ /// Gets the source where the breakpoint is mapped to, will be null if no mapping exists. Used only for debug purposes.
+ ///
+ public string MappedSource { get; private set; }
+
///
/// Gets the line number at which the breakpoint is set.
///
@@ -50,6 +55,7 @@ private BreakpointDetails()
///
///
///
+ ///
///
internal static BreakpointDetails Create(
string source,
@@ -57,7 +63,8 @@ internal static BreakpointDetails Create(
int? column = null,
string condition = null,
string hitCondition = null,
- string logMessage = null)
+ string logMessage = null,
+ string mappedSource = null)
{
Validate.IsNotNullOrEmptyString(nameof(source), source);
@@ -69,7 +76,8 @@ internal static BreakpointDetails Create(
ColumnNumber = column,
Condition = condition,
HitCondition = hitCondition,
- LogMessage = logMessage
+ LogMessage = logMessage,
+ MappedSource = mappedSource
};
}
@@ -79,10 +87,12 @@ internal static BreakpointDetails Create(
///
/// The Breakpoint instance from which details will be taken.
/// The BreakpointUpdateType to determine if the breakpoint is verified.
+ /// /// The breakpoint source from the debug client, if any.
/// A new instance of the BreakpointDetails class.
internal static BreakpointDetails Create(
Breakpoint breakpoint,
- BreakpointUpdateType updateType = BreakpointUpdateType.Set)
+ BreakpointUpdateType updateType = BreakpointUpdateType.Set,
+ BreakpointDetails sourceBreakpoint = null)
{
Validate.IsNotNull(nameof(breakpoint), breakpoint);
@@ -96,10 +106,11 @@ internal static BreakpointDetails Create(
{
Id = breakpoint.Id,
Verified = updateType != BreakpointUpdateType.Disabled,
- Source = lineBreakpoint.Script,
+ Source = sourceBreakpoint?.MappedSource is not null ? sourceBreakpoint.Source : lineBreakpoint.Script,
LineNumber = lineBreakpoint.Line,
ColumnNumber = lineBreakpoint.Column,
- Condition = lineBreakpoint.Action?.ToString()
+ Condition = lineBreakpoint.Action?.ToString(),
+ MappedSource = sourceBreakpoint?.MappedSource,
};
if (lineBreakpoint.Column > 0)
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
index 68d23c966..1c26c48de 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
@@ -79,6 +79,11 @@ public async Task Handle(SetBreakpointsArguments request
}
// At this point, the source file has been verified as a PowerShell script.
+ string mappedSource = null;
+ if (_debugService.TryGetMappedRemotePath(scriptFile.FilePath, out string remoteMappedPath))
+ {
+ mappedSource = remoteMappedPath;
+ }
IReadOnlyList breakpointDetails = request.Breakpoints
.Select((srcBreakpoint) => BreakpointDetails.Create(
scriptFile.FilePath,
@@ -86,7 +91,8 @@ public async Task Handle(SetBreakpointsArguments request
srcBreakpoint.Column,
srcBreakpoint.Condition,
srcBreakpoint.HitCondition,
- srcBreakpoint.LogMessage)).ToList();
+ srcBreakpoint.LogMessage,
+ mappedSource: mappedSource)).ToList();
// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
IReadOnlyList updatedBreakpointDetails = breakpointDetails;
@@ -98,8 +104,9 @@ public async Task Handle(SetBreakpointsArguments request
{
updatedBreakpointDetails =
await _debugService.SetLineBreakpointsAsync(
- scriptFile,
- breakpointDetails).ConfigureAwait(false);
+ mappedSource ?? scriptFile.FilePath,
+ breakpointDetails,
+ skipRemoteMapping: mappedSource is not null).ConfigureAwait(false);
}
catch (Exception e)
{
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
index 146bbeae0..747b2d2c0 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
@@ -1,161 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-using System.Management.Automation;
-using System.Management.Automation.Language;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Services;
-using Microsoft.PowerShell.EditorServices.Services.PowerShell;
-using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
-using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
-using Microsoft.PowerShell.EditorServices.Services.TextDocument;
-using Microsoft.PowerShell.EditorServices.Utility;
-using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
-using OmniSharp.Extensions.DebugAdapter.Protocol.Server;
namespace Microsoft.PowerShell.EditorServices.Handlers
{
internal class ConfigurationDoneHandler : IConfigurationDoneHandler
{
- // TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands'
- // `GetInvocationText` and that reveals some obscure implementation details we should
- // instead hide from the user with pretty strings (or perhaps not write out at all).
- //
- // This API is mostly used for F5 execution so it requires the foreground.
- private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new()
- {
- RequiresForeground = true,
- WriteInputToHost = true,
- WriteOutputToHost = true,
- ThrowOnError = false,
- AddToHistory = true,
- };
-
- private readonly ILogger _logger;
- private readonly IDebugAdapterServerFacade _debugAdapterServer;
private readonly DebugService _debugService;
private readonly DebugStateService _debugStateService;
- private readonly DebugEventHandlerService _debugEventHandlerService;
- private readonly IInternalPowerShellExecutionService _executionService;
- private readonly WorkspaceService _workspaceService;
- private readonly IPowerShellDebugContext _debugContext;
- // TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
- // (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
public ConfigurationDoneHandler(
- ILoggerFactory loggerFactory,
- IDebugAdapterServerFacade debugAdapterServer,
DebugService debugService,
- DebugStateService debugStateService,
- DebugEventHandlerService debugEventHandlerService,
- IInternalPowerShellExecutionService executionService,
- WorkspaceService workspaceService,
- IPowerShellDebugContext debugContext)
+ DebugStateService debugStateService)
{
- _logger = loggerFactory.CreateLogger();
- _debugAdapterServer = debugAdapterServer;
_debugService = debugService;
_debugStateService = debugStateService;
- _debugEventHandlerService = debugEventHandlerService;
- _executionService = executionService;
- _workspaceService = workspaceService;
- _debugContext = debugContext;
}
- public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
+ public async Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
{
_debugService.IsClientAttached = true;
- if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch))
- {
- // NOTE: This is an unawaited task because responding to "configuration done" means
- // setting up the debugger, and in our case that means starting the script but not
- // waiting for it to finish.
- Task _ = LaunchScriptAsync(_debugStateService.ScriptToLaunch).HandleErrorsAsync(_logger);
- }
-
- if (_debugStateService.IsInteractiveDebugSession && _debugService.IsDebuggerStopped)
- {
- if (_debugService.CurrentDebuggerStoppedEventArgs is not null)
- {
- // If this is an interactive session and there's a pending breakpoint, send that
- // information along to the debugger client.
- _debugEventHandlerService.TriggerDebuggerStopped(_debugService.CurrentDebuggerStoppedEventArgs);
- }
- else
- {
- // If this is an interactive session and there's a pending breakpoint that has
- // not been propagated through the debug service, fire the debug service's
- // OnDebuggerStop event.
- _debugService.OnDebuggerStopAsync(null, _debugContext.LastStopEventArgs);
- }
- }
-
- return Task.FromResult(new ConfigurationDoneResponse());
- }
-
- // NOTE: We test this function in `DebugServiceTests` so it both needs to be internal, and
- // use conditional-access on `_debugStateService` and `_debugAdapterServer` as its not set
- // by tests.
- internal async Task LaunchScriptAsync(string scriptToLaunch)
- {
- PSCommand command;
- if (System.IO.File.Exists(scriptToLaunch))
- {
- // For a saved file we just execute its path (after escaping it), with the configured operator
- // (which can't be called that because it's a reserved keyword in C#).
- string executeMode = _debugStateService?.ExecuteMode == "Call" ? "&" : ".";
- command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
- PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), _debugStateService?.Arguments, executeMode);
- }
- else // It's a URI to an untitled script, or a raw script.
- {
- bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
- if (isScriptFile)
- {
- // Parse untitled files with their `Untitled:` URI as the filename which will
- // cache the URI and contents within the PowerShell parser. By doing this, we
- // light up the ability to debug untitled files with line breakpoints.
- ScriptBlockAst ast = Parser.ParseInput(
- untitledScript.Contents,
- untitledScript.DocumentUri.ToString(),
- out Token[] _,
- out ParseError[] _);
-
- // In order to use utilize the parser's cache (and therefore hit line
- // breakpoints) we need to use the AST's `ScriptBlock` object. Due to
- // limitations in PowerShell's public API, this means we must use the
- // `PSCommand.AddArgument(object)` method, hence this hack where we dot-source
- // `$args[0]. Fortunately the dot-source operator maintains a stack of arguments
- // on each invocation, so passing the user's arguments directly in the initial
- // `AddScript` surprisingly works.
- command = PSCommandHelpers
- .BuildDotSourceCommandWithArguments("$args[0]", _debugStateService?.Arguments)
- .AddArgument(ast.GetScriptBlock());
- }
- else
- {
- // Without the new APIs we can only execute the untitled script's contents.
- // Command breakpoints and `Wait-Debugger` will work. We must wrap the script
- // with newlines so that any included comments don't break the command.
- command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
- string.Concat(
- "{" + System.Environment.NewLine,
- isScriptFile ? untitledScript.Contents : scriptToLaunch,
- System.Environment.NewLine + "}"),
- _debugStateService?.Arguments);
- }
- }
-
- await _executionService.ExecutePSCommandAsync(
- command,
- CancellationToken.None,
- s_debuggerExecutionOptions).ConfigureAwait(false);
+ // Tells the attach/launch request handler that the config is done
+ // and it can continue starting the script.
+ await _debugStateService.SetConfigurationDoneAsync(cancellationToken).ConfigureAwait(false);
- _debugAdapterServer?.SendNotification(EventNames.Terminated);
+ return new ConfigurationDoneResponse();
}
}
}
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs
index 798ccc621..7ca10ffce 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs
@@ -50,6 +50,7 @@ public async Task Handle(DisconnectArguments request, Cancel
// We should instead ensure that the debugger is in some valid state, lock it and then tear things down
_debugEventHandlerService.UnregisterEventHandlers();
+ _debugService.UnsetPathMappings();
if (!_debugStateService.ExecutionCompleted)
{
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
index 4201cc5e6..5d7947b3f 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
+using System.Management.Automation.Language;
using System.Management.Automation.Remoting;
using System.Threading;
using System.Threading.Tasks;
@@ -17,6 +18,7 @@
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
+using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
using OmniSharp.Extensions.DebugAdapter.Protocol.Server;
@@ -75,6 +77,12 @@ internal record PsesLaunchRequestArguments : LaunchRequestArguments
/// properties of the 'environmentVariables' are used as key/value pairs.
///
public Dictionary Env { get; set; }
+
+ ///
+ /// Gets or sets the path mappings for the debugging session. This is
+ /// only used when the current runspace is remote.
+ ///
+ public PathMapping[] PathMappings { get; set; } = [];
}
internal record PsesAttachRequestArguments : AttachRequestArguments
@@ -88,10 +96,52 @@ internal record PsesAttachRequestArguments : AttachRequestArguments
public string RunspaceName { get; set; }
public string CustomPipeName { get; set; }
+
+ ///
+ /// Gets or sets the path mappings for the remote debugging session.
+ ///
+ public PathMapping[] PathMappings { get; set; } = [];
+
+ /// Gets or sets a boolean value that determines whether to write the
+ /// PSES.Attached event to the target runspace after attaching.
+ ///
+ public bool NotifyOnAttach { get; set; }
}
internal class LaunchAndAttachHandler : ILaunchHandler, IAttachHandler, IOnDebugAdapterServerStarted
{
+ private const string _newAttachEventScript = @"
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory)]
+ [int]
+ $RunspaceId
+ )
+
+ $ErrorActionPreference = 'Stop'
+
+ $runspace = Get-Runspace -Id $RunspaceId
+ $runspace.Events.GenerateEvent(
+ 'PSES.Attached',
+ 'PSES',
+ @(),
+ $null)
+ ";
+
+ // TODO: We currently set `WriteInputToHost` as true, which writes our debugged commands'
+ // `GetInvocationText` and that reveals some obscure implementation details we should
+ // instead hide from the user with pretty strings (or perhaps not write out at all).
+ //
+ // This API is mostly used for F5 execution so it requires the foreground.
+ private static readonly PowerShellExecutionOptions s_debuggerExecutionOptions = new()
+ {
+ RequiresForeground = true,
+ WriteInputToHost = true,
+ WriteOutputToHost = true,
+ ThrowOnError = false,
+ AddToHistory = true,
+ };
+
private static readonly int s_currentPID = System.Diagnostics.Process.GetCurrentProcess().Id;
private static readonly Version s_minVersionForCustomPipeName = new(6, 2);
private readonly ILogger _logger;
@@ -99,6 +149,7 @@ internal class LaunchAndAttachHandler : ILaunchHandler();
+ // DebugServiceTests will call this with a null DebugStateService.
+ if (_debugStateService is not null)
+ {
+ _debugStateService.ServerStarted = new TaskCompletionSource();
+ }
_remoteFileManagerService = remoteFileManagerService;
}
public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken)
+ {
+ _debugService.SetPathMappings(request.PathMappings);
+ try
+ {
+ return await HandleImpl(request, cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ _debugService.UnsetPathMappings();
+ throw;
+ }
+ }
+
+ public async Task HandleImpl(PsesLaunchRequestArguments request, CancellationToken cancellationToken)
{
// The debugger has officially started. We use this to later check if we should stop it.
((PsesInternalHost)_executionService).DebugContext.IsActive = true;
@@ -205,10 +276,7 @@ await _executionService.ExecutePSCommandAsync(
// Store the launch parameters so that they can be used later
_debugStateService.NoDebug = request.NoDebug;
- _debugStateService.ScriptToLaunch = request.Script;
- _debugStateService.Arguments = request.Args;
_debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole;
- _debugStateService.ExecuteMode = request.ExecuteMode;
if (request.CreateTemporaryIntegratedConsole
&& !string.IsNullOrEmpty(request.Script)
@@ -219,22 +287,39 @@ await _executionService.ExecutePSCommandAsync(
// If the current session is remote, map the script path to the remote
// machine if necessary
- if (_debugStateService.ScriptToLaunch != null
+ string requestScript = request.Script;
+ if (requestScript != null
&& _runspaceContext.CurrentRunspace.IsOnRemoteMachine)
{
- _debugStateService.ScriptToLaunch =
- _remoteFileManagerService.GetMappedPath(
- _debugStateService.ScriptToLaunch,
- _runspaceContext.CurrentRunspace);
+ if (_debugService.TryGetMappedRemotePath(requestScript, out string remoteMappedPath))
+ {
+ requestScript = remoteMappedPath;
+ }
+ else
+ {
+ // If the script is not mapped, we will map it to the remote path
+ // using the RemoteFileManagerService.
+ requestScript =
+ _remoteFileManagerService.GetMappedPath(
+ requestScript,
+ _runspaceContext.CurrentRunspace);
+ }
}
// If no script is being launched, mark this as an interactive
// debugging session
- _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(_debugStateService.ScriptToLaunch);
+ _debugStateService.IsInteractiveDebugSession = string.IsNullOrEmpty(requestScript);
// Sends the InitializedEvent so that the debugger will continue
// sending configuration requests
- _debugStateService.ServerStarted.TrySetResult(true);
+ await _debugStateService.WaitForConfigurationDoneAsync("launch", cancellationToken).ConfigureAwait(false);
+
+ if (!_debugStateService.IsInteractiveDebugSession)
+ {
+ // NOTE: This is an unawaited task because we are starting the script but not
+ // waiting for it to finish.
+ Task _ = LaunchScriptAsync(requestScript, request.Args, request.ExecuteMode).HandleErrorsAsync(_logger);
+ }
return new LaunchResponse();
}
@@ -250,11 +335,13 @@ public async Task Handle(PsesAttachRequestArguments request, Can
_debugService.IsDebuggingRemoteRunspace = true;
try
{
+ _debugService.SetPathMappings(request.PathMappings);
return await HandleImpl(request, cancellationToken).ConfigureAwait(false);
}
catch
{
_debugService.IsDebuggingRemoteRunspace = false;
+ _debugService.UnsetPathMappings();
throw;
}
}
@@ -353,16 +440,90 @@ void RunspaceChangedHandler(object s, RunspaceChangedEventArgs _)
// Clear any existing breakpoints before proceeding
await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false);
- _debugStateService.WaitingForAttach = true;
+ // The debugger is now ready to receive breakpoint requests. We do
+ // this before running Debug-Runspace so the runspace is not busy
+ // and can set breakpoints before the final configuration done.
+ await _debugStateService.WaitForConfigurationDoneAsync("attach", cancellationToken).ConfigureAwait(false);
+
+ if (request.NotifyOnAttach)
+ {
+ // This isn't perfect as there is still a race condition
+ // this and Debug-Runspace setting up the debugger below but it
+ // is the best we can do without changes to PowerShell.
+ await _executionService.ExecutePSCommandAsync(
+ new PSCommand().AddScript(_newAttachEventScript, useLocalScope: true)
+ .AddParameter("RunspaceId", _debugStateService.RunspaceId),
+ cancellationToken).ConfigureAwait(false);
+ }
+
Task nonAwaitedTask = _executionService
.ExecutePSCommandAsync(debugRunspaceCmd, CancellationToken.None, PowerShellExecutionOptions.ImmediateInteractive)
.ContinueWith(OnExecutionCompletedAsync, TaskScheduler.Default);
- _debugStateService.ServerStarted.TrySetResult(true);
-
return new AttachResponse();
}
+ // NOTE: We test this function in `DebugServiceTests` so it both needs to be internal, and
+ // use conditional-access on `_debugStateService` and `_debugAdapterServer` as its not set
+ // by tests.
+ internal async Task LaunchScriptAsync(string scriptToLaunch, string[] arguments, string requestExecuteMode)
+ {
+ PSCommand command;
+ if (File.Exists(scriptToLaunch))
+ {
+ // For a saved file we just execute its path (after escaping it), with the configured operator
+ // (which can't be called that because it's a reserved keyword in C#).
+ string executeMode = requestExecuteMode == "Call" ? "&" : ".";
+ command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
+ PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), arguments, executeMode);
+ }
+ else // It's a URI to an untitled script, or a raw script.
+ {
+ bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
+ if (isScriptFile)
+ {
+ // Parse untitled files with their `Untitled:` URI as the filename which will
+ // cache the URI and contents within the PowerShell parser. By doing this, we
+ // light up the ability to debug untitled files with line breakpoints.
+ ScriptBlockAst ast = Parser.ParseInput(
+ untitledScript.Contents,
+ untitledScript.DocumentUri.ToString(),
+ out Token[] _,
+ out ParseError[] _);
+
+ // In order to use utilize the parser's cache (and therefore hit line
+ // breakpoints) we need to use the AST's `ScriptBlock` object. Due to
+ // limitations in PowerShell's public API, this means we must use the
+ // `PSCommand.AddArgument(object)` method, hence this hack where we dot-source
+ // `$args[0]. Fortunately the dot-source operator maintains a stack of arguments
+ // on each invocation, so passing the user's arguments directly in the initial
+ // `AddScript` surprisingly works.
+ command = PSCommandHelpers
+ .BuildDotSourceCommandWithArguments("$args[0]", arguments)
+ .AddArgument(ast.GetScriptBlock());
+ }
+ else
+ {
+ // Without the new APIs we can only execute the untitled script's contents.
+ // Command breakpoints and `Wait-Debugger` will work. We must wrap the script
+ // with newlines so that any included comments don't break the command.
+ command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
+ string.Concat(
+ "{" + Environment.NewLine,
+ isScriptFile ? untitledScript.Contents : scriptToLaunch,
+ Environment.NewLine + "}"),
+ arguments);
+ }
+ }
+
+ await _executionService.ExecutePSCommandAsync(
+ command,
+ CancellationToken.None,
+ s_debuggerExecutionOptions).ConfigureAwait(false);
+
+ _debugAdapterServer?.SendNotification(EventNames.Terminated);
+ }
+
private async Task AttachToComputer(string computerName, CancellationToken cancellationToken)
{
_debugStateService.IsRemoteAttach = true;
@@ -486,6 +647,7 @@ private async Task OnExecutionCompletedAsync(Task executeTask)
_debugEventHandlerService.UnregisterEventHandlers();
_debugService.IsDebuggingRemoteRunspace = false;
+ _debugService.UnsetPathMappings();
if (!isRunspaceClosed && _debugStateService.IsAttachSession)
{
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
index 858f6a815..735d672d1 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/StackTraceHandler.cs
@@ -38,7 +38,12 @@ public async Task Handle(StackTraceArguments request, Cancel
InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo
?? throw new RpcErrorException(0, null!, "InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug and you should report it.");
- StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo);
+ string? scriptNameOverride = null;
+ if (debugService.TryGetMappedLocalPath(invocationInfo.ScriptName, out string mappedLocalPath))
+ {
+ scriptNameOverride = mappedLocalPath;
+ }
+ StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo, scriptNameOverride: scriptNameOverride);
if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the subsequent requests.
{
@@ -116,13 +121,13 @@ public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id)
};
}
- public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new()
+ public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0, string? scriptNameOverride = null) => new()
{
Name = "",
Id = id,
Source = new()
{
- Path = invocationInfo.ScriptName
+ Path = scriptNameOverride ?? invocationInfo.ScriptName
},
Line = invocationInfo.ScriptLineNumber,
Column = invocationInfo.OffsetInLine,
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs b/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs
new file mode 100644
index 000000000..6bf6a6ad9
--- /dev/null
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/PathMapping.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+namespace Microsoft.PowerShell.EditorServices.Services;
+
+///
+/// Used for attach requests to map a local and remote path together.
+///
+internal record PathMapping
+{
+ ///
+ /// Gets or sets the local root of this mapping entry.
+ ///
+ public string? LocalRoot { get; set; }
+
+ ///
+ /// Gets or sets the remote root of this mapping entry.
+ ///
+ public string? RemoteRoot { get; set; }
+}
+
+#nullable disable
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs
index 763094be4..344b798f7 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs
@@ -91,10 +91,12 @@ public static async Task GetDscCapabilityAsync(
.AddCommand(@"Microsoft.PowerShell.Core\Import-Module")
.AddParameter("Name", "PSDesiredStateConfiguration")
.AddParameter("PassThru")
- .AddParameter("ErrorAction", ActionPreference.Ignore);
+ .AddParameter("ErrorAction", ActionPreference.Ignore)
+ .AddCommand(@"Microsoft.PowerShell.Utility\Select-Object")
+ .AddParameter("ExpandProperty", "Name");
- IReadOnlyList dscModule =
- await executionService.ExecutePSCommandAsync(
+ IReadOnlyList dscModule =
+ await executionService.ExecutePSCommandAsync(
psCommand,
CancellationToken.None,
new PowerShellExecutionOptions { ThrowOnError = false })
diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
index b51f3d8dd..ceed8a333 100644
--- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
+++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs
@@ -2,12 +2,15 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml.Linq;
using Microsoft.PowerShell.EditorServices.Handlers;
using Nerdbank.Streams;
using OmniSharp.Extensions.DebugAdapter.Client;
@@ -65,6 +68,12 @@ public class DebugAdapterProtocolMessageTests(ITestOutputHelper output) : IAsync
///
private readonly TaskCompletionSource terminatedTcs = new();
+ private readonly TaskCompletionSource serverInitializedTcs = new();
+ ///
+ /// This task is useful for waiting until the server has sent the Initialized event.
+ ///
+ private Task serverInitialized => serverInitializedTcs.Task;
+
public async Task InitializeAsync()
{
// Cleanup testScriptLogPath if it exists due to an interrupted previous run
@@ -105,12 +114,18 @@ send until a launch is sent.
.WithInput(psesStream)
.WithOutput(psesStream)
// The "early" return mentioned above
- .OnInitialized(async (dapClient, _, _, _) => initializedLanguageClientTcs.SetResult(dapClient))
+ .OnInitialized((dapClient, _, _, _) =>
+ {
+ initializedLanguageClientTcs.SetResult(dapClient);
+ return Task.CompletedTask;
+ })
// This TCS is useful to wait for a breakpoint to be hit
- .OnStopped(async (StoppedEvent e) =>
+ .OnStopped((StoppedEvent e) =>
{
- nextStoppedTcs.SetResult(e);
+ TaskCompletionSource currentStoppedTcs = nextStoppedTcs;
nextStoppedTcs = new();
+
+ currentStoppedTcs.SetResult(e);
})
.OnRequest("startDebugging", (StartDebuggingAttachRequestArguments request) =>
{
@@ -122,6 +137,10 @@ send until a launch is sent.
terminatedTcs.SetResult(e);
return Task.CompletedTask;
})
+ .OnDebugAdapterInitialized((initEvent) =>
+ {
+ serverInitializedTcs.SetResult(initEvent);
+ })
;
});
@@ -255,9 +274,11 @@ public void CanInitializeWithCorrectServerSettings()
public async Task UsesDotSourceOperatorAndQuotesAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
- await client.LaunchScript(filePath);
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
Assert.NotNull(configDoneResponse);
+ await launchTask;
string actual = await ReadScriptLogLineAsync();
Assert.StartsWith(". '", actual);
@@ -267,9 +288,11 @@ public async Task UsesDotSourceOperatorAndQuotesAsync()
public async Task UsesCallOperatorWithSettingAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
- await client.LaunchScript(filePath, executeMode: "Call");
+ Task launchTask = client.LaunchScript(filePath, executeMode: "Call");
+ await serverInitialized;
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
Assert.NotNull(configDoneResponse);
+ await launchTask;
string actual = await ReadScriptLogLineAsync();
Assert.StartsWith("& '", actual);
@@ -280,10 +303,12 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync()
{
string filePath = NewTestFile(GenerateLoggingScript("works"));
- await client.LaunchScript(filePath);
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
Assert.NotNull(configDoneResponse);
+ await launchTask;
string actual = await ReadScriptLogLineAsync();
Assert.Equal("works", actual);
@@ -301,7 +326,8 @@ public async Task CanSetBreakpointsAsync()
"after breakpoint"
));
- await client.LaunchScript(filePath);
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
// {"command":"setBreakpoints","arguments":{"source":{"name":"dfsdfg.ps1","path":"/Users/tyleonha/Code/PowerShell/Misc/foo/dfsdfg.ps1"},"lines":[2],"breakpoints":[{"line":2}],"sourceModified":false},"type":"request","seq":3}
SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
@@ -319,6 +345,8 @@ public async Task CanSetBreakpointsAsync()
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
Assert.NotNull(configDoneResponse);
+ await launchTask;
+
// Wait until we hit the breakpoint
StoppedEvent stoppedEvent = await nextStopped;
Assert.Equal("breakpoint", stoppedEvent.Reason);
@@ -362,8 +390,10 @@ await client.SetBreakpoints(
);
// Signal to start the script
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
- await client.LaunchScript(filePath);
+ await launchTask;
// Try to get the stacktrace, which should throw as we are not currently at a breakpoint.
await Assert.ThrowsAsync(() => client.RequestStackTrace(
@@ -382,7 +412,8 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
));
// Request a launch. Note that per DAP spec, launch doesn't actually begin until ConfigDone finishes.
- await client.LaunchScript(filePath);
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
{
@@ -398,6 +429,8 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
_ = client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ await launchTask;
+
// Wait for the breakpoint to be hit
StoppedEvent stoppedEvent = await nextStopped;
Assert.Equal("breakpoint", stoppedEvent.Reason);
@@ -444,7 +477,8 @@ public async Task CanStepPastSystemWindowsForms()
"Write-Host $form"
}));
- await client.LaunchScript(filePath);
+ Task launchTask = client.LaunchScript(filePath);
+ await serverInitialized;
SetFunctionBreakpointsResponse setBreakpointsResponse = await client.SetFunctionBreakpoints(
new SetFunctionBreakpointsArguments
@@ -458,6 +492,8 @@ public async Task CanStepPastSystemWindowsForms()
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
Assert.NotNull(configDoneResponse);
+ await launchTask;
+
await Task.Delay(5000);
VariablesResponse variablesResponse = await client.RequestVariables(
@@ -482,9 +518,11 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync()
// PsesLaunchRequestArguments.Script, which is then assigned to
// DebugStateService.ScriptToLaunch in that handler, and finally used by the
// ConfigurationDoneHandler in LaunchScriptAsync.
- await client.LaunchScript(script);
+ Task launchTask = client.LaunchScript(script);
+ await serverInitialized;
_ = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ await launchTask;
// We can check that the script was invoked as expected, which is to dot-source a script
// block with the contents surrounded by newlines. While we can't check that the last
@@ -531,8 +569,10 @@ public async Task CanRunPesterTestFile()
}
}", isPester: true);
- await client.LaunchScript($"Invoke-Pester -Script '{pesterTest}'");
+ Task launchTask = client.LaunchScript($"Invoke-Pester -Script '{pesterTest}'");
+ await serverInitialized;
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ await launchTask;
Assert.Equal("pester", await ReadScriptLogLineAsync());
}
@@ -565,8 +605,10 @@ public async Task CanLaunchScriptWithNewChildAttachSession(
terminatedTcs.TrySetCanceled();
});
- await client.LaunchScript(script);
+ Task launchTask = client.LaunchScript(script);
+ await serverInitialized;
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ await launchTask;
StartDebuggingAttachRequestArguments attachRequest = await startDebuggingAttachRequestTcs.Task;
Assert.Equal("attach", attachRequest.Request);
@@ -599,8 +641,10 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
terminatedTcs.TrySetCanceled();
});
- await client.LaunchScript(script);
+ Task launchTask = client.LaunchScript(script);
+ await serverInitialized;
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ await launchTask;
StartDebuggingAttachRequestArguments attachRequest = await startDebuggingAttachRequestTcs.Task;
Assert.Equal("attach", attachRequest.Request);
@@ -613,8 +657,369 @@ public async Task CanLaunchScriptWithNewChildAttachSessionAsJob()
await terminatedTcs.Task;
}
- private record StartDebuggingAttachRequestArguments(PsesAttachRequestArguments Configuration, string Request);
+ [SkippableFact]
+ public async Task CanAttachScriptWithEventWait()
+ {
+ Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
+ "Breakpoints can't be set in Constrained Language Mode.");
+
+ string[] logStatements = ["before breakpoint", "after breakpoint"];
+
+ await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) =>
+ {
+ Task nextStoppedTask = nextStopped;
+
+ Task attachTask = client.Attach(
+ new PsesAttachRequestArguments
+ {
+ ProcessId = processId,
+ RunspaceId = runspaceId,
+ NotifyOnAttach = true,
+ });
+
+ await serverInitialized;
+
+ SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
+ {
+ Source = new Source { Name = Path.GetFileName(filePath), Path = filePath },
+ Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 2 } },
+ SourceModified = false,
+ });
+
+ Breakpoint breakpoint = setBreakpointsResponse.Breakpoints.First();
+ Assert.True(breakpoint.Verified);
+ Assert.NotNull(breakpoint.Source);
+ Assert.Equal(filePath, breakpoint.Source.Path, ignoreCase: s_isWindows);
+ Assert.Equal(2, breakpoint.Line);
+
+ ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ Assert.NotNull(configDoneResponse);
+
+ AttachResponse attachResponse = await attachTask;
+ Assert.NotNull(attachResponse);
+
+ StoppedEvent stoppedEvent;
+ if (PsesStdioLanguageServerProcessHost.IsWindowsPowerShell)
+ {
+ // WinPS will break on first statement when Debug-Runspace
+ // is called. This does not happen in pwsh 7.
+ stoppedEvent = await nextStoppedTask;
+ Assert.Equal("step", stoppedEvent.Reason);
+ Assert.NotNull(stoppedEvent.ThreadId);
+
+ nextStoppedTask = nextStopped;
+
+ await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId });
+ await client.RequestContinue(new ContinueArguments { ThreadId = (int)stoppedEvent.ThreadId });
+ }
+
+ // Wait until we hit the breakpoint
+ stoppedEvent = await nextStopped;
+ Assert.Equal("breakpoint", stoppedEvent.Reason);
+
+ // The code before the breakpoint should have already run
+ Assert.Equal("before breakpoint", await ReadScriptLogLineAsync());
+
+ // Assert that the stopped breakpoint is the one we set
+ StackTraceResponse stackTraceResponse = await client.RequestStackTrace(new StackTraceArguments { ThreadId = 1 });
+ DapStackFrame? stoppedTopFrame = stackTraceResponse.StackFrames?.First();
+ Assert.NotNull(stoppedTopFrame);
+ Assert.Equal(2, stoppedTopFrame.Line);
+
+ _ = await client.RequestContinue(new ContinueArguments { ThreadId = 1 });
+
+ string afterBreakpointActual = await ReadScriptLogLineAsync();
+ Assert.Equal("after breakpoint", afterBreakpointActual);
+ }, waitForNotifyEvent: true);
+ }
+
+ [SkippableFact]
+ public async Task CanAttachScriptWithPathMappings()
+ {
+ Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
+ "Breakpoints can't be set in Constrained Language Mode.");
+
+ string[] logStatements = ["$PSCommandPath", "after breakpoint"];
+
+ await RunWithAttachableProcess(logStatements, async (filePath, processId, runspaceId) =>
+ {
+ string localParent = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ string localScriptPath = Path.Combine(localParent, Path.GetFileName(filePath));
+ Directory.CreateDirectory(localParent);
+ File.Copy(filePath, localScriptPath);
+
+ Task nextStoppedTask = nextStopped;
+
+ Task attachTask = client.Attach(
+ new PsesAttachRequestArguments
+ {
+ ProcessId = processId,
+ RunspaceId = runspaceId,
+ PathMappings = [
+ new()
+ {
+ LocalRoot = localParent + Path.DirectorySeparatorChar,
+ RemoteRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar
+ }
+ ]
+ });
+
+ await serverInitialized;
+
+ SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
+ {
+ Source = new Source { Name = Path.GetFileName(localScriptPath), Path = localScriptPath },
+ Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 2 } },
+ SourceModified = false,
+ });
+
+ Breakpoint breakpoint = setBreakpointsResponse.Breakpoints.First();
+ Assert.True(breakpoint.Verified);
+ Assert.NotNull(breakpoint.Source);
+ Assert.Equal(localScriptPath, breakpoint.Source.Path, ignoreCase: s_isWindows);
+ Assert.Equal(2, breakpoint.Line);
+
+ ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
+ Assert.NotNull(configDoneResponse);
+
+ AttachResponse attachResponse = await attachTask;
+ Assert.NotNull(attachResponse);
+
+ // Wait-Debugger stop
+ StoppedEvent stoppedEvent = await nextStoppedTask;
+ Assert.Equal("step", stoppedEvent.Reason);
+ Assert.NotNull(stoppedEvent.ThreadId);
+
+ nextStoppedTask = nextStopped;
+
+ // It is important we wait for the stack trace before continue.
+ // The stopped event starts to get the stack trace info in the
+ // background and requesting the stack trace is the only way to
+ // ensure it is done and won't conflict with the continue request.
+ await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId });
+ await client.RequestContinue(new ContinueArguments { ThreadId = (int)stoppedEvent.ThreadId });
+
+ // Wait until we hit the breakpoint
+ stoppedEvent = await nextStoppedTask;
+ Assert.Equal("breakpoint", stoppedEvent.Reason);
+ Assert.NotNull(stoppedEvent.ThreadId);
-#nullable disable
+ // The code before the breakpoint should have already run
+ // It will contain the actual script being run
+ string beforeBreakpointActual = await ReadScriptLogLineAsync();
+ Assert.Equal(filePath, beforeBreakpointActual);
+
+ // Assert that the stopped breakpoint is the one we set
+ StackTraceResponse stackTraceResponse = await client.RequestStackTrace(new StackTraceArguments { ThreadId = (int)stoppedEvent.ThreadId });
+ DapStackFrame? stoppedTopFrame = stackTraceResponse.StackFrames?.First();
+
+ // The top frame should have a source path of our local script.
+ Assert.NotNull(stoppedTopFrame);
+ Assert.Equal(2, stoppedTopFrame.Line);
+ Assert.NotNull(stoppedTopFrame.Source);
+ Assert.Equal(localScriptPath, stoppedTopFrame.Source.Path, ignoreCase: s_isWindows);
+
+ await client.RequestContinue(new ContinueArguments { ThreadId = 1 });
+
+ string afterBreakpointActual = await ReadScriptLogLineAsync();
+ Assert.Equal("after breakpoint", afterBreakpointActual);
+ });
+ }
+
+ private async Task RunWithAttachableProcess(string[] logStatements, Func action, bool waitForNotifyEvent = false)
+ {
+ /*
+ There is no public API in pwsh to wait for an attach event. We
+ use reflection to wait until the AvailabilityChanged event is
+ subscribed to by Debug-Runspace as a marker that it is ready to
+ continue.
+
+ We also run the test script in another runspace as WinPS'
+ Debug-Runspace will break on the first statement after the
+ attach and we want that to be the Wait-Debugger call.
+
+ We can use https://github.com/PowerShell/PowerShell/pull/25788
+ once that is merged and we are running against that version but
+ WinPS will always need this.
+ */
+ string scriptEntrypoint = @"
+ param([string]$TestScript, [string]$WaitScript)
+
+ $debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility
+ $runspaceBase = [PSObject].Assembly.GetType(
+ 'System.Management.Automation.Runspaces.RunspaceBase')
+ $availabilityChangedField = $runspaceBase.GetField(
+ 'AvailabilityChanged',
+ [System.Reflection.BindingFlags]'NonPublic, Instance')
+ if (-not $availabilityChangedField) {
+ throw 'Failed to get AvailabilityChanged event field'
+ }
+
+ $ps = [PowerShell]::Create()
+ $runspace = $ps.Runspace
+
+ if ($WaitScript) {
+ $null = $ps.AddScript($WaitScript, $true).AddStatement()
+ }
+ else {
+ $null = $ps.AddCommand('Wait-Debugger').AddStatement()
+ }
+ $null = $ps.AddCommand($TestScript)
+
+ # Let the runner know what Runspace to attach to and that it
+ # is ready to run.
+ 'RID: {0}' -f $runspace.Id
+
+ $start = Get-Date
+ while ($true) {
+ $subscribed = $availabilityChangedField.GetValue($runspace) |
+ Where-Object Target -is $debugRunspaceCmd.ImplementingType
+ if ($subscribed) {
+ break
+ }
+
+ if (((Get-Date) - $start).TotalSeconds -gt 10) {
+ throw 'Timeout waiting for Debug-Runspace to be subscribed.'
+ }
+ }
+
+ $ps.Invoke()
+ foreach ($e in $ps.Streams.Error) {
+ Write-Error -ErrorRecord $e
+ }
+
+ # Keep running until the runner has deleted the test script to
+ # ensure the process doesn't finish before the test does in
+ # normal circumstances.
+ while (Test-Path -LiteralPath $TestScript) {
+ Start-Sleep -Seconds 1
+ }
+ ";
+
+ string filePath = NewTestFile(GenerateLoggingScript(logStatements));
+
+ List args = [filePath];
+ if (waitForNotifyEvent)
+ {
+ args.Add(@"
+ $e = Wait-Event -SourceIdentifier PSES.Attached -Timeout 10
+ if ($e) {
+ $e | Remove-Event -Force
+ }
+ else {
+ throw 'Timed out waiting for PSES.Attached event.'
+ }
+ ");
+ }
+ string encArgs = CreatePwshEncodedArgs(args.ToArray());
+ string encCommand = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptEntrypoint));
+
+ ProcessStartInfo psi = new ProcessStartInfo
+ {
+ FileName = PsesStdioLanguageServerProcessHost.PwshExe,
+ Arguments = $"-NoLogo -NoProfile -EncodedCommand {encCommand} -EncodedArguments {encArgs}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+ psi.EnvironmentVariables["TERM"] = "dumb"; // Avoids color/VT sequences in test output.
+
+ TaskCompletionSource ridOutput = new();
+
+ // Task shouldn't take longer than 30 seconds to complete.
+ using CancellationTokenSource debugTaskCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ using CancellationTokenRegistration _ = debugTaskCts.Token.Register(ridOutput.SetCanceled);
+ using Process? psProc = Process.Start(psi);
+ try
+ {
+ Assert.NotNull(psProc);
+ psProc.OutputDataReceived += (sender, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ if (args.Data.StartsWith("RID: "))
+ {
+ int rid = int.Parse(args.Data.Substring(5));
+ ridOutput.SetResult(rid);
+ }
+
+ output.WriteLine("STDOUT: {0}", args.Data);
+ }
+ };
+ psProc.ErrorDataReceived += (sender, args) =>
+ {
+ if (!string.IsNullOrEmpty(args.Data))
+ {
+ output.WriteLine("STDERR: {0}", args.Data);
+ }
+ };
+ psProc.EnableRaisingEvents = true;
+ psProc.BeginOutputReadLine();
+ psProc.BeginErrorReadLine();
+
+ Task procExited = psProc.WaitForExitAsync(debugTaskCts.Token);
+ Task waitRid = ridOutput.Task;
+
+ // Wait for the process to fail or the script to start.
+ Task finishedTask = await Task.WhenAny(waitRid, procExited);
+ if (finishedTask == procExited)
+ {
+ await procExited;
+ Assert.Fail("The attached process exited before the PowerShell entrypoint could start.");
+ }
+ int rid = await waitRid;
+
+ Task debugTask = action(filePath, psProc.Id, rid);
+ finishedTask = await Task.WhenAny(procExited, debugTask);
+ if (finishedTask == procExited)
+ {
+ await procExited;
+ Assert.Fail("Attached process exited before the script could start.");
+ }
+
+ await debugTask;
+
+ File.Delete(filePath);
+ psProc.Kill();
+ await procExited;
+ }
+ catch
+ {
+ if (psProc is not null && !psProc.HasExited)
+ {
+ psProc.Kill();
+ }
+
+ throw;
+ }
+ }
+
+ private static string CreatePwshEncodedArgs(params string[] args)
+ {
+ // Only way to pass args to -EncodedCommand is to use CLIXML with
+ // -EncodedArguments. Not pretty but the structure isn't too
+ // complex and saves us trying to embed/escape strings in a script.
+ string clixmlNamespace = "http://schemas.microsoft.com/powershell/2004/04";
+ string clixml = new XDocument(
+ new XDeclaration("1.0", "utf-16", "yes"),
+ new XElement(XName.Get("Objs", clixmlNamespace),
+ new XAttribute("Version", "1.1.0.1"),
+ new XElement(XName.Get("Obj", clixmlNamespace),
+ new XAttribute("RefId", "0"),
+ new XElement(XName.Get("TN", clixmlNamespace),
+ new XAttribute("RefId", "0"),
+ new XElement(XName.Get("T", clixmlNamespace), "System.Collections.ArrayList"),
+ new XElement(XName.Get("T", clixmlNamespace), "System.Object")
+ ),
+ new XElement(XName.Get("LST", clixmlNamespace),
+ args.Select(s => new XElement(XName.Get("S", clixmlNamespace), s))
+ )
+ ))).ToString(SaveOptions.DisableFormatting);
+
+ return Convert.ToBase64String(Encoding.Unicode.GetBytes(clixml));
+ }
+
+ private record StartDebuggingAttachRequestArguments(PsesAttachRequestArguments Configuration, string Request);
}
}
diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
index 03690ec21..ab4f4ee49 100644
--- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
@@ -201,7 +201,7 @@ await debugService.SetCommandBreakpointsAsync(
public async Task DebuggerAcceptsScriptArgs(string[] args)
{
IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync(
- oddPathScriptFile,
+ oddPathScriptFile.FilePath,
new[] { BreakpointDetails.Create(oddPathScriptFile.FilePath, 3) });
Assert.Single(breakpoints);
@@ -310,7 +310,7 @@ public async Task DebuggerSetsAndClearsLineBreakpoints()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 10)
@@ -323,7 +323,7 @@ await debugService.SetLineBreakpointsAsync(
Assert.Equal(10, breakpoints[1].LineNumber);
breakpoints = await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 2) });
confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
@@ -331,7 +331,7 @@ await debugService.SetLineBreakpointsAsync(
Assert.Equal(2, breakpoints[0].LineNumber);
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
Array.Empty());
IReadOnlyList remainingBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
@@ -342,7 +342,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerStopsOnLineBreakpoints()
{
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 7)
@@ -361,7 +361,7 @@ public async Task DebuggerStopsOnConditionalBreakpoints()
const int breakpointValue2 = 20;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"),
});
@@ -397,7 +397,7 @@ public async Task DebuggerStopsOnHitConditionBreakpoint()
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, null, $"{hitCount}"),
});
@@ -420,7 +420,7 @@ public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, "$i % 2 -eq 0", $"{hitCount}") });
Task _ = ExecuteDebugFileAsync();
@@ -441,7 +441,7 @@ public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
// TODO: Add this breakpoint back when it stops moving around?! The ordering
// of these two breakpoints seems to do with which framework executes the
@@ -469,7 +469,7 @@ public async Task DebuggerFindsParsableButInvalidSimpleBreakpointConditions()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5, column: null, condition: "$i == 100"),
BreakpointDetails.Create(debugScriptFile.FilePath, 7, column: null, condition: "$i > 100")
@@ -548,14 +548,14 @@ await debugService.SetCommandBreakpointsAsync(
else
{
await debugService.SetLineBreakpointsAsync(
- scriptFile,
+ scriptFile.FilePath,
new[] { BreakpointDetails.Create(scriptPath, 1) });
}
- ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
+ LaunchAndAttachHandler launchAndAttachHandler = new(
+ NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null);
- Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
+ Task _ = launchAndAttachHandler.LaunchScriptAsync(scriptPath, [], "DotSource");
await AssertDebuggerStopped(scriptPath, 1);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.CommandVariablesName);
@@ -574,9 +574,9 @@ await debugService.SetLineBreakpointsAsync(
[Fact]
public async Task RecordsF5CommandInPowerShellHistory()
{
- ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
- await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
+ LaunchAndAttachHandler launchAndAttachHandler = new(
+ NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null);
+ await launchAndAttachHandler.LaunchScriptAsync(debugScriptFile.FilePath, [], "DotSource");
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
new PSCommand().AddScript("(Get-History).CommandLine"),
@@ -614,9 +614,9 @@ public async Task RecordsF8CommandInHistory()
[Fact]
public async Task OddFilePathsLaunchCorrectly()
{
- ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
- await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
+ LaunchAndAttachHandler launchAndAttachHandler = new(
+ NullLoggerFactory.Instance, null, null, null, debugService, psesHost, psesHost, workspace, null, null);
+ await launchAndAttachHandler.LaunchScriptAsync(oddPathScriptFile.FilePath, [], "DotSource");
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
new PSCommand().AddScript("(Get-History).CommandLine"),
@@ -630,7 +630,7 @@ public async Task OddFilePathsLaunchCorrectly()
public async Task DebuggerVariableStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 8) });
Task _ = ExecuteVariableScriptFileAsync();
@@ -648,7 +648,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerGetsVariables()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 21) });
Task _ = ExecuteVariableScriptFileAsync();
@@ -698,7 +698,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerSetsVariablesNoConversion()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
Task _ = ExecuteVariableScriptFileAsync();
@@ -751,7 +751,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerSetsVariablesWithConversion()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
// Execute the script and wait for the breakpoint to be hit
@@ -807,7 +807,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableEnumDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 15) });
// Execute the script and wait for the breakpoint to be hit
@@ -827,7 +827,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableHashtableDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 11) });
// Execute the script and wait for the breakpoint to be hit
@@ -860,7 +860,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableNullStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 16) });
// Execute the script and wait for the breakpoint to be hit
@@ -880,7 +880,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariablePSObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 17) });
// Execute the script and wait for the breakpoint to be hit
@@ -1076,7 +1076,7 @@ await GetVariables(VariableContainerDetails.ScriptScopeName),
public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 18) });
// Execute the script and wait for the breakpoint to be hit
@@ -1105,7 +1105,7 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableProcessObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 19) });
// Execute the script and wait for the breakpoint to be hit