Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugSession.ts
5236 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 { getActiveWindow } from '../../../../base/browser/dom.js';
7
import * as aria from '../../../../base/browser/ui/aria/aria.js';
8
import { mainWindow } from '../../../../base/browser/window.js';
9
import { distinct } from '../../../../base/common/arrays.js';
10
import { Queue, RunOnceScheduler, raceTimeout } from '../../../../base/common/async.js';
11
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
12
import { canceled } from '../../../../base/common/errors.js';
13
import { Emitter, Event } from '../../../../base/common/event.js';
14
import { normalizeDriveLetter } from '../../../../base/common/labels.js';
15
import { Lazy } from '../../../../base/common/lazy.js';
16
import { Disposable, DisposableMap, DisposableStore, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js';
17
import { mixin } from '../../../../base/common/objects.js';
18
import * as platform from '../../../../base/common/platform.js';
19
import * as resources from '../../../../base/common/resources.js';
20
import Severity from '../../../../base/common/severity.js';
21
import { isDefined } from '../../../../base/common/types.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { generateUuid } from '../../../../base/common/uuid.js';
24
import { IPosition, Position } from '../../../../editor/common/core/position.js';
25
import { localize } from '../../../../nls.js';
26
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
27
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
28
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
29
import { ILogService } from '../../../../platform/log/common/log.js';
30
import { FocusMode } from '../../../../platform/native/common/native.js';
31
import { INotificationService } from '../../../../platform/notification/common/notification.js';
32
import { IProductService } from '../../../../platform/product/common/productService.js';
33
import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js';
34
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
35
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
36
import { ViewContainerLocation } from '../../../common/views.js';
37
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
38
import { IHostService } from '../../../services/host/browser/host.js';
39
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
40
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
41
import { LiveTestResult } from '../../testing/common/testResult.js';
42
import { ITestResultService } from '../../testing/common/testResultService.js';
43
import { ITestService } from '../../testing/common/testService.js';
44
import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugLocationReferenced, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID, isFrameDeemphasized } from '../common/debug.js';
45
import { DebugCompoundRoot } from '../common/debugCompoundRoot.js';
46
import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from '../common/debugModel.js';
47
import { Source } from '../common/debugSource.js';
48
import { filterExceptionsFromTelemetry } from '../common/debugUtils.js';
49
import { INewReplElementData, ReplModel } from '../common/replModel.js';
50
import { RawDebugSession } from './rawDebugSession.js';
51
52
const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500;
53
54
export class DebugSession implements IDebugSession {
55
parentSession: IDebugSession | undefined;
56
rememberedCapabilities?: DebugProtocol.Capabilities;
57
58
private _subId: string | undefined;
59
raw: RawDebugSession | undefined; // used in tests
60
private initialized = false;
61
private _options: IDebugSessionOptions;
62
63
private sources = new Map<string, Source>();
64
private threads = new Map<number, Thread>();
65
private threadIds: number[] = [];
66
private cancellationMap = new Map<number, CancellationTokenSource[]>();
67
private readonly rawListeners = new DisposableStore();
68
private readonly globalDisposables = new DisposableStore();
69
private fetchThreadsScheduler = new Lazy(() => {
70
const inst = new RunOnceScheduler(() => {
71
this.fetchThreads();
72
}, 100);
73
this.rawListeners.add(inst);
74
return inst;
75
});
76
private passFocusScheduler: RunOnceScheduler;
77
private lastContinuedThreadId: number | undefined;
78
private repl: ReplModel;
79
private stoppedDetails: IRawStoppedDetails[] = [];
80
private readonly statusQueue = this.rawListeners.add(new ThreadStatusScheduler());
81
82
/** Test run this debug session was spawned by */
83
public readonly correlatedTestRun?: LiveTestResult;
84
/** Whether we terminated the correlated run yet. Used so a 2nd terminate request goes through to the underlying session. */
85
private didTerminateTestRun?: boolean;
86
87
private readonly _onDidChangeState = new Emitter<void>();
88
private readonly _onDidEndAdapter = new Emitter<AdapterEndEvent | undefined>();
89
90
private readonly _onDidLoadedSource = new Emitter<LoadedSourceEvent>();
91
private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>();
92
private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>();
93
private readonly _onDidProgressUpdate = new Emitter<DebugProtocol.ProgressUpdateEvent>();
94
private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>();
95
private readonly _onDidInvalidMemory = new Emitter<DebugProtocol.MemoryEvent>();
96
97
private readonly _onDidChangeREPLElements = new Emitter<IReplElement | undefined>();
98
99
private _name: string | undefined;
100
private readonly _onDidChangeName = new Emitter<string>();
101
102
/**
103
* Promise set while enabling dependent breakpoints to block the debugger
104
* from continuing from a stopped state.
105
*/
106
private _waitToResume?: Promise<unknown>;
107
108
constructor(
109
private id: string,
110
private _configuration: { resolved: IConfig; unresolved: IConfig | undefined },
111
public root: IWorkspaceFolder | undefined,
112
private model: DebugModel,
113
options: IDebugSessionOptions | undefined,
114
@IDebugService private readonly debugService: IDebugService,
115
@ITelemetryService private readonly telemetryService: ITelemetryService,
116
@IHostService private readonly hostService: IHostService,
117
@IConfigurationService private readonly configurationService: IConfigurationService,
118
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
119
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
120
@IProductService private readonly productService: IProductService,
121
@INotificationService private readonly notificationService: INotificationService,
122
@ILifecycleService lifecycleService: ILifecycleService,
123
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
124
@IInstantiationService private readonly instantiationService: IInstantiationService,
125
@ICustomEndpointTelemetryService private readonly customEndpointTelemetryService: ICustomEndpointTelemetryService,
126
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
127
@ILogService private readonly logService: ILogService,
128
@ITestService private readonly testService: ITestService,
129
@ITestResultService testResultService: ITestResultService,
130
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
131
) {
132
this._options = options || {};
133
this.parentSession = this._options.parentSession;
134
if (this.hasSeparateRepl()) {
135
this.repl = new ReplModel(this.configurationService);
136
} else {
137
this.repl = (this.parentSession as DebugSession).repl;
138
}
139
140
const toDispose = this.globalDisposables;
141
const replListener = toDispose.add(new MutableDisposable());
142
replListener.value = this.repl.onDidChangeElements((e) => this._onDidChangeREPLElements.fire(e));
143
if (lifecycleService) {
144
toDispose.add(lifecycleService.onWillShutdown(() => {
145
this.shutdown();
146
dispose(toDispose);
147
}));
148
}
149
150
// Cast here, it's not possible to reference a hydrated result in this code path.
151
this.correlatedTestRun = options?.testRun
152
? (testResultService.getResult(options.testRun.runId) as LiveTestResult)
153
: this.parentSession?.correlatedTestRun;
154
155
if (this.correlatedTestRun) {
156
// Listen to the test completing because the user might have taken the cancel action rather than stopping the session.
157
toDispose.add(this.correlatedTestRun.onComplete(() => this.terminate()));
158
}
159
160
const compoundRoot = this._options.compoundRoot;
161
if (compoundRoot) {
162
toDispose.add(compoundRoot.onDidSessionStop(() => this.terminate()));
163
}
164
this.passFocusScheduler = new RunOnceScheduler(() => {
165
// If there is some session or thread that is stopped pass focus to it
166
if (this.debugService.getModel().getSessions().some(s => s.state === State.Stopped) || this.getAllThreads().some(t => t.stopped)) {
167
if (typeof this.lastContinuedThreadId === 'number') {
168
const thread = this.debugService.getViewModel().focusedThread;
169
if (thread && thread.threadId === this.lastContinuedThreadId && !thread.stopped) {
170
const toFocusThreadId = this.getStoppedDetails()?.threadId;
171
const toFocusThread = typeof toFocusThreadId === 'number' ? this.getThread(toFocusThreadId) : undefined;
172
this.debugService.focusStackFrame(undefined, toFocusThread);
173
}
174
} else {
175
const session = this.debugService.getViewModel().focusedSession;
176
if (session && session.getId() === this.getId() && session.state !== State.Stopped) {
177
this.debugService.focusStackFrame(undefined);
178
}
179
}
180
}
181
}, 800);
182
183
const parent = this._options.parentSession;
184
if (parent) {
185
toDispose.add(parent.onDidEndAdapter(() => {
186
// copy the parent repl and get a new detached repl for this child, and
187
// remove its parent, if it's still running
188
if (!this.hasSeparateRepl() && this.raw?.isInShutdown === false) {
189
this.repl = this.repl.clone();
190
replListener.value = this.repl.onDidChangeElements((e) => this._onDidChangeREPLElements.fire(e));
191
this.parentSession = undefined;
192
}
193
}));
194
}
195
}
196
197
getId(): string {
198
return this.id;
199
}
200
201
setSubId(subId: string | undefined) {
202
this._subId = subId;
203
}
204
205
getMemory(memoryReference: string): IMemoryRegion {
206
return new MemoryRegion(memoryReference, this);
207
}
208
209
get subId(): string | undefined {
210
return this._subId;
211
}
212
213
get configuration(): IConfig {
214
return this._configuration.resolved;
215
}
216
217
get unresolvedConfiguration(): IConfig | undefined {
218
return this._configuration.unresolved;
219
}
220
221
get lifecycleManagedByParent(): boolean {
222
return !!this._options.lifecycleManagedByParent;
223
}
224
225
get compact(): boolean {
226
return !!this._options.compact;
227
}
228
229
get saveBeforeRestart(): boolean {
230
return this._options.saveBeforeRestart ?? !this._options?.parentSession;
231
}
232
233
get compoundRoot(): DebugCompoundRoot | undefined {
234
return this._options.compoundRoot;
235
}
236
237
get suppressDebugStatusbar(): boolean {
238
return this._options.suppressDebugStatusbar ?? false;
239
}
240
241
get suppressDebugToolbar(): boolean {
242
return this._options.suppressDebugToolbar ?? false;
243
}
244
245
get suppressDebugView(): boolean {
246
return this._options.suppressDebugView ?? false;
247
}
248
249
250
get autoExpandLazyVariables(): boolean {
251
// This tiny helper avoids converting the entire debug model to use service injection
252
const screenReaderOptimized = this.accessibilityService.isScreenReaderOptimized();
253
const value = this.configurationService.getValue<IDebugConfiguration>('debug').autoExpandLazyVariables;
254
return value === 'auto' && screenReaderOptimized || value === 'on';
255
}
256
257
setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) {
258
this._configuration = configuration;
259
}
260
261
getLabel(): string {
262
const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1;
263
return includeRoot && this.root ? `${this.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.name;
264
}
265
266
setName(name: string): void {
267
this._name = name;
268
this._onDidChangeName.fire(name);
269
}
270
271
get name(): string {
272
return this._name || this.configuration.name;
273
}
274
275
get state(): State {
276
if (!this.initialized) {
277
return State.Initializing;
278
}
279
if (!this.raw) {
280
return State.Inactive;
281
}
282
283
const focusedThread = this.debugService.getViewModel().focusedThread;
284
if (focusedThread && focusedThread.session === this) {
285
return focusedThread.stopped ? State.Stopped : State.Running;
286
}
287
if (this.getAllThreads().some(t => t.stopped)) {
288
return State.Stopped;
289
}
290
291
return State.Running;
292
}
293
294
get capabilities(): DebugProtocol.Capabilities {
295
return this.raw ? this.raw.capabilities : Object.create(null);
296
}
297
298
//---- events
299
get onDidChangeState(): Event<void> {
300
return this._onDidChangeState.event;
301
}
302
303
get onDidEndAdapter(): Event<AdapterEndEvent | undefined> {
304
return this._onDidEndAdapter.event;
305
}
306
307
get onDidChangeReplElements(): Event<IReplElement | undefined> {
308
return this._onDidChangeREPLElements.event;
309
}
310
311
get onDidChangeName(): Event<string> {
312
return this._onDidChangeName.event;
313
}
314
315
//---- DAP events
316
317
get onDidCustomEvent(): Event<DebugProtocol.Event> {
318
return this._onDidCustomEvent.event;
319
}
320
321
get onDidLoadedSource(): Event<LoadedSourceEvent> {
322
return this._onDidLoadedSource.event;
323
}
324
325
get onDidProgressStart(): Event<DebugProtocol.ProgressStartEvent> {
326
return this._onDidProgressStart.event;
327
}
328
329
get onDidProgressUpdate(): Event<DebugProtocol.ProgressUpdateEvent> {
330
return this._onDidProgressUpdate.event;
331
}
332
333
get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> {
334
return this._onDidProgressEnd.event;
335
}
336
337
get onDidInvalidateMemory(): Event<DebugProtocol.MemoryEvent> {
338
return this._onDidInvalidMemory.event;
339
}
340
341
//---- DAP requests
342
343
/**
344
* create and initialize a new debug adapter for this session
345
*/
346
async initialize(dbgr: IDebugger): Promise<void> {
347
348
if (this.raw) {
349
// if there was already a connection make sure to remove old listeners
350
await this.shutdown();
351
}
352
353
try {
354
const debugAdapter = await dbgr.createDebugAdapter(this);
355
this.raw = this.instantiationService.createInstance(RawDebugSession, debugAdapter, dbgr, this.id, this.configuration.name);
356
357
await this.raw.start();
358
this.registerListeners();
359
await this.raw.initialize({
360
clientID: 'vscode',
361
clientName: this.productService.nameLong,
362
adapterID: this.configuration.type,
363
pathFormat: 'path',
364
linesStartAt1: true,
365
columnsStartAt1: true,
366
supportsVariableType: true, // #8858
367
supportsVariablePaging: true, // #9537
368
supportsRunInTerminalRequest: true, // #10574
369
locale: platform.language, // #169114
370
supportsProgressReporting: true, // #92253
371
supportsInvalidatedEvent: true, // #106745
372
supportsMemoryReferences: true, //#129684
373
supportsArgsCanBeInterpretedByShell: true, // #149910
374
supportsMemoryEvent: true, // #133643
375
supportsStartDebuggingRequest: true,
376
supportsANSIStyling: true,
377
});
378
379
this.initialized = true;
380
this._onDidChangeState.fire();
381
this.rememberedCapabilities = this.raw.capabilities;
382
this.debugService.setExceptionBreakpointsForSession(this, (this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []);
383
this.debugService.getModel().registerBreakpointModes(this.configuration.type, this.raw.capabilities.breakpointModes || []);
384
} catch (err) {
385
this.initialized = true;
386
this._onDidChangeState.fire();
387
await this.shutdown();
388
throw err;
389
}
390
}
391
392
/**
393
* launch or attach to the debuggee
394
*/
395
async launchOrAttach(config: IConfig): Promise<void> {
396
if (!this.raw) {
397
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'launch or attach'));
398
}
399
if (this.parentSession && this.parentSession.state === State.Inactive) {
400
throw canceled();
401
}
402
403
// __sessionID only used for EH debugging (but we add it always for now...)
404
config.__sessionId = this.getId();
405
try {
406
await this.raw.launchOrAttach(config);
407
} catch (err) {
408
this.shutdown();
409
throw err;
410
}
411
}
412
413
/**
414
* Terminate any linked test run.
415
*/
416
cancelCorrelatedTestRun() {
417
if (this.correlatedTestRun && !this.correlatedTestRun.completedAt) {
418
this.didTerminateTestRun = true;
419
this.testService.cancelTestRun(this.correlatedTestRun.id);
420
}
421
}
422
423
/**
424
* terminate the current debug adapter session
425
*/
426
async terminate(restart = false): Promise<void> {
427
if (!this.raw) {
428
// Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent
429
this.onDidExitAdapter();
430
}
431
432
this.cancelAllRequests();
433
if (this._options.lifecycleManagedByParent && this.parentSession) {
434
await this.parentSession.terminate(restart);
435
} else if (this.correlatedTestRun && !this.correlatedTestRun.completedAt && !this.didTerminateTestRun) {
436
this.cancelCorrelatedTestRun();
437
} else if (this.raw) {
438
if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') {
439
await this.raw.terminate(restart);
440
} else {
441
await this.raw.disconnect({ restart, terminateDebuggee: true });
442
}
443
}
444
445
if (!restart) {
446
this._options.compoundRoot?.sessionStopped();
447
}
448
}
449
450
/**
451
* end the current debug adapter session
452
*/
453
async disconnect(restart = false, suspend = false): Promise<void> {
454
if (!this.raw) {
455
// Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent
456
this.onDidExitAdapter();
457
}
458
459
this.cancelAllRequests();
460
if (this._options.lifecycleManagedByParent && this.parentSession) {
461
await this.parentSession.disconnect(restart, suspend);
462
} else if (this.raw) {
463
// TODO terminateDebuggee should be undefined by default?
464
await this.raw.disconnect({ restart, terminateDebuggee: false, suspendDebuggee: suspend });
465
}
466
467
if (!restart) {
468
this._options.compoundRoot?.sessionStopped();
469
}
470
}
471
472
/**
473
* restart debug adapter session
474
*/
475
async restart(): Promise<void> {
476
if (!this.raw) {
477
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restart'));
478
}
479
480
this.cancelAllRequests();
481
if (this._options.lifecycleManagedByParent && this.parentSession) {
482
await this.parentSession.restart();
483
} else {
484
await this.raw.restart({ arguments: this.configuration });
485
}
486
}
487
488
async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise<void> {
489
if (!this.raw) {
490
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints'));
491
}
492
493
if (!this.raw.readyForBreakpoints) {
494
return Promise.resolve(undefined);
495
}
496
497
const rawSource = this.getRawSource(modelUri);
498
if (breakpointsToSend.length && !rawSource.adapterData) {
499
rawSource.adapterData = breakpointsToSend[0].adapterData;
500
}
501
// Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959
502
if (rawSource.path) {
503
rawSource.path = normalizeDriveLetter(rawSource.path);
504
}
505
506
const response = await this.raw.setBreakpoints({
507
source: rawSource,
508
lines: breakpointsToSend.map(bp => bp.sessionAgnosticData.lineNumber),
509
breakpoints: breakpointsToSend.map(bp => bp.toDAP()),
510
sourceModified
511
});
512
if (response?.body) {
513
const data = new Map<string, DebugProtocol.Breakpoint>();
514
for (let i = 0; i < breakpointsToSend.length; i++) {
515
data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]);
516
}
517
518
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
519
}
520
}
521
522
async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise<void> {
523
if (!this.raw) {
524
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'function breakpoints'));
525
}
526
527
if (this.raw.readyForBreakpoints) {
528
const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) });
529
if (response?.body) {
530
const data = new Map<string, DebugProtocol.Breakpoint>();
531
for (let i = 0; i < fbpts.length; i++) {
532
data.set(fbpts[i].getId(), response.body.breakpoints[i]);
533
}
534
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
535
}
536
}
537
}
538
539
async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise<void> {
540
if (!this.raw) {
541
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exception breakpoints'));
542
}
543
544
if (this.raw.readyForBreakpoints) {
545
const args: DebugProtocol.SetExceptionBreakpointsArguments = this.capabilities.supportsExceptionFilterOptions ? {
546
filters: [],
547
filterOptions: exbpts.map(exb => {
548
if (exb.condition) {
549
return { filterId: exb.filter, condition: exb.condition };
550
}
551
552
return { filterId: exb.filter };
553
})
554
} : { filters: exbpts.map(exb => exb.filter) };
555
556
const response = await this.raw.setExceptionBreakpoints(args);
557
if (response?.body && response.body.breakpoints) {
558
const data = new Map<string, DebugProtocol.Breakpoint>();
559
for (let i = 0; i < exbpts.length; i++) {
560
data.set(exbpts[i].getId(), response.body.breakpoints[i]);
561
}
562
563
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
564
}
565
}
566
}
567
568
dataBytesBreakpointInfo(address: string, bytes: number): Promise<IDataBreakpointInfoResponse | undefined> {
569
if (this.raw?.capabilities.supportsDataBreakpointBytes === false) {
570
throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes"));
571
}
572
573
return this._dataBreakpointInfo({ name: address, bytes, asAddress: true });
574
}
575
576
dataBreakpointInfo(name: string, variablesReference?: number, frameId?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> {
577
return this._dataBreakpointInfo({ name, variablesReference, frameId });
578
}
579
580
private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> {
581
if (!this.raw) {
582
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info'));
583
}
584
if (!this.raw.readyForBreakpoints) {
585
throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints"));
586
}
587
588
const response = await this.raw.dataBreakpointInfo(args);
589
return response?.body;
590
}
591
592
async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise<void> {
593
if (!this.raw) {
594
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints'));
595
}
596
597
if (this.raw.readyForBreakpoints) {
598
const converted = await Promise.all(dataBreakpoints.map(async bp => {
599
try {
600
const dap = await bp.toDAP(this);
601
return { dap, bp };
602
} catch (e) {
603
return { bp, message: e.message };
604
}
605
}));
606
const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) });
607
if (response?.body) {
608
const data = new Map<string, DebugProtocol.Breakpoint>();
609
let i = 0;
610
for (const dap of converted) {
611
if (!dap.dap) {
612
data.set(dap.bp.getId(), dap.message);
613
} else if (i < response.body.breakpoints.length) {
614
data.set(dap.bp.getId(), response.body.breakpoints[i++]);
615
}
616
}
617
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
618
}
619
}
620
}
621
622
async sendInstructionBreakpoints(instructionBreakpoints: IInstructionBreakpoint[]): Promise<void> {
623
if (!this.raw) {
624
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'instruction breakpoints'));
625
}
626
627
if (this.raw.readyForBreakpoints) {
628
const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) });
629
if (response?.body) {
630
const data = new Map<string, DebugProtocol.Breakpoint>();
631
for (let i = 0; i < instructionBreakpoints.length; i++) {
632
data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]);
633
}
634
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
635
}
636
}
637
}
638
639
async breakpointsLocations(uri: URI, lineNumber: number): Promise<IPosition[]> {
640
if (!this.raw) {
641
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints locations'));
642
}
643
644
const source = this.getRawSource(uri);
645
const response = await this.raw.breakpointLocations({ source, line: lineNumber });
646
if (!response || !response.body || !response.body.breakpoints) {
647
return [];
648
}
649
650
const positions = response.body.breakpoints.map(bp => ({ lineNumber: bp.line, column: bp.column || 1 }));
651
652
return distinct(positions, p => `${p.lineNumber}:${p.column}`);
653
}
654
655
getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined {
656
return this.model.getDebugProtocolBreakpoint(breakpointId, this.getId());
657
}
658
659
customRequest(request: string, args: any): Promise<DebugProtocol.Response | undefined> {
660
if (!this.raw) {
661
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", request));
662
}
663
664
return this.raw.custom(request, args);
665
}
666
667
stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise<DebugProtocol.StackTraceResponse | undefined> {
668
if (!this.raw) {
669
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stackTrace'));
670
}
671
672
const sessionToken = this.getNewCancellationToken(threadId, token);
673
return this.raw.stackTrace({ threadId, startFrame, levels }, sessionToken);
674
}
675
676
async exceptionInfo(threadId: number): Promise<IExceptionInfo | undefined> {
677
if (!this.raw) {
678
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exceptionInfo'));
679
}
680
681
const response = await this.raw.exceptionInfo({ threadId });
682
if (response) {
683
return {
684
id: response.body.exceptionId,
685
description: response.body.description,
686
breakMode: response.body.breakMode,
687
details: response.body.details
688
};
689
}
690
691
return undefined;
692
}
693
694
scopes(frameId: number, threadId: number): Promise<DebugProtocol.ScopesResponse | undefined> {
695
if (!this.raw) {
696
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'scopes'));
697
}
698
699
const token = this.getNewCancellationToken(threadId);
700
return this.raw.scopes({ frameId }, token);
701
}
702
703
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse | undefined> {
704
if (!this.raw) {
705
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'variables'));
706
}
707
708
const token = threadId ? this.getNewCancellationToken(threadId) : undefined;
709
return this.raw.variables({ variablesReference, filter, start, count }, token);
710
}
711
712
evaluate(expression: string, frameId: number, context?: string, location?: { line: number; column: number; source: DebugProtocol.Source }): Promise<DebugProtocol.EvaluateResponse | undefined> {
713
if (!this.raw) {
714
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate'));
715
}
716
717
return this.raw.evaluate({ expression, frameId, context, line: location?.line, column: location?.column, source: location?.source });
718
}
719
720
async restartFrame(frameId: number, threadId: number): Promise<void> {
721
await this.waitForTriggeredBreakpoints();
722
if (!this.raw) {
723
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restartFrame'));
724
}
725
726
await this.raw.restartFrame({ frameId }, threadId);
727
}
728
729
private setLastSteppingGranularity(threadId: number, granularity?: DebugProtocol.SteppingGranularity) {
730
const thread = this.getThread(threadId);
731
if (thread) {
732
thread.lastSteppingGranularity = granularity;
733
}
734
}
735
736
async next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void> {
737
await this.waitForTriggeredBreakpoints();
738
if (!this.raw) {
739
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'next'));
740
}
741
742
this.setLastSteppingGranularity(threadId, granularity);
743
await this.raw.next({ threadId, granularity });
744
}
745
746
async stepIn(threadId: number, targetId?: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void> {
747
await this.waitForTriggeredBreakpoints();
748
if (!this.raw) {
749
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepIn'));
750
}
751
752
this.setLastSteppingGranularity(threadId, granularity);
753
await this.raw.stepIn({ threadId, targetId, granularity });
754
}
755
756
async stepOut(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void> {
757
await this.waitForTriggeredBreakpoints();
758
if (!this.raw) {
759
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepOut'));
760
}
761
762
this.setLastSteppingGranularity(threadId, granularity);
763
await this.raw.stepOut({ threadId, granularity });
764
}
765
766
async stepBack(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise<void> {
767
await this.waitForTriggeredBreakpoints();
768
if (!this.raw) {
769
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepBack'));
770
}
771
772
this.setLastSteppingGranularity(threadId, granularity);
773
await this.raw.stepBack({ threadId, granularity });
774
}
775
776
async continue(threadId: number): Promise<void> {
777
await this.waitForTriggeredBreakpoints();
778
if (!this.raw) {
779
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'continue'));
780
}
781
782
await this.raw.continue({ threadId });
783
}
784
785
async reverseContinue(threadId: number): Promise<void> {
786
await this.waitForTriggeredBreakpoints();
787
if (!this.raw) {
788
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'reverse continue'));
789
}
790
791
await this.raw.reverseContinue({ threadId });
792
}
793
794
async pause(threadId: number): Promise<void> {
795
if (!this.raw) {
796
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'pause'));
797
}
798
799
await this.raw.pause({ threadId });
800
}
801
802
async terminateThreads(threadIds?: number[]): Promise<void> {
803
if (!this.raw) {
804
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'terminateThreads'));
805
}
806
807
await this.raw.terminateThreads({ threadIds });
808
}
809
810
setVariable(variablesReference: number, name: string, value: string): Promise<DebugProtocol.SetVariableResponse | undefined> {
811
if (!this.raw) {
812
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setVariable'));
813
}
814
815
return this.raw.setVariable({ variablesReference, name, value });
816
}
817
818
setExpression(frameId: number, expression: string, value: string): Promise<DebugProtocol.SetExpressionResponse | undefined> {
819
if (!this.raw) {
820
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setExpression'));
821
}
822
823
return this.raw.setExpression({ expression, value, frameId });
824
}
825
826
gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise<DebugProtocol.GotoTargetsResponse | undefined> {
827
if (!this.raw) {
828
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'gotoTargets'));
829
}
830
831
return this.raw.gotoTargets({ source, line, column });
832
}
833
834
goto(threadId: number, targetId: number): Promise<DebugProtocol.GotoResponse | undefined> {
835
if (!this.raw) {
836
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'goto'));
837
}
838
839
return this.raw.goto({ threadId, targetId });
840
}
841
842
loadSource(resource: URI): Promise<DebugProtocol.SourceResponse | undefined> {
843
if (!this.raw) {
844
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'loadSource')));
845
}
846
847
const source = this.getSourceForUri(resource);
848
let rawSource: DebugProtocol.Source;
849
if (source) {
850
rawSource = source.raw;
851
} else {
852
// create a Source
853
const data = Source.getEncodedDebugData(resource);
854
rawSource = { path: data.path, sourceReference: data.sourceReference };
855
}
856
857
return this.raw.source({ sourceReference: rawSource.sourceReference || 0, source: rawSource });
858
}
859
860
async getLoadedSources(): Promise<Source[]> {
861
if (!this.raw) {
862
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'getLoadedSources')));
863
}
864
865
const response = await this.raw.loadedSources({});
866
if (response?.body && response.body.sources) {
867
return response.body.sources.map(src => this.getSource(src));
868
} else {
869
return [];
870
}
871
}
872
873
async completions(frameId: number | undefined, threadId: number, text: string, position: Position, token: CancellationToken): Promise<DebugProtocol.CompletionsResponse | undefined> {
874
if (!this.raw) {
875
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'completions')));
876
}
877
const sessionCancelationToken = this.getNewCancellationToken(threadId, token);
878
879
return this.raw.completions({
880
frameId,
881
text,
882
column: position.column,
883
line: position.lineNumber,
884
}, sessionCancelationToken);
885
}
886
887
async stepInTargets(frameId: number): Promise<{ id: number; label: string }[] | undefined> {
888
if (!this.raw) {
889
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepInTargets')));
890
}
891
892
const response = await this.raw.stepInTargets({ frameId });
893
return response?.body.targets;
894
}
895
896
async cancel(progressId: string): Promise<DebugProtocol.CancelResponse | undefined> {
897
if (!this.raw) {
898
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'cancel')));
899
}
900
901
return this.raw.cancel({ progressId });
902
}
903
904
async disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise<DebugProtocol.DisassembledInstruction[] | undefined> {
905
if (!this.raw) {
906
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble')));
907
}
908
909
const response = await this.raw.disassemble({ memoryReference, offset, instructionOffset, instructionCount, resolveSymbols: true });
910
return response?.body?.instructions;
911
}
912
913
readMemory(memoryReference: string, offset: number, count: number): Promise<DebugProtocol.ReadMemoryResponse | undefined> {
914
if (!this.raw) {
915
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'readMemory')));
916
}
917
918
return this.raw.readMemory({ count, memoryReference, offset });
919
}
920
921
writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise<DebugProtocol.WriteMemoryResponse | undefined> {
922
if (!this.raw) {
923
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble')));
924
}
925
926
return this.raw.writeMemory({ memoryReference, offset, allowPartial, data });
927
}
928
929
async resolveLocationReference(locationReference: number): Promise<IDebugLocationReferenced> {
930
if (!this.raw) {
931
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'locations'));
932
}
933
934
const location = await this.raw.locations({ locationReference });
935
if (!location?.body) {
936
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'locations'));
937
}
938
939
const source = this.getSource(location.body.source);
940
return { column: 1, ...location.body, source };
941
}
942
943
//---- threads
944
945
getThread(threadId: number): Thread | undefined {
946
return this.threads.get(threadId);
947
}
948
949
getAllThreads(): IThread[] {
950
const result: IThread[] = [];
951
this.threadIds.forEach((threadId) => {
952
const thread = this.threads.get(threadId);
953
if (thread) {
954
result.push(thread);
955
}
956
});
957
return result;
958
}
959
960
clearThreads(removeThreads: boolean, reference: number | undefined = undefined): void {
961
if (reference !== undefined && reference !== null) {
962
const thread = this.threads.get(reference);
963
if (thread) {
964
thread.clearCallStack();
965
thread.stoppedDetails = undefined;
966
thread.stopped = false;
967
968
if (removeThreads) {
969
this.threads.delete(reference);
970
}
971
}
972
} else {
973
this.threads.forEach(thread => {
974
thread.clearCallStack();
975
thread.stoppedDetails = undefined;
976
thread.stopped = false;
977
});
978
979
if (removeThreads) {
980
this.threads.clear();
981
this.threadIds = [];
982
ExpressionContainer.allValues.clear();
983
}
984
}
985
}
986
987
getStoppedDetails(): IRawStoppedDetails | undefined {
988
return this.stoppedDetails.length >= 1 ? this.stoppedDetails[0] : undefined;
989
}
990
991
rawUpdate(data: IRawModelUpdate): void {
992
this.threadIds = [];
993
data.threads.forEach(thread => {
994
this.threadIds.push(thread.id);
995
if (!this.threads.has(thread.id)) {
996
// A new thread came in, initialize it.
997
this.threads.set(thread.id, new Thread(this, thread.name, thread.id));
998
} else if (thread.name) {
999
// Just the thread name got updated #18244
1000
const oldThread = this.threads.get(thread.id);
1001
if (oldThread) {
1002
oldThread.name = thread.name;
1003
}
1004
}
1005
});
1006
this.threads.forEach(t => {
1007
// Remove all old threads which are no longer part of the update #75980
1008
if (this.threadIds.indexOf(t.threadId) === -1) {
1009
this.threads.delete(t.threadId);
1010
}
1011
});
1012
1013
const stoppedDetails = data.stoppedDetails;
1014
if (stoppedDetails) {
1015
// Set the availability of the threads' callstacks depending on
1016
// whether the thread is stopped or not
1017
if (stoppedDetails.allThreadsStopped) {
1018
this.threads.forEach(thread => {
1019
thread.stoppedDetails = thread.threadId === stoppedDetails.threadId ? stoppedDetails : { reason: thread.stoppedDetails?.reason };
1020
thread.stopped = true;
1021
thread.clearCallStack();
1022
});
1023
} else {
1024
const thread = typeof stoppedDetails.threadId === 'number' ? this.threads.get(stoppedDetails.threadId) : undefined;
1025
if (thread) {
1026
// One thread is stopped, only update that thread.
1027
thread.stoppedDetails = stoppedDetails;
1028
thread.clearCallStack();
1029
thread.stopped = true;
1030
}
1031
}
1032
}
1033
}
1034
1035
private waitForTriggeredBreakpoints() {
1036
if (!this._waitToResume) {
1037
return;
1038
}
1039
1040
return raceTimeout(
1041
this._waitToResume,
1042
TRIGGERED_BREAKPOINT_MAX_DELAY
1043
);
1044
}
1045
1046
private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise<void> {
1047
if (this.raw) {
1048
const response = await this.raw.threads();
1049
if (response?.body && response.body.threads) {
1050
this.model.rawUpdate({
1051
sessionId: this.getId(),
1052
threads: response.body.threads,
1053
stoppedDetails
1054
});
1055
}
1056
}
1057
}
1058
1059
initializeForTest(raw: RawDebugSession): void {
1060
this.raw = raw;
1061
this.registerListeners();
1062
}
1063
1064
//---- private
1065
1066
private registerListeners(): void {
1067
if (!this.raw) {
1068
return;
1069
}
1070
1071
this.rawListeners.add(this.raw.onDidInitialize(async () => {
1072
aria.status(
1073
this.configuration.noDebug
1074
? localize('debuggingStartedNoDebug', "Started running without debugging.")
1075
: localize('debuggingStarted', "Debugging started.")
1076
);
1077
1078
const sendConfigurationDone = async () => {
1079
if (this.raw && this.raw.capabilities.supportsConfigurationDoneRequest) {
1080
try {
1081
await this.raw.configurationDone();
1082
} catch (e) {
1083
// Disconnect the debug session on configuration done error #10596
1084
this.notificationService.error(e);
1085
this.raw?.disconnect({});
1086
}
1087
}
1088
1089
return undefined;
1090
};
1091
1092
// Send all breakpoints
1093
try {
1094
await this.debugService.sendAllBreakpoints(this);
1095
} finally {
1096
await sendConfigurationDone();
1097
await this.fetchThreads();
1098
}
1099
}));
1100
1101
1102
const statusQueue = this.statusQueue;
1103
this.rawListeners.add(this.raw.onDidStop(event => this.handleStop(event.body)));
1104
1105
this.rawListeners.add(this.raw.onDidThread(event => {
1106
statusQueue.cancel([event.body.threadId]);
1107
if (event.body.reason === 'started') {
1108
if (!this.fetchThreadsScheduler.value.isScheduled()) {
1109
this.fetchThreadsScheduler.value.schedule();
1110
}
1111
} else if (event.body.reason === 'exited') {
1112
this.model.clearThreads(this.getId(), true, event.body.threadId);
1113
const viewModel = this.debugService.getViewModel();
1114
const focusedThread = viewModel.focusedThread;
1115
this.passFocusScheduler.cancel();
1116
if (focusedThread && event.body.threadId === focusedThread.threadId) {
1117
// De-focus the thread in case it was focused
1118
this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false });
1119
}
1120
}
1121
}));
1122
1123
this.rawListeners.add(this.raw.onDidTerminateDebugee(async event => {
1124
aria.status(localize('debuggingStopped', "Debugging stopped."));
1125
if (event.body && event.body.restart) {
1126
await this.debugService.restartSession(this, event.body.restart);
1127
} else if (this.raw) {
1128
await this.raw.disconnect({ terminateDebuggee: false });
1129
}
1130
}));
1131
1132
this.rawListeners.add(this.raw.onDidContinued(async event => {
1133
const allThreads = event.body.allThreadsContinued !== false;
1134
1135
let affectedThreads: number[] | Promise<number[]>;
1136
if (!allThreads) {
1137
affectedThreads = [event.body.threadId];
1138
if (this.threadIds.includes(event.body.threadId)) {
1139
affectedThreads = [event.body.threadId];
1140
} else {
1141
this.fetchThreadsScheduler.rawValue?.cancel();
1142
affectedThreads = this.fetchThreads().then(() => [event.body.threadId]);
1143
}
1144
} else if (this.fetchThreadsScheduler.value.isScheduled()) {
1145
this.fetchThreadsScheduler.value.cancel();
1146
affectedThreads = this.fetchThreads().then(() => this.threadIds);
1147
} else {
1148
affectedThreads = this.threadIds;
1149
}
1150
1151
statusQueue.cancel(allThreads ? undefined : [event.body.threadId]);
1152
await statusQueue.run(affectedThreads, threadId => {
1153
this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId);
1154
const tokens = this.cancellationMap.get(threadId);
1155
this.cancellationMap.delete(threadId);
1156
tokens?.forEach(t => t.dispose(true));
1157
this.model.clearThreads(this.getId(), false, threadId);
1158
return Promise.resolve();
1159
});
1160
1161
// We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321
1162
this.lastContinuedThreadId = allThreads ? undefined : event.body.threadId;
1163
this.passFocusScheduler.schedule();
1164
this._onDidChangeState.fire();
1165
}));
1166
1167
const outputQueue = new Queue<void>();
1168
this.rawListeners.add(this.raw.onDidOutput(async event => {
1169
const outputSeverity = event.body.category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info;
1170
1171
// When a variables event is received, execute immediately to obtain the variables value #126967
1172
if (event.body.variablesReference) {
1173
const source = event.body.source && event.body.line ? {
1174
lineNumber: event.body.line,
1175
column: event.body.column ? event.body.column : 1,
1176
source: this.getSource(event.body.source)
1177
} : undefined;
1178
const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid());
1179
const children = container.getChildren();
1180
// we should put appendToRepl into queue to make sure the logs to be displayed in correct order
1181
// see https://github.com/microsoft/vscode/issues/126967#issuecomment-874954269
1182
outputQueue.queue(async () => {
1183
const resolved = await children;
1184
// For single logged variables, try to use the output if we can so
1185
// present a better (i.e. ANSI-aware) representation of the output
1186
if (resolved.length === 1) {
1187
this.appendToRepl({ output: event.body.output, expression: resolved[0], sev: outputSeverity, source }, event.body.category === 'important');
1188
return;
1189
}
1190
1191
resolved.forEach((child) => {
1192
// Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names)
1193
// eslint-disable-next-line local/code-no-any-casts
1194
(<any>child).name = null;
1195
this.appendToRepl({ output: '', expression: child, sev: outputSeverity, source }, event.body.category === 'important');
1196
});
1197
});
1198
return;
1199
}
1200
outputQueue.queue(async () => {
1201
if (!event.body || !this.raw) {
1202
return;
1203
}
1204
1205
if (event.body.category === 'telemetry') {
1206
// only log telemetry events from debug adapter if the debug extension provided the telemetry key
1207
// and the user opted in telemetry
1208
const telemetryEndpoint = this.raw.dbgr.getCustomTelemetryEndpoint();
1209
if (telemetryEndpoint && this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) {
1210
// __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly.
1211
let data = event.body.data;
1212
if (!telemetryEndpoint.sendErrorTelemetry && event.body.data) {
1213
data = filterExceptionsFromTelemetry(event.body.data);
1214
}
1215
1216
this.customEndpointTelemetryService.publicLog(telemetryEndpoint, event.body.output, data);
1217
}
1218
1219
return;
1220
}
1221
1222
// Make sure to append output in the correct order by properly waiting on preivous promises #33822
1223
const source = event.body.source && event.body.line ? {
1224
lineNumber: event.body.line,
1225
column: event.body.column ? event.body.column : 1,
1226
source: this.getSource(event.body.source)
1227
} : undefined;
1228
1229
if (event.body.group === 'start' || event.body.group === 'startCollapsed') {
1230
const expanded = event.body.group === 'start';
1231
this.repl.startGroup(this, event.body.output || '', expanded, source);
1232
return;
1233
}
1234
if (event.body.group === 'end') {
1235
this.repl.endGroup();
1236
if (!event.body.output) {
1237
// Only return if the end event does not have additional output in it
1238
return;
1239
}
1240
}
1241
1242
if (typeof event.body.output === 'string') {
1243
this.appendToRepl({ output: event.body.output, sev: outputSeverity, source }, event.body.category === 'important');
1244
}
1245
});
1246
}));
1247
1248
this.rawListeners.add(this.raw.onDidBreakpoint(event => {
1249
const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined;
1250
const breakpoint = this.model.getBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id);
1251
const functionBreakpoint = this.model.getFunctionBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id);
1252
const dataBreakpoint = this.model.getDataBreakpoints().find(dbp => dbp.getIdFromAdapter(this.getId()) === id);
1253
const exceptionBreakpoint = this.model.getExceptionBreakpoints().find(excbp => excbp.getIdFromAdapter(this.getId()) === id);
1254
1255
if (event.body.reason === 'new' && event.body.breakpoint.source && event.body.breakpoint.line) {
1256
const source = this.getSource(event.body.breakpoint.source);
1257
const bps = this.model.addBreakpoints(source.uri, [{
1258
column: event.body.breakpoint.column,
1259
enabled: true,
1260
lineNumber: event.body.breakpoint.line,
1261
}], false);
1262
if (bps.length === 1) {
1263
const data = new Map<string, DebugProtocol.Breakpoint>([[bps[0].getId(), event.body.breakpoint]]);
1264
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
1265
}
1266
}
1267
1268
if (event.body.reason === 'removed') {
1269
if (breakpoint) {
1270
this.model.removeBreakpoints([breakpoint]);
1271
}
1272
if (functionBreakpoint) {
1273
this.model.removeFunctionBreakpoints(functionBreakpoint.getId());
1274
}
1275
if (dataBreakpoint) {
1276
this.model.removeDataBreakpoints(dataBreakpoint.getId());
1277
}
1278
}
1279
1280
if (event.body.reason === 'changed') {
1281
if (breakpoint) {
1282
if (!breakpoint.column) {
1283
event.body.breakpoint.column = undefined;
1284
}
1285
const data = new Map<string, DebugProtocol.Breakpoint>([[breakpoint.getId(), event.body.breakpoint]]);
1286
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
1287
}
1288
if (functionBreakpoint) {
1289
const data = new Map<string, DebugProtocol.Breakpoint>([[functionBreakpoint.getId(), event.body.breakpoint]]);
1290
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
1291
}
1292
if (dataBreakpoint) {
1293
const data = new Map<string, DebugProtocol.Breakpoint>([[dataBreakpoint.getId(), event.body.breakpoint]]);
1294
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
1295
}
1296
if (exceptionBreakpoint) {
1297
const data = new Map<string, DebugProtocol.Breakpoint>([[exceptionBreakpoint.getId(), event.body.breakpoint]]);
1298
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
1299
}
1300
}
1301
}));
1302
1303
this.rawListeners.add(this.raw.onDidLoadedSource(event => {
1304
this._onDidLoadedSource.fire({
1305
reason: event.body.reason,
1306
source: this.getSource(event.body.source)
1307
});
1308
}));
1309
1310
this.rawListeners.add(this.raw.onDidCustomEvent(event => {
1311
this._onDidCustomEvent.fire(event);
1312
}));
1313
1314
this.rawListeners.add(this.raw.onDidProgressStart(event => {
1315
this._onDidProgressStart.fire(event);
1316
}));
1317
this.rawListeners.add(this.raw.onDidProgressUpdate(event => {
1318
this._onDidProgressUpdate.fire(event);
1319
}));
1320
this.rawListeners.add(this.raw.onDidProgressEnd(event => {
1321
this._onDidProgressEnd.fire(event);
1322
}));
1323
this.rawListeners.add(this.raw.onDidInvalidateMemory(event => {
1324
this._onDidInvalidMemory.fire(event);
1325
}));
1326
this.rawListeners.add(this.raw.onDidInvalidated(async event => {
1327
const areas = event.body.areas || ['all'];
1328
// If invalidated event only requires to update variables or watch, do that, otherwise refetch threads https://github.com/microsoft/vscode/issues/106745
1329
if (areas.includes('threads') || areas.includes('stacks') || areas.includes('all')) {
1330
this.cancelAllRequests();
1331
this.model.clearThreads(this.getId(), true);
1332
1333
const details = this.stoppedDetails.slice();
1334
this.stoppedDetails.length = 0;
1335
if (details.length) {
1336
await Promise.all(details.map(d => this.handleStop(d)));
1337
} else if (!this.fetchThreadsScheduler.value.isScheduled()) {
1338
// threads are fetched as a side-effect of processing the stopped
1339
// event(s), but if there are none, schedule a thread update manually (#282777)
1340
this.fetchThreadsScheduler.value.schedule();
1341
}
1342
}
1343
1344
const viewModel = this.debugService.getViewModel();
1345
if (viewModel.focusedSession === this) {
1346
viewModel.updateViews();
1347
}
1348
}));
1349
1350
this.rawListeners.add(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event)));
1351
}
1352
1353
private async handleStop(event: IRawStoppedDetails) {
1354
this.passFocusScheduler.cancel();
1355
this.stoppedDetails.push(event);
1356
1357
// do this very eagerly if we have hitBreakpointIds, since it may take a
1358
// moment for breakpoints to set and we want to do our best to not miss
1359
// anything
1360
if (event.hitBreakpointIds) {
1361
this._waitToResume = this.enableDependentBreakpoints(event.hitBreakpointIds);
1362
}
1363
1364
this.statusQueue.run(
1365
this.fetchThreads(event).then(() => event.threadId === undefined ? this.threadIds : [event.threadId]),
1366
async (threadId, token) => {
1367
const hasLotsOfThreads = event.threadId === undefined && this.threadIds.length > 10;
1368
1369
// If the focus for the current session is on a non-existent thread, clear the focus.
1370
const focusedThread = this.debugService.getViewModel().focusedThread;
1371
const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId);
1372
if (focusedThreadDoesNotExist) {
1373
this.debugService.focusStackFrame(undefined, undefined);
1374
}
1375
1376
const thread = typeof threadId === 'number' ? this.getThread(threadId) : undefined;
1377
if (thread) {
1378
// Call fetch call stack twice, the first only return the top stack frame.
1379
// Second retrieves the rest of the call stack. For performance reasons #25605
1380
// Second call is only done if there's few threads that stopped in this event.
1381
const promises = this.model.refreshTopOfCallstack(<Thread>thread, /* fetchFullStack= */!hasLotsOfThreads);
1382
const focus = async () => {
1383
if (focusedThreadDoesNotExist || (!event.preserveFocusHint && thread.getCallStack().length)) {
1384
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
1385
if (!focusedStackFrame || focusedStackFrame.thread.session === this) {
1386
// Only take focus if nothing is focused, or if the focus is already on the current session
1387
const preserveFocus = !this.configurationService.getValue<IDebugConfiguration>('debug').focusEditorOnBreak;
1388
await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus });
1389
}
1390
1391
if (thread.stoppedDetails && !token.isCancellationRequested) {
1392
if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue<IDebugConfiguration>('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) {
1393
await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
1394
}
1395
1396
if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) {
1397
const activeWindow = getActiveWindow();
1398
if (!activeWindow.document.hasFocus()) {
1399
await this.hostService.focus(mainWindow, { mode: FocusMode.Force /* Application may not be active */ });
1400
}
1401
}
1402
}
1403
}
1404
};
1405
1406
await promises.topCallStack;
1407
1408
if (!event.hitBreakpointIds) { // if hitBreakpointIds are present, this is handled earlier on
1409
this._waitToResume = this.enableDependentBreakpoints(thread);
1410
}
1411
1412
if (token.isCancellationRequested) {
1413
return;
1414
}
1415
1416
focus();
1417
1418
await promises.wholeCallStack;
1419
if (token.isCancellationRequested) {
1420
return;
1421
}
1422
1423
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
1424
if (!focusedStackFrame || isFrameDeemphasized(focusedStackFrame)) {
1425
// The top stack frame can be deemphesized so try to focus again #68616
1426
focus();
1427
}
1428
}
1429
this._onDidChangeState.fire();
1430
},
1431
);
1432
}
1433
1434
private async enableDependentBreakpoints(hitBreakpointIdsOrThread: Thread | number[]) {
1435
let breakpoints: IBreakpoint[];
1436
if (Array.isArray(hitBreakpointIdsOrThread)) {
1437
breakpoints = this.model.getBreakpoints().filter(bp => hitBreakpointIdsOrThread.includes(bp.getIdFromAdapter(this.id)!));
1438
} else {
1439
const frame = hitBreakpointIdsOrThread.getTopStackFrame();
1440
if (frame === undefined) {
1441
return;
1442
}
1443
1444
if (hitBreakpointIdsOrThread.stoppedDetails && hitBreakpointIdsOrThread.stoppedDetails.reason !== 'breakpoint') {
1445
return;
1446
}
1447
1448
breakpoints = this.getBreakpointsAtPosition(frame.source.uri, frame.range.startLineNumber, frame.range.endLineNumber, frame.range.startColumn, frame.range.endColumn);
1449
}
1450
1451
// find the current breakpoints
1452
1453
// check if the current breakpoints are dependencies, and if so collect and send the dependents to DA
1454
const urisToResend = new Set<string>();
1455
this.model.getBreakpoints({ triggeredOnly: true, enabledOnly: true }).forEach(bp => {
1456
breakpoints.forEach(cbp => {
1457
if (bp.enabled && bp.triggeredBy === cbp.getId()) {
1458
bp.setSessionDidTrigger(this.getId());
1459
urisToResend.add(bp.uri.toString());
1460
}
1461
});
1462
});
1463
1464
const results: Promise<any>[] = [];
1465
urisToResend.forEach((uri) => results.push(this.debugService.sendBreakpoints(URI.parse(uri), undefined, this)));
1466
return Promise.all(results);
1467
}
1468
1469
private getBreakpointsAtPosition(uri: URI, startLineNumber: number, endLineNumber: number, startColumn: number, endColumn: number): IBreakpoint[] {
1470
return this.model.getBreakpoints({ uri: uri }).filter(bp => {
1471
if (bp.lineNumber < startLineNumber || bp.lineNumber > endLineNumber) {
1472
return false;
1473
}
1474
1475
if (bp.column && (bp.column < startColumn || bp.column > endColumn)) {
1476
return false;
1477
}
1478
return true;
1479
});
1480
}
1481
1482
private onDidExitAdapter(event?: AdapterEndEvent): void {
1483
this.initialized = true;
1484
this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined);
1485
this.shutdown();
1486
this._onDidEndAdapter.fire(event);
1487
}
1488
1489
// Disconnects and clears state. Session can be initialized again for a new connection.
1490
private shutdown(): void {
1491
this.rawListeners.clear();
1492
if (this.raw) {
1493
// Send out disconnect and immediatly dispose (do not wait for response) #127418
1494
this.raw.disconnect({});
1495
this.raw.dispose();
1496
this.raw = undefined;
1497
}
1498
this.passFocusScheduler.cancel();
1499
this.passFocusScheduler.dispose();
1500
this.model.clearThreads(this.getId(), true);
1501
this.sources.clear();
1502
this.threads.clear();
1503
this.threadIds = [];
1504
this.stoppedDetails = [];
1505
this._onDidChangeState.fire();
1506
}
1507
1508
public dispose() {
1509
this.cancelAllRequests();
1510
this.rawListeners.dispose();
1511
this.globalDisposables.dispose();
1512
this._onDidChangeState.dispose();
1513
this._onDidEndAdapter.dispose();
1514
this._onDidLoadedSource.dispose();
1515
this._onDidCustomEvent.dispose();
1516
this._onDidProgressStart.dispose();
1517
this._onDidProgressUpdate.dispose();
1518
this._onDidProgressEnd.dispose();
1519
this._onDidInvalidMemory.dispose();
1520
this._onDidChangeREPLElements.dispose();
1521
this._onDidChangeName.dispose();
1522
this._waitToResume = undefined;
1523
}
1524
1525
//---- sources
1526
1527
getSourceForUri(uri: URI): Source | undefined {
1528
return this.sources.get(this.uriIdentityService.asCanonicalUri(uri).toString());
1529
}
1530
1531
getSource(raw?: DebugProtocol.Source): Source {
1532
let source = new Source(raw, this.getId(), this.uriIdentityService, this.logService);
1533
const uriKey = source.uri.toString();
1534
const found = this.sources.get(uriKey);
1535
if (found) {
1536
source = found;
1537
// merge attributes of new into existing
1538
source.raw = mixin(source.raw, raw);
1539
if (source.raw && raw) {
1540
// Always take the latest presentation hint from adapter #42139
1541
source.raw.presentationHint = raw.presentationHint;
1542
}
1543
} else {
1544
this.sources.set(uriKey, source);
1545
}
1546
1547
return source;
1548
}
1549
1550
private getRawSource(uri: URI): DebugProtocol.Source {
1551
const source = this.getSourceForUri(uri);
1552
if (source) {
1553
return source.raw;
1554
} else {
1555
const data = Source.getEncodedDebugData(uri);
1556
return { name: data.name, path: data.path, sourceReference: data.sourceReference };
1557
}
1558
}
1559
1560
private getNewCancellationToken(threadId: number, token?: CancellationToken): CancellationToken {
1561
const tokenSource = new CancellationTokenSource(token);
1562
const tokens = this.cancellationMap.get(threadId) || [];
1563
tokens.push(tokenSource);
1564
this.cancellationMap.set(threadId, tokens);
1565
1566
return tokenSource.token;
1567
}
1568
1569
private cancelAllRequests(): void {
1570
this.cancellationMap.forEach(tokens => tokens.forEach(t => t.dispose(true)));
1571
this.cancellationMap.clear();
1572
}
1573
1574
// REPL
1575
1576
getReplElements(): IReplElement[] {
1577
return this.repl.getReplElements();
1578
}
1579
1580
hasSeparateRepl(): boolean {
1581
return !this.parentSession || this._options.repl !== 'mergeWithParent';
1582
}
1583
1584
removeReplExpressions(): void {
1585
this.repl.removeReplExpressions();
1586
}
1587
1588
async addReplExpression(stackFrame: IStackFrame | undefined, expression: string): Promise<void> {
1589
await this.repl.addReplExpression(this, stackFrame, expression);
1590
// Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some.
1591
this.debugService.getViewModel().updateViews();
1592
}
1593
1594
appendToRepl(data: INewReplElementData, isImportant?: boolean): void {
1595
this.repl.appendToRepl(this, data);
1596
if (isImportant) {
1597
this.notificationService.notify({ message: data.output.toString(), severity: data.sev, source: this.name });
1598
}
1599
}
1600
}
1601
1602
/**
1603
* Keeps track of events for threads, and cancels any previous operations for
1604
* a thread when the thread goes into a new state. Currently, the operations a thread has are:
1605
*
1606
* - started
1607
* - stopped
1608
* - continue
1609
* - exited
1610
*
1611
* In each case, the new state preempts the old state, so we don't need to
1612
* queue work, just cancel old work. It's up to the caller to make sure that
1613
* no UI effects happen at the point when the `token` is cancelled.
1614
*/
1615
export class ThreadStatusScheduler extends Disposable {
1616
/**
1617
* An array of set of thread IDs. When a 'stopped' event is encountered, the
1618
* editor refreshes its thread IDs. In the meantime, the thread may change
1619
* state it again. So the editor puts a Set into this array when it starts
1620
* the refresh, and checks it after the refresh is finished, to see if
1621
* any of the threads it looked up should now be invalidated.
1622
*/
1623
private pendingCancellations: Set<number | undefined>[] = [];
1624
1625
/**
1626
* Cancellation tokens for currently-running operations on threads.
1627
*/
1628
private readonly threadOps = this._register(new DisposableMap<number, CancellationTokenSource>());
1629
1630
/**
1631
* Runs the operation.
1632
* If thread is undefined it affects all threads.
1633
*/
1634
public async run(threadIdsP: Promise<number[]> | number[], operation: (threadId: number, ct: CancellationToken) => Promise<unknown>) {
1635
const cancelledWhileLookingUpThreads = new Set<number | undefined>();
1636
this.pendingCancellations.push(cancelledWhileLookingUpThreads);
1637
const threadIds = await threadIdsP;
1638
1639
// Now that we got our threads,
1640
// 1. Remove our pending set, and
1641
// 2. Cancel any slower callers who might also have found this thread
1642
for (let i = 0; i < this.pendingCancellations.length; i++) {
1643
const s = this.pendingCancellations[i];
1644
if (s === cancelledWhileLookingUpThreads) {
1645
this.pendingCancellations.splice(i, 1);
1646
break;
1647
} else {
1648
for (const threadId of threadIds) {
1649
s.add(threadId);
1650
}
1651
}
1652
}
1653
1654
if (cancelledWhileLookingUpThreads.has(undefined)) {
1655
return;
1656
}
1657
1658
await Promise.all(threadIds.map(threadId => {
1659
if (cancelledWhileLookingUpThreads.has(threadId)) {
1660
return;
1661
}
1662
this.threadOps.get(threadId)?.cancel();
1663
const cts = new CancellationTokenSource();
1664
this.threadOps.set(threadId, cts);
1665
return operation(threadId, cts.token);
1666
}));
1667
}
1668
1669
/**
1670
* Cancels all ongoing state operations on the given threads.
1671
* If threads is undefined it cancel all threads.
1672
*/
1673
public cancel(threadIds?: readonly number[]) {
1674
if (!threadIds) {
1675
for (const [_, op] of this.threadOps) {
1676
op.cancel();
1677
}
1678
this.threadOps.clearAndDisposeAll();
1679
for (const s of this.pendingCancellations) {
1680
s.add(undefined);
1681
}
1682
} else {
1683
for (const threadId of threadIds) {
1684
this.threadOps.get(threadId)?.cancel();
1685
this.threadOps.deleteAndDispose(threadId);
1686
for (const s of this.pendingCancellations) {
1687
s.add(threadId);
1688
}
1689
}
1690
}
1691
}
1692
}
1693
1694