/*--------------------------------------------------------------------------------------------- * 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 { ParsedArgs } from 'vs/platform/environment/common/environment'; import { IWindowsService } from 'vs/platform/windows/common/windows'; 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'; /** * 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 windowsService: IWindowsService, private readonly openerService: IOpenerService ) { 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. */ start(): Promise { if (!this.debugAdapter) { return Promise.reject(new Error('no debug adapter')); } return this.debugAdapter.startSession().then(() => { this.startTime = new Date().getTime(); }, err => { return Promise.reject(err); }); } /** * Send client capabilities to the debug adapter and receive DA capabilities in return. */ initialize(args: DebugProtocol.InitializeRequestArguments): Promise { return this.send('initialize', args).then((response: DebugProtocol.InitializeResponse) => { this.mergeCapabilities(response.body); return response; }); } /** * Terminate the debuggee and shutdown the adapter */ disconnect(restart = false): Promise { return this.shutdown(undefined, restart); } //---- DAP requests launchOrAttach(config: IConfig): Promise { return this.send(config.request, config).then(response => { 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')); } next(args: DebugProtocol.NextArguments): Promise { return this.send('next', args).then(response => { this.fireSimulatedContinuedEvent(args.threadId); return response; }); } stepIn(args: DebugProtocol.StepInArguments): Promise { return this.send('stepIn', args).then(response => { this.fireSimulatedContinuedEvent(args.threadId); return response; }); } stepOut(args: DebugProtocol.StepOutArguments): Promise { return this.send('stepOut', args).then(response => { this.fireSimulatedContinuedEvent(args.threadId); return response; }); } continue(args: DebugProtocol.ContinueArguments): Promise { return this.send('continue', args).then(response => { 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')); } restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): Promise { if (this.capabilities.supportsRestartFrame) { return this.send('restartFrame', args).then(response => { 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); } 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); } stepBack(args: DebugProtocol.StepBackArguments): Promise { if (this.capabilities.supportsStepBack) { return this.send('stepBack', args).then(response => { if (response.body === undefined) { // TODO@AW why this check? this.fireSimulatedContinuedEvent(args.threadId); } return response; }); } return Promise.reject(new Error('stepBack not supported')); } reverseContinue(args: DebugProtocol.ReverseContinueArguments): Promise { if (this.capabilities.supportsStepBack) { return this.send('reverseContinue', args).then(response => { 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')); } goto(args: DebugProtocol.GotoArguments): Promise { if (this.capabilities.supportsGotoTargetsRequest) { return this.send('goto', args).then(res => { this.fireSimulatedContinuedEvent(args.threadId); return res; }); } 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 shutdown(error?: Error, restart = false): Promise { if (!this.inShutdown) { this.inShutdown = true; if (this.debugAdapter) { return this.send('disconnect', { restart }, undefined, 500).then(() => { this.stopAdapter(error); }, () => { // ignore error this.stopAdapter(error); }); } return this.stopAdapter(error); } return Promise.resolve(undefined); } private stopAdapter(error?: Error): Promise { if (this.debugAdapter) { const da = this.debugAdapter; this.debugAdapter = null; return da.stopSession().then(_ => { this.debugAdapterStopped = true; this.fireAdapterExitEvent(error); }, err => { this.fireAdapterExitEvent(error); }); } else { this.fireAdapterExitEvent(error); } return Promise.resolve(undefined); } 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': dbgr.runInTerminal(request.arguments as DebugProtocol.RunInTerminalRequestArguments).then(shellProcessId => { const resp = response as DebugProtocol.RunInTerminalResponse; resp.body = {}; if (typeof shellProcessId === 'number') { resp.body.shellProcessId = shellProcessId; } safeSendResponse(resp); }, 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 { let args: ParsedArgs = { _: [] }; for (let arg of vscodeArgs.args) { if (arg.prefix) { 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(); const v = args[key]; if (v) { v.push(value); } else { args[key] = [value]; } } else if (key === 'extensionDevelopmentPath') { const v = args[key]; if (v) { v.push(value); } else { args[key] = [value]; } } else { (args)[key] = value; } } else { const match = /^--(.+)$/.exec(a2); if (match && match.length === 2) { const key = match[1]; (args)[key] = true; } 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.windowsService.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('no debug adapter found')); return; } let cancelationListener: IDisposable; const requestId = this.debugAdapter.sendRequest(command, args, (response: R) => { 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 = errorResponse && errorResponse.body ? errorResponse.body.error : null; const errorMessage = errorResponse ? 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; if (error && 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(error.url)); return Promise.resolve(null); })] }); } 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); } }