Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugService.ts
5242 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 * as aria from '../../../../base/browser/ui/aria/aria.js';
7
import { IAction, toAction } from '../../../../base/common/actions.js';
8
import { distinct } from '../../../../base/common/arrays.js';
9
import { RunOnceScheduler, raceTimeout } from '../../../../base/common/async.js';
10
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
11
import { isErrorWithActions } from '../../../../base/common/errorMessage.js';
12
import * as errors from '../../../../base/common/errors.js';
13
import { Emitter, Event } from '../../../../base/common/event.js';
14
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
15
import { deepClone, equals } from '../../../../base/common/objects.js';
16
17
import severity from '../../../../base/common/severity.js';
18
import { URI, URI as uri } from '../../../../base/common/uri.js';
19
import { generateUuid } from '../../../../base/common/uuid.js';
20
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
21
import { ITextModel } from '../../../../editor/common/model.js';
22
import * as nls from '../../../../nls.js';
23
import { ICommandService } from '../../../../platform/commands/common/commands.js';
24
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
25
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
26
import { IExtensionHostDebugService } from '../../../../platform/debug/common/extensionHostDebug.js';
27
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
28
import { FileChangeType, FileChangesEvent, IFileService } from '../../../../platform/files/common/files.js';
29
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
30
import { INotificationService } from '../../../../platform/notification/common/notification.js';
31
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
32
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
33
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
34
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
35
import { EditorsOrder } from '../../../common/editor.js';
36
import { EditorInput } from '../../../common/editor/editorInput.js';
37
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
38
import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js';
39
import { IEditorService } from '../../../services/editor/common/editorService.js';
40
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
41
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
42
import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
43
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
44
import { IViewsService } from '../../../services/views/common/viewsService.js';
45
import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from '../../files/common/files.js';
46
import { ITestService } from '../../testing/common/testService.js';
47
import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, IGuessedDebugger, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from '../common/debug.js';
48
import { DebugCompoundRoot } from '../common/debugCompoundRoot.js';
49
import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from '../common/debugModel.js';
50
import { Source } from '../common/debugSource.js';
51
import { DebugStorage, IChosenEnvironment } from '../common/debugStorage.js';
52
import { DebugTelemetry } from '../common/debugTelemetry.js';
53
import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from '../common/debugUtils.js';
54
import { ViewModel } from '../common/debugViewModel.js';
55
import { DisassemblyViewInput } from '../common/disassemblyViewInput.js';
56
import { AdapterManager } from './debugAdapterManager.js';
57
import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from './debugCommands.js';
58
import { ConfigurationManager } from './debugConfigurationManager.js';
59
import { DebugMemoryFileSystemProvider } from './debugMemory.js';
60
import { DebugSession } from './debugSession.js';
61
import { DebugTaskRunner, TaskRunResult } from './debugTaskRunner.js';
62
63
export class DebugService implements IDebugService {
64
declare readonly _serviceBrand: undefined;
65
66
private readonly _onDidChangeState: Emitter<State>;
67
private readonly _onDidNewSession: Emitter<IDebugSession>;
68
private readonly _onWillNewSession: Emitter<IDebugSession>;
69
private readonly _onDidEndSession: Emitter<{ session: IDebugSession; restart: boolean }>;
70
private readonly restartingSessions = new Set<IDebugSession>();
71
private debugStorage: DebugStorage;
72
private model: DebugModel;
73
private viewModel: ViewModel;
74
private telemetry: DebugTelemetry;
75
private taskRunner: DebugTaskRunner;
76
private configurationManager: ConfigurationManager;
77
private adapterManager: AdapterManager;
78
private readonly disposables = new DisposableStore();
79
private debugType!: IContextKey<string>;
80
private debugState!: IContextKey<string>;
81
private inDebugMode!: IContextKey<boolean>;
82
private debugUx!: IContextKey<string>;
83
private hasDebugged!: IContextKey<boolean>;
84
private breakpointsExist!: IContextKey<boolean>;
85
private disassemblyViewFocus!: IContextKey<boolean>;
86
private breakpointsToSendOnResourceSaved: Set<URI>;
87
private initializing = false;
88
private _initializingOptions: IDebugSessionOptions | undefined;
89
private previousState: State | undefined;
90
private sessionCancellationTokens = new Map<string, CancellationTokenSource>();
91
private activity: IDisposable | undefined;
92
private chosenEnvironments: Record<string, IChosenEnvironment>;
93
private haveDoneLazySetup = false;
94
95
constructor(
96
@IEditorService private readonly editorService: IEditorService,
97
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
98
@IViewsService private readonly viewsService: IViewsService,
99
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
100
@INotificationService private readonly notificationService: INotificationService,
101
@IDialogService private readonly dialogService: IDialogService,
102
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
103
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
104
@IContextKeyService private readonly contextKeyService: IContextKeyService,
105
@ILifecycleService private readonly lifecycleService: ILifecycleService,
106
@IInstantiationService private readonly instantiationService: IInstantiationService,
107
@IExtensionService private readonly extensionService: IExtensionService,
108
@IFileService private readonly fileService: IFileService,
109
@IConfigurationService private readonly configurationService: IConfigurationService,
110
@IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService,
111
@IActivityService private readonly activityService: IActivityService,
112
@ICommandService private readonly commandService: ICommandService,
113
@IQuickInputService private readonly quickInputService: IQuickInputService,
114
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
115
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
116
@ITestService private readonly testService: ITestService,
117
) {
118
this.breakpointsToSendOnResourceSaved = new Set<URI>();
119
120
this._onDidChangeState = this.disposables.add(new Emitter<State>());
121
this._onDidNewSession = this.disposables.add(new Emitter<IDebugSession>());
122
this._onWillNewSession = this.disposables.add(new Emitter<IDebugSession>());
123
this._onDidEndSession = this.disposables.add(new Emitter());
124
125
this.adapterManager = this.instantiationService.createInstance(AdapterManager, {
126
onDidNewSession: this.onDidNewSession,
127
configurationManager: () => this.configurationManager,
128
});
129
this.disposables.add(this.adapterManager);
130
this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.adapterManager);
131
this.disposables.add(this.configurationManager);
132
this.debugStorage = this.disposables.add(this.instantiationService.createInstance(DebugStorage));
133
134
this.chosenEnvironments = this.debugStorage.loadChosenEnvironments();
135
136
this.model = this.instantiationService.createInstance(DebugModel, this.debugStorage);
137
this.telemetry = this.instantiationService.createInstance(DebugTelemetry, this.model);
138
139
this.viewModel = new ViewModel(contextKeyService);
140
this.taskRunner = this.instantiationService.createInstance(DebugTaskRunner);
141
142
this.disposables.add(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
143
this.disposables.add(this.lifecycleService.onWillShutdown(this.dispose, this));
144
145
this.disposables.add(this.extensionHostDebugService.onAttachSession(event => {
146
const session = this.model.getSession(event.sessionId, true);
147
if (session) {
148
// EH was started in debug mode -> attach to it
149
session.configuration.request = 'attach';
150
session.configuration.port = event.port;
151
session.setSubId(event.subId);
152
this.launchOrAttachToSession(session);
153
}
154
}));
155
this.disposables.add(this.extensionHostDebugService.onTerminateSession(event => {
156
const session = this.model.getSession(event.sessionId);
157
if (session && session.subId === event.subId) {
158
session.disconnect();
159
}
160
}));
161
162
this.disposables.add(this.viewModel.onDidFocusStackFrame(() => {
163
this.onStateChange();
164
}));
165
this.disposables.add(this.viewModel.onDidFocusSession((session: IDebugSession | undefined) => {
166
this.onStateChange();
167
168
if (session) {
169
this.setExceptionBreakpointFallbackSession(session.getId());
170
}
171
}));
172
this.disposables.add(Event.any(this.adapterManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => {
173
const debugUxValue = (this.state !== State.Inactive || (this.configurationManager.getAllConfigurations().length > 0 && this.adapterManager.hasEnabledDebuggers())) ? 'default' : 'simple';
174
this.debugUx.set(debugUxValue);
175
this.debugStorage.storeDebugUxState(debugUxValue);
176
}));
177
this.disposables.add(this.model.onDidChangeCallStack(() => {
178
const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length;
179
this.activity?.dispose();
180
if (numberOfSessions > 0) {
181
const viewContainer = this.viewDescriptorService.getViewContainerByViewId(CALLSTACK_VIEW_ID);
182
if (viewContainer) {
183
this.activity = this.activityService.showViewContainerActivity(viewContainer.id, { badge: new NumberBadge(numberOfSessions, n => n === 1 ? nls.localize('1activeSession', "1 active session") : nls.localize('nActiveSessions', "{0} active sessions", n)) });
184
}
185
}
186
}));
187
188
this.disposables.add(editorService.onDidActiveEditorChange(() => {
189
this.contextKeyService.bufferChangeEvents(() => {
190
if (editorService.activeEditor === DisassemblyViewInput.instance) {
191
this.disassemblyViewFocus.set(true);
192
} else {
193
// This key can be initialized a tick after this event is fired
194
this.disassemblyViewFocus?.reset();
195
}
196
});
197
}));
198
199
this.disposables.add(this.lifecycleService.onBeforeShutdown(() => {
200
for (const editor of editorService.editors) {
201
// Editors will not be valid on window reload, so close them.
202
if (editor.resource?.scheme === DEBUG_MEMORY_SCHEME) {
203
editor.dispose();
204
}
205
}
206
}));
207
208
this.disposables.add(extensionService.onWillStop(evt => {
209
evt.veto(
210
this.model.getSessions().length > 0,
211
nls.localize('active debug session', 'A debug session is still running that would terminate.'),
212
);
213
}));
214
215
this.initContextKeys(contextKeyService);
216
}
217
218
private initContextKeys(contextKeyService: IContextKeyService): void {
219
queueMicrotask(() => {
220
contextKeyService.bufferChangeEvents(() => {
221
this.debugType = CONTEXT_DEBUG_TYPE.bindTo(contextKeyService);
222
this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService);
223
this.hasDebugged = CONTEXT_HAS_DEBUGGED.bindTo(contextKeyService);
224
this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService);
225
this.debugUx = CONTEXT_DEBUG_UX.bindTo(contextKeyService);
226
this.debugUx.set(this.debugStorage.loadDebugUxState());
227
this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService);
228
// Need to set disassemblyViewFocus here to make it in the same context as the debug event handlers
229
this.disassemblyViewFocus = CONTEXT_DISASSEMBLY_VIEW_FOCUS.bindTo(contextKeyService);
230
});
231
232
const setBreakpointsExistContext = () => this.breakpointsExist.set(!!(this.model.getBreakpoints().length || this.model.getDataBreakpoints().length || this.model.getFunctionBreakpoints().length));
233
setBreakpointsExistContext();
234
this.disposables.add(this.model.onDidChangeBreakpoints(() => setBreakpointsExistContext()));
235
});
236
}
237
238
getModel(): IDebugModel {
239
return this.model;
240
}
241
242
getViewModel(): IViewModel {
243
return this.viewModel;
244
}
245
246
getConfigurationManager(): IConfigurationManager {
247
return this.configurationManager;
248
}
249
250
getAdapterManager(): IAdapterManager {
251
return this.adapterManager;
252
}
253
254
sourceIsNotAvailable(uri: uri): void {
255
this.model.sourceIsNotAvailable(uri);
256
}
257
258
dispose(): void {
259
this.disposables.dispose();
260
}
261
262
//---- state management
263
264
get state(): State {
265
const focusedSession = this.viewModel.focusedSession;
266
if (focusedSession) {
267
return focusedSession.state;
268
}
269
270
return this.initializing ? State.Initializing : State.Inactive;
271
}
272
273
get initializingOptions(): IDebugSessionOptions | undefined {
274
return this._initializingOptions;
275
}
276
277
private startInitializingState(options?: IDebugSessionOptions): void {
278
if (!this.initializing) {
279
this.initializing = true;
280
this._initializingOptions = options;
281
this.onStateChange();
282
}
283
}
284
285
private endInitializingState(): void {
286
if (this.initializing) {
287
this.initializing = false;
288
this._initializingOptions = undefined;
289
this.onStateChange();
290
}
291
}
292
293
private cancelTokens(id: string | undefined): void {
294
if (id) {
295
const token = this.sessionCancellationTokens.get(id);
296
if (token) {
297
token.cancel();
298
this.sessionCancellationTokens.delete(id);
299
}
300
} else {
301
this.sessionCancellationTokens.forEach(t => t.cancel());
302
this.sessionCancellationTokens.clear();
303
}
304
}
305
306
private onStateChange(): void {
307
const state = this.state;
308
if (this.previousState !== state) {
309
this.contextKeyService.bufferChangeEvents(() => {
310
this.debugState.set(getStateLabel(state));
311
this.inDebugMode.set(state !== State.Inactive);
312
// Only show the simple ux if debug is not yet started and if no launch.json exists
313
const debugUxValue = ((state !== State.Inactive && state !== State.Initializing) || (this.adapterManager.hasEnabledDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple';
314
this.debugUx.set(debugUxValue);
315
this.debugStorage.storeDebugUxState(debugUxValue);
316
});
317
this.previousState = state;
318
this._onDidChangeState.fire(state);
319
}
320
}
321
322
get onDidChangeState(): Event<State> {
323
return this._onDidChangeState.event;
324
}
325
326
get onDidNewSession(): Event<IDebugSession> {
327
return this._onDidNewSession.event;
328
}
329
330
get onWillNewSession(): Event<IDebugSession> {
331
return this._onWillNewSession.event;
332
}
333
334
get onDidEndSession(): Event<{ session: IDebugSession; restart: boolean }> {
335
return this._onDidEndSession.event;
336
}
337
338
private lazySetup() {
339
if (!this.haveDoneLazySetup) {
340
// Registering fs providers is slow
341
// https://github.com/microsoft/vscode/issues/159886
342
this.disposables.add(this.fileService.registerProvider(DEBUG_MEMORY_SCHEME, this.disposables.add(new DebugMemoryFileSystemProvider(this))));
343
this.haveDoneLazySetup = true;
344
}
345
}
346
347
//---- life cycle management
348
349
/**
350
* main entry point
351
* properly manages compounds, checks for errors and handles the initializing state.
352
*/
353
async startDebugging(launch: ILaunch | undefined, configOrName?: IConfig | string, options?: IDebugSessionOptions, saveBeforeStart = !options?.parentSession): Promise<boolean> {
354
const message = options && options.noDebug ? nls.localize('runTrust', "Running executes build tasks and program code from your workspace.") : nls.localize('debugTrust', "Debugging executes build tasks and program code from your workspace.");
355
const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ message });
356
if (!trust) {
357
return false;
358
}
359
360
this.lazySetup();
361
this.startInitializingState(options);
362
this.hasDebugged.set(true);
363
try {
364
// make sure to save all files and that the configuration is up to date
365
await this.extensionService.activateByEvent('onDebug');
366
if (saveBeforeStart) {
367
await saveAllBeforeDebugStart(this.configurationService, this.editorService);
368
}
369
await this.extensionService.whenInstalledExtensionsRegistered();
370
371
let config: IConfig | undefined;
372
let compound: ICompound | undefined;
373
if (!configOrName) {
374
configOrName = this.configurationManager.selectedConfiguration.name;
375
}
376
if (typeof configOrName === 'string' && launch) {
377
config = launch.getConfiguration(configOrName);
378
compound = launch.getCompound(configOrName);
379
} else if (typeof configOrName !== 'string') {
380
config = configOrName;
381
}
382
383
if (compound) {
384
// we are starting a compound debug, first do some error checking and than start each configuration in the compound
385
if (!compound.configurations) {
386
throw new Error(nls.localize({ key: 'compoundMustHaveConfigurations', comment: ['compound indicates a "compounds" configuration item', '"configurations" is an attribute and should not be localized'] },
387
"Compound must have \"configurations\" attribute set in order to start multiple configurations."));
388
}
389
if (compound.preLaunchTask) {
390
const taskResult = await this.taskRunner.runTaskAndCheckErrors(launch?.workspace || this.contextService.getWorkspace(), compound.preLaunchTask);
391
if (taskResult === TaskRunResult.Failure) {
392
this.endInitializingState();
393
return false;
394
}
395
}
396
if (compound.stopAll) {
397
options = { ...options, compoundRoot: new DebugCompoundRoot() };
398
}
399
400
const values = await Promise.all(compound.configurations.map(configData => {
401
const name = typeof configData === 'string' ? configData : configData.name;
402
if (name === compound.name) {
403
return Promise.resolve(false);
404
}
405
406
let launchForName: ILaunch | undefined;
407
if (typeof configData === 'string') {
408
const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name));
409
if (launchesContainingName.length === 1) {
410
launchForName = launchesContainingName[0];
411
} else if (launch && launchesContainingName.length > 1 && launchesContainingName.indexOf(launch) >= 0) {
412
// If there are multiple launches containing the configuration give priority to the configuration in the current launch
413
launchForName = launch;
414
} else {
415
throw new Error(launchesContainingName.length === 0 ? nls.localize('noConfigurationNameInWorkspace', "Could not find launch configuration '{0}' in the workspace.", name)
416
: nls.localize('multipleConfigurationNamesInWorkspace', "There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name));
417
}
418
} else if (configData.folder) {
419
const launchesMatchingConfigData = this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.name === configData.folder && !!l.getConfiguration(configData.name));
420
if (launchesMatchingConfigData.length === 1) {
421
launchForName = launchesMatchingConfigData[0];
422
} else {
423
throw new Error(nls.localize('noFolderWithName', "Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", configData.folder, configData.name, compound.name));
424
}
425
}
426
427
return this.createSession(launchForName, launchForName!.getConfiguration(name), options);
428
}));
429
430
const result = values.every(success => !!success); // Compound launch is a success only if each configuration launched successfully
431
this.endInitializingState();
432
return result;
433
}
434
435
if (configOrName && !config) {
436
const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", typeof configOrName === 'string' ? configOrName : configOrName.name) :
437
nls.localize('launchJsonDoesNotExist', "'launch.json' does not exist for passed workspace folder.");
438
throw new Error(message);
439
}
440
441
const result = await this.createSession(launch, config, options);
442
this.endInitializingState();
443
return result;
444
} catch (err) {
445
// make sure to get out of initializing state, and propagate the result
446
this.notificationService.error(err);
447
this.endInitializingState();
448
return Promise.reject(err);
449
}
450
}
451
452
/**
453
* gets the debugger for the type, resolves configurations by providers, substitutes variables and runs prelaunch tasks
454
*/
455
private async createSession(launch: ILaunch | undefined, config: IConfig | undefined, options?: IDebugSessionOptions): Promise<boolean> {
456
// We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes.
457
// Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config.
458
let type: string | undefined;
459
if (config) {
460
type = config.type;
461
} else {
462
// a no-folder workspace has no launch.config
463
config = Object.create(null) as IConfig;
464
}
465
if (options && options.noDebug) {
466
config.noDebug = true;
467
} else if (options && typeof options.noDebug === 'undefined' && options.parentSession && options.parentSession.configuration.noDebug) {
468
config.noDebug = true;
469
}
470
const unresolvedConfig = deepClone(config);
471
472
let guess: IGuessedDebugger | undefined;
473
let activeEditor: EditorInput | undefined;
474
if (!type) {
475
activeEditor = this.editorService.activeEditor;
476
if (activeEditor && activeEditor.resource) {
477
const chosen = this.chosenEnvironments[activeEditor.resource.toString()];
478
if (chosen) {
479
type = chosen.type;
480
if (chosen.dynamicLabel) {
481
const dyn = await this.configurationManager.getDynamicConfigurationsByType(chosen.type);
482
const found = dyn.find(d => d.label === chosen.dynamicLabel);
483
if (found) {
484
launch = found.launch;
485
Object.assign(config, found.config);
486
}
487
}
488
}
489
}
490
491
if (!type) {
492
guess = await this.adapterManager.guessDebugger(false);
493
if (guess) {
494
type = guess.debugger.type;
495
if (guess.withConfig) {
496
launch = guess.withConfig.launch;
497
Object.assign(config, guess.withConfig.config);
498
}
499
}
500
}
501
}
502
503
const initCancellationToken = new CancellationTokenSource();
504
const sessionId = generateUuid();
505
this.sessionCancellationTokens.set(sessionId, initCancellationToken);
506
507
const configByProviders = await this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config, initCancellationToken.token);
508
// a falsy config indicates an aborted launch
509
if (configByProviders && configByProviders.type) {
510
try {
511
let resolvedConfig = await this.substituteVariables(launch, configByProviders);
512
if (!resolvedConfig) {
513
// User cancelled resolving of interactive variables, silently return
514
return false;
515
}
516
517
if (initCancellationToken.token.isCancellationRequested) {
518
// User cancelled, silently return
519
return false;
520
}
521
522
// Check for concurrent sessions before running preLaunchTask to avoid running the task if user cancels
523
let userConfirmedConcurrentSession = false;
524
if (options?.startedByUser && resolvedConfig && resolvedConfig.suppressMultipleSessionWarning !== true) {
525
// Check if there's already a session with the same launch configuration
526
const existingSessions = this.model.getSessions();
527
const workspace = launch?.workspace;
528
529
const existingSession = existingSessions.find(s =>
530
s.configuration.name === resolvedConfig!.name &&
531
s.configuration.type === resolvedConfig!.type &&
532
s.configuration.request === resolvedConfig!.request &&
533
s.root === workspace
534
);
535
536
if (existingSession) {
537
// There is already a session with the same configuration, prompt user before running preLaunchTask
538
const confirmed = await this.confirmConcurrentSession(existingSession.getLabel());
539
if (!confirmed) {
540
return false;
541
}
542
userConfirmedConcurrentSession = true;
543
}
544
}
545
546
const workspace = launch?.workspace || this.contextService.getWorkspace();
547
const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask);
548
if (taskResult === TaskRunResult.Failure) {
549
return false;
550
}
551
552
const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, resolvedConfig.type, resolvedConfig, initCancellationToken.token);
553
if (!cfg) {
554
if (launch && type && cfg === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null".
555
await launch.openConfigFile({ preserveFocus: true, type }, initCancellationToken.token);
556
}
557
return false;
558
}
559
resolvedConfig = cfg;
560
561
const dbg = this.adapterManager.getDebugger(resolvedConfig.type);
562
if (!dbg || (configByProviders.request !== 'attach' && configByProviders.request !== 'launch')) {
563
let message: string;
564
if (configByProviders.request !== 'attach' && configByProviders.request !== 'launch') {
565
message = configByProviders.request ? nls.localize('debugRequestNotSupported', "Attribute '{0}' has an unsupported value '{1}' in the chosen debug configuration.", 'request', configByProviders.request)
566
: nls.localize('debugRequesMissing', "Attribute '{0}' is missing from the chosen debug configuration.", 'request');
567
568
} else {
569
message = resolvedConfig.type ? nls.localize('debugTypeNotSupported', "Configured debug type '{0}' is not supported.", resolvedConfig.type) :
570
nls.localize('debugTypeMissing', "Missing property 'type' for the chosen launch configuration.");
571
}
572
573
const actionList: IAction[] = [];
574
575
actionList.push(toAction({
576
id: 'installAdditionalDebuggers',
577
label: nls.localize({ key: 'installAdditionalDebuggers', comment: ['Placeholder is the debug type, so for example "node", "python"'] }, "Install {0} Extension", resolvedConfig.type),
578
enabled: true,
579
run: async () => this.commandService.executeCommand('debug.installAdditionalDebuggers', resolvedConfig?.type)
580
}));
581
582
await this.showError(message, actionList); return false;
583
}
584
585
if (!dbg.enabled) {
586
await this.showError(debuggerDisabledMessage(dbg.type), []);
587
return false;
588
}
589
590
const result = await this.doCreateSession(sessionId, launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options, userConfirmedConcurrentSession);
591
if (result && guess && activeEditor && activeEditor.resource) {
592
// Remeber user choice of environment per active editor to make starting debugging smoother #124770
593
this.chosenEnvironments[activeEditor.resource.toString()] = { type: guess.debugger.type, dynamicLabel: guess.withConfig?.label };
594
this.debugStorage.storeChosenEnvironments(this.chosenEnvironments);
595
}
596
return result;
597
} catch (err) {
598
if (err && err.message) {
599
await this.showError(err.message);
600
} else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
601
await this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type."));
602
}
603
if (launch && !initCancellationToken.token.isCancellationRequested) {
604
await launch.openConfigFile({ preserveFocus: true }, initCancellationToken.token);
605
}
606
607
return false;
608
}
609
}
610
611
if (launch && type && configByProviders === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null".
612
await launch.openConfigFile({ preserveFocus: true, type }, initCancellationToken.token);
613
}
614
615
return false;
616
}
617
618
/**
619
* instantiates the new session, initializes the session, registers session listeners and reports telemetry
620
*/
621
private async doCreateSession(sessionId: string, root: IWorkspaceFolder | undefined, configuration: { resolved: IConfig; unresolved: IConfig | undefined }, options?: IDebugSessionOptions, userConfirmedConcurrentSession = false): Promise<boolean> {
622
623
const session = this.instantiationService.createInstance(DebugSession, sessionId, configuration, root, this.model, options);
624
if (!userConfirmedConcurrentSession && options?.startedByUser && this.model.getSessions().some(s =>
625
s.configuration.name === configuration.resolved.name &&
626
s.configuration.type === configuration.resolved.type &&
627
s.configuration.request === configuration.resolved.request &&
628
s.root === root
629
) && configuration.resolved.suppressMultipleSessionWarning !== true) {
630
// There is already a session with the same configuration, prompt user #127721
631
const confirmed = await this.confirmConcurrentSession(session.getLabel());
632
if (!confirmed) {
633
return false;
634
}
635
}
636
637
this.model.addSession(session);
638
639
// since the Session is now properly registered under its ID and hooked, we can announce it
640
// this event doesn't go to extensions
641
this._onWillNewSession.fire(session);
642
643
const openDebug = this.configurationService.getValue<IDebugConfiguration>('debug').openDebug;
644
// Open debug viewlet based on the visibility of the side bar and openDebug setting. Do not open for 'run without debug'
645
if (!configuration.resolved.noDebug && (openDebug === 'openOnSessionStart' || (openDebug !== 'neverOpen' && this.viewModel.firstSessionStart)) && !session.suppressDebugView) {
646
await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
647
}
648
649
try {
650
await this.launchOrAttachToSession(session);
651
652
const internalConsoleOptions = session.configuration.internalConsoleOptions || this.configurationService.getValue<IDebugConfiguration>('debug').internalConsoleOptions;
653
if (internalConsoleOptions === 'openOnSessionStart' || (this.viewModel.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) {
654
this.viewsService.openView(REPL_VIEW_ID, false);
655
}
656
657
this.viewModel.firstSessionStart = false;
658
const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;
659
const sessions = this.model.getSessions();
660
const shownSessions = showSubSessions ? sessions : sessions.filter(s => !s.parentSession);
661
if (shownSessions.length > 1) {
662
this.viewModel.setMultiSessionView(true);
663
}
664
665
// since the initialized response has arrived announce the new Session (including extensions)
666
this._onDidNewSession.fire(session);
667
668
return true;
669
} catch (error) {
670
671
if (errors.isCancellationError(error)) {
672
// don't show 'canceled' error messages to the user #7906
673
return false;
674
}
675
676
// Show the repl if some error got logged there #5870
677
if (session && session.getReplElements().length > 0) {
678
this.viewsService.openView(REPL_VIEW_ID, false);
679
}
680
681
if (session.configuration && session.configuration.request === 'attach' && session.configuration.__autoAttach) {
682
// ignore attach timeouts in auto attach mode
683
return false;
684
}
685
686
const errorMessage = error instanceof Error ? error.message : error;
687
if (error.showUser !== false) {
688
// Only show the error when showUser is either not defined, or is true #128484
689
await this.showError(errorMessage, isErrorWithActions(error) ? error.actions : []);
690
}
691
return false;
692
}
693
}
694
695
private async confirmConcurrentSession(sessionLabel: string): Promise<boolean> {
696
const result = await this.dialogService.confirm({
697
message: nls.localize('multipleSession', "'{0}' is already running. Do you want to start another instance?", sessionLabel)
698
});
699
return result.confirmed;
700
}
701
702
private async launchOrAttachToSession(session: IDebugSession, forceFocus = false): Promise<void> {
703
// register listeners as the very first thing!
704
this.registerSessionListeners(session);
705
706
const dbgr = this.adapterManager.getDebugger(session.configuration.type);
707
try {
708
await session.initialize(dbgr!);
709
await session.launchOrAttach(session.configuration);
710
const launchJsonExists = !!session.root && !!this.configurationService.getValue<IGlobalConfig>('launch', { resource: session.root.uri });
711
await this.telemetry.logDebugSessionStart(dbgr!, launchJsonExists);
712
713
if (forceFocus || !this.viewModel.focusedSession || (session.parentSession === this.viewModel.focusedSession && session.compact)) {
714
await this.focusStackFrame(undefined, undefined, session);
715
}
716
} catch (err) {
717
if (this.viewModel.focusedSession === session) {
718
await this.focusStackFrame(undefined);
719
}
720
return Promise.reject(err);
721
}
722
}
723
724
private registerSessionListeners(session: IDebugSession): void {
725
const listenerDisposables = new DisposableStore();
726
this.disposables.add(listenerDisposables);
727
728
const sessionRunningScheduler = listenerDisposables.add(new RunOnceScheduler(() => {
729
// Do not immediatly defocus the stack frame if the session is running
730
if (session.state === State.Running && this.viewModel.focusedSession === session) {
731
this.viewModel.setFocus(undefined, this.viewModel.focusedThread, session, false);
732
}
733
}, 200));
734
listenerDisposables.add(session.onDidChangeState(() => {
735
if (session.state === State.Running && this.viewModel.focusedSession === session) {
736
sessionRunningScheduler.schedule();
737
}
738
if (session === this.viewModel.focusedSession) {
739
this.onStateChange();
740
}
741
}));
742
listenerDisposables.add(this.onDidEndSession(e => {
743
if (e.session === session) {
744
this.disposables.delete(listenerDisposables);
745
}
746
}));
747
listenerDisposables.add(session.onDidEndAdapter(async adapterExitEvent => {
748
749
if (adapterExitEvent) {
750
if (adapterExitEvent.error) {
751
this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString()));
752
}
753
this.telemetry.logDebugSessionStop(session, adapterExitEvent);
754
}
755
756
// 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905
757
const extensionDebugSession = getExtensionHostDebugSession(session);
758
if (extensionDebugSession && extensionDebugSession.state === State.Running && extensionDebugSession.configuration.noDebug) {
759
this.extensionHostDebugService.close(extensionDebugSession.getId());
760
}
761
762
if (session.configuration.postDebugTask) {
763
const root = session.root ?? this.contextService.getWorkspace();
764
try {
765
await this.taskRunner.runTask(root, session.configuration.postDebugTask);
766
} catch (err) {
767
this.notificationService.error(err);
768
}
769
}
770
this.endInitializingState();
771
this.cancelTokens(session.getId());
772
773
if (this.configurationService.getValue<IDebugConfiguration>('debug').closeReadonlyTabsOnEnd) {
774
const editorsToClose = this.editorService.getEditors(EditorsOrder.SEQUENTIAL).filter(({ editor }) => {
775
return editor.resource?.scheme === DEBUG_SCHEME && session.getId() === Source.getEncodedDebugData(editor.resource).sessionId;
776
});
777
this.editorService.closeEditors(editorsToClose);
778
}
779
this._onDidEndSession.fire({ session, restart: this.restartingSessions.has(session) });
780
781
const focusedSession = this.viewModel.focusedSession;
782
if (focusedSession && focusedSession.getId() === session.getId()) {
783
const { session, thread, stackFrame } = getStackFrameThreadAndSessionToFocus(this.model, undefined, undefined, undefined, focusedSession);
784
this.viewModel.setFocus(stackFrame, thread, session, false);
785
}
786
787
if (this.model.getSessions().length === 0) {
788
this.viewModel.setMultiSessionView(false);
789
790
if (this.layoutService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue<IDebugConfiguration>('debug').openExplorerOnEnd) {
791
this.paneCompositeService.openPaneComposite(EXPLORER_VIEWLET_ID, ViewContainerLocation.Sidebar);
792
}
793
794
// Data breakpoints that can not be persisted should be cleared when a session ends
795
const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist);
796
dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId()));
797
798
if (this.configurationService.getValue<IDebugConfiguration>('debug').console.closeOnEnd) {
799
const debugConsoleContainer = this.viewDescriptorService.getViewContainerByViewId(REPL_VIEW_ID);
800
if (debugConsoleContainer && this.viewsService.isViewContainerVisible(debugConsoleContainer.id)) {
801
this.viewsService.closeViewContainer(debugConsoleContainer.id);
802
}
803
}
804
}
805
806
this.model.removeExceptionBreakpointsForSession(session.getId());
807
// session.dispose(); TODO@roblourens
808
}));
809
}
810
811
async restartSession(session: IDebugSession, restartData?: any): Promise<void> {
812
if (session.saveBeforeRestart) {
813
await saveAllBeforeDebugStart(this.configurationService, this.editorService);
814
}
815
816
const isAutoRestart = !!restartData;
817
818
const runTasks: () => Promise<TaskRunResult> = async () => {
819
if (isAutoRestart) {
820
// Do not run preLaunch and postDebug tasks for automatic restarts
821
return Promise.resolve(TaskRunResult.Success);
822
}
823
824
const root = session.root || this.contextService.getWorkspace();
825
await this.taskRunner.runTask(root, session.configuration.preRestartTask);
826
await this.taskRunner.runTask(root, session.configuration.postDebugTask);
827
828
const taskResult1 = await this.taskRunner.runTaskAndCheckErrors(root, session.configuration.preLaunchTask);
829
if (taskResult1 !== TaskRunResult.Success) {
830
return taskResult1;
831
}
832
833
return this.taskRunner.runTaskAndCheckErrors(root, session.configuration.postRestartTask);
834
};
835
836
const extensionDebugSession = getExtensionHostDebugSession(session);
837
if (extensionDebugSession) {
838
const taskResult = await runTasks();
839
if (taskResult === TaskRunResult.Success) {
840
this.extensionHostDebugService.reload(extensionDebugSession.getId());
841
}
842
843
return;
844
}
845
846
// Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration
847
let needsToSubstitute = false;
848
let unresolved: IConfig | undefined;
849
const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined;
850
if (launch) {
851
unresolved = launch.getConfiguration(session.configuration.name);
852
if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) {
853
unresolved.noDebug = session.configuration.noDebug;
854
needsToSubstitute = true;
855
}
856
}
857
858
let resolved: IConfig | undefined | null = session.configuration;
859
if (launch && needsToSubstitute && unresolved) {
860
const initCancellationToken = new CancellationTokenSource();
861
this.sessionCancellationTokens.set(session.getId(), initCancellationToken);
862
const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, initCancellationToken.token);
863
if (resolvedByProviders) {
864
resolved = await this.substituteVariables(launch, resolvedByProviders);
865
if (resolved && !initCancellationToken.token.isCancellationRequested) {
866
resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, resolved.type, resolved, initCancellationToken.token);
867
}
868
} else {
869
resolved = resolvedByProviders;
870
}
871
}
872
if (resolved) {
873
session.setConfiguration({ resolved, unresolved });
874
}
875
session.configuration.__restart = restartData;
876
877
const doRestart = async (fn: () => Promise<boolean | undefined>) => {
878
this.restartingSessions.add(session);
879
let didRestart = false;
880
try {
881
didRestart = (await fn()) !== false;
882
} catch (e) {
883
didRestart = false;
884
throw e;
885
} finally {
886
this.restartingSessions.delete(session);
887
// we previously may have issued an onDidEndSession with restart: true,
888
// assuming the adapter exited (in `registerSessionListeners`). But the
889
// restart failed, so emit the final termination now.
890
if (!didRestart) {
891
this._onDidEndSession.fire({ session, restart: false });
892
}
893
}
894
};
895
896
for (const breakpoint of this.model.getBreakpoints({ triggeredOnly: true })) {
897
breakpoint.setSessionDidTrigger(session.getId(), false);
898
}
899
900
// For debug sessions spawned by test runs, cancel the test run and stop
901
// the session, then start the test run again; tests have no notion of restarts.
902
if (session.correlatedTestRun) {
903
if (!session.correlatedTestRun.completedAt) {
904
session.cancelCorrelatedTestRun();
905
await Event.toPromise(session.correlatedTestRun.onComplete);
906
// todo@connor4312 is there any reason to wait for the debug session to
907
// terminate? I don't think so, test extension should already handle any
908
// state conflicts...
909
}
910
911
this.testService.runResolvedTests(session.correlatedTestRun.request);
912
return;
913
}
914
915
if (session.capabilities.supportsRestartRequest) {
916
const taskResult = await runTasks();
917
if (taskResult === TaskRunResult.Success) {
918
await doRestart(async () => {
919
await session.restart();
920
return true;
921
});
922
}
923
924
return;
925
}
926
927
const shouldFocus = !!this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId();
928
return doRestart(async () => {
929
// If the restart is automatic -> disconnect, otherwise -> terminate #55064
930
if (isAutoRestart) {
931
await session.disconnect(true);
932
} else {
933
await session.terminate(true);
934
}
935
936
return new Promise<boolean>((c, e) => {
937
setTimeout(async () => {
938
const taskResult = await runTasks();
939
if (taskResult !== TaskRunResult.Success) {
940
return c(false);
941
}
942
943
if (!resolved) {
944
return c(false);
945
}
946
947
try {
948
await this.launchOrAttachToSession(session, shouldFocus);
949
this._onDidNewSession.fire(session);
950
c(true);
951
} catch (error) {
952
e(error);
953
}
954
}, 300);
955
});
956
});
957
}
958
959
async stopSession(session: IDebugSession | undefined, disconnect = false, suspend = false): Promise<any> {
960
if (session) {
961
return disconnect ? session.disconnect(undefined, suspend) : session.terminate();
962
}
963
964
const sessions = this.model.getSessions();
965
if (sessions.length === 0) {
966
this.taskRunner.cancel();
967
// User might have cancelled starting of a debug session, and in some cases the quick pick is left open
968
await this.quickInputService.cancel();
969
this.endInitializingState();
970
this.cancelTokens(undefined);
971
}
972
973
return Promise.all(sessions.map(s => disconnect ? s.disconnect(undefined, suspend) : s.terminate()));
974
}
975
976
private async substituteVariables(launch: ILaunch | undefined, config: IConfig): Promise<IConfig | undefined> {
977
const dbg = this.adapterManager.getDebugger(config.type);
978
if (dbg) {
979
let folder: IWorkspaceFolder | undefined = undefined;
980
if (launch && launch.workspace) {
981
folder = launch.workspace;
982
} else {
983
const folders = this.contextService.getWorkspace().folders;
984
if (folders.length === 1) {
985
folder = folders[0];
986
}
987
}
988
try {
989
return await dbg.substituteVariables(folder, config);
990
} catch (err) {
991
if (err.message !== errors.canceledName) {
992
this.showError(err.message, undefined, !!launch?.getConfiguration(config.name));
993
}
994
return undefined; // bail out
995
}
996
}
997
return Promise.resolve(config);
998
}
999
1000
private async showError(message: string, errorActions: ReadonlyArray<IAction> = [], promptLaunchJson = true): Promise<void> {
1001
const configureAction = toAction({ id: DEBUG_CONFIGURE_COMMAND_ID, label: DEBUG_CONFIGURE_LABEL, enabled: true, run: () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID) });
1002
// Don't append the standard command if id of any provided action indicates it is a command
1003
const actions = errorActions.filter((action) => action.id.endsWith('.command')).length > 0 ?
1004
errorActions :
1005
[...errorActions, ...(promptLaunchJson ? [configureAction] : [])];
1006
await this.dialogService.prompt({
1007
type: severity.Error,
1008
message,
1009
buttons: actions.map(action => ({
1010
label: action.label,
1011
run: () => action.run()
1012
})),
1013
cancelButton: true
1014
});
1015
}
1016
1017
//---- focus management
1018
1019
async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void> {
1020
const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session);
1021
1022
if (stackFrame) {
1023
const editor = await stackFrame.openInEditor(this.editorService, options?.preserveFocus ?? true, options?.sideBySide, options?.pinned);
1024
if (editor) {
1025
if (editor.input === DisassemblyViewInput.instance) {
1026
// Go to address is invoked via setFocus
1027
} else {
1028
const control = editor.getControl();
1029
if (stackFrame && isCodeEditor(control) && control.hasModel()) {
1030
const model = control.getModel();
1031
const lineNumber = stackFrame.range.startLineNumber;
1032
if (lineNumber >= 1 && lineNumber <= model.getLineCount()) {
1033
const lineContent = control.getModel().getLineContent(lineNumber);
1034
aria.alert(nls.localize({ key: 'debuggingPaused', comment: ['First placeholder is the file line content, second placeholder is the reason why debugging is stopped, for example "breakpoint", third is the stack frame name, and last is the line number.'] },
1035
"{0}, debugging paused {1}, {2}:{3}", lineContent, thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber));
1036
}
1037
}
1038
}
1039
}
1040
}
1041
if (session) {
1042
this.debugType.set(session.configuration.type);
1043
} else {
1044
this.debugType.reset();
1045
}
1046
1047
this.viewModel.setFocus(stackFrame, thread, session, !!options?.explicit);
1048
}
1049
1050
//---- watches
1051
1052
addWatchExpression(name?: string): void {
1053
const we = this.model.addWatchExpression(name);
1054
if (!name) {
1055
this.viewModel.setSelectedExpression(we, false);
1056
}
1057
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1058
}
1059
1060
renameWatchExpression(id: string, newName: string): void {
1061
this.model.renameWatchExpression(id, newName);
1062
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1063
}
1064
1065
moveWatchExpression(id: string, position: number): void {
1066
this.model.moveWatchExpression(id, position);
1067
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1068
}
1069
1070
removeWatchExpressions(id?: string): void {
1071
this.model.removeWatchExpressions(id);
1072
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1073
}
1074
1075
//---- breakpoints
1076
1077
canSetBreakpointsIn(model: ITextModel): boolean {
1078
return this.adapterManager.canSetBreakpointsIn(model);
1079
}
1080
1081
async enableOrDisableBreakpoints(enable: boolean, breakpoint?: IEnablement): Promise<void> {
1082
if (breakpoint) {
1083
this.model.setEnablement(breakpoint, enable);
1084
this.debugStorage.storeBreakpoints(this.model);
1085
if (breakpoint instanceof Breakpoint) {
1086
await this.makeTriggeredBreakpointsMatchEnablement(enable, breakpoint);
1087
await this.sendBreakpoints(breakpoint.originalUri);
1088
} else if (breakpoint instanceof FunctionBreakpoint) {
1089
await this.sendFunctionBreakpoints();
1090
} else if (breakpoint instanceof DataBreakpoint) {
1091
await this.sendDataBreakpoints();
1092
} else if (breakpoint instanceof InstructionBreakpoint) {
1093
await this.sendInstructionBreakpoints();
1094
} else {
1095
await this.sendExceptionBreakpoints();
1096
}
1097
} else {
1098
this.model.enableOrDisableAllBreakpoints(enable);
1099
this.debugStorage.storeBreakpoints(this.model);
1100
await this.sendAllBreakpoints();
1101
}
1102
this.debugStorage.storeBreakpoints(this.model);
1103
}
1104
1105
async addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[], ariaAnnounce = true): Promise<IBreakpoint[]> {
1106
const breakpoints = this.model.addBreakpoints(uri, rawBreakpoints);
1107
if (ariaAnnounce) {
1108
breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath)));
1109
}
1110
1111
// In some cases we need to store breakpoints before we send them because sending them can take a long time
1112
// And after sending them because the debug adapter can attach adapter data to a breakpoint
1113
this.debugStorage.storeBreakpoints(this.model);
1114
await this.sendBreakpoints(uri);
1115
this.debugStorage.storeBreakpoints(this.model);
1116
return breakpoints;
1117
}
1118
1119
async updateBreakpoints(uri: uri, data: Map<string, IBreakpointUpdateData>, sendOnResourceSaved: boolean): Promise<void> {
1120
this.model.updateBreakpoints(data);
1121
this.debugStorage.storeBreakpoints(this.model);
1122
if (sendOnResourceSaved) {
1123
this.breakpointsToSendOnResourceSaved.add(uri);
1124
} else {
1125
await this.sendBreakpoints(uri);
1126
this.debugStorage.storeBreakpoints(this.model);
1127
}
1128
}
1129
1130
async removeBreakpoints(id?: string | string[]): Promise<void> {
1131
const breakpoints = this.model.getBreakpoints();
1132
const toRemove = id === undefined
1133
? breakpoints
1134
: id instanceof Array
1135
? breakpoints.filter(bp => id.includes(bp.getId()))
1136
: breakpoints.filter(bp => bp.getId() === id);
1137
// note: using the debugger-resolved uri for aria to reflect UI state
1138
toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath)));
1139
const urisToClear = new Set(toRemove.map(bp => bp.originalUri.toString()));
1140
1141
this.model.removeBreakpoints(toRemove);
1142
this.unlinkTriggeredBreakpoints(breakpoints, toRemove).forEach(uri => urisToClear.add(uri.toString()));
1143
1144
this.debugStorage.storeBreakpoints(this.model);
1145
await Promise.all([...urisToClear].map(uri => this.sendBreakpoints(URI.parse(uri))));
1146
}
1147
1148
setBreakpointsActivated(activated: boolean): Promise<void> {
1149
this.model.setBreakpointsActivated(activated);
1150
return this.sendAllBreakpoints();
1151
}
1152
1153
async addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): Promise<void> {
1154
this.model.addFunctionBreakpoint(opts ?? { name: '' }, id);
1155
// If opts not provided, sending the breakpoint is handled by a later to call to `updateFunctionBreakpoint`
1156
if (opts) {
1157
this.debugStorage.storeBreakpoints(this.model);
1158
await this.sendFunctionBreakpoints();
1159
this.debugStorage.storeBreakpoints(this.model);
1160
}
1161
}
1162
1163
async updateFunctionBreakpoint(id: string, update: { name?: string; hitCondition?: string; condition?: string }): Promise<void> {
1164
this.model.updateFunctionBreakpoint(id, update);
1165
this.debugStorage.storeBreakpoints(this.model);
1166
await this.sendFunctionBreakpoints();
1167
}
1168
1169
async removeFunctionBreakpoints(id?: string): Promise<void> {
1170
this.model.removeFunctionBreakpoints(id);
1171
this.debugStorage.storeBreakpoints(this.model);
1172
await this.sendFunctionBreakpoints();
1173
}
1174
1175
async addDataBreakpoint(opts: IDataBreakpointOptions): Promise<void> {
1176
this.model.addDataBreakpoint(opts);
1177
this.debugStorage.storeBreakpoints(this.model);
1178
await this.sendDataBreakpoints();
1179
this.debugStorage.storeBreakpoints(this.model);
1180
}
1181
1182
async updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): Promise<void> {
1183
this.model.updateDataBreakpoint(id, update);
1184
this.debugStorage.storeBreakpoints(this.model);
1185
await this.sendDataBreakpoints();
1186
}
1187
1188
async removeDataBreakpoints(id?: string): Promise<void> {
1189
this.model.removeDataBreakpoints(id);
1190
this.debugStorage.storeBreakpoints(this.model);
1191
await this.sendDataBreakpoints();
1192
}
1193
1194
async addInstructionBreakpoint(opts: IInstructionBreakpointOptions): Promise<void> {
1195
this.model.addInstructionBreakpoint(opts);
1196
this.debugStorage.storeBreakpoints(this.model);
1197
await this.sendInstructionBreakpoints();
1198
this.debugStorage.storeBreakpoints(this.model);
1199
}
1200
1201
async removeInstructionBreakpoints(instructionReference?: string, offset?: number): Promise<void> {
1202
this.model.removeInstructionBreakpoints(instructionReference, offset);
1203
this.debugStorage.storeBreakpoints(this.model);
1204
await this.sendInstructionBreakpoints();
1205
}
1206
1207
setExceptionBreakpointFallbackSession(sessionId: string) {
1208
this.model.setExceptionBreakpointFallbackSession(sessionId);
1209
this.debugStorage.storeBreakpoints(this.model);
1210
}
1211
1212
setExceptionBreakpointsForSession(session: IDebugSession, filters: DebugProtocol.ExceptionBreakpointsFilter[]): void {
1213
this.model.setExceptionBreakpointsForSession(session.getId(), filters);
1214
this.debugStorage.storeBreakpoints(this.model);
1215
}
1216
1217
async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise<void> {
1218
this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition);
1219
this.debugStorage.storeBreakpoints(this.model);
1220
await this.sendExceptionBreakpoints();
1221
}
1222
1223
async sendAllBreakpoints(session?: IDebugSession): Promise<void> {
1224
const setBreakpointsPromises = distinct(this.model.getBreakpoints(), bp => bp.originalUri.toString())
1225
.map(bp => this.sendBreakpoints(bp.originalUri, false, session));
1226
1227
// If sending breakpoints to one session which we know supports the configurationDone request, can make all requests in parallel
1228
if (session?.capabilities.supportsConfigurationDoneRequest) {
1229
await Promise.all([
1230
...setBreakpointsPromises,
1231
this.sendFunctionBreakpoints(session),
1232
this.sendDataBreakpoints(session),
1233
this.sendInstructionBreakpoints(session),
1234
this.sendExceptionBreakpoints(session),
1235
]);
1236
} else {
1237
await Promise.all(setBreakpointsPromises);
1238
await this.sendFunctionBreakpoints(session);
1239
await this.sendDataBreakpoints(session);
1240
await this.sendInstructionBreakpoints(session);
1241
// send exception breakpoints at the end since some debug adapters may rely on the order - this was the case before
1242
// the configurationDone request was introduced.
1243
await this.sendExceptionBreakpoints(session);
1244
}
1245
}
1246
1247
/**
1248
* Removes the condition of triggered breakpoints that depended on
1249
* breakpoints in `removedBreakpoints`. Returns the URIs of resources that
1250
* had their breakpoints changed in this way.
1251
*/
1252
private unlinkTriggeredBreakpoints(allBreakpoints: readonly IBreakpoint[], removedBreakpoints: readonly IBreakpoint[]): uri[] {
1253
const affectedUris: uri[] = [];
1254
for (const removed of removedBreakpoints) {
1255
for (const existing of allBreakpoints) {
1256
if (!removedBreakpoints.includes(existing) && existing.triggeredBy === removed.getId()) {
1257
this.model.updateBreakpoints(new Map([[existing.getId(), { triggeredBy: undefined }]]));
1258
affectedUris.push(existing.originalUri);
1259
}
1260
}
1261
}
1262
1263
return affectedUris;
1264
}
1265
1266
private async makeTriggeredBreakpointsMatchEnablement(enable: boolean, breakpoint: Breakpoint) {
1267
if (enable) {
1268
/** If the breakpoint is being enabled, also ensure its triggerer is enabled */
1269
if (breakpoint.triggeredBy) {
1270
const trigger = this.model.getBreakpoints().find(bp => breakpoint.triggeredBy === bp.getId());
1271
if (trigger && !trigger.enabled) {
1272
await this.enableOrDisableBreakpoints(enable, trigger);
1273
}
1274
}
1275
}
1276
1277
1278
/** Makes its triggeree states match the state of this breakpoint */
1279
await Promise.all(this.model.getBreakpoints()
1280
.filter(bp => bp.triggeredBy === breakpoint.getId() && bp.enabled !== enable)
1281
.map(bp => this.enableOrDisableBreakpoints(enable, bp))
1282
);
1283
}
1284
1285
public async sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): Promise<void> {
1286
const breakpointsToSend = this.model.getBreakpoints({ originalUri: modelUri, enabledOnly: true });
1287
await sendToOneOrAllSessions(this.model, session, async s => {
1288
if (!s.configuration.noDebug) {
1289
const sessionBps = breakpointsToSend.filter(bp => !bp.triggeredBy || bp.getSessionDidTrigger(s.getId()));
1290
await s.sendBreakpoints(modelUri, sessionBps, sourceModified);
1291
}
1292
});
1293
}
1294
1295
private async sendFunctionBreakpoints(session?: IDebugSession): Promise<void> {
1296
const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1297
1298
await sendToOneOrAllSessions(this.model, session, async s => {
1299
if (s.capabilities.supportsFunctionBreakpoints && !s.configuration.noDebug) {
1300
await s.sendFunctionBreakpoints(breakpointsToSend);
1301
}
1302
});
1303
}
1304
1305
private async sendDataBreakpoints(session?: IDebugSession): Promise<void> {
1306
const breakpointsToSend = this.model.getDataBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1307
1308
await sendToOneOrAllSessions(this.model, session, async s => {
1309
if (s.capabilities.supportsDataBreakpoints && !s.configuration.noDebug) {
1310
await s.sendDataBreakpoints(breakpointsToSend);
1311
}
1312
});
1313
}
1314
1315
private async sendInstructionBreakpoints(session?: IDebugSession): Promise<void> {
1316
const breakpointsToSend = this.model.getInstructionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1317
1318
await sendToOneOrAllSessions(this.model, session, async s => {
1319
if (s.capabilities.supportsInstructionBreakpoints && !s.configuration.noDebug) {
1320
await s.sendInstructionBreakpoints(breakpointsToSend);
1321
}
1322
});
1323
}
1324
1325
private sendExceptionBreakpoints(session?: IDebugSession): Promise<void> {
1326
return sendToOneOrAllSessions(this.model, session, async s => {
1327
const enabledExceptionBps = this.model.getExceptionBreakpointsForSession(s.getId()).filter(exb => exb.enabled);
1328
if (s.capabilities.supportsConfigurationDoneRequest && (!s.capabilities.exceptionBreakpointFilters || s.capabilities.exceptionBreakpointFilters.length === 0)) {
1329
// Only call `setExceptionBreakpoints` as specified in dap protocol #90001
1330
return;
1331
}
1332
if (!s.configuration.noDebug) {
1333
await s.sendExceptionBreakpoints(enabledExceptionBps);
1334
}
1335
});
1336
}
1337
1338
private onFileChanges(fileChangesEvent: FileChangesEvent): void {
1339
const toRemove = this.model.getBreakpoints().filter(bp =>
1340
fileChangesEvent.contains(bp.originalUri, FileChangeType.DELETED));
1341
if (toRemove.length) {
1342
this.model.removeBreakpoints(toRemove);
1343
}
1344
1345
const toSend: URI[] = [];
1346
for (const uri of this.breakpointsToSendOnResourceSaved) {
1347
if (fileChangesEvent.contains(uri, FileChangeType.UPDATED)) {
1348
toSend.push(uri);
1349
}
1350
}
1351
1352
for (const uri of toSend) {
1353
this.breakpointsToSendOnResourceSaved.delete(uri);
1354
this.sendBreakpoints(uri, true);
1355
}
1356
}
1357
1358
async runTo(uri: uri, lineNumber: number, column?: number): Promise<void> {
1359
let breakpointToRemove: IBreakpoint | undefined;
1360
let threadToContinue = this.getViewModel().focusedThread;
1361
const addTempBreakPoint = async () => {
1362
const bpExists = !!(this.getModel().getBreakpoints({ column, lineNumber, uri }).length);
1363
1364
if (!bpExists) {
1365
const addResult = await this.addAndValidateBreakpoints(uri, lineNumber, column);
1366
if (addResult.thread) {
1367
threadToContinue = addResult.thread;
1368
}
1369
1370
if (addResult.breakpoint) {
1371
breakpointToRemove = addResult.breakpoint;
1372
}
1373
}
1374
return { threadToContinue, breakpointToRemove };
1375
};
1376
const removeTempBreakPoint = (state: State): boolean => {
1377
if (state === State.Stopped || state === State.Inactive) {
1378
if (breakpointToRemove) {
1379
this.removeBreakpoints(breakpointToRemove.getId());
1380
}
1381
return true;
1382
}
1383
return false;
1384
};
1385
1386
await addTempBreakPoint();
1387
if (this.state === State.Inactive) {
1388
// If no session exists start the debugger
1389
const { launch, name, getConfig } = this.getConfigurationManager().selectedConfiguration;
1390
const config = await getConfig();
1391
const configOrName = config ? Object.assign(deepClone(config), {}) : name;
1392
const listener = this.onDidChangeState(state => {
1393
if (removeTempBreakPoint(state)) {
1394
listener.dispose();
1395
}
1396
});
1397
await this.startDebugging(launch, configOrName, undefined, true);
1398
}
1399
if (this.state === State.Stopped) {
1400
const focusedSession = this.getViewModel().focusedSession;
1401
if (!focusedSession || !threadToContinue) {
1402
return;
1403
}
1404
1405
const listener = threadToContinue.session.onDidChangeState(() => {
1406
if (removeTempBreakPoint(focusedSession.state)) {
1407
listener.dispose();
1408
}
1409
});
1410
await threadToContinue.continue();
1411
}
1412
}
1413
1414
private async addAndValidateBreakpoints(uri: URI, lineNumber: number, column?: number) {
1415
const debugModel = this.getModel();
1416
const viewModel = this.getViewModel();
1417
1418
const breakpoints = await this.addBreakpoints(uri, [{ lineNumber, column }], false);
1419
const breakpoint = breakpoints?.[0];
1420
if (!breakpoint) {
1421
return { breakpoint: undefined, thread: viewModel.focusedThread };
1422
}
1423
1424
// If the breakpoint was not initially verified, wait up to 2s for it to become so.
1425
// Inherently racey if multiple sessions can verify async, but not solvable...
1426
if (!breakpoint.verified) {
1427
let listener: IDisposable;
1428
await raceTimeout(new Promise<void>(resolve => {
1429
listener = debugModel.onDidChangeBreakpoints(() => {
1430
if (breakpoint.verified) {
1431
resolve();
1432
}
1433
});
1434
}), 2000);
1435
listener!.dispose();
1436
}
1437
1438
// Look at paused threads for sessions that verified this bp. Prefer, in order:
1439
const enum Score {
1440
/** The focused thread */
1441
Focused,
1442
/** Any other stopped thread of a session that verified the bp */
1443
Verified,
1444
/** Any thread that verified and paused in the same file */
1445
VerifiedAndPausedInFile,
1446
/** The focused thread if it verified the breakpoint */
1447
VerifiedAndFocused,
1448
}
1449
1450
let bestThread = viewModel.focusedThread;
1451
let bestScore = Score.Focused;
1452
for (const sessionId of breakpoint.sessionsThatVerified) {
1453
const session = debugModel.getSession(sessionId);
1454
if (!session) {
1455
continue;
1456
}
1457
1458
const threads = session.getAllThreads().filter(t => t.stopped);
1459
if (bestScore < Score.VerifiedAndFocused) {
1460
if (viewModel.focusedThread && threads.includes(viewModel.focusedThread)) {
1461
bestThread = viewModel.focusedThread;
1462
bestScore = Score.VerifiedAndFocused;
1463
}
1464
}
1465
1466
if (bestScore < Score.VerifiedAndPausedInFile) {
1467
const pausedInThisFile = threads.find(t => {
1468
const top = t.getTopStackFrame();
1469
return top && this.uriIdentityService.extUri.isEqual(top.source.uri, uri);
1470
});
1471
1472
if (pausedInThisFile) {
1473
bestThread = pausedInThisFile;
1474
bestScore = Score.VerifiedAndPausedInFile;
1475
}
1476
}
1477
1478
if (bestScore < Score.Verified) {
1479
bestThread = threads[0];
1480
bestScore = Score.VerifiedAndPausedInFile;
1481
}
1482
}
1483
1484
return { thread: bestThread, breakpoint };
1485
}
1486
}
1487
1488
export function getStackFrameThreadAndSessionToFocus(model: IDebugModel, stackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, avoidSession?: IDebugSession): { stackFrame: IStackFrame | undefined; thread: IThread | undefined; session: IDebugSession | undefined } {
1489
if (!session) {
1490
if (stackFrame || thread) {
1491
session = stackFrame ? stackFrame.thread.session : thread!.session;
1492
} else {
1493
const sessions = model.getSessions();
1494
const stoppedSession = sessions.find(s => s.state === State.Stopped);
1495
// Make sure to not focus session that is going down
1496
session = stoppedSession || sessions.find(s => s !== avoidSession && s !== avoidSession?.parentSession) || (sessions.length ? sessions[0] : undefined);
1497
}
1498
}
1499
1500
if (!thread) {
1501
if (stackFrame) {
1502
thread = stackFrame.thread;
1503
} else {
1504
const threads = session ? session.getAllThreads() : undefined;
1505
const stoppedThread = threads && threads.find(t => t.stopped);
1506
thread = stoppedThread || (threads && threads.length ? threads[0] : undefined);
1507
}
1508
}
1509
1510
if (!stackFrame && thread) {
1511
stackFrame = thread.getTopStackFrame();
1512
}
1513
1514
return { session, thread, stackFrame };
1515
}
1516
1517
async function sendToOneOrAllSessions(model: DebugModel, session: IDebugSession | undefined, send: (session: IDebugSession) => Promise<void>): Promise<void> {
1518
if (session) {
1519
await send(session);
1520
} else {
1521
await Promise.all(model.getSessions().map(s => send(s)));
1522
}
1523
}
1524
1525