Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/browser/mainThreadDebugService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
7
import { URI as uri, UriComponents } from '../../../base/common/uri.js';
8
import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from '../../contrib/debug/common/debug.js';
9
import {
10
ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext,
11
IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto
12
} from '../common/extHost.protocol.js';
13
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
14
import severity from '../../../base/common/severity.js';
15
import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js';
16
import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js';
17
import { convertToVSCPaths, convertToDAPaths, isSessionAttach } from '../../contrib/debug/common/debugUtils.js';
18
import { ErrorNoTelemetry } from '../../../base/common/errors.js';
19
import { IDebugVisualizerService } from '../../contrib/debug/common/debugVisualizers.js';
20
import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js';
21
import { Event } from '../../../base/common/event.js';
22
import { isDefined } from '../../../base/common/types.js';
23
24
@extHostNamedCustomer(MainContext.MainThreadDebugService)
25
export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory {
26
27
private readonly _proxy: ExtHostDebugServiceShape;
28
private readonly _toDispose = new DisposableStore();
29
private readonly _debugAdapters: Map<number, ExtensionHostDebugAdapter>;
30
private _debugAdaptersHandleCounter = 1;
31
private readonly _debugConfigurationProviders: Map<number, IDebugConfigurationProvider>;
32
private readonly _debugAdapterDescriptorFactories: Map<number, IDebugAdapterDescriptorFactory>;
33
private readonly _extHostKnownSessions: Set<DebugSessionUUID>;
34
private readonly _visualizerHandles = new Map<string, IDisposable>();
35
private readonly _visualizerTreeHandles = new Map<string, IDisposable>();
36
37
constructor(
38
extHostContext: IExtHostContext,
39
@IDebugService private readonly debugService: IDebugService,
40
@IDebugVisualizerService private readonly visualizerService: IDebugVisualizerService,
41
) {
42
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService);
43
44
const sessionListeners = new DisposableMap<IDebugSession, DisposableStore>();
45
this._toDispose.add(sessionListeners);
46
this._toDispose.add(debugService.onDidNewSession(session => {
47
this._proxy.$acceptDebugSessionStarted(this.getSessionDto(session));
48
const store = sessionListeners.get(session);
49
store?.add(session.onDidChangeName(name => {
50
this._proxy.$acceptDebugSessionNameChanged(this.getSessionDto(session), name);
51
}));
52
}));
53
// Need to start listening early to new session events because a custom event can come while a session is initialising
54
this._toDispose.add(debugService.onWillNewSession(session => {
55
let store = sessionListeners.get(session);
56
if (!store) {
57
store = new DisposableStore();
58
sessionListeners.set(session, store);
59
}
60
store.add(session.onDidCustomEvent(event => this._proxy.$acceptDebugSessionCustomEvent(this.getSessionDto(session), event)));
61
}));
62
this._toDispose.add(debugService.onDidEndSession(({ session, restart }) => {
63
this._proxy.$acceptDebugSessionTerminated(this.getSessionDto(session));
64
this._extHostKnownSessions.delete(session.getId());
65
66
// keep the session listeners around since we still will get events after they restart
67
if (!restart) {
68
sessionListeners.deleteAndDispose(session);
69
}
70
71
// any restarted session will create a new DA, so always throw the old one away.
72
for (const [handle, value] of this._debugAdapters) {
73
if (value.session === session) {
74
this._debugAdapters.delete(handle);
75
// break;
76
}
77
}
78
}));
79
this._toDispose.add(debugService.getViewModel().onDidFocusSession(session => {
80
this._proxy.$acceptDebugSessionActiveChanged(this.getSessionDto(session));
81
}));
82
this._toDispose.add(toDisposable(() => {
83
for (const [handle, da] of this._debugAdapters) {
84
da.fireError(handle, new Error('Extension host shut down'));
85
}
86
}));
87
88
this._debugAdapters = new Map();
89
this._debugConfigurationProviders = new Map();
90
this._debugAdapterDescriptorFactories = new Map();
91
this._extHostKnownSessions = new Set();
92
93
const viewModel = this.debugService.getViewModel();
94
this._toDispose.add(Event.any(viewModel.onDidFocusStackFrame, viewModel.onDidFocusThread)(() => {
95
const stackFrame = viewModel.focusedStackFrame;
96
const thread = viewModel.focusedThread;
97
if (stackFrame) {
98
this._proxy.$acceptStackFrameFocus({
99
kind: 'stackFrame',
100
threadId: stackFrame.thread.threadId,
101
frameId: stackFrame.frameId,
102
sessionId: stackFrame.thread.session.getId(),
103
} satisfies IStackFrameFocusDto);
104
} else if (thread) {
105
this._proxy.$acceptStackFrameFocus({
106
kind: 'thread',
107
threadId: thread.threadId,
108
sessionId: thread.session.getId(),
109
} satisfies IThreadFocusDto);
110
} else {
111
this._proxy.$acceptStackFrameFocus(undefined);
112
}
113
}));
114
115
this.sendBreakpointsAndListen();
116
}
117
118
$registerDebugVisualizerTree(treeId: string, canEdit: boolean): void {
119
this._visualizerTreeHandles.set(treeId, this.visualizerService.registerTree(treeId, {
120
disposeItem: id => this._proxy.$disposeVisualizedTree(id),
121
getChildren: e => this._proxy.$getVisualizerTreeItemChildren(treeId, e),
122
getTreeItem: e => this._proxy.$getVisualizerTreeItem(treeId, e),
123
editItem: canEdit ? ((e, v) => this._proxy.$editVisualizerTreeItem(e, v)) : undefined
124
}));
125
}
126
127
$unregisterDebugVisualizerTree(treeId: string): void {
128
this._visualizerTreeHandles.get(treeId)?.dispose();
129
this._visualizerTreeHandles.delete(treeId);
130
}
131
132
$registerDebugVisualizer(extensionId: string, id: string): void {
133
const handle = this.visualizerService.register({
134
extensionId: new ExtensionIdentifier(extensionId),
135
id,
136
disposeDebugVisualizers: ids => this._proxy.$disposeDebugVisualizers(ids),
137
executeDebugVisualizerCommand: id => this._proxy.$executeDebugVisualizerCommand(id),
138
provideDebugVisualizers: (context, token) => this._proxy.$provideDebugVisualizers(extensionId, id, context, token).then(r => r.map(IDebugVisualization.deserialize)),
139
resolveDebugVisualizer: (viz, token) => this._proxy.$resolveDebugVisualizer(viz.id, token),
140
});
141
this._visualizerHandles.set(`${extensionId}/${id}`, handle);
142
}
143
144
$unregisterDebugVisualizer(extensionId: string, id: string): void {
145
const key = `${extensionId}/${id}`;
146
this._visualizerHandles.get(key)?.dispose();
147
this._visualizerHandles.delete(key);
148
}
149
150
private sendBreakpointsAndListen(): void {
151
// set up a handler to send more
152
this._toDispose.add(this.debugService.getModel().onDidChangeBreakpoints(e => {
153
// Ignore session only breakpoint events since they should only reflect in the UI
154
if (e && !e.sessionOnly) {
155
const delta: IBreakpointsDeltaDto = {};
156
if (e.added) {
157
delta.added = this.convertToDto(e.added);
158
}
159
if (e.removed) {
160
delta.removed = e.removed.map(x => x.getId());
161
}
162
if (e.changed) {
163
delta.changed = this.convertToDto(e.changed);
164
}
165
166
if (delta.added || delta.removed || delta.changed) {
167
this._proxy.$acceptBreakpointsDelta(delta);
168
}
169
}
170
}));
171
172
// send all breakpoints
173
const bps = this.debugService.getModel().getBreakpoints();
174
const fbps = this.debugService.getModel().getFunctionBreakpoints();
175
const dbps = this.debugService.getModel().getDataBreakpoints();
176
if (bps.length > 0 || fbps.length > 0) {
177
this._proxy.$acceptBreakpointsDelta({
178
added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps))
179
});
180
}
181
}
182
183
public dispose(): void {
184
this._toDispose.dispose();
185
}
186
187
// interface IDebugAdapterProvider
188
189
createDebugAdapter(session: IDebugSession): IDebugAdapter {
190
const handle = this._debugAdaptersHandleCounter++;
191
const da = new ExtensionHostDebugAdapter(this, handle, this._proxy, session);
192
this._debugAdapters.set(handle, da);
193
return da;
194
}
195
196
substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
197
return Promise.resolve(this._proxy.$substituteVariables(folder ? folder.uri : undefined, config));
198
}
199
200
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise<number | undefined> {
201
return this._proxy.$runInTerminal(args, sessionId);
202
}
203
204
// RPC methods (MainThreadDebugServiceShape)
205
206
public $registerDebugTypes(debugTypes: string[]) {
207
this._toDispose.add(this.debugService.getAdapterManager().registerDebugAdapterFactory(debugTypes, this));
208
}
209
210
public $registerBreakpoints(DTOs: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void> {
211
212
for (const dto of DTOs) {
213
if (dto.type === 'sourceMulti') {
214
const rawbps = dto.lines.map((l): IBreakpointData => ({
215
id: l.id,
216
enabled: l.enabled,
217
lineNumber: l.line + 1,
218
column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784
219
condition: l.condition,
220
hitCondition: l.hitCondition,
221
logMessage: l.logMessage,
222
mode: l.mode,
223
}));
224
this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps);
225
} else if (dto.type === 'function') {
226
this.debugService.addFunctionBreakpoint({
227
name: dto.functionName,
228
mode: dto.mode,
229
condition: dto.condition,
230
hitCondition: dto.hitCondition,
231
enabled: dto.enabled,
232
logMessage: dto.logMessage
233
}, dto.id);
234
} else if (dto.type === 'data') {
235
this.debugService.addDataBreakpoint({
236
description: dto.label,
237
src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId },
238
canPersist: dto.canPersist,
239
accessTypes: dto.accessTypes,
240
accessType: dto.accessType,
241
mode: dto.mode
242
});
243
}
244
}
245
return Promise.resolve();
246
}
247
248
public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise<void> {
249
breakpointIds.forEach(id => this.debugService.removeBreakpoints(id));
250
functionBreakpointIds.forEach(id => this.debugService.removeFunctionBreakpoints(id));
251
dataBreakpointIds.forEach(id => this.debugService.removeDataBreakpoints(id));
252
return Promise.resolve();
253
}
254
255
public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, handle: number): Promise<void> {
256
257
const provider: IDebugConfigurationProvider = {
258
type: debugType,
259
triggerKind: providerTriggerKind
260
};
261
if (hasProvide) {
262
provider.provideDebugConfigurations = (folder, token) => {
263
return this._proxy.$provideDebugConfigurations(handle, folder, token);
264
};
265
}
266
if (hasResolve) {
267
provider.resolveDebugConfiguration = (folder, config, token) => {
268
return this._proxy.$resolveDebugConfiguration(handle, folder, config, token);
269
};
270
}
271
if (hasResolve2) {
272
provider.resolveDebugConfigurationWithSubstitutedVariables = (folder, config, token) => {
273
return this._proxy.$resolveDebugConfigurationWithSubstitutedVariables(handle, folder, config, token);
274
};
275
}
276
this._debugConfigurationProviders.set(handle, provider);
277
this._toDispose.add(this.debugService.getConfigurationManager().registerDebugConfigurationProvider(provider));
278
279
return Promise.resolve(undefined);
280
}
281
282
public $unregisterDebugConfigurationProvider(handle: number): void {
283
const provider = this._debugConfigurationProviders.get(handle);
284
if (provider) {
285
this._debugConfigurationProviders.delete(handle);
286
this.debugService.getConfigurationManager().unregisterDebugConfigurationProvider(provider);
287
}
288
}
289
290
public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise<void> {
291
292
const provider: IDebugAdapterDescriptorFactory = {
293
type: debugType,
294
createDebugAdapterDescriptor: session => {
295
return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session)));
296
}
297
};
298
this._debugAdapterDescriptorFactories.set(handle, provider);
299
this._toDispose.add(this.debugService.getAdapterManager().registerDebugAdapterDescriptorFactory(provider));
300
301
return Promise.resolve(undefined);
302
}
303
304
public $unregisterDebugAdapterDescriptorFactory(handle: number): void {
305
const provider = this._debugAdapterDescriptorFactories.get(handle);
306
if (provider) {
307
this._debugAdapterDescriptorFactories.delete(handle);
308
this.debugService.getAdapterManager().unregisterDebugAdapterDescriptorFactory(provider);
309
}
310
}
311
312
private getSession(sessionId: DebugSessionUUID | undefined): IDebugSession | undefined {
313
if (sessionId) {
314
return this.debugService.getModel().getSession(sessionId, true);
315
}
316
return undefined;
317
}
318
319
public async $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, options: IStartDebuggingOptions): Promise<boolean> {
320
const folderUri = folder ? uri.revive(folder) : undefined;
321
const launch = this.debugService.getConfigurationManager().getLaunch(folderUri);
322
const parentSession = this.getSession(options.parentSessionID);
323
const saveBeforeStart = typeof options.suppressSaveBeforeStart === 'boolean' ? !options.suppressSaveBeforeStart : undefined;
324
const debugOptions: IDebugSessionOptions = {
325
noDebug: options.noDebug,
326
parentSession,
327
lifecycleManagedByParent: options.lifecycleManagedByParent,
328
repl: options.repl,
329
compact: options.compact,
330
compoundRoot: parentSession?.compoundRoot,
331
saveBeforeRestart: saveBeforeStart,
332
testRun: options.testRun,
333
334
suppressDebugStatusbar: options.suppressDebugStatusbar,
335
suppressDebugToolbar: options.suppressDebugToolbar,
336
suppressDebugView: options.suppressDebugView,
337
};
338
try {
339
return this.debugService.startDebugging(launch, nameOrConfig, debugOptions, saveBeforeStart);
340
} catch (err) {
341
throw new ErrorNoTelemetry(err && err.message ? err.message : 'cannot start debugging');
342
}
343
}
344
345
public $setDebugSessionName(sessionId: DebugSessionUUID, name: string): void {
346
const session = this.debugService.getModel().getSession(sessionId);
347
session?.setName(name);
348
}
349
350
public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Promise<any> {
351
const session = this.debugService.getModel().getSession(sessionId, true);
352
if (session) {
353
return session.customRequest(request, args).then(response => {
354
if (response && response.success) {
355
return response.body;
356
} else {
357
return Promise.reject(new ErrorNoTelemetry(response ? response.message : 'custom request failed'));
358
}
359
});
360
}
361
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
362
}
363
364
public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise<DebugProtocol.Breakpoint | undefined> {
365
const session = this.debugService.getModel().getSession(sessionId, true);
366
if (session) {
367
return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId));
368
}
369
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
370
}
371
372
public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise<void> {
373
if (sessionId) {
374
const session = this.debugService.getModel().getSession(sessionId, true);
375
if (session) {
376
return this.debugService.stopSession(session, isSessionAttach(session));
377
}
378
} else { // stop all
379
return this.debugService.stopSession(undefined);
380
}
381
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
382
}
383
384
public $appendDebugConsole(value: string): void {
385
// Use warning as severity to get the orange color for messages coming from the debug extension
386
const session = this.debugService.getViewModel().focusedSession;
387
session?.appendToRepl({ output: value, sev: severity.Warning });
388
}
389
390
public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) {
391
this.getDebugAdapter(handle).acceptMessage(convertToVSCPaths(message, false));
392
}
393
394
public $acceptDAError(handle: number, name: string, message: string, stack: string) {
395
// don't use getDebugAdapter since an error can be expected on a post-close
396
this._debugAdapters.get(handle)?.fireError(handle, new Error(`${name}: ${message}\n${stack}`));
397
}
398
399
public $acceptDAExit(handle: number, code: number, signal: string) {
400
// don't use getDebugAdapter since an error can be expected on a post-close
401
this._debugAdapters.get(handle)?.fireExit(handle, code, signal);
402
}
403
404
private getDebugAdapter(handle: number): ExtensionHostDebugAdapter {
405
const adapter = this._debugAdapters.get(handle);
406
if (!adapter) {
407
throw new Error('Invalid debug adapter');
408
}
409
return adapter;
410
}
411
412
// dto helpers
413
414
public $sessionCached(sessionID: string) {
415
// remember that the EH has cached the session and we do not have to send it again
416
this._extHostKnownSessions.add(sessionID);
417
}
418
419
420
getSessionDto(session: undefined): undefined;
421
getSessionDto(session: IDebugSession): IDebugSessionDto;
422
getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined;
423
getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined {
424
if (session) {
425
const sessionID = <DebugSessionUUID>session.getId();
426
if (this._extHostKnownSessions.has(sessionID)) {
427
return sessionID;
428
} else {
429
// this._sessions.add(sessionID); // #69534: see $sessionCached above
430
return {
431
id: sessionID,
432
type: session.configuration.type,
433
name: session.name,
434
folderUri: session.root ? session.root.uri : undefined,
435
configuration: session.configuration,
436
parent: session.parentSession?.getId(),
437
};
438
}
439
}
440
return undefined;
441
}
442
443
private convertToDto(bps: (ReadonlyArray<IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IInstructionBreakpoint>)): Array<ISourceBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto> {
444
return bps.map(bp => {
445
if ('name' in bp) {
446
const fbp: IFunctionBreakpoint = bp;
447
return {
448
type: 'function',
449
id: fbp.getId(),
450
enabled: fbp.enabled,
451
condition: fbp.condition,
452
hitCondition: fbp.hitCondition,
453
logMessage: fbp.logMessage,
454
functionName: fbp.name
455
} satisfies IFunctionBreakpointDto;
456
} else if ('src' in bp) {
457
const dbp: IDataBreakpoint = bp;
458
return {
459
type: 'data',
460
id: dbp.getId(),
461
dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address,
462
enabled: dbp.enabled,
463
condition: dbp.condition,
464
hitCondition: dbp.hitCondition,
465
logMessage: dbp.logMessage,
466
accessType: dbp.accessType,
467
label: dbp.description,
468
canPersist: dbp.canPersist
469
} satisfies IDataBreakpointDto;
470
} else if ('uri' in bp) {
471
const sbp: IBreakpoint = bp;
472
return {
473
type: 'source',
474
id: sbp.getId(),
475
enabled: sbp.enabled,
476
condition: sbp.condition,
477
hitCondition: sbp.hitCondition,
478
logMessage: sbp.logMessage,
479
uri: sbp.uri,
480
line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0,
481
character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0,
482
} satisfies ISourceBreakpointDto;
483
} else {
484
return undefined;
485
}
486
}).filter(isDefined);
487
}
488
}
489
490
/**
491
* DebugAdapter that communicates via extension protocol with another debug adapter.
492
*/
493
class ExtensionHostDebugAdapter extends AbstractDebugAdapter {
494
495
constructor(private readonly _ds: MainThreadDebugService, private _handle: number, private _proxy: ExtHostDebugServiceShape, readonly session: IDebugSession) {
496
super();
497
}
498
499
fireError(handle: number, err: Error) {
500
this._onError.fire(err);
501
}
502
503
fireExit(handle: number, code: number, signal: string) {
504
this._onExit.fire(code);
505
}
506
507
startSession(): Promise<void> {
508
return Promise.resolve(this._proxy.$startDASession(this._handle, this._ds.getSessionDto(this.session)));
509
}
510
511
sendMessage(message: DebugProtocol.ProtocolMessage): void {
512
this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, true));
513
}
514
515
async stopSession(): Promise<void> {
516
await this.cancelPendingRequests();
517
return Promise.resolve(this._proxy.$stopDASession(this._handle));
518
}
519
}
520
521