Skip to content

Commit e84d5a8

Browse files
Initial switch to breakpoint APIs
1 parent e813fbd commit e84d5a8

File tree

10 files changed

+613
-332
lines changed

10 files changed

+613
-332
lines changed

src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public static IServiceCollection AddPsesDebugServices(
7676
.AddSingleton(languageServiceProvider.GetService<RemoteFileManagerService>())
7777
.AddSingleton<PsesDebugServer>(psesDebugServer)
7878
.AddSingleton<DebugService>()
79+
.AddSingleton<BreakpointService>()
7980
.AddSingleton<DebugStateService>(new DebugStateService
8081
{
8182
OwnsEditorSession = useTempSession

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 18 additions & 320 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Linq.Expressions;
10+
using System.Management.Automation;
11+
using System.Reflection;
12+
13+
namespace Microsoft.PowerShell.EditorServices.Services.DebugAdapter
14+
15+
{
16+
internal static class BreakpointApiUtils
17+
{
18+
#region Private Static Fields
19+
20+
private static readonly Lazy<Func<Debugger, string, int, int, ScriptBlock, LineBreakpoint>> s_setLineBreakpointLazy;
21+
22+
private static readonly Lazy<Func<Debugger, string, ScriptBlock, string, CommandBreakpoint>> s_setCommandBreakpointLazy;
23+
24+
private static readonly Lazy<Func<Debugger, List<Breakpoint>>> s_getBreakpointsLazy;
25+
26+
private static readonly Lazy<Func<Debugger, Breakpoint, bool>> s_removeBreakpointLazy;
27+
28+
private static readonly Lazy<Func<string, int, int, ScriptBlock, LineBreakpoint>> s_newLineBreakpointLazy;
29+
30+
private static readonly Lazy<Func<string, WildcardPattern, string, ScriptBlock, CommandBreakpoint>> s_newCommandBreakpointLazy;
31+
32+
#endregion
33+
34+
#region Static Constructor
35+
36+
static BreakpointApiUtils()
37+
{
38+
// If this version of PowerShell does not support the new Breakpoint APIs introduced in PowerShell 7.0.0-preview.4,
39+
// do nothing as this class will not get used.
40+
if (typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance) == null)
41+
{
42+
return;
43+
}
44+
45+
s_setLineBreakpointLazy = new Lazy<Func<Debugger, string, int, int, ScriptBlock, LineBreakpoint>>(() =>
46+
{
47+
MethodInfo setLineBreakpointMethod = typeof(Debugger).GetMethod("SetLineBreakpoint", BindingFlags.Public | BindingFlags.Instance);
48+
49+
return (Func<Debugger, string, int, int, ScriptBlock, LineBreakpoint>)Delegate.CreateDelegate(
50+
typeof(Func<Debugger, string, int, int, ScriptBlock, LineBreakpoint>),
51+
firstArgument: null,
52+
setLineBreakpointMethod);
53+
});
54+
55+
s_setCommandBreakpointLazy = new Lazy<Func<Debugger, string, ScriptBlock, string, CommandBreakpoint>>(() =>
56+
{
57+
MethodInfo setCommandBreakpointMethod = typeof(Debugger).GetMethod("SetCommandBreakpoint", BindingFlags.Public | BindingFlags.Instance);
58+
59+
return (Func<Debugger, string, ScriptBlock, string, CommandBreakpoint>)Delegate.CreateDelegate(
60+
typeof(Func<Debugger, string, ScriptBlock, string, CommandBreakpoint>),
61+
firstArgument: null,
62+
setCommandBreakpointMethod);
63+
});
64+
65+
s_getBreakpointsLazy = new Lazy<Func<Debugger, List<Breakpoint>>>(() =>
66+
{
67+
MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("GetBreakpoints", BindingFlags.Public | BindingFlags.Instance);
68+
69+
return (Func<Debugger, List<Breakpoint>>)Delegate.CreateDelegate(
70+
typeof(Func<Debugger, List<Breakpoint>>),
71+
firstArgument: null,
72+
removeBreakpointMethod);
73+
});
74+
75+
s_removeBreakpointLazy = new Lazy<Func<Debugger, Breakpoint, bool>>(() =>
76+
{
77+
MethodInfo removeBreakpointMethod = typeof(Debugger).GetMethod("RemoveBreakpoint", BindingFlags.Public | BindingFlags.Instance);
78+
79+
return (Func<Debugger, Breakpoint, bool>)Delegate.CreateDelegate(
80+
typeof(Func<Debugger, Breakpoint, bool>),
81+
firstArgument: null,
82+
removeBreakpointMethod);
83+
});
84+
}
85+
86+
#endregion
87+
88+
#region Public Static Properties
89+
90+
private static Func<Debugger, string, int, int, ScriptBlock, LineBreakpoint> SetLineBreakpointDelegate => s_setLineBreakpointLazy.Value;
91+
92+
private static Func<Debugger, string, ScriptBlock, string, CommandBreakpoint> SetCommandBreakpointDelegate => s_setCommandBreakpointLazy.Value;
93+
94+
private static Func<Debugger, List<Breakpoint>> GetBreakpointsDelegate => s_getBreakpointsLazy.Value;
95+
96+
private static Func<Debugger, Breakpoint, bool> RemoveBreakpointDelegate => s_removeBreakpointLazy.Value;
97+
98+
private static Func<string, int, int, ScriptBlock, LineBreakpoint> CreateLineBreakpointDelegate => s_newLineBreakpointLazy.Value;
99+
100+
private static Func<string, WildcardPattern, string, ScriptBlock, CommandBreakpoint> CreateCommandBreakpointDelegate => s_newCommandBreakpointLazy.Value;
101+
102+
#endregion
103+
104+
#region Public Static Methods
105+
106+
public static IEnumerable<Breakpoint> SetBreakpoints(Debugger debugger, IEnumerable<BreakpointDetailsBase> breakpoints)
107+
{
108+
var psBreakpoints = new List<Breakpoint>(breakpoints.Count());
109+
110+
foreach (BreakpointDetailsBase breakpoint in breakpoints)
111+
{
112+
Breakpoint psBreakpoint;
113+
switch (breakpoint)
114+
{
115+
case BreakpointDetails lineBreakpoint:
116+
psBreakpoint = SetLineBreakpointDelegate(debugger, lineBreakpoint.Source, lineBreakpoint.LineNumber, lineBreakpoint.ColumnNumber ?? 0, null);
117+
break;
118+
119+
case CommandBreakpointDetails commandBreakpoint:
120+
psBreakpoint = SetCommandBreakpointDelegate(debugger, commandBreakpoint.Name, null, null);
121+
break;
122+
123+
default:
124+
throw new NotImplementedException("Other breakpoints not supported yet");
125+
}
126+
127+
psBreakpoints.Add(psBreakpoint);
128+
}
129+
130+
return psBreakpoints;
131+
}
132+
133+
public static List<Breakpoint> GetBreakpoints(Debugger debugger)
134+
{
135+
return GetBreakpointsDelegate(debugger);
136+
}
137+
138+
public static bool RemoveBreakpoint(Debugger debugger, Breakpoint breakpoint)
139+
{
140+
return RemoveBreakpointDelegate(debugger, breakpoint);
141+
}
142+
143+
#endregion
144+
}
145+
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,8 @@ public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request
145145

146146
// When you set a breakpoint in the right pane of a Git diff window on a PS1 file,
147147
// the Source.Path comes through as Untitled-X. That's why we check for IsUntitledPath.
148-
if (!ScriptFile.IsUntitledPath(request.Source.Path) &&
149-
!_workspaceService.TryGetFile(
150-
request.Source.Path,
151-
out scriptFile))
148+
if (!_workspaceService.TryGetFile(request.Source.Path, out scriptFile) &&
149+
!ScriptFile.IsUntitledPath(request.Source.Path))
152150
{
153151
string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set.";
154152
var srcBreakpoints = request.Breakpoints
@@ -164,7 +162,9 @@ public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request
164162

165163
// Verify source file is a PowerShell script file.
166164
string fileExtension = Path.GetExtension(scriptFile?.FilePath ?? "")?.ToLower();
167-
if (string.IsNullOrEmpty(fileExtension) || ((fileExtension != ".ps1") && (fileExtension != ".psm1")))
165+
bool isUntitledPath = ScriptFile.IsUntitledPath(request.Source.Path);
166+
if ((!isUntitledPath && fileExtension != ".ps1" && fileExtension != ".psm1") ||
167+
(!VersionUtils.IsPS7OrGreater && isUntitledPath))
168168
{
169169
_logger.LogWarning(
170170
$"Attempted to set breakpoints on a non-PowerShell file: {request.Source.Path}");

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6+
using System.Management.Automation;
7+
using System.Management.Automation.Language;
68
using System.Threading;
79
using System.Threading.Tasks;
810
using Microsoft.Extensions.Logging;
911
using Microsoft.PowerShell.EditorServices.Services;
1012
using Microsoft.PowerShell.EditorServices.Services.PowerShellContext;
1113
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
14+
using Microsoft.PowerShell.EditorServices.Utility;
1215
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
1316
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
1417
using OmniSharp.Extensions.JsonRpc;
@@ -97,8 +100,18 @@ private async Task LaunchScriptAsync(string scriptToLaunch)
97100
{
98101
ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch);
99102

100-
await _powerShellContextService
101-
.ExecuteScriptStringAsync(untitledScript.Contents, true, true);
103+
if (VersionUtils.IsPS7OrGreater)
104+
{
105+
ScriptBlockAst ast = Parser.ParseInput(untitledScript.Contents, untitledScript.DocumentUri, out Token[] tokens, out ParseError[] errors);
106+
107+
// This seems to be the simplest way to invoke a script block (which contains breakpoint information) via the PowerShell API.
108+
PSCommand cmd = new PSCommand().AddScript("& $args[0]").AddArgument(ast.GetScriptBlock());
109+
await _powerShellContextService.ExecuteCommandAsync<object>(cmd, sendOutputToHost: true, sendErrorToHost:true);
110+
}
111+
else
112+
{
113+
await _powerShellContextService.ExecuteScriptStringAsync(untitledScript.Contents, writeInputToHost: true, writeOutputToHost: true);
114+
}
102115
}
103116
else
104117
{

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ internal class InitializeHandler : IInitializeHandler
1515
{
1616
private readonly ILogger<InitializeHandler> _logger;
1717
private readonly DebugService _debugService;
18+
private readonly BreakpointService _breakpointService;
1819

1920
public InitializeHandler(
2021
ILoggerFactory factory,
21-
DebugService debugService)
22+
DebugService debugService,
23+
BreakpointService breakpointService)
2224
{
2325
_logger = factory.CreateLogger<InitializeHandler>();
2426
_debugService = debugService;
27+
_breakpointService = breakpointService;
2528
}
2629

2730
public async Task<InitializeResponse> Handle(InitializeRequestArguments request, CancellationToken cancellationToken)
2831
{
2932
// Clear any existing breakpoints before proceeding
30-
await _debugService.ClearAllBreakpointsAsync();
33+
await _breakpointService.RemoveAllBreakpointsAsync();
3134

3235
// Now send the Initialize response to continue setup
3336
return new InitializeResponse

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ internal class AttachHandler : IPsesAttachHandler
212212

213213
private readonly ILogger<AttachHandler> _logger;
214214
private readonly DebugService _debugService;
215+
private readonly BreakpointService _breakpointService;
215216
private readonly PowerShellContextService _powerShellContextService;
216217
private readonly DebugStateService _debugStateService;
217218
private readonly DebugEventHandlerService _debugEventHandlerService;
@@ -223,11 +224,13 @@ public AttachHandler(
223224
DebugService debugService,
224225
PowerShellContextService powerShellContextService,
225226
DebugStateService debugStateService,
227+
BreakpointService breakpointService,
226228
DebugEventHandlerService debugEventHandlerService)
227229
{
228230
_logger = factory.CreateLogger<AttachHandler>();
229231
_jsonRpcServer = jsonRpcServer;
230232
_debugService = debugService;
233+
_breakpointService = breakpointService;
231234
_powerShellContextService = powerShellContextService;
232235
_debugStateService = debugStateService;
233236
_debugEventHandlerService = debugEventHandlerService;
@@ -323,7 +326,7 @@ await _powerShellContextService.ExecuteScriptStringAsync(
323326
}
324327

325328
// Clear any existing breakpoints before proceeding
326-
await _debugService.ClearAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false);
329+
await _breakpointService.RemoveAllBreakpointsAsync().ConfigureAwait(continueOnCapturedContext: false);
327330

328331
// Execute the Debug-Runspace command but don't await it because it
329332
// will block the debug adapter initialization process. The
@@ -357,6 +360,7 @@ await _powerShellContextService.ExecuteScriptStringAsync(
357360
.ExecuteScriptStringAsync(debugRunspaceCmd)
358361
.ContinueWith(OnExecutionCompletedAsync);
359362

363+
_jsonRpcServer.SendNotification(EventNames.Initialized);
360364
return Unit.Value;
361365
}
362366

src/PowerShellEditorServices/Services/PowerShellContext/Session/Capabilities/DscBreakpointCapability.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal class DscBreakpointCapability : IRunspaceCapability
2424
private Dictionary<string, int[]> breakpointsPerFile =
2525
new Dictionary<string, int[]>();
2626

27-
public async Task<List<BreakpointDetails>> SetLineBreakpointsAsync(
27+
public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
2828
PowerShellContextService powerShellContext,
2929
string scriptPath,
3030
BreakpointDetails[] breakpoints)
@@ -68,7 +68,7 @@ await powerShellContext.ExecuteScriptStringAsync(
6868
breakpoint.Verified = true;
6969
}
7070

71-
return breakpoints.ToList();
71+
return breakpoints.ToArray();
7272
}
7373

7474
public bool IsDscResourcePath(string scriptPath)

src/PowerShellEditorServices/Utility/VersionUtils.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ internal static class VersionUtils
4949
/// </summary>
5050
public static bool IsPS7 { get; } = PSVersion.Major == 7;
5151

52+
/// <summary>
53+
/// True if we are running in PowerShell 7, false otherwise.
54+
/// </summary>
55+
public static bool IsPS7OrGreater { get; } = PSVersion.Major >= 7;
56+
5257
/// <summary>
5358
/// True if we are running in on Windows, false otherwise.
5459
/// </summary>

0 commit comments

Comments
 (0)