diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
index 748e21c6d..007b75d49 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
@@ -18,6 +18,83 @@ namespace Microsoft.PowerShell.EditorServices.Services
{
internal class BreakpointService
{
+ ///
+ /// 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
+ /// case something changes it has a fallback to Set-PSBreakpoint.
+ ///
+ private const string _setPSBreakpointLegacy = @"
+ [CmdletBinding(DefaultParameterSetName = 'Line')]
+ param (
+ [Parameter()]
+ [ScriptBlock]
+ $Action,
+
+ [Parameter(ParameterSetName = 'Command')]
+ [Parameter(ParameterSetName = 'Line', Mandatory = $true)]
+ [string]
+ $Script,
+
+ [Parameter(ParameterSetName = 'Line')]
+ [int]
+ $Line,
+
+ [Parameter(ParameterSetName = 'Line')]
+ [int]
+ $Column,
+
+ [Parameter(ParameterSetName = 'Command', Mandatory = $true)]
+ [string]
+ $Command
+ )
+
+ if ($Script) {
+ # If using Set-PSBreakpoint we need to escape any wildcard patterns.
+ $PSBoundParameters['Script'] = [WildcardPattern]::Escape($Script)
+ }
+ else {
+ # WinPS must use null for the Script if unset.
+ $Script = [NullString]::Value
+ }
+
+ if ($PSCmdlet.ParameterSetName -eq 'Command') {
+ $cmdCtor = [System.Management.Automation.CommandBreakpoint].GetConstructor(
+ [System.Reflection.BindingFlags]'NonPublic, Public, Instance',
+ $null,
+ [type[]]@([string], [System.Management.Automation.WildcardPattern], [string], [ScriptBlock]),
+ $null)
+
+ if (-not $cmdCtor) {
+ Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
+ return
+ }
+
+ $pattern = [System.Management.Automation.WildcardPattern]::Get(
+ $Command,
+ [System.Management.Automation.WildcardOptions]'Compiled, IgnoreCase')
+ $b = $cmdCtor.Invoke(@($Script, $pattern, $Command, $Action))
+ }
+ else {
+ $lineCtor = [System.Management.Automation.LineBreakpoint].GetConstructor(
+ [System.Reflection.BindingFlags]'NonPublic, Public, Instance',
+ $null,
+ [type[]]@([string], [int], [int], [ScriptBlock]),
+ $null)
+
+ if (-not $lineCtor) {
+ Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
+ return
+ }
+
+ $b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
+ }
+
+ [Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
+ [System.Management.Automation.Breakpoint[]]@($b))
+
+ $b
+ ";
+
private readonly ILogger _logger;
private readonly IInternalPowerShellExecutionService _executionService;
private readonly PsesInternalHost _editorServicesHost;
@@ -57,7 +134,7 @@ public async Task> GetBreakpointsAsync()
.ConfigureAwait(false);
}
- public async Task> SetBreakpointsAsync(string escapedScriptPath, IReadOnlyList breakpoints)
+ public async Task> SetBreakpointsAsync(IReadOnlyList breakpoints)
{
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
{
@@ -114,9 +191,11 @@ public async Task> SetBreakpointsAsync(string e
psCommand.AddStatement();
}
+ // Don't use Set-PSBreakpoint as that will try and validate the Script
+ // path which may or may not exist.
psCommand
- .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
- .AddParameter("Script", escapedScriptPath)
+ .AddScript(_setPSBreakpointLegacy, useLocalScope: true)
+ .AddParameter("Script", breakpoint.Source)
.AddParameter("Line", breakpoint.LineNumber);
// Check if the user has specified the column number for the breakpoint.
@@ -184,7 +263,7 @@ public async Task> SetCommandBreakpoints
}
psCommand
- .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
+ .AddScript(_setPSBreakpointLegacy, useLocalScope: true)
.AddParameter("Command", breakpoint.Name);
// Check if this is a "conditional" line breakpoint.
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
index 195d0508b..c0029fd42 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
@@ -164,7 +164,7 @@ public async Task> SetLineBreakpointsAsync(
await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false);
}
- return await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false);
+ return await _breakpointService.SetBreakpointsAsync(breakpoints).ConfigureAwait(false);
}
return await dscBreakpoints
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
index 4c99ff747..68d23c966 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
@@ -11,7 +11,6 @@
using Microsoft.PowerShell.EditorServices.Logging;
using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
-using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
@@ -31,20 +30,17 @@ internal class BreakpointHandlers : ISetFunctionBreakpointsHandler, ISetBreakpoi
private readonly DebugService _debugService;
private readonly DebugStateService _debugStateService;
private readonly WorkspaceService _workspaceService;
- private readonly IRunspaceContext _runspaceContext;
public BreakpointHandlers(
ILoggerFactory loggerFactory,
DebugService debugService,
DebugStateService debugStateService,
- WorkspaceService workspaceService,
- IRunspaceContext runspaceContext)
+ WorkspaceService workspaceService)
{
_logger = loggerFactory.CreateLogger();
_debugService = debugService;
_debugStateService = debugStateService;
_workspaceService = workspaceService;
- _runspaceContext = runspaceContext;
}
public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken)
@@ -182,12 +178,11 @@ public Task Handle(SetExceptionBreakpointsArgum
Task.FromResult(new SetExceptionBreakpointsResponse());
- private bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
+ private static bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
{
- // PowerShell 7 and above support breakpoints in untitled files
if (ScriptFile.IsUntitledPath(requestedPath))
{
- return BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace);
+ return true;
}
if (string.IsNullOrEmpty(resolvedScriptFile?.FilePath))
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
index 038f61955..146bbeae0 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
@@ -7,11 +7,9 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.PowerShell.EditorServices.Services;
-using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
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.PowerShell.Runspace;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
@@ -44,7 +42,6 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler
private readonly IInternalPowerShellExecutionService _executionService;
private readonly WorkspaceService _workspaceService;
private readonly IPowerShellDebugContext _debugContext;
- private readonly IRunspaceContext _runspaceContext;
// TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
// (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
@@ -56,8 +53,7 @@ public ConfigurationDoneHandler(
DebugEventHandlerService debugEventHandlerService,
IInternalPowerShellExecutionService executionService,
WorkspaceService workspaceService,
- IPowerShellDebugContext debugContext,
- IRunspaceContext runspaceContext)
+ IPowerShellDebugContext debugContext)
{
_logger = loggerFactory.CreateLogger();
_debugAdapterServer = debugAdapterServer;
@@ -67,7 +63,6 @@ public ConfigurationDoneHandler(
_executionService = executionService;
_workspaceService = workspaceService;
_debugContext = debugContext;
- _runspaceContext = runspaceContext;
}
public Task Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
@@ -119,13 +114,11 @@ internal async Task LaunchScriptAsync(string scriptToLaunch)
else // It's a URI to an untitled script, or a raw script.
{
bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
- if (isScriptFile && BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
+ 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. This is
- // only possible with PowerShell 7's new breakpoint APIs since the old API,
- // Set-PSBreakpoint, validates that the given path points to a real file.
+ // light up the ability to debug untitled files with line breakpoints.
ScriptBlockAst ast = Parser.ParseInput(
untitledScript.Contents,
untitledScript.DocumentUri.ToString(),
diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
index d9656c855..03690ec21 100644
--- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
@@ -527,10 +527,11 @@ await debugService.SetCommandBreakpointsAsync(
Assert.Equal("True > ", prompt.ValueString);
}
- [SkippableFact]
- public async Task DebuggerBreaksInUntitledScript()
+ [Theory]
+ [InlineData("Command")]
+ [InlineData("Line")]
+ public async Task DebuggerBreaksInUntitledScript(string breakpointType)
{
- Skip.IfNot(VersionUtils.PSEdition == "Core", "Untitled script breakpoints only supported in PowerShell Core");
const string contents = "Write-Output $($MyInvocation.Line)";
const string scriptPath = "untitled:Untitled-1";
Assert.True(ScriptFile.IsUntitledPath(scriptPath));
@@ -539,11 +540,20 @@ public async Task DebuggerBreaksInUntitledScript()
Assert.Equal(contents, scriptFile.Contents);
Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _));
- await debugService.SetCommandBreakpointsAsync(
- new[] { CommandBreakpointDetails.Create("Write-Output") });
+ if (breakpointType == "Command")
+ {
+ await debugService.SetCommandBreakpointsAsync(
+ new[] { CommandBreakpointDetails.Create("Write-Output") });
+ }
+ else
+ {
+ await debugService.SetLineBreakpointsAsync(
+ scriptFile,
+ new[] { BreakpointDetails.Create(scriptPath, 1) });
+ }
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
await AssertDebuggerStopped(scriptPath, 1);
@@ -565,7 +575,7 @@ await debugService.SetCommandBreakpointsAsync(
public async Task RecordsF5CommandInPowerShellHistory()
{
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
@@ -605,7 +615,7 @@ public async Task RecordsF8CommandInHistory()
public async Task OddFilePathsLaunchCorrectly()
{
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(