/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. 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 { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as debug from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { formatPII } from 'vs/workbench/parts/debug/common/debugUtils'; import { SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; import { Queue } from 'vs/base/common/async'; export interface SessionExitedEvent extends debug.DebugEvent { body: { exitCode: number, sessionId: string }; } export interface SessionTerminatedEvent extends debug.DebugEvent { body: { restart?: boolean, sessionId: string }; } export class RawDebugSession implements debug.IRawSession { private debugAdapter: debug.IDebugAdapter; public emittedStopped: boolean; public initialised: boolean; private cachedInitServerP: TPromise; private startTime: number; public disconnected: boolean; private requestQueue = new Queue(); private _capabilities: DebugProtocol.Capabilities; private allThreadsContinued: boolean; private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; private readonly _onDidContinued: Emitter; private readonly _onDidTerminateDebugee: Emitter; private readonly _onDidExitDebugee: Emitter; private readonly _onDidExitAdapter: Emitter<{ sessionId: string }>; private readonly _onDidThread: Emitter; private readonly _onDidOutput: Emitter; private readonly _onDidBreakpoint: Emitter; private readonly _onDidCustomEvent: Emitter; private readonly _onDidEvent: Emitter; constructor( private id: string, private debugServerPort: number, private _debugger: Debugger, public customTelemetryService: ITelemetryService, public root: IWorkspaceFolder, @INotificationService private notificationService: INotificationService, @ITelemetryService private telemetryService: ITelemetryService, @IOutputService private outputService: IOutputService ) { this.emittedStopped = false; this.initialised = false; this.allThreadsContinued = true; this.requestQueue.queue(() => { if (this.cachedInitServerP) { return this.cachedInitServerP; } const startSessionP = this.startSession(); this.cachedInitServerP = startSessionP.then(() => { this.startTime = new Date().getTime(); }, err => { this.cachedInitServerP = null; return TPromise.wrapError(err); }); return this.cachedInitServerP; }); this._onDidInitialize = new Emitter(); this._onDidStop = new Emitter(); this._onDidContinued = new Emitter(); this._onDidTerminateDebugee = new Emitter(); this._onDidExitDebugee = new Emitter(); this._onDidExitAdapter = new Emitter<{ sessionId: string }>(); this._onDidThread = new Emitter(); this._onDidOutput = new Emitter(); this._onDidBreakpoint = new Emitter(); this._onDidCustomEvent = new Emitter(); this._onDidEvent = new Emitter(); } public getId(): string { return this.id; } public get onDidInitialize(): Event { return this._onDidInitialize.event; } public get onDidStop(): Event { return this._onDidStop.event; } public get onDidContinued(): Event { return this._onDidContinued.event; } public get onDidTerminateDebugee(): Event { return this._onDidTerminateDebugee.event; } public get onDidExitDebugee(): Event { return this._onDidExitDebugee.event; } public get onDidExitAdapter(): Event<{ sessionId: string }> { return this._onDidExitAdapter.event; } public get onDidThread(): Event { return this._onDidThread.event; } public get onDidOutput(): Event { return this._onDidOutput.event; } public get onDidBreakpoint(): Event { return this._onDidBreakpoint.event; } public get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } public get onDidEvent(): Event { return this._onDidEvent.event; } private startSession(): TPromise { return this._debugger.createDebugAdapter(this.root, this.outputService, this.debugServerPort).then(debugAdapter => { this.debugAdapter = debugAdapter; this.debugAdapter.onError(err => this.onDebugAdapterError(err)); this.debugAdapter.onEvent(event => this.onDapEvent(event)); this.debugAdapter.onRequest(request => this.dispatchRequest(request)); this.debugAdapter.onExit(code => this.onDebugAdapterExit()); return this.debugAdapter.startSession(); }); } public custom(request: string, args: any): TPromise { return this.send(request, args); } private send(command: string, args: any, cancelOnDisconnect = true): TPromise { return this.requestQueue.queue(() => { const promise = this.internalSend(command, args).then(response => response, (errorResponse: DebugProtocol.ErrorResponse) => { const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; const errorMessage = errorResponse ? errorResponse.message : ''; const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; if (error && error.sendTelemetry) { /* __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 }); } } 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 TPromise.wrapError(errors.create(userMessage, { actions: [new Action('debug.moreInfo', label, null, true, () => { window.open(error.url); return TPromise.as(null); })] })); } return errors.isPromiseCanceledError(errorResponse) ? undefined : TPromise.wrapError(new Error(userMessage)); }); return promise; }); } private internalSend(command: string, args: any): TPromise { let errorCallback: (error: Error) => void; return new TPromise((completeDispatch, errorDispatch) => { errorCallback = errorDispatch; this.debugAdapter.sendRequest(command, args, (result: R) => { if (result.success) { completeDispatch(result); } else { errorDispatch(result); } }); }, () => errorCallback(errors.canceled())); } private onDapEvent(event: debug.DebugEvent): void { event.sessionId = this.id; if (event.event === 'initialized') { this.initialised = true; this._onDidInitialize.fire(event); } else if (event.event === 'capabilities' && event.body) { const capabilites = (event).body.capabilities; this._capabilities = objects.mixin(this._capabilities, capabilites); } else if (event.event === 'stopped') { this.emittedStopped = true; this._onDidStop.fire(event); } else if (event.event === 'continued') { this.allThreadsContinued = (event).body.allThreadsContinued === false ? false : true; this._onDidContinued.fire(event); } else if (event.event === 'thread') { this._onDidThread.fire(event); } else if (event.event === 'output') { this._onDidOutput.fire(event); } else if (event.event === 'breakpoint') { this._onDidBreakpoint.fire(event); } else if (event.event === 'terminated') { this._onDidTerminateDebugee.fire(event); } else if (event.event === 'exit') { this._onDidExitDebugee.fire(event); } else { this._onDidCustomEvent.fire(event); } this._onDidEvent.fire(event); } public get capabilities(): DebugProtocol.Capabilities { return this._capabilities || {}; } public initialize(args: DebugProtocol.InitializeRequestArguments): TPromise { return this.send('initialize', args).then(response => this.readCapabilities(response)); } private readCapabilities(response: DebugProtocol.Response): DebugProtocol.Response { if (response) { this._capabilities = objects.mixin(this._capabilities, response.body); } return response; } public launch(args: DebugProtocol.LaunchRequestArguments): TPromise { return this.send('launch', args).then(response => this.readCapabilities(response)); } public attach(args: DebugProtocol.AttachRequestArguments): TPromise { return this.send('attach', args).then(response => this.readCapabilities(response)); } private clearRequestQueue(): void { // If the debug session is initialitised it is fine to clear all pending requests // This is mostly done by "step" requests which change the state of the debuggee // Thus canceling other requests like "variables" and "evaluate" makes snese if (this.initialised) { this.requestQueue.cancel(); } } public next(args: DebugProtocol.NextArguments): TPromise { this.clearRequestQueue(); return this.send('next', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public stepIn(args: DebugProtocol.StepInArguments): TPromise { this.clearRequestQueue(); return this.send('stepIn', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public stepOut(args: DebugProtocol.StepOutArguments): TPromise { this.clearRequestQueue(); return this.send('stepOut', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public continue(args: DebugProtocol.ContinueArguments): TPromise { this.clearRequestQueue(); return this.send('continue', args).then(response => { if (response && response.body && response.body.allThreadsContinued !== undefined) { this.allThreadsContinued = response.body.allThreadsContinued; } this.fireFakeContinued(args.threadId, this.allThreadsContinued); return response; }); } public pause(args: DebugProtocol.PauseArguments): TPromise { return this.send('pause', args); } public terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise { return this.send('terminateThreads', args); } public setVariable(args: DebugProtocol.SetVariableArguments): TPromise { return this.send('setVariable', args); } public restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise { this.clearRequestQueue(); return this.send('restartFrame', args).then(response => { this.fireFakeContinued(threadId); return response; }); } public completions(args: DebugProtocol.CompletionsArguments): TPromise { return this.send('completions', args); } public disconnect(restart = false, force = false): TPromise { this.clearRequestQueue(); if (this.disconnected && force) { return this.stopServer(); } if (this.debugAdapter && !this.disconnected) { // point of no return: from now on don't report any errors this.disconnected = true; return this.send('disconnect', { restart: restart }, false).then(() => this.stopServer(), () => this.stopServer()); } return TPromise.as(null); } public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise { return this.send('setBreakpoints', args); } public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): TPromise { return this.send('setFunctionBreakpoints', args); } public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): TPromise { return this.send('setExceptionBreakpoints', args); } public configurationDone(): TPromise { return this.send('configurationDone', null); } public stackTrace(args: DebugProtocol.StackTraceArguments): TPromise { return this.send('stackTrace', args); } public exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): TPromise { return this.send('exceptionInfo', args); } public scopes(args: DebugProtocol.ScopesArguments): TPromise { return this.send('scopes', args); } public variables(args: DebugProtocol.VariablesArguments): TPromise { return this.send('variables', args); } public source(args: DebugProtocol.SourceArguments): TPromise { return this.send('source', args); } public threads(): TPromise { return this.send('threads', null); } public evaluate(args: DebugProtocol.EvaluateArguments): TPromise { return this.send('evaluate', args); } public stepBack(args: DebugProtocol.StepBackArguments): TPromise { this.clearRequestQueue(); return this.send('stepBack', args).then(response => { if (response.body === undefined) { this.fireFakeContinued(args.threadId); } return response; }); } public reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise { this.clearRequestQueue(); return this.send('reverseContinue', args).then(response => { if (response.body === undefined) { this.fireFakeContinued(args.threadId); } return response; }); } public getLengthInSeconds(): number { return (new Date().getTime() - this.startTime) / 1000; } private dispatchRequest(request: DebugProtocol.Request): void { const response: DebugProtocol.Response = { type: 'response', seq: 0, command: request.command, request_seq: request.seq, success: true }; if (request.command === 'runInTerminal') { this._debugger.runInTerminal(request.arguments).then(_ => { response.body = {}; this.debugAdapter.sendResponse(response); }, err => { response.success = false; response.message = err.message; this.debugAdapter.sendResponse(response); }); } else if (request.command === 'handshake') { try { const vsda = require.__$__nodeRequire('vsda'); const obj = new vsda.signer(); const sig = obj.sign(request.arguments.value); response.body = { signature: sig }; this.debugAdapter.sendResponse(response); } catch (e) { response.success = false; response.message = e.message; this.debugAdapter.sendResponse(response); } } else { response.success = false; response.message = `unknown request '${request.command}'`; this.debugAdapter.sendResponse(response); } } private fireFakeContinued(threadId: number, allThreadsContinued = false): void { this._onDidContinued.fire({ type: 'event', event: 'continued', body: { threadId, allThreadsContinued }, seq: undefined }); } private stopServer(): TPromise { if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { this.debugAdapter.stopSession(); this.cachedInitServerP = null; } this._onDidExitAdapter.fire({ sessionId: this.getId() }); this.disconnected = true; if (!this.debugAdapter || this.debugAdapter instanceof SocketDebugAdapter) { return TPromise.as(null); } return this.debugAdapter.stopSession(); } private onDebugAdapterError(err: Error): void { this.notificationService.error(err.message || err.toString()); this.stopServer().done(null, errors.onUnexpectedError); } private onDebugAdapterExit(): void { this.debugAdapter = null; this.cachedInitServerP = null; if (!this.disconnected) { this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } this._onDidExitAdapter.fire({ sessionId: this.getId() }); } public dispose(): void { this.disconnect().done(null, errors.onUnexpectedError); } }