/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as aria from 'vs/base/browser/ui/aria/aria'; import { distinct } from 'vs/base/common/arrays'; import { Queue, RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import * as resources from 'vs/base/common/resources'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; import { INewReplElementData, ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; export class DebugSession implements IDebugSession, IDisposable { parentSession: IDebugSession | undefined; private _subId: string | undefined; raw: RawDebugSession | undefined; // used in tests private initialized = false; private _options: IDebugSessionOptions; private sources = new Map(); private threads = new Map(); private threadIds: number[] = []; private cancellationMap = new Map(); private readonly rawListeners = new DisposableStore(); private readonly globalDisposables = new DisposableStore(); private fetchThreadsScheduler: RunOnceScheduler | undefined; private passFocusScheduler: RunOnceScheduler; private lastContinuedThreadId: number | undefined; private repl: ReplModel; private stoppedDetails: IRawStoppedDetails[] = []; private readonly statusQueue = this.rawListeners.add(new ThreadStatusScheduler()); private readonly _onDidChangeState = new Emitter(); private readonly _onDidEndAdapter = new Emitter(); private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidProgressStart = new Emitter(); private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidInvalidMemory = new Emitter(); private readonly _onDidChangeREPLElements = new Emitter(); private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); /** * Promise set while enabling dependent breakpoints to block the debugger * from continuing from a stopped state. */ private _waitToResume?: Promise; constructor( private id: string, private _configuration: { resolved: IConfig; unresolved: IConfig | undefined }, public root: IWorkspaceFolder | undefined, private model: DebugModel, options: IDebugSessionOptions | undefined, @IDebugService private readonly debugService: IDebugService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IHostService private readonly hostService: IHostService, @IConfigurationService private readonly configurationService: IConfigurationService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, @INotificationService private readonly notificationService: INotificationService, @ILifecycleService lifecycleService: ILifecycleService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ICustomEndpointTelemetryService private readonly customEndpointTelemetryService: ICustomEndpointTelemetryService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @ILogService private readonly logService: ILogService ) { this._options = options || {}; this.parentSession = this._options.parentSession; if (this.hasSeparateRepl()) { this.repl = new ReplModel(this.configurationService); } else { this.repl = (this.parentSession as DebugSession).repl; } const toDispose = this.globalDisposables; const replListener = toDispose.add(new MutableDisposable()); replListener.value = this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); if (lifecycleService) { toDispose.add(lifecycleService.onWillShutdown(() => { this.shutdown(); dispose(toDispose); })); } const compoundRoot = this._options.compoundRoot; if (compoundRoot) { toDispose.add(compoundRoot.onDidSessionStop(() => this.terminate())); } this.passFocusScheduler = new RunOnceScheduler(() => { // If there is some session or thread that is stopped pass focus to it if (this.debugService.getModel().getSessions().some(s => s.state === State.Stopped) || this.getAllThreads().some(t => t.stopped)) { if (typeof this.lastContinuedThreadId === 'number') { const thread = this.debugService.getViewModel().focusedThread; if (thread && thread.threadId === this.lastContinuedThreadId && !thread.stopped) { const toFocusThreadId = this.getStoppedDetails()?.threadId; const toFocusThread = typeof toFocusThreadId === 'number' ? this.getThread(toFocusThreadId) : undefined; this.debugService.focusStackFrame(undefined, toFocusThread); } } else { const session = this.debugService.getViewModel().focusedSession; if (session && session.getId() === this.getId() && session.state !== State.Stopped) { this.debugService.focusStackFrame(undefined); } } } }, 800); const parent = this._options.parentSession; if (parent) { toDispose.add(parent.onDidEndAdapter(() => { // copy the parent repl and get a new detached repl for this child, and // remove its parent, if it's still running if (!this.hasSeparateRepl() && this.raw?.isInShutdown === false) { this.repl = this.repl.clone(); replListener.value = this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()); this.parentSession = undefined; } })); } } getId(): string { return this.id; } setSubId(subId: string | undefined) { this._subId = subId; } getMemory(memoryReference: string): IMemoryRegion { return new MemoryRegion(memoryReference, this); } get subId(): string | undefined { return this._subId; } get configuration(): IConfig { return this._configuration.resolved; } get unresolvedConfiguration(): IConfig | undefined { return this._configuration.unresolved; } get lifecycleManagedByParent(): boolean { return !!this._options.lifecycleManagedByParent; } get compact(): boolean { return !!this._options.compact; } get saveBeforeRestart(): boolean { return this._options.saveBeforeRestart ?? !this._options?.parentSession; } get compoundRoot(): DebugCompoundRoot | undefined { return this._options.compoundRoot; } get suppressDebugStatusbar(): boolean { return this._options.suppressDebugStatusbar ?? false; } get suppressDebugToolbar(): boolean { return this._options.suppressDebugToolbar ?? false; } get suppressDebugView(): boolean { return this._options.suppressDebugView ?? false; } get autoExpandLazyVariables(): boolean { // This tiny helper avoids converting the entire debug model to use service injection return this.configurationService.getValue('debug').autoExpandLazyVariables; } setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) { this._configuration = configuration; } getLabel(): string { const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1; return includeRoot && this.root ? `${this.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.name; } setName(name: string): void { this._name = name; this._onDidChangeName.fire(name); } get name(): string { return this._name || this.configuration.name; } get state(): State { if (!this.initialized) { return State.Initializing; } if (!this.raw) { return State.Inactive; } const focusedThread = this.debugService.getViewModel().focusedThread; if (focusedThread && focusedThread.session === this) { return focusedThread.stopped ? State.Stopped : State.Running; } if (this.getAllThreads().some(t => t.stopped)) { return State.Stopped; } return State.Running; } get capabilities(): DebugProtocol.Capabilities { return this.raw ? this.raw.capabilities : Object.create(null); } //---- events get onDidChangeState(): Event { return this._onDidChangeState.event; } get onDidEndAdapter(): Event { return this._onDidEndAdapter.event; } get onDidChangeReplElements(): Event { return this._onDidChangeREPLElements.event; } get onDidChangeName(): Event { return this._onDidChangeName.event; } //---- DAP events get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } get onDidLoadedSource(): Event { return this._onDidLoadedSource.event; } get onDidProgressStart(): Event { return this._onDidProgressStart.event; } get onDidProgressUpdate(): Event { return this._onDidProgressUpdate.event; } get onDidProgressEnd(): Event { return this._onDidProgressEnd.event; } get onDidInvalidateMemory(): Event { return this._onDidInvalidMemory.event; } //---- DAP requests /** * create and initialize a new debug adapter for this session */ async initialize(dbgr: IDebugger): Promise { if (this.raw) { // if there was already a connection make sure to remove old listeners await this.shutdown(); } try { const debugAdapter = await dbgr.createDebugAdapter(this); this.raw = this.instantiationService.createInstance(RawDebugSession, debugAdapter, dbgr, this.id, this.configuration.name); await this.raw.start(); this.registerListeners(); await this.raw.initialize({ clientID: 'vscode', clientName: this.productService.nameLong, adapterID: this.configuration.type, pathFormat: 'path', linesStartAt1: true, columnsStartAt1: true, supportsVariableType: true, // #8858 supportsVariablePaging: true, // #9537 supportsRunInTerminalRequest: true, // #10574 locale: platform.language, // #169114 supportsProgressReporting: true, // #92253 supportsInvalidatedEvent: true, // #106745 supportsMemoryReferences: true, //#129684 supportsArgsCanBeInterpretedByShell: true, // #149910 supportsMemoryEvent: true, // #133643 supportsStartDebuggingRequest: true }); this.initialized = true; this._onDidChangeState.fire(); this.debugService.setExceptionBreakpointsForSession(this, (this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); await this.shutdown(); throw err; } } /** * launch or attach to the debuggee */ async launchOrAttach(config: IConfig): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'launch or attach')); } if (this.parentSession && this.parentSession.state === State.Inactive) { throw canceled(); } // __sessionID only used for EH debugging (but we add it always for now...) config.__sessionId = this.getId(); try { await this.raw.launchOrAttach(config); } catch (err) { this.shutdown(); throw err; } } /** * terminate the current debug adapter session */ async terminate(restart = false): Promise { if (!this.raw) { // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent this.onDidExitAdapter(); } this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.terminate(restart); } else if (this.raw) { if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { await this.raw.terminate(restart); } else { await this.raw.disconnect({ restart, terminateDebuggee: true }); } } if (!restart) { this._options.compoundRoot?.sessionStopped(); } } /** * end the current debug adapter session */ async disconnect(restart = false, suspend = false): Promise { if (!this.raw) { // Adapter went down but it did not send a 'terminated' event, simulate like the event has been sent this.onDidExitAdapter(); } this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.disconnect(restart, suspend); } else if (this.raw) { // TODO terminateDebuggee should be undefined by default? await this.raw.disconnect({ restart, terminateDebuggee: false, suspendDebuggee: suspend }); } if (!restart) { this._options.compoundRoot?.sessionStopped(); } } /** * restart debug adapter session */ async restart(): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restart')); } this.cancelAllRequests(); if (this._options.lifecycleManagedByParent && this.parentSession) { await this.parentSession.restart(); } else { await this.raw.restart({ arguments: this.configuration }); } } async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints')); } if (!this.raw.readyForBreakpoints) { return Promise.resolve(undefined); } const rawSource = this.getRawSource(modelUri); if (breakpointsToSend.length && !rawSource.adapterData) { rawSource.adapterData = breakpointsToSend[0].adapterData; } // Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959 if (rawSource.path) { rawSource.path = normalizeDriveLetter(rawSource.path); } const response = await this.raw.setBreakpoints({ source: rawSource, lines: breakpointsToSend.map(bp => bp.sessionAgnosticData.lineNumber), breakpoints: breakpointsToSend.map(bp => ({ line: bp.sessionAgnosticData.lineNumber, column: bp.sessionAgnosticData.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })), sourceModified }); if (response && response.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'function breakpoints')); } if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts }); if (response && response.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } } async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exception breakpoints')); } if (this.raw.readyForBreakpoints) { const args: DebugProtocol.SetExceptionBreakpointsArguments = this.capabilities.supportsExceptionFilterOptions ? { filters: [], filterOptions: exbpts.map(exb => { if (exb.condition) { return { filterId: exb.filter, condition: exb.condition }; } return { filterId: exb.filter }; }) } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); if (response && response.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } } async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } if (!this.raw.readyForBreakpoints) { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); return response?.body; } async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints')); } if (this.raw.readyForBreakpoints) { const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints }); if (response && response.body) { const data = new Map(); for (let i = 0; i < dataBreakpoints.length; i++) { data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } } async sendInstructionBreakpoints(instructionBreakpoints: IInstructionBreakpoint[]): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'instruction breakpoints')); } if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toJSON()) }); if (response && response.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } } async breakpointsLocations(uri: URI, lineNumber: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints locations')); } const source = this.getRawSource(uri); const response = await this.raw.breakpointLocations({ source, line: lineNumber }); if (!response || !response.body || !response.body.breakpoints) { return []; } const positions = response.body.breakpoints.map(bp => ({ lineNumber: bp.line, column: bp.column || 1 })); return distinct(positions, p => `${p.lineNumber}:${p.column}`); } getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined { return this.model.getDebugProtocolBreakpoint(breakpointId, this.getId()); } customRequest(request: string, args: any): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", request)); } return this.raw.custom(request, args); } stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stackTrace')); } const sessionToken = this.getNewCancellationToken(threadId, token); return this.raw.stackTrace({ threadId, startFrame, levels }, sessionToken); } async exceptionInfo(threadId: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exceptionInfo')); } const response = await this.raw.exceptionInfo({ threadId }); if (response) { return { id: response.body.exceptionId, description: response.body.description, breakMode: response.body.breakMode, details: response.body.details }; } return undefined; } scopes(frameId: number, threadId: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'scopes')); } const token = this.getNewCancellationToken(threadId); return this.raw.scopes({ frameId }, token); } variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'variables')); } const token = threadId ? this.getNewCancellationToken(threadId) : undefined; return this.raw.variables({ variablesReference, filter, start, count }, token); } evaluate(expression: string, frameId: number, context?: string): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate')); } return this.raw.evaluate({ expression, frameId, context }); } async restartFrame(frameId: number, threadId: number): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restartFrame')); } await this.raw.restartFrame({ frameId }, threadId); } private setLastSteppingGranularity(threadId: number, granularity?: DebugProtocol.SteppingGranularity) { const thread = this.getThread(threadId); if (thread) { thread.lastSteppingGranularity = granularity; } } async next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'next')); } this.setLastSteppingGranularity(threadId, granularity); await this.raw.next({ threadId, granularity }); } async stepIn(threadId: number, targetId?: number, granularity?: DebugProtocol.SteppingGranularity): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepIn')); } this.setLastSteppingGranularity(threadId, granularity); await this.raw.stepIn({ threadId, targetId, granularity }); } async stepOut(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepOut')); } this.setLastSteppingGranularity(threadId, granularity); await this.raw.stepOut({ threadId, granularity }); } async stepBack(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepBack')); } this.setLastSteppingGranularity(threadId, granularity); await this.raw.stepBack({ threadId, granularity }); } async continue(threadId: number): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'continue')); } await this.raw.continue({ threadId }); } async reverseContinue(threadId: number): Promise { await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'reverse continue')); } await this.raw.reverseContinue({ threadId }); } async pause(threadId: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'pause')); } await this.raw.pause({ threadId }); } async terminateThreads(threadIds?: number[]): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'terminateThreads')); } await this.raw.terminateThreads({ threadIds }); } setVariable(variablesReference: number, name: string, value: string): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setVariable')); } return this.raw.setVariable({ variablesReference, name, value }); } setExpression(frameId: number, expression: string, value: string): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setExpression')); } return this.raw.setExpression({ expression, value, frameId }); } gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'gotoTargets')); } return this.raw.gotoTargets({ source, line, column }); } goto(threadId: number, targetId: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'goto')); } return this.raw.goto({ threadId, targetId }); } loadSource(resource: URI): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'loadSource'))); } const source = this.getSourceForUri(resource); let rawSource: DebugProtocol.Source; if (source) { rawSource = source.raw; } else { // create a Source const data = Source.getEncodedDebugData(resource); rawSource = { path: data.path, sourceReference: data.sourceReference }; } return this.raw.source({ sourceReference: rawSource.sourceReference || 0, source: rawSource }); } async getLoadedSources(): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'getLoadedSources'))); } const response = await this.raw.loadedSources({}); if (response && response.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; } } async completions(frameId: number | undefined, threadId: number, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'completions'))); } const sessionCancelationToken = this.getNewCancellationToken(threadId, token); return this.raw.completions({ frameId, text, column: position.column, line: position.lineNumber, }, sessionCancelationToken); } async stepInTargets(frameId: number): Promise<{ id: number; label: string }[] | undefined> { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepInTargets'))); } const response = await this.raw.stepInTargets({ frameId }); return response?.body.targets; } async cancel(progressId: string): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'cancel'))); } return this.raw.cancel({ progressId }); } async disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble'))); } const response = await this.raw.disassemble({ memoryReference, offset, instructionOffset, instructionCount, resolveSymbols: true }); return response?.body?.instructions; } readMemory(memoryReference: string, offset: number, count: number): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'readMemory'))); } return this.raw.readMemory({ count, memoryReference, offset }); } writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble'))); } return this.raw.writeMemory({ memoryReference, offset, allowPartial, data }); } //---- threads getThread(threadId: number): Thread | undefined { return this.threads.get(threadId); } getAllThreads(): IThread[] { const result: IThread[] = []; this.threadIds.forEach((threadId) => { const thread = this.threads.get(threadId); if (thread) { result.push(thread); } }); return result; } clearThreads(removeThreads: boolean, reference: number | undefined = undefined): void { if (reference !== undefined && reference !== null) { const thread = this.threads.get(reference); if (thread) { thread.clearCallStack(); thread.stoppedDetails = undefined; thread.stopped = false; if (removeThreads) { this.threads.delete(reference); } } } else { this.threads.forEach(thread => { thread.clearCallStack(); thread.stoppedDetails = undefined; thread.stopped = false; }); if (removeThreads) { this.threads.clear(); this.threadIds = []; ExpressionContainer.allValues.clear(); } } } getStoppedDetails(): IRawStoppedDetails | undefined { return this.stoppedDetails.length >= 1 ? this.stoppedDetails[0] : undefined; } rawUpdate(data: IRawModelUpdate): void { this.threadIds = []; data.threads.forEach(thread => { this.threadIds.push(thread.id); if (!this.threads.has(thread.id)) { // A new thread came in, initialize it. this.threads.set(thread.id, new Thread(this, thread.name, thread.id)); } else if (thread.name) { // Just the thread name got updated #18244 const oldThread = this.threads.get(thread.id); if (oldThread) { oldThread.name = thread.name; } } }); this.threads.forEach(t => { // Remove all old threads which are no longer part of the update #75980 if (this.threadIds.indexOf(t.threadId) === -1) { this.threads.delete(t.threadId); } }); const stoppedDetails = data.stoppedDetails; if (stoppedDetails) { // Set the availability of the threads' callstacks depending on // whether the thread is stopped or not if (stoppedDetails.allThreadsStopped) { this.threads.forEach(thread => { thread.stoppedDetails = thread.threadId === stoppedDetails.threadId ? stoppedDetails : { reason: thread.stoppedDetails?.reason }; thread.stopped = true; thread.clearCallStack(); }); } else { const thread = typeof stoppedDetails.threadId === 'number' ? this.threads.get(stoppedDetails.threadId) : undefined; if (thread) { // One thread is stopped, only update that thread. thread.stoppedDetails = stoppedDetails; thread.clearCallStack(); thread.stopped = true; } } } } private waitForTriggeredBreakpoints() { if (!this._waitToResume) { return; } return raceTimeout( this._waitToResume, TRIGGERED_BREAKPOINT_MAX_DELAY ); } private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); if (response && response.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, stoppedDetails }); } } } initializeForTest(raw: RawDebugSession): void { this.raw = raw; this.registerListeners(); } //---- private private registerListeners(): void { if (!this.raw) { return; } this.rawListeners.add(this.raw.onDidInitialize(async () => { aria.status( this.configuration.noDebug ? localize('debuggingStartedNoDebug', "Started running without debugging.") : localize('debuggingStarted', "Debugging started.") ); const sendConfigurationDone = async () => { if (this.raw && this.raw.capabilities.supportsConfigurationDoneRequest) { try { await this.raw.configurationDone(); } catch (e) { // Disconnect the debug session on configuration done error #10596 this.notificationService.error(e); this.raw?.disconnect({}); } } return undefined; }; // Send all breakpoints try { await this.debugService.sendAllBreakpoints(this); } finally { await sendConfigurationDone(); await this.fetchThreads(); } })); const statusQueue = this.statusQueue; this.rawListeners.add(this.raw.onDidStop(event => this.handleStop(event.body))); this.rawListeners.add(this.raw.onDidThread(event => { statusQueue.cancel([event.body.threadId]); if (event.body.reason === 'started') { // debounce to reduce threadsRequest frequency and improve performance if (!this.fetchThreadsScheduler) { this.fetchThreadsScheduler = new RunOnceScheduler(() => { this.fetchThreads(); }, 100); this.rawListeners.add(this.fetchThreadsScheduler); } if (!this.fetchThreadsScheduler.isScheduled()) { this.fetchThreadsScheduler.schedule(); } } else if (event.body.reason === 'exited') { this.model.clearThreads(this.getId(), true, event.body.threadId); const viewModel = this.debugService.getViewModel(); const focusedThread = viewModel.focusedThread; this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } } })); this.rawListeners.add(this.raw.onDidTerminateDebugee(async event => { aria.status(localize('debuggingStopped', "Debugging stopped.")); if (event.body && event.body.restart) { await this.debugService.restartSession(this, event.body.restart); } else if (this.raw) { await this.raw.disconnect({ terminateDebuggee: false }); } })); this.rawListeners.add(this.raw.onDidContinued(event => { const allThreads = event.body.allThreadsContinued !== false; statusQueue.cancel(allThreads ? undefined : [event.body.threadId]); const threadId = allThreads ? undefined : event.body.threadId; if (typeof threadId === 'number') { this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); const tokens = this.cancellationMap.get(threadId); this.cancellationMap.delete(threadId); tokens?.forEach(t => t.dispose(true)); } else { this.stoppedDetails = []; this.cancelAllRequests(); } this.lastContinuedThreadId = threadId; // We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321 this.passFocusScheduler.schedule(); this.model.clearThreads(this.getId(), false, threadId); this._onDidChangeState.fire(); })); const outputQueue = new Queue(); this.rawListeners.add(this.raw.onDidOutput(async event => { const outputSeverity = event.body.category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info; // When a variables event is received, execute immediately to obtain the variables value #126967 if (event.body.variablesReference) { const source = event.body.source && event.body.line ? { lineNumber: event.body.line, column: event.body.column ? event.body.column : 1, source: this.getSource(event.body.source) } : undefined; const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); const children = container.getChildren(); // we should put appendToRepl into queue to make sure the logs to be displayed in correct order // see https://github.com/microsoft/vscode/issues/126967#issuecomment-874954269 outputQueue.queue(async () => { const resolved = await children; // For single logged variables, try to use the output if we can so // present a better (i.e. ANSI-aware) representation of the output if (resolved.length === 1) { this.appendToRepl({ output: event.body.output, expression: resolved[0], sev: outputSeverity, source }, event.body.category === 'important'); return; } resolved.forEach((child) => { // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) (child).name = null; this.appendToRepl({ output: '', expression: child, sev: outputSeverity, source }, event.body.category === 'important'); }); }); return; } outputQueue.queue(async () => { if (!event.body || !this.raw) { return; } if (event.body.category === 'telemetry') { // only log telemetry events from debug adapter if the debug extension provided the telemetry key // and the user opted in telemetry const telemetryEndpoint = this.raw.dbgr.getCustomTelemetryEndpoint(); if (telemetryEndpoint && this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. let data = event.body.data; if (!telemetryEndpoint.sendErrorTelemetry && event.body.data) { data = filterExceptionsFromTelemetry(event.body.data); } this.customEndpointTelemetryService.publicLog(telemetryEndpoint, event.body.output, data); } return; } // Make sure to append output in the correct order by properly waiting on preivous promises #33822 const source = event.body.source && event.body.line ? { lineNumber: event.body.line, column: event.body.column ? event.body.column : 1, source: this.getSource(event.body.source) } : undefined; if (event.body.group === 'start' || event.body.group === 'startCollapsed') { const expanded = event.body.group === 'start'; this.repl.startGroup(event.body.output || '', expanded, source); return; } if (event.body.group === 'end') { this.repl.endGroup(); if (!event.body.output) { // Only return if the end event does not have additional output in it return; } } if (typeof event.body.output === 'string') { this.appendToRepl({ output: event.body.output, sev: outputSeverity, source }, event.body.category === 'important'); } }); })); this.rawListeners.add(this.raw.onDidBreakpoint(event => { const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; const breakpoint = this.model.getBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id); const functionBreakpoint = this.model.getFunctionBreakpoints().find(bp => bp.getIdFromAdapter(this.getId()) === id); const dataBreakpoint = this.model.getDataBreakpoints().find(dbp => dbp.getIdFromAdapter(this.getId()) === id); const exceptionBreakpoint = this.model.getExceptionBreakpoints().find(excbp => excbp.getIdFromAdapter(this.getId()) === id); if (event.body.reason === 'new' && event.body.breakpoint.source && event.body.breakpoint.line) { const source = this.getSource(event.body.breakpoint.source); const bps = this.model.addBreakpoints(source.uri, [{ column: event.body.breakpoint.column, enabled: true, lineNumber: event.body.breakpoint.line, }], false); if (bps.length === 1) { const data = new Map([[bps[0].getId(), event.body.breakpoint]]); this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } if (event.body.reason === 'removed') { if (breakpoint) { this.model.removeBreakpoints([breakpoint]); } if (functionBreakpoint) { this.model.removeFunctionBreakpoints(functionBreakpoint.getId()); } if (dataBreakpoint) { this.model.removeDataBreakpoints(dataBreakpoint.getId()); } } if (event.body.reason === 'changed') { if (breakpoint) { if (!breakpoint.column) { event.body.breakpoint.column = undefined; } const data = new Map([[breakpoint.getId(), event.body.breakpoint]]); this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } if (functionBreakpoint) { const data = new Map([[functionBreakpoint.getId(), event.body.breakpoint]]); this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } if (dataBreakpoint) { const data = new Map([[dataBreakpoint.getId(), event.body.breakpoint]]); this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } if (exceptionBreakpoint) { const data = new Map([[exceptionBreakpoint.getId(), event.body.breakpoint]]); this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } } })); this.rawListeners.add(this.raw.onDidLoadedSource(event => { this._onDidLoadedSource.fire({ reason: event.body.reason, source: this.getSource(event.body.source) }); })); this.rawListeners.add(this.raw.onDidCustomEvent(event => { this._onDidCustomEvent.fire(event); })); this.rawListeners.add(this.raw.onDidProgressStart(event => { this._onDidProgressStart.fire(event); })); this.rawListeners.add(this.raw.onDidProgressUpdate(event => { this._onDidProgressUpdate.fire(event); })); this.rawListeners.add(this.raw.onDidProgressEnd(event => { this._onDidProgressEnd.fire(event); })); this.rawListeners.add(this.raw.onDidInvalidateMemory(event => { this._onDidInvalidMemory.fire(event); })); this.rawListeners.add(this.raw.onDidInvalidated(async event => { const areas = event.body.areas || ['all']; // If invalidated event only requires to update variables or watch, do that, otherwise refetch threads https://github.com/microsoft/vscode/issues/106745 if (areas.includes('threads') || areas.includes('stacks') || areas.includes('all')) { this.cancelAllRequests(); this.model.clearThreads(this.getId(), true); const details = this.stoppedDetails; this.stoppedDetails.length = 1; await Promise.all(details.map(d => this.handleStop(d))); } const viewModel = this.debugService.getViewModel(); if (viewModel.focusedSession === this) { viewModel.updateViews(); } })); this.rawListeners.add(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event))); } private async handleStop(event: IRawStoppedDetails) { this.passFocusScheduler.cancel(); this.stoppedDetails.push(event); // do this very eagerly if we have hitBreakpointIds, since it may take a // moment for breakpoints to set and we want to do our best to not miss // anything if (event.hitBreakpointIds) { this._waitToResume = this.enableDependentBreakpoints(event.hitBreakpointIds); } this.statusQueue.run( this.fetchThreads(event).then(() => event.threadId === undefined ? this.threadIds : [event.threadId]), async (threadId, token) => { const hasLotsOfThreads = event.threadId === undefined && this.threadIds.length > 10; // If the focus for the current session is on a non-existent thread, clear the focus. const focusedThread = this.debugService.getViewModel().focusedThread; const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId); if (focusedThreadDoesNotExist) { this.debugService.focusStackFrame(undefined, undefined); } const thread = typeof threadId === 'number' ? this.getThread(threadId) : undefined; if (thread) { // Call fetch call stack twice, the first only return the top stack frame. // Second retrieves the rest of the call stack. For performance reasons #25605 // Second call is only done if there's few threads that stopped in this event. const promises = this.model.refreshTopOfCallstack(thread, /* fetchFullStack= */!hasLotsOfThreads); const focus = async () => { if (focusedThreadDoesNotExist || (!event.preserveFocusHint && thread.getCallStack().length)) { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session const preserveFocus = !this.configurationService.getValue('debug').focusEditorOnBreak; await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); } if (thread.stoppedDetails && !token.isCancellationRequested) { if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) { await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); } if (this.configurationService.getValue('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { const activeWindow = getActiveWindow(); if (!activeWindow.document.hasFocus()) { await this.hostService.focus(mainWindow, { force: true /* Application may not be active */ }); } } } } }; await promises.topCallStack; if (!event.hitBreakpointIds) { // if hitBreakpointIds are present, this is handled earlier on this._waitToResume = this.enableDependentBreakpoints(thread); } if (token.isCancellationRequested) { return; } focus(); await promises.wholeCallStack; if (token.isCancellationRequested) { return; } const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize' || focusedStackFrame.presentationHint === 'deemphasize') { // The top stack frame can be deemphesized so try to focus again #68616 focus(); } } this._onDidChangeState.fire(); }, ); } private async enableDependentBreakpoints(hitBreakpointIdsOrThread: Thread | number[]) { let breakpoints: IBreakpoint[]; if (Array.isArray(hitBreakpointIdsOrThread)) { breakpoints = this.model.getBreakpoints().filter(bp => hitBreakpointIdsOrThread.includes(bp.getIdFromAdapter(this.id)!)); } else { const frame = hitBreakpointIdsOrThread.getTopStackFrame(); if (frame === undefined) { return; } if (hitBreakpointIdsOrThread.stoppedDetails && hitBreakpointIdsOrThread.stoppedDetails.reason !== 'breakpoint') { return; } breakpoints = this.getBreakpointsAtPosition(frame.source.uri, frame.range.startLineNumber, frame.range.endLineNumber, frame.range.startColumn, frame.range.endColumn); } // find the current breakpoints // check if the current breakpoints are dependencies, and if so collect and send the dependents to DA const urisToResend = new Set(); this.model.getBreakpoints({ triggeredOnly: true, enabledOnly: true }).forEach(bp => { breakpoints.forEach(cbp => { if (bp.enabled && bp.triggeredBy === cbp.getId()) { bp.setSessionDidTrigger(this.getId()); urisToResend.add(bp.uri.toString()); } }); }); const results: Promise[] = []; urisToResend.forEach((uri) => results.push(this.debugService.sendBreakpoints(URI.parse(uri), undefined, this))); return Promise.all(results); } private getBreakpointsAtPosition(uri: URI, startLineNumber: number, endLineNumber: number, startColumn: number, endColumn: number): IBreakpoint[] { return this.model.getBreakpoints({ uri: uri }).filter(bp => { if (bp.lineNumber < startLineNumber || bp.lineNumber > endLineNumber) { return false; } if (bp.column && (bp.column < startColumn || bp.column > endColumn)) { return false; } return true; }); } private onDidExitAdapter(event?: AdapterEndEvent): void { this.initialized = true; this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); this.shutdown(); this._onDidEndAdapter.fire(event); } // Disconnects and clears state. Session can be initialized again for a new connection. private shutdown(): void { this.rawListeners.clear(); if (this.raw) { // Send out disconnect and immediatly dispose (do not wait for response) #127418 this.raw.disconnect({}); this.raw.dispose(); this.raw = undefined; } this.fetchThreadsScheduler?.dispose(); this.fetchThreadsScheduler = undefined; this.passFocusScheduler.cancel(); this.passFocusScheduler.dispose(); this.model.clearThreads(this.getId(), true); this._onDidChangeState.fire(); } public dispose() { this.cancelAllRequests(); this.rawListeners.dispose(); this.globalDisposables.dispose(); } //---- sources getSourceForUri(uri: URI): Source | undefined { return this.sources.get(this.uriIdentityService.asCanonicalUri(uri).toString()); } getSource(raw?: DebugProtocol.Source): Source { let source = new Source(raw, this.getId(), this.uriIdentityService, this.logService); const uriKey = source.uri.toString(); const found = this.sources.get(uriKey); if (found) { source = found; // merge attributes of new into existing source.raw = mixin(source.raw, raw); if (source.raw && raw) { // Always take the latest presentation hint from adapter #42139 source.raw.presentationHint = raw.presentationHint; } } else { this.sources.set(uriKey, source); } return source; } private getRawSource(uri: URI): DebugProtocol.Source { const source = this.getSourceForUri(uri); if (source) { return source.raw; } else { const data = Source.getEncodedDebugData(uri); return { name: data.name, path: data.path, sourceReference: data.sourceReference }; } } private getNewCancellationToken(threadId: number, token?: CancellationToken): CancellationToken { const tokenSource = new CancellationTokenSource(token); const tokens = this.cancellationMap.get(threadId) || []; tokens.push(tokenSource); this.cancellationMap.set(threadId, tokens); return tokenSource.token; } private cancelAllRequests(): void { this.cancellationMap.forEach(tokens => tokens.forEach(t => t.dispose(true))); this.cancellationMap.clear(); } // REPL getReplElements(): IReplElement[] { return this.repl.getReplElements(); } hasSeparateRepl(): boolean { return !this.parentSession || this._options.repl !== 'mergeWithParent'; } removeReplExpressions(): void { this.repl.removeReplExpressions(); } async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { await this.repl.addReplExpression(this, stackFrame, name); // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. this.debugService.getViewModel().updateViews(); } appendToRepl(data: INewReplElementData, isImportant?: boolean): void { this.repl.appendToRepl(this, data); if (isImportant) { this.notificationService.notify({ message: data.output.toString(), severity: data.sev, source: this.name }); } } } /** * Keeps track of events for threads, and cancels any previous operations for * a thread when the thread goes into a new state. Currently, the operations a thread has are: * * - started * - stopped * - continue * - exited * * In each case, the new state preempts the old state, so we don't need to * queue work, just cancel old work. It's up to the caller to make sure that * no UI effects happen at the point when the `token` is cancelled. */ export class ThreadStatusScheduler extends Disposable { /** * An array of set of thread IDs. When a 'stopped' event is encountered, the * editor refreshes its thread IDs. In the meantime, the thread may change * state it again. So the editor puts a Set into this array when it starts * the refresh, and checks it after the refresh is finished, to see if * any of the threads it looked up should now be invalidated. */ private pendingCancellations: Set[] = []; /** * Cancellation tokens for currently-running operations on threads. */ private readonly threadOps = this._register(new DisposableMap()); /** * Runs the operation. * If thread is undefined it affects all threads. */ public async run(threadIdsP: Promise, operation: (threadId: number, ct: CancellationToken) => Promise) { const cancelledWhileLookingUpThreads = new Set(); this.pendingCancellations.push(cancelledWhileLookingUpThreads); const threadIds = await threadIdsP; // Now that we got our threads, // 1. Remove our pending set, and // 2. Cancel any slower callers who might also have found this thread for (let i = 0; i < this.pendingCancellations.length; i++) { const s = this.pendingCancellations[i]; if (s === cancelledWhileLookingUpThreads) { this.pendingCancellations.splice(i, 1); break; } else { for (const threadId of threadIds) { s.add(threadId); } } } if (cancelledWhileLookingUpThreads.has(undefined)) { return; } await Promise.all(threadIds.map(threadId => { if (cancelledWhileLookingUpThreads.has(threadId)) { return; } this.threadOps.get(threadId)?.cancel(); const cts = new CancellationTokenSource(); this.threadOps.set(threadId, cts); return operation(threadId, cts.token); })); } /** * Cancels all ongoing state operations on the given threads. * If threads is undefined it cancel all threads. */ public cancel(threadIds?: readonly number[]) { if (!threadIds) { for (const [_, op] of this.threadOps) { op.cancel(); } this.threadOps.clearAndDisposeAll(); for (const s of this.pendingCancellations) { s.add(undefined); } } else { for (const threadId of threadIds) { this.threadOps.get(threadId)?.cancel(); this.threadOps.deleteAndDispose(threadId); for (const s of this.pendingCancellations) { s.add(threadId); } } } } }