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
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as aria from '../../../../base/browser/ui/aria/aria.js';
7
import { Action, IAction } 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 = new Emitter<State>();
121
this._onDidNewSession = new Emitter<IDebugSession>();
122
this._onWillNewSession = new Emitter<IDebugSession>();
123
this._onDidEndSession = 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(new Action(
576
'installAdditionalDebuggers',
577
nls.localize({ key: 'installAdditionalDebuggers', comment: ['Placeholder is the debug type, so for example "node", "python"'] }, "Install {0} Extension", resolvedConfig.type),
578
undefined,
579
true,
580
async () => this.commandService.executeCommand('debug.installAdditionalDebuggers', resolvedConfig?.type)
581
));
582
583
await this.showError(message, actionList);
584
585
return false;
586
}
587
588
if (!dbg.enabled) {
589
await this.showError(debuggerDisabledMessage(dbg.type), []);
590
return false;
591
}
592
593
const result = await this.doCreateSession(sessionId, launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options, userConfirmedConcurrentSession);
594
if (result && guess && activeEditor && activeEditor.resource) {
595
// Remeber user choice of environment per active editor to make starting debugging smoother #124770
596
this.chosenEnvironments[activeEditor.resource.toString()] = { type: guess.debugger.type, dynamicLabel: guess.withConfig?.label };
597
this.debugStorage.storeChosenEnvironments(this.chosenEnvironments);
598
}
599
return result;
600
} catch (err) {
601
if (err && err.message) {
602
await this.showError(err.message);
603
} else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
604
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."));
605
}
606
if (launch && !initCancellationToken.token.isCancellationRequested) {
607
await launch.openConfigFile({ preserveFocus: true }, initCancellationToken.token);
608
}
609
610
return false;
611
}
612
}
613
614
if (launch && type && configByProviders === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null".
615
await launch.openConfigFile({ preserveFocus: true, type }, initCancellationToken.token);
616
}
617
618
return false;
619
}
620
621
/**
622
* instantiates the new session, initializes the session, registers session listeners and reports telemetry
623
*/
624
private async doCreateSession(sessionId: string, root: IWorkspaceFolder | undefined, configuration: { resolved: IConfig; unresolved: IConfig | undefined }, options?: IDebugSessionOptions, userConfirmedConcurrentSession = false): Promise<boolean> {
625
626
const session = this.instantiationService.createInstance(DebugSession, sessionId, configuration, root, this.model, options);
627
if (!userConfirmedConcurrentSession && options?.startedByUser && this.model.getSessions().some(s =>
628
s.configuration.name === configuration.resolved.name &&
629
s.configuration.type === configuration.resolved.type &&
630
s.configuration.request === configuration.resolved.request &&
631
s.root === root
632
) && configuration.resolved.suppressMultipleSessionWarning !== true) {
633
// There is already a session with the same configuration, prompt user #127721
634
const confirmed = await this.confirmConcurrentSession(session.getLabel());
635
if (!confirmed) {
636
return false;
637
}
638
}
639
640
this.model.addSession(session);
641
642
// since the Session is now properly registered under its ID and hooked, we can announce it
643
// this event doesn't go to extensions
644
this._onWillNewSession.fire(session);
645
646
const openDebug = this.configurationService.getValue<IDebugConfiguration>('debug').openDebug;
647
// Open debug viewlet based on the visibility of the side bar and openDebug setting. Do not open for 'run without debug'
648
if (!configuration.resolved.noDebug && (openDebug === 'openOnSessionStart' || (openDebug !== 'neverOpen' && this.viewModel.firstSessionStart)) && !session.suppressDebugView) {
649
await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar);
650
}
651
652
try {
653
await this.launchOrAttachToSession(session);
654
655
const internalConsoleOptions = session.configuration.internalConsoleOptions || this.configurationService.getValue<IDebugConfiguration>('debug').internalConsoleOptions;
656
if (internalConsoleOptions === 'openOnSessionStart' || (this.viewModel.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) {
657
this.viewsService.openView(REPL_VIEW_ID, false);
658
}
659
660
this.viewModel.firstSessionStart = false;
661
const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;
662
const sessions = this.model.getSessions();
663
const shownSessions = showSubSessions ? sessions : sessions.filter(s => !s.parentSession);
664
if (shownSessions.length > 1) {
665
this.viewModel.setMultiSessionView(true);
666
}
667
668
// since the initialized response has arrived announce the new Session (including extensions)
669
this._onDidNewSession.fire(session);
670
671
return true;
672
} catch (error) {
673
674
if (errors.isCancellationError(error)) {
675
// don't show 'canceled' error messages to the user #7906
676
return false;
677
}
678
679
// Show the repl if some error got logged there #5870
680
if (session && session.getReplElements().length > 0) {
681
this.viewsService.openView(REPL_VIEW_ID, false);
682
}
683
684
if (session.configuration && session.configuration.request === 'attach' && session.configuration.__autoAttach) {
685
// ignore attach timeouts in auto attach mode
686
return false;
687
}
688
689
const errorMessage = error instanceof Error ? error.message : error;
690
if (error.showUser !== false) {
691
// Only show the error when showUser is either not defined, or is true #128484
692
await this.showError(errorMessage, isErrorWithActions(error) ? error.actions : []);
693
}
694
return false;
695
}
696
}
697
698
private async confirmConcurrentSession(sessionLabel: string): Promise<boolean> {
699
const result = await this.dialogService.confirm({
700
message: nls.localize('multipleSession', "'{0}' is already running. Do you want to start another instance?", sessionLabel)
701
});
702
return result.confirmed;
703
}
704
705
private async launchOrAttachToSession(session: IDebugSession, forceFocus = false): Promise<void> {
706
// register listeners as the very first thing!
707
this.registerSessionListeners(session);
708
709
const dbgr = this.adapterManager.getDebugger(session.configuration.type);
710
try {
711
await session.initialize(dbgr!);
712
await session.launchOrAttach(session.configuration);
713
const launchJsonExists = !!session.root && !!this.configurationService.getValue<IGlobalConfig>('launch', { resource: session.root.uri });
714
await this.telemetry.logDebugSessionStart(dbgr!, launchJsonExists);
715
716
if (forceFocus || !this.viewModel.focusedSession || (session.parentSession === this.viewModel.focusedSession && session.compact)) {
717
await this.focusStackFrame(undefined, undefined, session);
718
}
719
} catch (err) {
720
if (this.viewModel.focusedSession === session) {
721
await this.focusStackFrame(undefined);
722
}
723
return Promise.reject(err);
724
}
725
}
726
727
private registerSessionListeners(session: IDebugSession): void {
728
const listenerDisposables = new DisposableStore();
729
this.disposables.add(listenerDisposables);
730
731
const sessionRunningScheduler = listenerDisposables.add(new RunOnceScheduler(() => {
732
// Do not immediatly defocus the stack frame if the session is running
733
if (session.state === State.Running && this.viewModel.focusedSession === session) {
734
this.viewModel.setFocus(undefined, this.viewModel.focusedThread, session, false);
735
}
736
}, 200));
737
listenerDisposables.add(session.onDidChangeState(() => {
738
if (session.state === State.Running && this.viewModel.focusedSession === session) {
739
sessionRunningScheduler.schedule();
740
}
741
if (session === this.viewModel.focusedSession) {
742
this.onStateChange();
743
}
744
}));
745
listenerDisposables.add(this.onDidEndSession(e => {
746
if (e.session === session) {
747
this.disposables.delete(listenerDisposables);
748
}
749
}));
750
listenerDisposables.add(session.onDidEndAdapter(async adapterExitEvent => {
751
752
if (adapterExitEvent) {
753
if (adapterExitEvent.error) {
754
this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString()));
755
}
756
this.telemetry.logDebugSessionStop(session, adapterExitEvent);
757
}
758
759
// 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905
760
const extensionDebugSession = getExtensionHostDebugSession(session);
761
if (extensionDebugSession && extensionDebugSession.state === State.Running && extensionDebugSession.configuration.noDebug) {
762
this.extensionHostDebugService.close(extensionDebugSession.getId());
763
}
764
765
if (session.configuration.postDebugTask) {
766
const root = session.root ?? this.contextService.getWorkspace();
767
try {
768
await this.taskRunner.runTask(root, session.configuration.postDebugTask);
769
} catch (err) {
770
this.notificationService.error(err);
771
}
772
}
773
this.endInitializingState();
774
this.cancelTokens(session.getId());
775
776
if (this.configurationService.getValue<IDebugConfiguration>('debug').closeReadonlyTabsOnEnd) {
777
const editorsToClose = this.editorService.getEditors(EditorsOrder.SEQUENTIAL).filter(({ editor }) => {
778
return editor.resource?.scheme === DEBUG_SCHEME && session.getId() === Source.getEncodedDebugData(editor.resource).sessionId;
779
});
780
this.editorService.closeEditors(editorsToClose);
781
}
782
this._onDidEndSession.fire({ session, restart: this.restartingSessions.has(session) });
783
784
const focusedSession = this.viewModel.focusedSession;
785
if (focusedSession && focusedSession.getId() === session.getId()) {
786
const { session, thread, stackFrame } = getStackFrameThreadAndSessionToFocus(this.model, undefined, undefined, undefined, focusedSession);
787
this.viewModel.setFocus(stackFrame, thread, session, false);
788
}
789
790
if (this.model.getSessions().length === 0) {
791
this.viewModel.setMultiSessionView(false);
792
793
if (this.layoutService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue<IDebugConfiguration>('debug').openExplorerOnEnd) {
794
this.paneCompositeService.openPaneComposite(EXPLORER_VIEWLET_ID, ViewContainerLocation.Sidebar);
795
}
796
797
// Data breakpoints that can not be persisted should be cleared when a session ends
798
const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist);
799
dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId()));
800
801
if (this.configurationService.getValue<IDebugConfiguration>('debug').console.closeOnEnd) {
802
const debugConsoleContainer = this.viewDescriptorService.getViewContainerByViewId(REPL_VIEW_ID);
803
if (debugConsoleContainer && this.viewsService.isViewContainerVisible(debugConsoleContainer.id)) {
804
this.viewsService.closeViewContainer(debugConsoleContainer.id);
805
}
806
}
807
}
808
809
this.model.removeExceptionBreakpointsForSession(session.getId());
810
// session.dispose(); TODO@roblourens
811
}));
812
}
813
814
async restartSession(session: IDebugSession, restartData?: any): Promise<void> {
815
if (session.saveBeforeRestart) {
816
await saveAllBeforeDebugStart(this.configurationService, this.editorService);
817
}
818
819
const isAutoRestart = !!restartData;
820
821
const runTasks: () => Promise<TaskRunResult> = async () => {
822
if (isAutoRestart) {
823
// Do not run preLaunch and postDebug tasks for automatic restarts
824
return Promise.resolve(TaskRunResult.Success);
825
}
826
827
const root = session.root || this.contextService.getWorkspace();
828
await this.taskRunner.runTask(root, session.configuration.preRestartTask);
829
await this.taskRunner.runTask(root, session.configuration.postDebugTask);
830
831
const taskResult1 = await this.taskRunner.runTaskAndCheckErrors(root, session.configuration.preLaunchTask);
832
if (taskResult1 !== TaskRunResult.Success) {
833
return taskResult1;
834
}
835
836
return this.taskRunner.runTaskAndCheckErrors(root, session.configuration.postRestartTask);
837
};
838
839
const extensionDebugSession = getExtensionHostDebugSession(session);
840
if (extensionDebugSession) {
841
const taskResult = await runTasks();
842
if (taskResult === TaskRunResult.Success) {
843
this.extensionHostDebugService.reload(extensionDebugSession.getId());
844
}
845
846
return;
847
}
848
849
// Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration
850
let needsToSubstitute = false;
851
let unresolved: IConfig | undefined;
852
const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined;
853
if (launch) {
854
unresolved = launch.getConfiguration(session.configuration.name);
855
if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) {
856
unresolved.noDebug = session.configuration.noDebug;
857
needsToSubstitute = true;
858
}
859
}
860
861
let resolved: IConfig | undefined | null = session.configuration;
862
if (launch && needsToSubstitute && unresolved) {
863
const initCancellationToken = new CancellationTokenSource();
864
this.sessionCancellationTokens.set(session.getId(), initCancellationToken);
865
const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, initCancellationToken.token);
866
if (resolvedByProviders) {
867
resolved = await this.substituteVariables(launch, resolvedByProviders);
868
if (resolved && !initCancellationToken.token.isCancellationRequested) {
869
resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, resolved.type, resolved, initCancellationToken.token);
870
}
871
} else {
872
resolved = resolvedByProviders;
873
}
874
}
875
if (resolved) {
876
session.setConfiguration({ resolved, unresolved });
877
}
878
session.configuration.__restart = restartData;
879
880
const doRestart = async (fn: () => Promise<boolean | undefined>) => {
881
this.restartingSessions.add(session);
882
let didRestart = false;
883
try {
884
didRestart = (await fn()) !== false;
885
} catch (e) {
886
didRestart = false;
887
throw e;
888
} finally {
889
this.restartingSessions.delete(session);
890
// we previously may have issued an onDidEndSession with restart: true,
891
// assuming the adapter exited (in `registerSessionListeners`). But the
892
// restart failed, so emit the final termination now.
893
if (!didRestart) {
894
this._onDidEndSession.fire({ session, restart: false });
895
}
896
}
897
};
898
899
for (const breakpoint of this.model.getBreakpoints({ triggeredOnly: true })) {
900
breakpoint.setSessionDidTrigger(session.getId(), false);
901
}
902
903
// For debug sessions spawned by test runs, cancel the test run and stop
904
// the session, then start the test run again; tests have no notion of restarts.
905
if (session.correlatedTestRun) {
906
if (!session.correlatedTestRun.completedAt) {
907
session.cancelCorrelatedTestRun();
908
await Event.toPromise(session.correlatedTestRun.onComplete);
909
// todo@connor4312 is there any reason to wait for the debug session to
910
// terminate? I don't think so, test extension should already handle any
911
// state conflicts...
912
}
913
914
this.testService.runResolvedTests(session.correlatedTestRun.request);
915
return;
916
}
917
918
if (session.capabilities.supportsRestartRequest) {
919
const taskResult = await runTasks();
920
if (taskResult === TaskRunResult.Success) {
921
await doRestart(async () => {
922
await session.restart();
923
return true;
924
});
925
}
926
927
return;
928
}
929
930
const shouldFocus = !!this.viewModel.focusedSession && session.getId() === this.viewModel.focusedSession.getId();
931
return doRestart(async () => {
932
// If the restart is automatic -> disconnect, otherwise -> terminate #55064
933
if (isAutoRestart) {
934
await session.disconnect(true);
935
} else {
936
await session.terminate(true);
937
}
938
939
return new Promise<boolean>((c, e) => {
940
setTimeout(async () => {
941
const taskResult = await runTasks();
942
if (taskResult !== TaskRunResult.Success) {
943
return c(false);
944
}
945
946
if (!resolved) {
947
return c(false);
948
}
949
950
try {
951
await this.launchOrAttachToSession(session, shouldFocus);
952
this._onDidNewSession.fire(session);
953
c(true);
954
} catch (error) {
955
e(error);
956
}
957
}, 300);
958
});
959
});
960
}
961
962
async stopSession(session: IDebugSession | undefined, disconnect = false, suspend = false): Promise<any> {
963
if (session) {
964
return disconnect ? session.disconnect(undefined, suspend) : session.terminate();
965
}
966
967
const sessions = this.model.getSessions();
968
if (sessions.length === 0) {
969
this.taskRunner.cancel();
970
// User might have cancelled starting of a debug session, and in some cases the quick pick is left open
971
await this.quickInputService.cancel();
972
this.endInitializingState();
973
this.cancelTokens(undefined);
974
}
975
976
return Promise.all(sessions.map(s => disconnect ? s.disconnect(undefined, suspend) : s.terminate()));
977
}
978
979
private async substituteVariables(launch: ILaunch | undefined, config: IConfig): Promise<IConfig | undefined> {
980
const dbg = this.adapterManager.getDebugger(config.type);
981
if (dbg) {
982
let folder: IWorkspaceFolder | undefined = undefined;
983
if (launch && launch.workspace) {
984
folder = launch.workspace;
985
} else {
986
const folders = this.contextService.getWorkspace().folders;
987
if (folders.length === 1) {
988
folder = folders[0];
989
}
990
}
991
try {
992
return await dbg.substituteVariables(folder, config);
993
} catch (err) {
994
if (err.message !== errors.canceledName) {
995
this.showError(err.message, undefined, !!launch?.getConfiguration(config.name));
996
}
997
return undefined; // bail out
998
}
999
}
1000
return Promise.resolve(config);
1001
}
1002
1003
private async showError(message: string, errorActions: ReadonlyArray<IAction> = [], promptLaunchJson = true): Promise<void> {
1004
const configureAction = new Action(DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, undefined, true, () => this.commandService.executeCommand(DEBUG_CONFIGURE_COMMAND_ID));
1005
// Don't append the standard command if id of any provided action indicates it is a command
1006
const actions = errorActions.filter((action) => action.id.endsWith('.command')).length > 0 ?
1007
errorActions :
1008
[...errorActions, ...(promptLaunchJson ? [configureAction] : [])];
1009
await this.dialogService.prompt({
1010
type: severity.Error,
1011
message,
1012
buttons: actions.map(action => ({
1013
label: action.label,
1014
run: () => action.run()
1015
})),
1016
cancelButton: true
1017
});
1018
}
1019
1020
//---- focus management
1021
1022
async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise<void> {
1023
const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session);
1024
1025
if (stackFrame) {
1026
const editor = await stackFrame.openInEditor(this.editorService, options?.preserveFocus ?? true, options?.sideBySide, options?.pinned);
1027
if (editor) {
1028
if (editor.input === DisassemblyViewInput.instance) {
1029
// Go to address is invoked via setFocus
1030
} else {
1031
const control = editor.getControl();
1032
if (stackFrame && isCodeEditor(control) && control.hasModel()) {
1033
const model = control.getModel();
1034
const lineNumber = stackFrame.range.startLineNumber;
1035
if (lineNumber >= 1 && lineNumber <= model.getLineCount()) {
1036
const lineContent = control.getModel().getLineContent(lineNumber);
1037
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.'] },
1038
"{0}, debugging paused {1}, {2}:{3}", lineContent, thread && thread.stoppedDetails ? `, reason ${thread.stoppedDetails.reason}` : '', stackFrame.source ? stackFrame.source.name : '', stackFrame.range.startLineNumber));
1039
}
1040
}
1041
}
1042
}
1043
}
1044
if (session) {
1045
this.debugType.set(session.configuration.type);
1046
} else {
1047
this.debugType.reset();
1048
}
1049
1050
this.viewModel.setFocus(stackFrame, thread, session, !!options?.explicit);
1051
}
1052
1053
//---- watches
1054
1055
addWatchExpression(name?: string): void {
1056
const we = this.model.addWatchExpression(name);
1057
if (!name) {
1058
this.viewModel.setSelectedExpression(we, false);
1059
}
1060
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1061
}
1062
1063
renameWatchExpression(id: string, newName: string): void {
1064
this.model.renameWatchExpression(id, newName);
1065
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1066
}
1067
1068
moveWatchExpression(id: string, position: number): void {
1069
this.model.moveWatchExpression(id, position);
1070
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1071
}
1072
1073
removeWatchExpressions(id?: string): void {
1074
this.model.removeWatchExpressions(id);
1075
this.debugStorage.storeWatchExpressions(this.model.getWatchExpressions());
1076
}
1077
1078
//---- breakpoints
1079
1080
canSetBreakpointsIn(model: ITextModel): boolean {
1081
return this.adapterManager.canSetBreakpointsIn(model);
1082
}
1083
1084
async enableOrDisableBreakpoints(enable: boolean, breakpoint?: IEnablement): Promise<void> {
1085
if (breakpoint) {
1086
this.model.setEnablement(breakpoint, enable);
1087
this.debugStorage.storeBreakpoints(this.model);
1088
if (breakpoint instanceof Breakpoint) {
1089
await this.makeTriggeredBreakpointsMatchEnablement(enable, breakpoint);
1090
await this.sendBreakpoints(breakpoint.originalUri);
1091
} else if (breakpoint instanceof FunctionBreakpoint) {
1092
await this.sendFunctionBreakpoints();
1093
} else if (breakpoint instanceof DataBreakpoint) {
1094
await this.sendDataBreakpoints();
1095
} else if (breakpoint instanceof InstructionBreakpoint) {
1096
await this.sendInstructionBreakpoints();
1097
} else {
1098
await this.sendExceptionBreakpoints();
1099
}
1100
} else {
1101
this.model.enableOrDisableAllBreakpoints(enable);
1102
this.debugStorage.storeBreakpoints(this.model);
1103
await this.sendAllBreakpoints();
1104
}
1105
this.debugStorage.storeBreakpoints(this.model);
1106
}
1107
1108
async addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[], ariaAnnounce = true): Promise<IBreakpoint[]> {
1109
const breakpoints = this.model.addBreakpoints(uri, rawBreakpoints);
1110
if (ariaAnnounce) {
1111
breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath)));
1112
}
1113
1114
// In some cases we need to store breakpoints before we send them because sending them can take a long time
1115
// And after sending them because the debug adapter can attach adapter data to a breakpoint
1116
this.debugStorage.storeBreakpoints(this.model);
1117
await this.sendBreakpoints(uri);
1118
this.debugStorage.storeBreakpoints(this.model);
1119
return breakpoints;
1120
}
1121
1122
async updateBreakpoints(uri: uri, data: Map<string, IBreakpointUpdateData>, sendOnResourceSaved: boolean): Promise<void> {
1123
this.model.updateBreakpoints(data);
1124
this.debugStorage.storeBreakpoints(this.model);
1125
if (sendOnResourceSaved) {
1126
this.breakpointsToSendOnResourceSaved.add(uri);
1127
} else {
1128
await this.sendBreakpoints(uri);
1129
this.debugStorage.storeBreakpoints(this.model);
1130
}
1131
}
1132
1133
async removeBreakpoints(id?: string): Promise<void> {
1134
const breakpoints = this.model.getBreakpoints();
1135
const toRemove = breakpoints.filter(bp => !id || bp.getId() === id);
1136
// note: using the debugger-resolved uri for aria to reflect UI state
1137
toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath)));
1138
const urisToClear = new Set(toRemove.map(bp => bp.originalUri.toString()));
1139
1140
this.model.removeBreakpoints(toRemove);
1141
this.unlinkTriggeredBreakpoints(breakpoints, toRemove).forEach(uri => urisToClear.add(uri.toString()));
1142
1143
this.debugStorage.storeBreakpoints(this.model);
1144
await Promise.all([...urisToClear].map(uri => this.sendBreakpoints(URI.parse(uri))));
1145
}
1146
1147
setBreakpointsActivated(activated: boolean): Promise<void> {
1148
this.model.setBreakpointsActivated(activated);
1149
return this.sendAllBreakpoints();
1150
}
1151
1152
async addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): Promise<void> {
1153
this.model.addFunctionBreakpoint(opts ?? { name: '' }, id);
1154
// If opts not provided, sending the breakpoint is handled by a later to call to `updateFunctionBreakpoint`
1155
if (opts) {
1156
this.debugStorage.storeBreakpoints(this.model);
1157
await this.sendFunctionBreakpoints();
1158
this.debugStorage.storeBreakpoints(this.model);
1159
}
1160
}
1161
1162
async updateFunctionBreakpoint(id: string, update: { name?: string; hitCondition?: string; condition?: string }): Promise<void> {
1163
this.model.updateFunctionBreakpoint(id, update);
1164
this.debugStorage.storeBreakpoints(this.model);
1165
await this.sendFunctionBreakpoints();
1166
}
1167
1168
async removeFunctionBreakpoints(id?: string): Promise<void> {
1169
this.model.removeFunctionBreakpoints(id);
1170
this.debugStorage.storeBreakpoints(this.model);
1171
await this.sendFunctionBreakpoints();
1172
}
1173
1174
async addDataBreakpoint(opts: IDataBreakpointOptions): Promise<void> {
1175
this.model.addDataBreakpoint(opts);
1176
this.debugStorage.storeBreakpoints(this.model);
1177
await this.sendDataBreakpoints();
1178
this.debugStorage.storeBreakpoints(this.model);
1179
}
1180
1181
async updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): Promise<void> {
1182
this.model.updateDataBreakpoint(id, update);
1183
this.debugStorage.storeBreakpoints(this.model);
1184
await this.sendDataBreakpoints();
1185
}
1186
1187
async removeDataBreakpoints(id?: string): Promise<void> {
1188
this.model.removeDataBreakpoints(id);
1189
this.debugStorage.storeBreakpoints(this.model);
1190
await this.sendDataBreakpoints();
1191
}
1192
1193
async addInstructionBreakpoint(opts: IInstructionBreakpointOptions): Promise<void> {
1194
this.model.addInstructionBreakpoint(opts);
1195
this.debugStorage.storeBreakpoints(this.model);
1196
await this.sendInstructionBreakpoints();
1197
this.debugStorage.storeBreakpoints(this.model);
1198
}
1199
1200
async removeInstructionBreakpoints(instructionReference?: string, offset?: number): Promise<void> {
1201
this.model.removeInstructionBreakpoints(instructionReference, offset);
1202
this.debugStorage.storeBreakpoints(this.model);
1203
await this.sendInstructionBreakpoints();
1204
}
1205
1206
setExceptionBreakpointFallbackSession(sessionId: string) {
1207
this.model.setExceptionBreakpointFallbackSession(sessionId);
1208
this.debugStorage.storeBreakpoints(this.model);
1209
}
1210
1211
setExceptionBreakpointsForSession(session: IDebugSession, filters: DebugProtocol.ExceptionBreakpointsFilter[]): void {
1212
this.model.setExceptionBreakpointsForSession(session.getId(), filters);
1213
this.debugStorage.storeBreakpoints(this.model);
1214
}
1215
1216
async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise<void> {
1217
this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition);
1218
this.debugStorage.storeBreakpoints(this.model);
1219
await this.sendExceptionBreakpoints();
1220
}
1221
1222
async sendAllBreakpoints(session?: IDebugSession): Promise<void> {
1223
const setBreakpointsPromises = distinct(this.model.getBreakpoints(), bp => bp.originalUri.toString())
1224
.map(bp => this.sendBreakpoints(bp.originalUri, false, session));
1225
1226
// If sending breakpoints to one session which we know supports the configurationDone request, can make all requests in parallel
1227
if (session?.capabilities.supportsConfigurationDoneRequest) {
1228
await Promise.all([
1229
...setBreakpointsPromises,
1230
this.sendFunctionBreakpoints(session),
1231
this.sendDataBreakpoints(session),
1232
this.sendInstructionBreakpoints(session),
1233
this.sendExceptionBreakpoints(session),
1234
]);
1235
} else {
1236
await Promise.all(setBreakpointsPromises);
1237
await this.sendFunctionBreakpoints(session);
1238
await this.sendDataBreakpoints(session);
1239
await this.sendInstructionBreakpoints(session);
1240
// send exception breakpoints at the end since some debug adapters may rely on the order - this was the case before
1241
// the configurationDone request was introduced.
1242
await this.sendExceptionBreakpoints(session);
1243
}
1244
}
1245
1246
/**
1247
* Removes the condition of triggered breakpoints that depended on
1248
* breakpoints in `removedBreakpoints`. Returns the URIs of resources that
1249
* had their breakpoints changed in this way.
1250
*/
1251
private unlinkTriggeredBreakpoints(allBreakpoints: readonly IBreakpoint[], removedBreakpoints: readonly IBreakpoint[]): uri[] {
1252
const affectedUris: uri[] = [];
1253
for (const removed of removedBreakpoints) {
1254
for (const existing of allBreakpoints) {
1255
if (!removedBreakpoints.includes(existing) && existing.triggeredBy === removed.getId()) {
1256
this.model.updateBreakpoints(new Map([[existing.getId(), { triggeredBy: undefined }]]));
1257
affectedUris.push(existing.originalUri);
1258
}
1259
}
1260
}
1261
1262
return affectedUris;
1263
}
1264
1265
private async makeTriggeredBreakpointsMatchEnablement(enable: boolean, breakpoint: Breakpoint) {
1266
if (enable) {
1267
/** If the breakpoint is being enabled, also ensure its triggerer is enabled */
1268
if (breakpoint.triggeredBy) {
1269
const trigger = this.model.getBreakpoints().find(bp => breakpoint.triggeredBy === bp.getId());
1270
if (trigger && !trigger.enabled) {
1271
await this.enableOrDisableBreakpoints(enable, trigger);
1272
}
1273
}
1274
}
1275
1276
1277
/** Makes its triggeree states match the state of this breakpoint */
1278
await Promise.all(this.model.getBreakpoints()
1279
.filter(bp => bp.triggeredBy === breakpoint.getId() && bp.enabled !== enable)
1280
.map(bp => this.enableOrDisableBreakpoints(enable, bp))
1281
);
1282
}
1283
1284
public async sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): Promise<void> {
1285
const breakpointsToSend = this.model.getBreakpoints({ originalUri: modelUri, enabledOnly: true });
1286
await sendToOneOrAllSessions(this.model, session, async s => {
1287
if (!s.configuration.noDebug) {
1288
const sessionBps = breakpointsToSend.filter(bp => !bp.triggeredBy || bp.getSessionDidTrigger(s.getId()));
1289
await s.sendBreakpoints(modelUri, sessionBps, sourceModified);
1290
}
1291
});
1292
}
1293
1294
private async sendFunctionBreakpoints(session?: IDebugSession): Promise<void> {
1295
const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1296
1297
await sendToOneOrAllSessions(this.model, session, async s => {
1298
if (s.capabilities.supportsFunctionBreakpoints && !s.configuration.noDebug) {
1299
await s.sendFunctionBreakpoints(breakpointsToSend);
1300
}
1301
});
1302
}
1303
1304
private async sendDataBreakpoints(session?: IDebugSession): Promise<void> {
1305
const breakpointsToSend = this.model.getDataBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1306
1307
await sendToOneOrAllSessions(this.model, session, async s => {
1308
if (s.capabilities.supportsDataBreakpoints && !s.configuration.noDebug) {
1309
await s.sendDataBreakpoints(breakpointsToSend);
1310
}
1311
});
1312
}
1313
1314
private async sendInstructionBreakpoints(session?: IDebugSession): Promise<void> {
1315
const breakpointsToSend = this.model.getInstructionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated());
1316
1317
await sendToOneOrAllSessions(this.model, session, async s => {
1318
if (s.capabilities.supportsInstructionBreakpoints && !s.configuration.noDebug) {
1319
await s.sendInstructionBreakpoints(breakpointsToSend);
1320
}
1321
});
1322
}
1323
1324
private sendExceptionBreakpoints(session?: IDebugSession): Promise<void> {
1325
return sendToOneOrAllSessions(this.model, session, async s => {
1326
const enabledExceptionBps = this.model.getExceptionBreakpointsForSession(s.getId()).filter(exb => exb.enabled);
1327
if (s.capabilities.supportsConfigurationDoneRequest && (!s.capabilities.exceptionBreakpointFilters || s.capabilities.exceptionBreakpointFilters.length === 0)) {
1328
// Only call `setExceptionBreakpoints` as specified in dap protocol #90001
1329
return;
1330
}
1331
if (!s.configuration.noDebug) {
1332
await s.sendExceptionBreakpoints(enabledExceptionBps);
1333
}
1334
});
1335
}
1336
1337
private onFileChanges(fileChangesEvent: FileChangesEvent): void {
1338
const toRemove = this.model.getBreakpoints().filter(bp =>
1339
fileChangesEvent.contains(bp.originalUri, FileChangeType.DELETED));
1340
if (toRemove.length) {
1341
this.model.removeBreakpoints(toRemove);
1342
}
1343
1344
const toSend: URI[] = [];
1345
for (const uri of this.breakpointsToSendOnResourceSaved) {
1346
if (fileChangesEvent.contains(uri, FileChangeType.UPDATED)) {
1347
toSend.push(uri);
1348
}
1349
}
1350
1351
for (const uri of toSend) {
1352
this.breakpointsToSendOnResourceSaved.delete(uri);
1353
this.sendBreakpoints(uri, true);
1354
}
1355
}
1356
1357
async runTo(uri: uri, lineNumber: number, column?: number): Promise<void> {
1358
let breakpointToRemove: IBreakpoint | undefined;
1359
let threadToContinue = this.getViewModel().focusedThread;
1360
const addTempBreakPoint = async () => {
1361
const bpExists = !!(this.getModel().getBreakpoints({ column, lineNumber, uri }).length);
1362
1363
if (!bpExists) {
1364
const addResult = await this.addAndValidateBreakpoints(uri, lineNumber, column);
1365
if (addResult.thread) {
1366
threadToContinue = addResult.thread;
1367
}
1368
1369
if (addResult.breakpoint) {
1370
breakpointToRemove = addResult.breakpoint;
1371
}
1372
}
1373
return { threadToContinue, breakpointToRemove };
1374
};
1375
const removeTempBreakPoint = (state: State): boolean => {
1376
if (state === State.Stopped || state === State.Inactive) {
1377
if (breakpointToRemove) {
1378
this.removeBreakpoints(breakpointToRemove.getId());
1379
}
1380
return true;
1381
}
1382
return false;
1383
};
1384
1385
await addTempBreakPoint();
1386
if (this.state === State.Inactive) {
1387
// If no session exists start the debugger
1388
const { launch, name, getConfig } = this.getConfigurationManager().selectedConfiguration;
1389
const config = await getConfig();
1390
const configOrName = config ? Object.assign(deepClone(config), {}) : name;
1391
const listener = this.onDidChangeState(state => {
1392
if (removeTempBreakPoint(state)) {
1393
listener.dispose();
1394
}
1395
});
1396
await this.startDebugging(launch, configOrName, undefined, true);
1397
}
1398
if (this.state === State.Stopped) {
1399
const focusedSession = this.getViewModel().focusedSession;
1400
if (!focusedSession || !threadToContinue) {
1401
return;
1402
}
1403
1404
const listener = threadToContinue.session.onDidChangeState(() => {
1405
if (removeTempBreakPoint(focusedSession.state)) {
1406
listener.dispose();
1407
}
1408
});
1409
await threadToContinue.continue();
1410
}
1411
}
1412
1413
private async addAndValidateBreakpoints(uri: URI, lineNumber: number, column?: number) {
1414
const debugModel = this.getModel();
1415
const viewModel = this.getViewModel();
1416
1417
const breakpoints = await this.addBreakpoints(uri, [{ lineNumber, column }], false);
1418
const breakpoint = breakpoints?.[0];
1419
if (!breakpoint) {
1420
return { breakpoint: undefined, thread: viewModel.focusedThread };
1421
}
1422
1423
// If the breakpoint was not initially verified, wait up to 2s for it to become so.
1424
// Inherently racey if multiple sessions can verify async, but not solvable...
1425
if (!breakpoint.verified) {
1426
let listener: IDisposable;
1427
await raceTimeout(new Promise<void>(resolve => {
1428
listener = debugModel.onDidChangeBreakpoints(() => {
1429
if (breakpoint.verified) {
1430
resolve();
1431
}
1432
});
1433
}), 2000);
1434
listener!.dispose();
1435
}
1436
1437
// Look at paused threads for sessions that verified this bp. Prefer, in order:
1438
const enum Score {
1439
/** The focused thread */
1440
Focused,
1441
/** Any other stopped thread of a session that verified the bp */
1442
Verified,
1443
/** Any thread that verified and paused in the same file */
1444
VerifiedAndPausedInFile,
1445
/** The focused thread if it verified the breakpoint */
1446
VerifiedAndFocused,
1447
}
1448
1449
let bestThread = viewModel.focusedThread;
1450
let bestScore = Score.Focused;
1451
for (const sessionId of breakpoint.sessionsThatVerified) {
1452
const session = debugModel.getSession(sessionId);
1453
if (!session) {
1454
continue;
1455
}
1456
1457
const threads = session.getAllThreads().filter(t => t.stopped);
1458
if (bestScore < Score.VerifiedAndFocused) {
1459
if (viewModel.focusedThread && threads.includes(viewModel.focusedThread)) {
1460
bestThread = viewModel.focusedThread;
1461
bestScore = Score.VerifiedAndFocused;
1462
}
1463
}
1464
1465
if (bestScore < Score.VerifiedAndPausedInFile) {
1466
const pausedInThisFile = threads.find(t => {
1467
const top = t.getTopStackFrame();
1468
return top && this.uriIdentityService.extUri.isEqual(top.source.uri, uri);
1469
});
1470
1471
if (pausedInThisFile) {
1472
bestThread = pausedInThisFile;
1473
bestScore = Score.VerifiedAndPausedInFile;
1474
}
1475
}
1476
1477
if (bestScore < Score.Verified) {
1478
bestThread = threads[0];
1479
bestScore = Score.VerifiedAndPausedInFile;
1480
}
1481
}
1482
1483
return { thread: bestThread, breakpoint };
1484
}
1485
}
1486
1487
export function getStackFrameThreadAndSessionToFocus(model: IDebugModel, stackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, avoidSession?: IDebugSession): { stackFrame: IStackFrame | undefined; thread: IThread | undefined; session: IDebugSession | undefined } {
1488
if (!session) {
1489
if (stackFrame || thread) {
1490
session = stackFrame ? stackFrame.thread.session : thread!.session;
1491
} else {
1492
const sessions = model.getSessions();
1493
const stoppedSession = sessions.find(s => s.state === State.Stopped);
1494
// Make sure to not focus session that is going down
1495
session = stoppedSession || sessions.find(s => s !== avoidSession && s !== avoidSession?.parentSession) || (sessions.length ? sessions[0] : undefined);
1496
}
1497
}
1498
1499
if (!thread) {
1500
if (stackFrame) {
1501
thread = stackFrame.thread;
1502
} else {
1503
const threads = session ? session.getAllThreads() : undefined;
1504
const stoppedThread = threads && threads.find(t => t.stopped);
1505
thread = stoppedThread || (threads && threads.length ? threads[0] : undefined);
1506
}
1507
}
1508
1509
if (!stackFrame && thread) {
1510
stackFrame = thread.getTopStackFrame();
1511
}
1512
1513
return { session, thread, stackFrame };
1514
}
1515
1516
async function sendToOneOrAllSessions(model: DebugModel, session: IDebugSession | undefined, send: (session: IDebugSession) => Promise<void>): Promise<void> {
1517
if (session) {
1518
await send(session);
1519
} else {
1520
await Promise.all(model.getSessions().map(s => send(s)));
1521
}
1522
}
1523
1524