/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import * as objects from 'vs/base/common/objects'; import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { formatPII, isUri } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/contrib/debug/common/debug'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { env as processEnv } from 'vs/base/common/process'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { INotificationService } from 'vs/platform/notification/common/notification'; /** * This interface represents a single command line argument split into a "prefix" and a "path" half. * The optional "prefix" contains arbitrary text and the optional "path" contains a file system path. * Concatenating both results in the original command line argument. */ interface ILaunchVSCodeArgument { prefix?: string; path?: string; } interface ILaunchVSCodeArguments { args: ILaunchVSCodeArgument[]; env?: { [key: string]: string | null; }; } /** * Encapsulates the DebugAdapter lifecycle and some idiosyncrasies of the Debug Adapter Protocol. */ export class RawDebugSession implements IDisposable { private allThreadsContinued = true; private _readyForBreakpoints = false; private _capabilities: DebugProtocol.Capabilities; // shutdown private debugAdapterStopped = false; private inShutdown = false; private terminated = false; private firedAdapterExitEvent = false; // telemetry private startTime = 0; private didReceiveStoppedEvent = false; // DAP events private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; private readonly _onDidContinued: Emitter; private readonly _onDidTerminateDebugee: Emitter; private readonly _onDidExitDebugee: Emitter; private readonly _onDidThread: Emitter; private readonly _onDidOutput: Emitter; private readonly _onDidBreakpoint: Emitter; private readonly _onDidLoadedSource: Emitter; private readonly _onDidCustomEvent: Emitter; private readonly _onDidEvent: Emitter; // DA events private readonly _onDidExitAdapter: Emitter; private debugAdapter: IDebugAdapter | null; private toDispose: IDisposable[] = []; constructor( debugAdapter: IDebugAdapter, dbgr: IDebugger, private readonly telemetryService: ITelemetryService, public readonly customTelemetryService: ITelemetryService | undefined, private readonly extensionHostDebugService: IExtensionHostDebugService, private readonly openerService: IOpenerService, private readonly notificationService: INotificationService ) { this.debugAdapter = debugAdapter; this._capabilities = Object.create(null); this._onDidInitialize = new Emitter(); this._onDidStop = new Emitter(); this._onDidContinued = new Emitter(); this._onDidTerminateDebugee = new Emitter(); this._onDidExitDebugee = new Emitter(); this._onDidThread = new Emitter(); this._onDidOutput = new Emitter(); this._onDidBreakpoint = new Emitter(); this._onDidLoadedSource = new Emitter(); this._onDidCustomEvent = new Emitter(); this._onDidEvent = new Emitter(); this._onDidExitAdapter = new Emitter(); this.toDispose.push(this.debugAdapter.onError(err => { this.shutdown(err); })); this.toDispose.push(this.debugAdapter.onExit(code => { if (code !== 0) { this.shutdown(new Error(`exit code: ${code}`)); } else { // normal exit this.shutdown(); } })); this.debugAdapter.onEvent(event => { switch (event.event) { case 'initialized': this._readyForBreakpoints = true; this._onDidInitialize.fire(event); break; case 'loadedSource': this._onDidLoadedSource.fire(event); break; case 'capabilities': if (event.body) { const capabilities = (event).body.capabilities; this.mergeCapabilities(capabilities); } break; case 'stopped': this.didReceiveStoppedEvent = true; // telemetry: remember that debugger stopped successfully this._onDidStop.fire(event); break; case 'continued': this.allThreadsContinued = (event).body.allThreadsContinued === false ? false : true; this._onDidContinued.fire(event); break; case 'thread': this._onDidThread.fire(event); break; case 'output': this._onDidOutput.fire(event); break; case 'breakpoint': this._onDidBreakpoint.fire(event); break; case 'terminated': this._onDidTerminateDebugee.fire(event); break; case 'exit': this._onDidExitDebugee.fire(event); break; default: this._onDidCustomEvent.fire(event); break; } this._onDidEvent.fire(event); }); this.debugAdapter.onRequest(request => this.dispatchRequest(request, dbgr)); } get onDidExitAdapter(): Event { return this._onDidExitAdapter.event; } get capabilities(): DebugProtocol.Capabilities { return this._capabilities; } /** * DA is ready to accepts setBreakpoint requests. * Becomes true after "initialized" events has been received. */ get readyForBreakpoints(): boolean { return this._readyForBreakpoints; } //---- DAP events get onDidInitialize(): Event { return this._onDidInitialize.event; } get onDidStop(): Event { return this._onDidStop.event; } get onDidContinued(): Event { return this._onDidContinued.event; } get onDidTerminateDebugee(): Event { return this._onDidTerminateDebugee.event; } get onDidExitDebugee(): Event { return this._onDidExitDebugee.event; } get onDidThread(): Event { return this._onDidThread.event; } get onDidOutput(): Event { return this._onDidOutput.event; } get onDidBreakpoint(): Event { return this._onDidBreakpoint.event; } get onDidLoadedSource(): Event { return this._onDidLoadedSource.event; } get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } get onDidEvent(): Event { return this._onDidEvent.event; } //---- DebugAdapter lifecycle /** * Starts the underlying debug adapter and tracks the session time for telemetry. */ async start(): Promise { if (!this.debugAdapter) { return Promise.reject(new Error('no debug adapter')); } await this.debugAdapter.startSession(); this.startTime = new Date().getTime(); } /** * Send client capabilities to the debug adapter and receive DA capabilities in return. */ async initialize(args: DebugProtocol.InitializeRequestArguments): Promise { const response = await this.send('initialize', args); this.mergeCapabilities(response.body); return response; } /** * Terminate the debuggee and shutdown the adapter */ disconnect(restart = false): Promise { return this.shutdown(undefined, restart); } //---- DAP requests async launchOrAttach(config: IConfig): Promise { const response = await this.send(config.request, config); this.mergeCapabilities(response.body); return response; } /** * Try killing the debuggee softly... */ terminate(restart = false): Promise { if (this.capabilities.supportsTerminateRequest) { if (!this.terminated) { this.terminated = true; return this.send('terminate', { restart }); } return this.disconnect(restart); } return Promise.reject(new Error('terminated not supported')); } restart(): Promise { if (this.capabilities.supportsRestartRequest) { return this.send('restart', null); } return Promise.reject(new Error('restart not supported')); } async next(args: DebugProtocol.NextArguments): Promise { const response = await this.send('next', args); this.fireSimulatedContinuedEvent(args.threadId); return response; } async stepIn(args: DebugProtocol.StepInArguments): Promise { const response = await this.send('stepIn', args); this.fireSimulatedContinuedEvent(args.threadId); return response; } async stepOut(args: DebugProtocol.StepOutArguments): Promise { const response = await this.send('stepOut', args); this.fireSimulatedContinuedEvent(args.threadId); return response; } async continue(args: DebugProtocol.ContinueArguments): Promise { const response = await this.send('continue', args); if (response && response.body && response.body.allThreadsContinued !== undefined) { this.allThreadsContinued = response.body.allThreadsContinued; } this.fireSimulatedContinuedEvent(args.threadId, this.allThreadsContinued); return response; } pause(args: DebugProtocol.PauseArguments): Promise { return this.send('pause', args); } terminateThreads(args: DebugProtocol.TerminateThreadsArguments): Promise { if (this.capabilities.supportsTerminateThreadsRequest) { return this.send('terminateThreads', args); } return Promise.reject(new Error('terminateThreads not supported')); } setVariable(args: DebugProtocol.SetVariableArguments): Promise { if (this.capabilities.supportsSetVariable) { return this.send('setVariable', args); } return Promise.reject(new Error('setVariable not supported')); } async restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): Promise { if (this.capabilities.supportsRestartFrame) { const response = await this.send('restartFrame', args); this.fireSimulatedContinuedEvent(threadId); return response; } return Promise.reject(new Error('restartFrame not supported')); } completions(args: DebugProtocol.CompletionsArguments, token: CancellationToken): Promise { if (this.capabilities.supportsCompletionsRequest) { return this.send('completions', args, token); } return Promise.reject(new Error('completions not supported')); } setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): Promise { return this.send('setBreakpoints', args); } setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): Promise { if (this.capabilities.supportsFunctionBreakpoints) { return this.send('setFunctionBreakpoints', args); } return Promise.reject(new Error('setFunctionBreakpoints not supported')); } dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise { if (this.capabilities.supportsDataBreakpoints) { return this.send('dataBreakpointInfo', args); } return Promise.reject(new Error('dataBreakpointInfo not supported')); } setDataBreakpoints(args: DebugProtocol.SetDataBreakpointsArguments): Promise { if (this.capabilities.supportsDataBreakpoints) { return this.send('setDataBreakpoints', args); } return Promise.reject(new Error('setDataBreakpoints not supported')); } setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { return this.send('setExceptionBreakpoints', args); } breakpointLocations(args: DebugProtocol.BreakpointLocationsArguments): Promise { if (this.capabilities.supportsBreakpointLocationsRequest) { return this.send('breakpointLocations', args); } return Promise.reject(new Error('breakpointLocations is not supported')); } configurationDone(): Promise { if (this.capabilities.supportsConfigurationDoneRequest) { return this.send('configurationDone', null); } return Promise.reject(new Error('configurationDone not supported')); } stackTrace(args: DebugProtocol.StackTraceArguments, token: CancellationToken): Promise { return this.send('stackTrace', args, token); } exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise { if (this.capabilities.supportsExceptionInfoRequest) { return this.send('exceptionInfo', args); } return Promise.reject(new Error('exceptionInfo not supported')); } scopes(args: DebugProtocol.ScopesArguments, token: CancellationToken): Promise { return this.send('scopes', args, token); } variables(args: DebugProtocol.VariablesArguments, token?: CancellationToken): Promise { return this.send('variables', args, token); } source(args: DebugProtocol.SourceArguments): Promise { return this.send('source', args); } loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise { if (this.capabilities.supportsLoadedSourcesRequest) { return this.send('loadedSources', args); } return Promise.reject(new Error('loadedSources not supported')); } threads(): Promise { return this.send('threads', null); } evaluate(args: DebugProtocol.EvaluateArguments): Promise { return this.send('evaluate', args); } async stepBack(args: DebugProtocol.StepBackArguments): Promise { if (this.capabilities.supportsStepBack) { const response = await this.send('stepBack', args); if (response.body === undefined) { // TODO@AW why this check? this.fireSimulatedContinuedEvent(args.threadId); } return response; } return Promise.reject(new Error('stepBack not supported')); } async reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { if (this.capabilities.supportsStepBack) { const response = await this.send('reverseContinue', args); if (response.body === undefined) { // TODO@AW why this check? this.fireSimulatedContinuedEvent(args.threadId); } return response; } return Promise.reject(new Error('reverseContinue not supported')); } gotoTargets(args: DebugProtocol.GotoTargetsArguments): Promise { if (this.capabilities.supportsGotoTargetsRequest) { return this.send('gotoTargets', args); } return Promise.reject(new Error('gotoTargets is not supported')); } async goto(args: DebugProtocol.GotoArguments): Promise { if (this.capabilities.supportsGotoTargetsRequest) { const response = await this.send('goto', args); this.fireSimulatedContinuedEvent(args.threadId); return response; } return Promise.reject(new Error('goto is not supported')); } cancel(args: DebugProtocol.CancelArguments): Promise { return this.send('cancel', args); } custom(request: string, args: any): Promise { return this.send(request, args); } //---- private private async shutdown(error?: Error, restart = false): Promise { if (!this.inShutdown) { this.inShutdown = true; if (this.debugAdapter) { try { await this.send('disconnect', { restart }, undefined, 500); } finally { this.stopAdapter(error); } } else { return this.stopAdapter(error); } } } private async stopAdapter(error?: Error): Promise { try { if (this.debugAdapter) { const da = this.debugAdapter; this.debugAdapter = null; await da.stopSession(); this.debugAdapterStopped = true; } } finally { this.fireAdapterExitEvent(error); } } private fireAdapterExitEvent(error?: Error): void { if (!this.firedAdapterExitEvent) { this.firedAdapterExitEvent = true; const e: AdapterEndEvent = { emittedStopped: this.didReceiveStoppedEvent, sessionLengthInSeconds: (new Date().getTime() - this.startTime) / 1000 }; if (error && !this.debugAdapterStopped) { e.error = error; } this._onDidExitAdapter.fire(e); } } private async dispatchRequest(request: DebugProtocol.Request, dbgr: IDebugger): Promise { const response: DebugProtocol.Response = { type: 'response', seq: 0, command: request.command, request_seq: request.seq, success: true }; const safeSendResponse = (response: DebugProtocol.Response) => this.debugAdapter && this.debugAdapter.sendResponse(response); switch (request.command) { case 'launchVSCode': this.launchVsCode(request.arguments).then(_ => { response.body = { //processId: pid }; safeSendResponse(response); }, err => { response.success = false; response.message = err.message; safeSendResponse(response); }); break; case 'runInTerminal': try { const shellProcessId = await dbgr.runInTerminal(request.arguments as DebugProtocol.RunInTerminalRequestArguments); const resp = response as DebugProtocol.RunInTerminalResponse; resp.body = {}; if (typeof shellProcessId === 'number') { resp.body.shellProcessId = shellProcessId; } safeSendResponse(resp); } catch (err) { response.success = false; response.message = err.message; safeSendResponse(response); } break; default: response.success = false; response.message = `unknown request '${request.command}'`; safeSendResponse(response); break; } } private launchVsCode(vscodeArgs: ILaunchVSCodeArguments): Promise { const args: string[] = []; for (let arg of vscodeArgs.args) { const a2 = (arg.prefix || '') + (arg.path || ''); const match = /^--(.+)=(.+)$/.exec(a2); if (match && match.length === 3) { const key = match[1]; let value = match[2]; if ((key === 'file-uri' || key === 'folder-uri') && !isUri(arg.path)) { value = URI.file(value).toString(); } args.push(`--${key}=${value}`); } else { args.push(a2); } } let env: IProcessEnvironment = {}; if (vscodeArgs.env) { // merge environment variables into a copy of the process.env env = objects.mixin(processEnv, vscodeArgs.env); // and delete some if necessary Object.keys(env).filter(k => env[k] === null).forEach(key => delete env[key]); } return this.extensionHostDebugService.openExtensionDevelopmentHostWindow(args, env); } private send(command: string, args: any, token?: CancellationToken, timeout?: number): Promise { return new Promise((completeDispatch, errorDispatch) => { if (!this.debugAdapter) { errorDispatch(new Error(nls.localize('noDebugAdapter', "No debug adapter found. Can not send '{0}'.", command))); return; } let cancelationListener: IDisposable; const requestId = this.debugAdapter.sendRequest(command, args, (response: DebugProtocol.Response) => { if (cancelationListener) { cancelationListener.dispose(); } if (response.success) { completeDispatch(response); } else { errorDispatch(response); } }, timeout); if (token) { cancelationListener = token.onCancellationRequested(() => { cancelationListener.dispose(); if (this.capabilities.supportsCancelRequest) { this.cancel({ requestId }); } }); } }).then(undefined, err => Promise.reject(this.handleErrorResponse(err))); } private handleErrorResponse(errorResponse: DebugProtocol.Response): Error { if (errorResponse.command === 'canceled' && errorResponse.message === 'canceled') { return errors.canceled(); } const error: DebugProtocol.Message | undefined = errorResponse?.body?.error; const errorMessage = errorResponse?.message || ''; if (error && error.sendTelemetry) { const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; this.telemetryDebugProtocolErrorResponse(telemetryMessage); } const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage; const url = error?.url; if (error && url) { const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); return createErrorWithActions(userMessage, { actions: [new Action('debug.moreInfo', label, undefined, true, () => { this.openerService.open(URI.parse(url)); return Promise.resolve(null); })] }); } if (error && error.format && error.showUser) { this.notificationService.error(error.format); } return new Error(userMessage); } private mergeCapabilities(capabilities: DebugProtocol.Capabilities | undefined): void { if (capabilities) { this._capabilities = objects.mixin(this._capabilities, capabilities); } } private fireSimulatedContinuedEvent(threadId: number, allThreadsContinued = false): void { this._onDidContinued.fire({ type: 'event', event: 'continued', body: { threadId, allThreadsContinued }, seq: undefined! }); } private telemetryDebugProtocolErrorResponse(telemetryMessage: string | undefined) { /* __GDPR__ "debugProtocolErrorResponse" : { "error" : { "classification": "CallstackOrException", "purpose": "FeatureInsight" } } */ this.telemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); if (this.customTelemetryService) { /* __GDPR__TODO__ The message is sent in the name of the adapter but the adapter doesn't know about it. However, since adapters are an open-ended set, we can not declared the events statically either. */ this.customTelemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); } } dispose(): void { dispose(this.toDispose); } }