/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import nls = require('vs/nls'); import cp = require('child_process'); import net = require('net'); import Event, { Emitter } from 'vs/base/common/event'; import platform = require('vs/base/common/platform'); import objects = require('vs/base/common/objects'); import { Action } from 'vs/base/common/actions'; import errors = require('vs/base/common/errors'); import { TPromise } from 'vs/base/common/winjs.base'; import severity from 'vs/base/common/severity'; import stdfork = require('vs/base/node/stdFork'); import { IMessageService, CloseAction } from 'vs/platform/message/common/message'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution'; import debug = require('vs/workbench/parts/debug/common/debug'); import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; import v8 = require('vs/workbench/parts/debug/node/v8Protocol'); import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { TerminalSupport } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface SessionExitedEvent extends DebugProtocol.ExitedEvent { body: { exitCode: number, sessionId: string }; } export interface SessionTerminatedEvent extends DebugProtocol.TerminatedEvent { body: { restart?: boolean, sessionId: string }; } export class RawDebugSession extends v8.V8Protocol implements debug.ISession { public emittedStopped: boolean; public readyForBreakpoints: boolean; private serverProcess: cp.ChildProcess; private socket: net.Socket = null; private cachedInitServer: TPromise; private startTime: number; public disconnected: boolean; private sentPromises: TPromise[]; private _capabilities: DebugProtocol.Capabilities; private allThreadsContinued: boolean; private _onDidInitialize: Emitter; private _onDidStop: Emitter; private _onDidContinued: Emitter; private _onDidTerminateDebugee: Emitter; private _onDidExitAdapter: Emitter; private _onDidThread: Emitter; private _onDidOutput: Emitter; private _onDidBreakpoint: Emitter; private _onDidEvent: Emitter; constructor( id: string, private debugServerPort: number, private adapter: Adapter, private customTelemetryService: ITelemetryService, @IMessageService private messageService: IMessageService, @ITelemetryService private telemetryService: ITelemetryService, @IOutputService private outputService: IOutputService, @ITerminalService private terminalService: ITerminalService, @IExternalTerminalService private nativeTerminalService: IExternalTerminalService, @IConfigurationService private configurationService: IConfigurationService ) { super(id); this.emittedStopped = false; this.readyForBreakpoints = false; this.allThreadsContinued = false; this.sentPromises = []; this._onDidInitialize = new Emitter(); this._onDidStop = new Emitter(); this._onDidContinued = new Emitter(); this._onDidTerminateDebugee = new Emitter(); this._onDidExitAdapter = new Emitter(); this._onDidThread = new Emitter(); this._onDidOutput = new Emitter(); this._onDidBreakpoint = new Emitter(); this._onDidEvent = new Emitter(); } 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 onDidExitAdapter(): Event { 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 onDidEvent(): Event { return this._onDidEvent.event; } private initServer(): TPromise { if (this.cachedInitServer) { return this.cachedInitServer; } const serverPromise = this.debugServerPort ? this.connectServer(this.debugServerPort) : this.startServer(); this.cachedInitServer = serverPromise.then(() => { this.startTime = new Date().getTime(); }, err => { this.cachedInitServer = null; return TPromise.wrapError(err); }); return this.cachedInitServer; } public custom(request: string, args: any): TPromise { return this.send(request, args); } protected send(command: string, args: any, cancelOnDisconnect = true): TPromise { return this.initServer().then(() => { const promise = super.send(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 ? debug.formatPII(error.format, true, error.variables) : errorMessage; if (error && error.sendTelemetry) { this.telemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); if (this.customTelemetryService) { this.customTelemetryService.publicLog('debugProtocolErrorResponse', { error: telemetryMessage }); } } const userMessage = error ? debug.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: [CloseAction, 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)); }); if (cancelOnDisconnect) { this.sentPromises.push(promise); } return promise; }); } protected onEvent(event: DebugProtocol.Event): void { if (event.body) { event.body.sessionId = this.getId(); } else { event.body = { sessionId: this.getId() }; } if (event.event === 'initialized') { this.readyForBreakpoints = true; this._onDidInitialize.fire(event); } 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._onDidExitAdapter.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)); } public next(args: DebugProtocol.NextArguments): TPromise { return this.send('next', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public stepIn(args: DebugProtocol.StepInArguments): TPromise { return this.send('stepIn', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public stepOut(args: DebugProtocol.StepOutArguments): TPromise { return this.send('stepOut', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public continue(args: DebugProtocol.ContinueArguments): TPromise { return this.send('continue', args).then(response => { this.fireFakeContinued(args.threadId, this.allThreadsContinued); return response; }); } public pause(args: DebugProtocol.PauseArguments): TPromise { return this.send('pause', args); } public setVariable(args: DebugProtocol.SetVariableArguments): TPromise { return this.send('setVariable', args); } public restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise { 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 { if (this.disconnected && force) { return this.stopServer(); } // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. // Give a 1s timeout to give a chance for some promises to complete. setTimeout(() => { this.sentPromises.forEach(p => p && p.cancel()); this.sentPromises = []; }, 1000); if ((this.serverProcess || this.socket) && !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 { return this.send('stepBack', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise { return this.send('reverseContinue', args).then(response => { this.fireFakeContinued(args.threadId); return response; }); } public getLengthInSeconds(): number { return (new Date().getTime() - this.startTime) / 1000; } protected dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void { if (request.command === 'runInTerminal') { TerminalSupport.runInTerminal(this.terminalService, this.nativeTerminalService, this.configurationService, request.arguments, response).then(() => { this.sendResponse(response); }, e => { response.success = false; response.message = e.message; this.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.sendResponse(response); } catch (e) { response.success = false; response.message = e.message; this.sendResponse(response); } } else { response.success = false; response.message = `unknown request '${request.command}'`; this.sendResponse(response); } } private fireFakeContinued(threadId: number, allThreadsContinued = false): void { this._onDidContinued.fire({ type: 'event', event: 'continued', body: { threadId, allThreadsContinued }, seq: undefined }); } private connectServer(port: number): TPromise { return new TPromise((c, e) => { this.socket = net.createConnection(port, '127.0.0.1', () => { this.connect(this.socket, this.socket); c(null); }); this.socket.on('error', (err: any) => { e(err); }); this.socket.on('close', () => this.onServerExit()); }); } private startServer(): TPromise { return this.adapter.getAdapterExecutable().then(ae => this.launchServer(ae).then(() => { this.serverProcess.on('error', (err: Error) => this.onServerError(err)); this.serverProcess.on('exit', (code: number, signal: string) => this.onServerExit()); const sanitize = (s: string) => s.toString().replace(/\r?\n$/mg, ''); // this.serverProcess.stdout.on('data', (data: string) => { // console.log('%c' + sanitize(data), 'background: #ddd; font-style: italic;'); // }); this.serverProcess.stderr.on('data', (data: string) => { this.outputService.getChannel(ExtensionsChannelId).append(sanitize(data)); }); this.connect(this.serverProcess.stdout, this.serverProcess.stdin); })); } private launchServer(launch: debug.IAdapterExecutable): TPromise { return new TPromise((c, e) => { if (launch.command === 'node') { if (Array.isArray(launch.args) && launch.args.length > 0) { stdfork.fork(launch.args[0], launch.args.slice(1), {}, (err, child) => { if (err) { e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", launch.args[0]))); } this.serverProcess = child; c(null); }); } else { e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."))); } } else { this.serverProcess = cp.spawn(launch.command, launch.args, { stdio: [ 'pipe', // stdin 'pipe', // stdout 'pipe' // stderr ], }); c(null); } }); } private stopServer(): TPromise { if (this.socket !== null) { this.socket.end(); this.cachedInitServer = null; } this.onEvent({ event: 'exit', type: 'event', seq: 0 }); if (!this.serverProcess) { return TPromise.as(null); } this.disconnected = true; let ret: TPromise; // when killing a process in windows its child // processes are *not* killed but become root // processes. Therefore we use TASKKILL.EXE if (platform.isWindows) { ret = new TPromise((c, e) => { const killer = cp.exec(`taskkill /F /T /PID ${this.serverProcess.pid}`, function (err, stdout, stderr) { if (err) { return e(err); } }); killer.on('exit', c); killer.on('error', e); }); } else { this.serverProcess.kill('SIGTERM'); ret = TPromise.as(null); } return ret; } protected onServerError(err: Error): void { this.messageService.show(severity.Error, nls.localize('stoppingDebugAdapter', "{0}. Stopping the debug adapter.", err.message)); this.stopServer().done(null, errors.onUnexpectedError); } private onServerExit(): void { this.serverProcess = null; this.cachedInitServer = null; if (!this.disconnected) { this.messageService.show(severity.Error, nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } this.onEvent({ event: 'exit', type: 'event', seq: 0 }); } public dispose(): void { this.disconnect().done(null, errors.onUnexpectedError); } }