Skip to content

Commit 13b5cbb

Browse files
committed
Fix running untitled scripts with arguments (but break debugging)
So the extant hack that enabled debugging untitled scripts is really untenable. It shifted the user's args by one since it ran the untitled script as the first arg to the dot-source operator. Unfortunately, there doesn't seem to be a clear way to break on lines in an untitled script. However, `Wait-Debugger` and command breakpoints still work. Attempting to be "helpful" by escaping arguments proved to cause more issues than it solved. We can instead massively simplify our script launching logic (and fix yet another test that had a "maybe bug") by just launching what the user gave us. This should also be easier for the user to debug.
1 parent c123557 commit 13b5cbb

File tree

5 files changed

+33
-178
lines changed

5 files changed

+33
-178
lines changed

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

+19-46
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Management.Automation;
5+
using System.Management.Automation.Language;
6+
using System.Threading;
7+
using System.Threading.Tasks;
48
using Microsoft.Extensions.Logging;
59
using Microsoft.PowerShell.EditorServices.Services;
610
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
@@ -13,10 +17,6 @@
1317
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
1418
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
1519
using OmniSharp.Extensions.DebugAdapter.Protocol.Server;
16-
using System.Management.Automation;
17-
using System.Management.Automation.Language;
18-
using System.Threading;
19-
using System.Threading.Tasks;
2020

2121
namespace Microsoft.PowerShell.EditorServices.Handlers
2222
{
@@ -77,7 +77,10 @@ public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request
7777

7878
if (!string.IsNullOrEmpty(_debugStateService.ScriptToLaunch))
7979
{
80-
LaunchScriptAsync(_debugStateService.ScriptToLaunch).HandleErrorsAsync(_logger);
80+
// NOTE: This is an unawaited task because responding to "configuration done" means
81+
// setting up the debugger, and in our case that means starting the script but not
82+
// waiting for it to finish.
83+
Task _ = LaunchScriptAsync(_debugStateService.ScriptToLaunch).HandleErrorsAsync(_logger);
8184
}
8285

8386
if (_debugStateService.IsInteractiveDebugSession && _debugService.IsDebuggerStopped)
@@ -102,48 +105,18 @@ public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request
102105

103106
private async Task LaunchScriptAsync(string scriptToLaunch)
104107
{
105-
// Is this an untitled script?
106-
if (ScriptFile.IsUntitledPath(scriptToLaunch))
107-
{
108-
ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch);
109-
110-
if (BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
111-
{
112-
// Parse untitled files with their `Untitled:` URI as the file name which will cache the URI & contents within the PowerShell parser.
113-
// By doing this, we light up the ability to debug Untitled files with breakpoints.
114-
// This is only possible via the direct usage of the breakpoint APIs in PowerShell because
115-
// Set-PSBreakpoint validates that paths are actually on the filesystem.
116-
ScriptBlockAst ast = Parser.ParseInput(untitledScript.Contents, untitledScript.DocumentUri.ToString(), out Token[] tokens, out ParseError[] errors);
117-
118-
// This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API.
119-
//
120-
// TODO: Fix this so the added script doesn't show up.
121-
var cmd = new PSCommand().AddScript(". $args[0]").AddArgument(ast.GetScriptBlock());
122-
await _executionService
123-
.ExecutePSCommandAsync<object>(cmd, CancellationToken.None, s_debuggerExecutionOptions)
124-
.ConfigureAwait(false);
125-
}
126-
else
127-
{
128-
await _executionService
129-
.ExecutePSCommandAsync(
130-
new PSCommand().AddScript(untitledScript.Contents),
131-
CancellationToken.None,
132-
s_debuggerExecutionOptions)
133-
.ConfigureAwait(false);
134-
}
135-
}
136-
else
137-
{
138-
// TODO: Fix this so the added script doesn't show up.
139-
await _executionService
140-
.ExecutePSCommandAsync(
141-
PSCommandHelpers.BuildCommandFromArguments(scriptToLaunch, _debugStateService.Arguments),
142-
CancellationToken.None,
143-
s_debuggerExecutionOptions)
144-
.ConfigureAwait(false);
145-
}
108+
// TODO: Theoretically we can make PowerShell respect line breakpoints in untitled
109+
// files, but the previous method was a hack that conflicted with correct passing of
110+
// arguments to the debugged script. We are prioritizing the latter over the former, as
111+
// command breakpoints and `Wait-Debugger` work fine.
112+
string command = ScriptFile.IsUntitledPath(scriptToLaunch)
113+
? string.Concat("{ ", _workspaceService.GetFile(scriptToLaunch).Contents, " }")
114+
: string.Concat('"', scriptToLaunch, '"');
146115

116+
await _executionService.ExecutePSCommandAsync(
117+
PSCommandHelpers.BuildCommandFromArguments(command, _debugStateService.Arguments),
118+
CancellationToken.None,
119+
s_debuggerExecutionOptions).ConfigureAwait(false);
147120
_debugAdapterServer.SendNotification(EventNames.Terminated);
148121
}
149122
}

src/PowerShellEditorServices/Utility/ArgumentUtils.cs

-40
This file was deleted.

src/PowerShellEditorServices/Utility/PSCommandExtensions.cs

+3-17
Original file line numberDiff line numberDiff line change
@@ -129,25 +129,11 @@ private static StringBuilder AddCommandText(this StringBuilder sb, Command comma
129129
return sb;
130130
}
131131

132-
public static PSCommand BuildCommandFromArguments(string command, IReadOnlyList<string> arguments)
132+
public static PSCommand BuildCommandFromArguments(string command, IEnumerable<string> arguments)
133133
{
134134
// HACK: We use AddScript instead of AddArgument/AddParameter to reuse Powershell parameter binding logic.
135-
// We quote the command parameter so that expressions can still be used in the arguments.
136-
var sb = new StringBuilder()
137-
.Append('.')
138-
.Append(' ')
139-
.Append('"')
140-
.Append(command)
141-
.Append('"');
142-
143-
foreach (string arg in arguments ?? System.Linq.Enumerable.Empty<string>())
144-
{
145-
sb
146-
.Append(' ')
147-
.Append(ArgumentEscaping.Escape(arg));
148-
}
149-
150-
return new PSCommand().AddScript(sb.ToString());
135+
string script = string.Concat(". ", command, " ", string.Join(" ", arguments ?? Array.Empty<string>()));
136+
return new PSCommand().AddScript(script);
151137
}
152138
}
153139
}

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

+11-6
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private VariableDetailsBase[] GetVariables(string scopeName)
9696
private Task ExecutePowerShellCommand(string command, params string[] args)
9797
{
9898
return psesHost.ExecutePSCommandAsync(
99-
PSCommandHelpers.BuildCommandFromArguments(command, args),
99+
PSCommandHelpers.BuildCommandFromArguments(string.Concat('"', command, '"'), args),
100100
CancellationToken.None);
101101
}
102102

@@ -176,8 +176,16 @@ await debugService.SetCommandBreakpointsAsync(
176176
Assert.Equal("[ArrayList: 0]", var.ValueString);
177177
}
178178

179-
[Fact]
180-
public async Task DebuggerAcceptsScriptArgs()
179+
// See https://www.thomasbogholm.net/2021/06/01/convenient-member-data-sources-with-xunit/
180+
public static IEnumerable<object[]> DebuggerAcceptsScriptArgsTestData => new List<object[]>()
181+
{
182+
new object[] { new object[] { "Foo -Param2 @('Bar','Baz') -Force Extra1" } },
183+
new object[] { new object[] { "Foo", "-Param2", "@('Bar','Baz')", "-Force", "Extra1" } }
184+
};
185+
186+
[Theory]
187+
[MemberData(nameof(DebuggerAcceptsScriptArgsTestData))]
188+
public async Task DebuggerAcceptsScriptArgs(string[] args)
181189
{
182190
// The path is intentionally odd (some escaped chars but not all) because we are testing
183191
// the internal path escaping mechanism - it should escape certains chars ([, ] and space) but
@@ -197,9 +205,6 @@ public async Task DebuggerAcceptsScriptArgs()
197205
Assert.True(breakpoint.Verified);
198206
});
199207

200-
// TODO: This test used to also pass the args as a single string, but that doesn't seem
201-
// to work any more. Perhaps that's a bug?
202-
var args = new[] { "Foo", "-Param2", "@('Bar','Baz')", "-Force", "Extra1" };
203208
Task _ = ExecutePowerShellCommand(debugWithParamsFile.FilePath, args);
204209

205210
AssertDebuggerStopped(debugWithParamsFile.FilePath, 3);

test/PowerShellEditorServices.Test/Utility/ArgumentEscapingTests.cs

-69
This file was deleted.

0 commit comments

Comments
 (0)