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(