Skip to content

Commit 3b58a91

Browse files
committed
Delay launch/attach until after config done
Updates the sequence of launching and attaching scripts until after configuration done is received. This aligns the behaviour of starting a debug session with other debug adapters. A side effect of this change is that it is now possible to set breakpoints and do other actions in an attached runspace target during the initialisation. Also adds a new option `NotifyOnAttach` for an attach request which will create the `PSES.Attached` event before calling `Debug-Runspace` allowing attached scripts to wait for an attach event.
1 parent 23ef91b commit 3b58a91

File tree

7 files changed

+568
-204
lines changed

7 files changed

+568
-204
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 190 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,154 @@ namespace Microsoft.PowerShell.EditorServices.Services
1818
{
1919
internal class BreakpointService
2020
{
21+
private const string _getPSBreakpointLegacy = @"
22+
[CmdletBinding()]
23+
param (
24+
[Parameter()]
25+
[string]
26+
$Script,
27+
28+
[Parameter()]
29+
[int]
30+
$RunspaceId = [Runspace]::DefaultRunspace.Id
31+
)
32+
33+
$runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) {
34+
Get-Runspace -Id $RunspaceId
35+
$null = $PSBoundParameters.Remove('RunspaceId')
36+
}
37+
else {
38+
[Runspace]::DefaultRunspace
39+
}
40+
41+
$debugger = $runspace.Debugger
42+
$getBreakpointsMeth = $debugger.GetType().GetMethod(
43+
'GetBreakpoints',
44+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
45+
$null,
46+
[type[]]@(),
47+
$null)
48+
49+
$runspaceIdProp = [System.Management.Automation.PSNoteProperty]::new(
50+
'RunspaceId',
51+
$runspaceId)
52+
53+
@(
54+
if (-not $getBreakpointsMeth) {
55+
if ($RunspaceId -ne [Runspace]::DefaultRunspace.Id) {
56+
throw 'Failed to find GetBreakpoints method on Debugger.'
57+
}
58+
59+
Microsoft.PowerShell.Utility\Get-PSBreakpoint @PSBoundParameters
60+
}
61+
else {
62+
$getBreakpointsMeth.Invoke($debugger, @()) | Where-Object {
63+
if ($Script) {
64+
$_.Script -eq $Script
65+
}
66+
else {
67+
$true
68+
}
69+
}
70+
}
71+
) | ForEach-Object {
72+
$_.PSObject.Properties.Add($runspaceIdProp)
73+
$_
74+
}
75+
";
76+
77+
private const string _removePSBreakpointLegacy = @"
78+
[CmdletBinding(DefaultParameterSetName = 'Breakpoint')]
79+
param (
80+
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Breakpoint')]
81+
[System.Management.Automation.Breakpoint[]]
82+
$Breakpoint,
83+
84+
[Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Id')]
85+
[int[]]
86+
$Id,
87+
88+
[Parameter(ParameterSetName = 'Id')]
89+
[int]
90+
$RunspaceId = [Runspace]::DefaultRunspace.Id
91+
)
92+
93+
begin {
94+
$removeBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod(
95+
'RemoveBreakpoint',
96+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
97+
$null,
98+
[type[]]@([System.Management.Automation.Breakpoint]),
99+
$null)
100+
$getBreakpointMeth = [Runspace]::DefaultRunspace.Debugger.GetType().GetMethod(
101+
'GetBreakpoint',
102+
[System.Reflection.BindingFlags]'NonPublic, Public, Instance',
103+
$null,
104+
[type[]]@([int]),
105+
$null)
106+
107+
$breakpointCollection = [System.Collections.Generic.List[System.Management.Automation.Breakpoint]]::new()
108+
}
109+
110+
process {
111+
if ($PSCmdlet.ParameterSetName -eq 'Id') {
112+
$runspace = Get-Runspace -Id $RunspaceId
113+
$runspaceProp = [System.Management.Automation.PSNoteProperty]::new(
114+
'Runspace',
115+
$Runspace)
116+
117+
$breakpoints = if ($getBreakpointMeth) {
118+
foreach ($breakpointId in $Id) {
119+
$getBreakpointMeth.Invoke($runspace.Debugger, @($breakpointId))
120+
}
121+
}
122+
elseif ($runspace -eq [Runspace]::DefaultRunspace) {
123+
Microsoft.PowerShell.Utility\Get-PSBreakpoint -Id $Id
124+
}
125+
else {
126+
throw 'Failed to find GetBreakpoint method on Debugger.'
127+
}
128+
129+
$breakpoints | ForEach-Object {
130+
$_.PSObject.Properties.Add($runspaceProp)
131+
$breakpointCollection.Add($_)
132+
}
133+
}
134+
else {
135+
foreach ($b in $Breakpoint) {
136+
# RunspaceId may be set by _getPSBreakpointLegacy when
137+
# targeting a breakpoint in a specific runspace.
138+
$runspace = if ($b.PSObject.Properties.Match('RunspaceId')) {
139+
Get-Runspace -Id $b.RunspaceId
140+
}
141+
else {
142+
[Runspace]::DefaultRunspace
143+
}
144+
145+
$b.PSObject.Properties.Add(
146+
[System.Management.Automation.PSNoteProperty]::new('Runspace', $runspace))
147+
$breakpointCollection.Add($b)
148+
}
149+
}
150+
}
151+
152+
end {
153+
foreach ($b in $breakpointCollection) {
154+
if ($removeBreakpointMeth) {
155+
$removeBreakpointMeth.Invoke($b.Runspace.Debugger, @($b))
156+
}
157+
elseif ($b.Runspace -eq [Runspace]::DefaultRunspace) {
158+
# If we don't have the method, we can only remove breakpoints
159+
# from the default runspace using Remove-PSBreakpoint.
160+
$b | Microsoft.PowerShell.Utility\Remove-PSBreakpoint
161+
}
162+
else {
163+
throw 'Failed to find RemoveBreakpoint method on Debugger.'
164+
}
165+
}
166+
}
167+
";
168+
21169
/// <summary>
22170
/// Code used on WinPS 5.1 to set breakpoints without Script path validation.
23171
/// It uses reflection because the APIs were not public until 7.0 but just in
@@ -45,7 +193,11 @@ internal class BreakpointService
45193
46194
[Parameter(ParameterSetName = 'Command', Mandatory = $true)]
47195
[string]
48-
$Command
196+
$Command,
197+
198+
[Parameter()]
199+
[int]
200+
$RunspaceId
49201
)
50202
51203
if ($Script) {
@@ -65,6 +217,9 @@ internal class BreakpointService
65217
$null)
66218
67219
if (-not $cmdCtor) {
220+
if ($PSBoundParameters.ContainsKey('RunspaceId')) {
221+
throw 'Failed to find constructor for CommandBreakpoint.'
222+
}
68223
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
69224
return
70225
}
@@ -82,15 +237,24 @@ internal class BreakpointService
82237
$null)
83238
84239
if (-not $lineCtor) {
240+
if ($PSBoundParameters.ContainsKey('RunspaceId')) {
241+
throw 'Failed to find constructor for LineBreakpoint.'
242+
}
85243
Microsoft.PowerShell.Utility\Set-PSBreakpoint @PSBoundParameters
86244
return
87245
}
88246
89247
$b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
90248
}
91249
92-
[Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
93-
[System.Management.Automation.Breakpoint[]]@($b))
250+
$runspace = if ($PSBoundParameters.ContainsKey('RunspaceId')) {
251+
Get-Runspace -Id $RunspaceId
252+
}
253+
else {
254+
[Runspace]::DefaultRunspace
255+
}
256+
257+
$runspace.Debugger.SetBreakpoints([System.Management.Automation.Breakpoint[]]@($b))
94258
95259
$b
96260
";
@@ -128,10 +292,14 @@ public async Task<IReadOnlyList<Breakpoint>> GetBreakpointsAsync()
128292
}
129293

130294
// Legacy behavior
131-
PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint");
295+
PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true);
296+
if (_debugStateService.RunspaceId is not null)
297+
{
298+
psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
299+
}
132300
return await _executionService
133-
.ExecutePSCommandAsync<Breakpoint>(psCommand, CancellationToken.None)
134-
.ConfigureAwait(false);
301+
.ExecutePSCommandAsync<Breakpoint>(psCommand, CancellationToken.None)
302+
.ConfigureAwait(false);
135303
}
136304

137305
public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(IReadOnlyList<BreakpointDetails> breakpoints)
@@ -211,6 +379,11 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(IReadOnl
211379
{
212380
psCommand.AddParameter("Action", actionScriptBlock);
213381
}
382+
383+
if (_debugStateService.RunspaceId is not null)
384+
{
385+
psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
386+
}
214387
}
215388

216389
// If no PSCommand was created then there are no breakpoints to set.
@@ -335,14 +508,17 @@ public async Task RemoveAllBreakpointsAsync(string scriptPath = null)
335508
}
336509

337510
// Legacy behavior
338-
PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-PSBreakpoint");
339-
511+
PSCommand psCommand = new PSCommand().AddScript(_getPSBreakpointLegacy, useLocalScope: true);
512+
if (_debugStateService.RunspaceId is not null)
513+
{
514+
psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
515+
}
340516
if (!string.IsNullOrEmpty(scriptPath))
341517
{
342518
psCommand.AddParameter("Script", scriptPath);
343519
}
344520

345-
psCommand.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint");
521+
psCommand.AddScript(_removePSBreakpointLegacy, useLocalScope: true);
346522
await _executionService.ExecutePSCommandAsync<object>(psCommand, CancellationToken.None).ConfigureAwait(false);
347523
}
348524
catch (Exception e)
@@ -378,8 +554,12 @@ public async Task RemoveBreakpointsAsync(IEnumerable<Breakpoint> breakpoints)
378554
if (breakpointIds.Any())
379555
{
380556
PSCommand psCommand = new PSCommand()
381-
.AddCommand(@"Microsoft.PowerShell.Utility\Remove-PSBreakpoint")
557+
.AddScript(_removePSBreakpointLegacy, useLocalScope: true)
382558
.AddParameter("Id", breakpoints.Select(b => b.Id).ToArray());
559+
if (_debugStateService.RunspaceId is not null)
560+
{
561+
psCommand.AddParameter("RunspaceId", _debugStateService.RunspaceId.Value);
562+
}
383563
await _executionService.ExecutePSCommandAsync<object>(psCommand, CancellationToken.None).ConfigureAwait(false);
384564
}
385565
}

src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,6 @@ private void OnRunspaceChanged(object sender, RunspaceChangedEventArgs e)
9494
{
9595
switch (e.ChangeAction)
9696
{
97-
case RunspaceChangeAction.Enter:
98-
if (_debugStateService.WaitingForAttach
99-
&& e.NewRunspace.RunspaceOrigin == RunspaceOrigin.DebuggedRunspace)
100-
{
101-
// Sends the InitializedEvent so that the debugger will continue
102-
// sending configuration requests
103-
_debugStateService.WaitingForAttach = false;
104-
_debugStateService.ServerStarted.TrySetResult(true);
105-
}
106-
return;
107-
10897
case RunspaceChangeAction.Exit:
10998
if (_debugContext.IsStopped)
11099
{

0 commit comments

Comments
 (0)