Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 48c996c

Browse files
Support breakpoints in untitled files in WinPS (#2248)
* Support breakpoints in untitled files in WinPS Adds support for setting breakpoints in untitled/unsaved files for Windows PowerShell 5.1. This aligns the breakpoint validation behaviour with the PowerShell 7.x API so that a breakpoint can be set for any ScriptBlock with a filename if it aligns with the client's filename. * Add fallback to Set-PSBreakpoint in case reflection can't find ctor * Fix up wildcard handling * Fix up command breakpoints for WinPS
1 parent 6bb322e commit 48c996c

File tree

5 files changed

+108
-31
lines changed

5 files changed

+108
-31
lines changed

‎src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs‎

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,83 @@ namespace Microsoft.PowerShell.EditorServices.Services
1818
{
1919
internal class BreakpointService
2020
{
21+
/// <summary>
22+
/// Code used on WinPS 5.1 to set breakpoints without Script path validation.
23+
/// It uses reflection because the APIs were not public until 7.0 but just in
24+
/// case something changes it has a fallback to Set-PSBreakpoint.
25+
/// </summary>
26+
private const string _setPSBreakpointLegacy = @"
27+
[CmdletBinding(DefaultParameterSetName = 'Line')]
28+
param (
29+
[Parameter()]
30+
[ScriptBlock]
31+
$Action,
32+
33+
[Parameter(ParameterSetName = 'Command')]
34+
[Parameter(ParameterSetName = 'Line', Mandatory = $true)]
35+
[string]
36+
$Script,
37+
38+
[Parameter(ParameterSetName = 'Line')]
39+
[int]
40+
$Line,
41+
42+
[Parameter(ParameterSetName = 'Line')]
43+
[int]
44+
$Column,
45+
46+
[Parameter(ParameterSetName = 'Command', Mandatory = $true)]
47+
[string]
48+
$Command
49+
)
50+
51+
if ($Script) {
52+
# If using Set-PSBreakpoint we need to escape any wildcard patterns.
53+
$PSBoundParameters['Script'] = [WildcardPattern]::Escape($Script)
54+
}
55+
else {
56+
# WinPS must use null for the Script if unset.
57+
$Script = [NullString]::Value
58+
}
59+
60+
if ($PSCmdlet.ParameterSetName -eq 'Command') {
61+
$cmdCtor = [System.Management.Automation.CommandBreakpoint].GetConstructor(
62+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
63+
$null,
64+
[type[]]@([string], [System.Management.Automation.WildcardPattern], [string], [ScriptBlock]),
65+
$null)
66+
67+
if (-not $cmdCtor) {
68+
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
69+
return
70+
}
71+
72+
$pattern = [System.Management.Automation.WildcardPattern]::Get(
73+
$Command,
74+
[System.Management.Automation.WildcardOptions]'Compiled, IgnoreCase')
75+
$b = $cmdCtor.Invoke(@($Script, $pattern, $Command, $Action))
76+
}
77+
else {
78+
$lineCtor = [System.Management.Automation.LineBreakpoint].GetConstructor(
79+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
80+
$null,
81+
[type[]]@([string], [int], [int], [ScriptBlock]),
82+
$null)
83+
84+
if (-not $lineCtor) {
85+
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
86+
return
87+
}
88+
89+
$b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
90+
}
91+
92+
[Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
93+
[System.Management.Automation.Breakpoint[]]@($b))
94+
95+
$b
96+
";
97+
2198
private readonly ILogger<BreakpointService> _logger;
2299
private readonly IInternalPowerShellExecutionService _executionService;
23100
private readonly PsesInternalHost _editorServicesHost;
@@ -57,7 +134,7 @@ public async Task<IReadOnlyList<Breakpoint>> GetBreakpointsAsync()
57134
.ConfigureAwait(false);
58135
}
59136

60-
public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(stringescapedScriptPath,IReadOnlyList<BreakpointDetails> breakpoints)
137+
public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(IReadOnlyList<BreakpointDetails> breakpoints)
61138
{
62139
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
63140
{
@@ -114,9 +191,11 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(string e
114191
psCommand.AddStatement();
115192
}
116193

194+
// Don't use Set-PSBreakpoint as that will try and validate the Script
195+
// path which may or may not exist.
117196
psCommand
118-
.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
119-
.AddParameter("Script", escapedScriptPath)
197+
.AddScript(_setPSBreakpointLegacy,useLocalScope:true)
198+
.AddParameter("Script", breakpoint.Source)
120199
.AddParameter("Line", breakpoint.LineNumber);
121200

122201
// Check if the user has specified the column number for the breakpoint.
@@ -184,7 +263,7 @@ public async Task<IReadOnlyList<CommandBreakpointDetails>> SetCommandBreakpoints
184263
}
185264

186265
psCommand
187-
.AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint")
266+
.AddScript(_setPSBreakpointLegacy,useLocalScope:true)
188267
.AddParameter("Command", breakpoint.Name);
189268

190269
// Check if this is a "conditional" line breakpoint.

‎src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
164164
await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false);
165165
}
166166

167-
return await _breakpointService.SetBreakpointsAsync(escapedScriptPath,breakpoints).ConfigureAwait(false);
167+
return await _breakpointService.SetBreakpointsAsync(breakpoints).ConfigureAwait(false);
168168
}
169169

170170
return await dscBreakpoints

‎src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.PowerShell.EditorServices.Logging;
1212
using Microsoft.PowerShell.EditorServices.Services;
1313
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
14-
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
1514
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1615
using Microsoft.PowerShell.EditorServices.Utility;
1716
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
@@ -31,20 +30,17 @@ internal class BreakpointHandlers : ISetFunctionBreakpointsHandler, ISetBreakpoi
3130
private readonly DebugService _debugService;
3231
private readonly DebugStateService _debugStateService;
3332
private readonly WorkspaceService _workspaceService;
34-
private readonly IRunspaceContext _runspaceContext;
3533

3634
public BreakpointHandlers(
3735
ILoggerFactory loggerFactory,
3836
DebugService debugService,
3937
DebugStateService debugStateService,
40-
WorkspaceService workspaceService,
41-
IRunspaceContext runspaceContext)
38+
WorkspaceService workspaceService)
4239
{
4340
_logger = loggerFactory.CreateLogger<BreakpointHandlers>();
4441
_debugService = debugService;
4542
_debugStateService = debugStateService;
4643
_workspaceService = workspaceService;
47-
_runspaceContext = runspaceContext;
4844
}
4945

5046
public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request, CancellationToken cancellationToken)
@@ -182,12 +178,11 @@ public Task<SetExceptionBreakpointsResponse> Handle(SetExceptionBreakpointsArgum
182178

183179
Task.FromResult(new SetExceptionBreakpointsResponse());
184180

185-
private bool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
181+
private staticbool IsFileSupportedForBreakpoints(string requestedPath, ScriptFile resolvedScriptFile)
186182
{
187-
// PowerShell 7 and above support breakpoints in untitled files
188183
if (ScriptFile.IsUntitledPath(requestedPath))
189184
{
190-
return BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace);
185+
return true;
191186
}
192187

193188
if (string.IsNullOrEmpty(resolvedScriptFile?.FilePath))

‎src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@
77
using System.Threading.Tasks;
88
using Microsoft.Extensions.Logging;
99
using Microsoft.PowerShell.EditorServices.Services;
10-
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
1110
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
1211
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
1312
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
14-
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
1513
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1614
using Microsoft.PowerShell.EditorServices.Utility;
1715
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
@@ -44,7 +42,6 @@ internal class ConfigurationDoneHandler : IConfigurationDoneHandler
4442
private readonly IInternalPowerShellExecutionService _executionService;
4543
private readonly WorkspaceService _workspaceService;
4644
private readonly IPowerShellDebugContext _debugContext;
47-
private readonly IRunspaceContext _runspaceContext;
4845

4946
// TODO: Decrease these arguments since they're a bunch of interfaces that can be simplified
5047
// (i.e., `IRunspaceContext` should just be available on `IPowerShellExecutionService`).
@@ -56,8 +53,7 @@ public ConfigurationDoneHandler(
5653
DebugEventHandlerService debugEventHandlerService,
5754
IInternalPowerShellExecutionService executionService,
5855
WorkspaceService workspaceService,
59-
IPowerShellDebugContext debugContext,
60-
IRunspaceContext runspaceContext)
56+
IPowerShellDebugContext debugContext)
6157
{
6258
_logger = loggerFactory.CreateLogger<ConfigurationDoneHandler>();
6359
_debugAdapterServer = debugAdapterServer;
@@ -67,7 +63,6 @@ public ConfigurationDoneHandler(
6763
_executionService = executionService;
6864
_workspaceService = workspaceService;
6965
_debugContext = debugContext;
70-
_runspaceContext = runspaceContext;
7166
}
7267

7368
public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request, CancellationToken cancellationToken)
@@ -119,13 +114,11 @@ internal async Task LaunchScriptAsync(string scriptToLaunch)
119114
else // It's a URI to an untitled script, or a raw script.
120115
{
121116
bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
122-
if (isScriptFile&&BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
117+
if (isScriptFile)
123118
{
124119
// Parse untitled files with their `Untitled:` URI as the filename which will
125120
// cache the URI and contents within the PowerShell parser. By doing this, we
126-
// light up the ability to debug untitled files with line breakpoints. This is
127-
// only possible with PowerShell 7's new breakpoint APIs since the old API,
128-
// Set-PSBreakpoint, validates that the given path points to a real file.
121+
// light up the ability to debug untitled files with line breakpoints.
129122
ScriptBlockAst ast = Parser.ParseInput(
130123
untitledScript.Contents,
131124
untitledScript.DocumentUri.ToString(),

‎test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,11 @@ await debugService.SetCommandBreakpointsAsync(
527527
Assert.Equal("True > ", prompt.ValueString);
528528
}
529529

530-
[SkippableFact]
531-
public async Task DebuggerBreaksInUntitledScript()
530+
[Theory]
531+
[InlineData("Command")]
532+
[InlineData("Line")]
533+
public async Task DebuggerBreaksInUntitledScript(string breakpointType)
532534
{
533-
Skip.IfNot(VersionUtils.PSEdition == "Core", "Untitled script breakpoints only supported in PowerShell Core");
534535
const string contents = "Write-Output $($MyInvocation.Line)";
535536
const string scriptPath = "untitled:Untitled-1";
536537
Assert.True(ScriptFile.IsUntitledPath(scriptPath));
@@ -539,11 +540,20 @@ public async Task DebuggerBreaksInUntitledScript()
539540
Assert.Equal(contents, scriptFile.Contents);
540541
Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _));
541542

542-
await debugService.SetCommandBreakpointsAsync(
543-
new[] { CommandBreakpointDetails.Create("Write-Output") });
543+
if (breakpointType == "Command")
544+
{
545+
await debugService.SetCommandBreakpointsAsync(
546+
new[] { CommandBreakpointDetails.Create("Write-Output") });
547+
}
548+
else
549+
{
550+
await debugService.SetLineBreakpointsAsync(
551+
scriptFile,
552+
new[] { BreakpointDetails.Create(scriptPath, 1) });
553+
}
544554

545555
ConfigurationDoneHandler configurationDoneHandler = new(
546-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null,psesHost);
556+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
547557

548558
Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
549559
await AssertDebuggerStopped(scriptPath, 1);
@@ -565,7 +575,7 @@ await debugService.SetCommandBreakpointsAsync(
565575
public async Task RecordsF5CommandInPowerShellHistory()
566576
{
567577
ConfigurationDoneHandler configurationDoneHandler = new(
568-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null,psesHost);
578+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
569579
await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
570580

571581
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(
@@ -605,7 +615,7 @@ public async Task RecordsF8CommandInHistory()
605615
public async Task OddFilePathsLaunchCorrectly()
606616
{
607617
ConfigurationDoneHandler configurationDoneHandler = new(
608-
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null,psesHost);
618+
NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
609619
await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
610620

611621
IReadOnlyList<string> historyResult = await psesHost.ExecutePSCommandAsync<string>(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /