diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 044af5fd345..f20e24fe548 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -6,7 +6,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import uri from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, @@ -14,6 +14,10 @@ import { } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import severity from 'vs/base/common/severity'; +import { AbstractDebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol'; +import { convertToDAPaths, convertToVSCPaths } from 'vs/workbench/parts/debug/node/DapPathConverter'; +import * as paths from 'vs/base/common/paths'; + @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape { @@ -21,6 +25,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { private _proxy: ExtHostDebugServiceShape; private _toDispose: IDisposable[]; private _breakpointEventsActive: boolean; + private _debugAdapters: Map; + private _debugAdaptersHandleCounter = 1; constructor( extHostContext: IExtHostContext, @@ -46,6 +52,18 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { } } })); + + this._debugAdapters = new Map(); + + // register a default DA provider + debugService.getConfigurationManager().registerDebugAdapterProvider('*', { + createDebugAdapter: (debugType, adapterInfo) => { + const handle = this._debugAdaptersHandleCounter++; + const da = new ExtensionHostDebugAdapter(handle, this._proxy, debugType, adapterInfo); + this._debugAdapters.set(handle, da); + return da; + } + }); } public dispose(): void { @@ -208,4 +226,59 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { this.debugService.logToRepl(value, severity.Warning); return TPromise.wrap(undefined); } + + public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) { + + convertToVSCPaths(message, source => { + if (typeof source.path === 'object') { + source.path = uri.revive(source.path).toString(); + } + }); + + this._debugAdapters.get(handle).acceptMessage(message); + } + + public $acceptDAError(handle: number, name: string, message: string, stack: string) { + this._debugAdapters.get(handle).fireError(handle, new Error(`${name}: ${message}\n${stack}`)); + } + + public $acceptDAExit(handle: number, code: number, signal: string) { + this._debugAdapters.get(handle).fireExit(handle, code, signal); + } +} + +class ExtensionHostDebugAdapter extends AbstractDebugAdapter { + + constructor(private _handle: number, private _proxy: ExtHostDebugServiceShape, private _debugType: string, private _adapterExecutable: IAdapterExecutable | null) { + super(); + } + + public fireError(handle: number, err: Error) { + this._onError.fire(err); + } + + public fireExit(handle: number, code: number, signal: string) { + this._onExit.fire(code); + } + + public startSession(): TPromise { + return this._proxy.$startDASession(this._handle, this._debugType, this._adapterExecutable); + } + + public sendMessage(message: DebugProtocol.ProtocolMessage): void { + + convertToDAPaths(message, source => { + if (paths.isAbsolute(source.path)) { + (source).path = uri.file(source.path); + } else { + (source).path = uri.parse(source.path); + } + }); + + this._proxy.$sendDAMessage(this._handle, message); + } + + public stopSession(): TPromise { + return this._proxy.$stopDASession(this._handle); + } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 1dc7ff8c566..34421925449 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -106,7 +106,7 @@ export function createApiFactory( const extHostCommands = rpcProtocol.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(rpcProtocol, extHostHeapService, extHostLogService)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands)); rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); - const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace)); + const extHostDebugService = rpcProtocol.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(rpcProtocol, extHostWorkspace, extensionService)); rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 3e8c56318dc..10b5e4097ab 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -465,6 +465,9 @@ export interface MainThreadSCMShape extends IDisposable { export type DebugSessionUUID = string; export interface MainThreadDebugServiceShape extends IDisposable { + $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage); + $acceptDAError(handle: number, name: string, message: string, stack: string); + $acceptDAExit(handle: number, code: number, signal: string); $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): TPromise; $unregisterDebugConfigurationProvider(handle: number): TPromise; $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise; @@ -783,6 +786,9 @@ export interface ISourceMultiBreakpointDto { } export interface ExtHostDebugServiceShape { + $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null): TPromise; + $stopDASession(handle: number): TPromise; + $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise; $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): TPromise; $provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise; $debugAdapterExecutable(handle: number, folder: UriComponents | undefined): TPromise; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 39e58cacd5f..32be057b591 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -12,17 +12,19 @@ import { IMainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; - import * as vscode from 'vscode'; import URI, { UriComponents } from 'vs/base/common/uri'; import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint } from 'vs/workbench/api/node/extHostTypes'; import { generateUuid } from 'vs/base/common/uuid'; +import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol'; +import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/DapPathConverter'; +import * as paths from 'vs/base/common/paths'; +import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; +import { IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; export class ExtHostDebugService implements ExtHostDebugServiceShape { - private _workspace: ExtHostWorkspace; - private _handleCounter: number; private _handlers: Map; @@ -52,10 +54,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private readonly _onDidChangeBreakpoints: Emitter; + private _debugAdapters: Map; - constructor(mainContext: IMainContext, workspace: ExtHostWorkspace) { - this._workspace = workspace; + constructor(mainContext: IMainContext, private _workspace: ExtHostWorkspace, private _extensionService: ExtHostExtensionService) { this._handleCounter = 0; this._handlers = new Map(); @@ -77,6 +79,51 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._breakpoints = new Map(); this._breakpointEventsActive = false; + + this._debugAdapters = new Map(); + } + + public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null): TPromise { + const mythis = this; + + const da = new class extends DebugAdapter { + + // DA -> VS Code + public acceptMessage(message: DebugProtocol.ProtocolMessage) { + convertToVSCPaths(message, source => { + if (paths.isAbsolute(source.path)) { + (source).path = URI.file(source.path); + } + }); + mythis._debugServiceProxy.$acceptDAMessage(handle, message); + } + + }(debugType, adpaterExecutable, this._extensionService.getAllExtensionDescriptions()); + + this._debugAdapters.set(handle, da); + da.onError(err => this._debugServiceProxy.$acceptDAError(handle, err.name, err.message, err.stack)); + da.onExit(code => this._debugServiceProxy.$acceptDAExit(handle, code, null)); + return da.startSession(); + } + + public $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise { + // VS Code -> DA + convertToDAPaths(message, source => { + if (typeof source.path === 'object') { + source.path = URI.revive(source.path).fsPath; + } + }); + const da = this._debugAdapters.get(handle); + if (da) { + da.sendMessage(message); + } + return void 0; + } + + public $stopDASession(handle: number): TPromise { + const da = this._debugAdapters.get(handle); + this._debugAdapters.delete(handle); + return da ? da.stopSession() : void 0; } private startBreakpoints() { diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 5524d16960d..3b3e4856a4f 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -20,6 +20,7 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const VIEWLET_ID = 'workbench.view.debug'; export const VARIABLES_VIEW_ID = 'workbench.debug.variablesView'; @@ -347,6 +348,7 @@ export interface IDebugConfiguration { hideActionBar: boolean; showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; internalConsoleOptions: 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart'; + extensionHostDebugAdapter: boolean; } export interface IGlobalConfig { @@ -380,6 +382,22 @@ export interface ICompound { configurations: (string | { name: string, folder: string })[]; } +export interface IDebugAdapter extends IDisposable { + readonly onError: Event; + readonly onExit: Event; + onRequest(callback: (request: DebugProtocol.Request) => void); + onEvent(callback: (event: DebugProtocol.Event) => void); + startSession(): TPromise; + sendMessage(message: DebugProtocol.ProtocolMessage): void; + sendResponse(response: DebugProtocol.Response): void; + sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void; + stopSession(): TPromise; +} + +export interface IDebugAdapterProvider { + createDebugAdapter(debugType: string, adapterInfo: IAdapterExecutable | null): IDebugAdapter; +} + export interface IAdapterExecutable { command?: string; args?: string[]; @@ -448,6 +466,9 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any): TPromise; debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise; + + registerDebugAdapterProvider(debugType: string, debugAdapterLauncher: IDebugAdapterProvider); + createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable | null): IDebugAdapter; } export interface ILaunch { diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index d808ca71e7e..2b6f3550af8 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -209,6 +209,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'launch' }, "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces"), default: { configurations: [], compounds: [] }, $ref: launchSchemaId + }, + 'debug.extensionHostDebugAdapter': { + type: 'boolean', + description: nls.localize({ comment: ['This is the description for a setting'], key: 'extensionHostDebugAdapter' }, "Run debug adapter in extension host"), + default: false } } }); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 2ab01a3c8af..eede6570bf0 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -26,8 +26,8 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IDebugConfigurationProvider, IRawAdapter, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; @@ -221,7 +221,7 @@ const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname'; const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; export class ConfigurationManager implements IConfigurationManager { - private adapters: Adapter[]; + private debuggers: Debugger[]; private breakpointModeIdsSet = new Set(); private launches: ILaunch[]; private selectedName: string; @@ -229,6 +229,8 @@ export class ConfigurationManager implements IConfigurationManager { private toDispose: IDisposable[]; private _onDidSelectConfigurationName = new Emitter(); private providers: IDebugConfigurationProvider[]; + private debugAdapterProviders: Map; + constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService, @@ -241,7 +243,7 @@ export class ConfigurationManager implements IConfigurationManager { @ILifecycleService lifecycleService: ILifecycleService ) { this.providers = []; - this.adapters = []; + this.debuggers = []; this.toDispose = []; this.registerListeners(lifecycleService); this.initLaunches(); @@ -250,6 +252,7 @@ export class ConfigurationManager implements IConfigurationManager { if (previousSelectedLaunch) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } + this.debugAdapterProviders = new Map(); } public registerDebugConfigurationProvider(handle: number, debugConfigurationProvider: IDebugConfigurationProvider): void { @@ -300,12 +303,24 @@ export class ConfigurationManager implements IConfigurationManager { return TPromise.as(undefined); } + public registerDebugAdapterProvider(debugType: string, debugAdapterLauncher: IDebugAdapterProvider) { + this.debugAdapterProviders.set(debugType, debugAdapterLauncher); + } + + public createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable): IDebugAdapter { + let dap = this.debugAdapterProviders.get(debugType); + if (!dap) { + dap = this.debugAdapterProviders.get('*'); + } + return dap.createDebugAdapter(debugType, adapterExecutable); + } + private registerListeners(lifecycleService: ILifecycleService): void { debuggersExtPoint.setHandler((extensions) => { extensions.forEach(extension => { extension.value.forEach(rawAdapter => { if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) { - extension.collector.error(nls.localize('debugNoType', "Debug adapter 'type' can not be omitted and must be of type 'string'.")); + extension.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'.")); } if (rawAdapter.enableBreakpointsFor) { rawAdapter.enableBreakpointsFor.languageIds.forEach(modeId => { @@ -313,17 +328,17 @@ export class ConfigurationManager implements IConfigurationManager { }); } - const duplicate = this.adapters.filter(a => a.type === rawAdapter.type).pop(); + const duplicate = this.debuggers.filter(a => a.type === rawAdapter.type).pop(); if (duplicate) { duplicate.merge(rawAdapter, extension.description); } else { - this.adapters.push(new Adapter(this, rawAdapter, extension.description, this.configurationService, this.commandService)); + this.debuggers.push(new Debugger(this, rawAdapter, extension.description, this.configurationService, this.commandService)); } }); }); // update the schema to include all attributes, snippets and types from extensions. - this.adapters.forEach(adapter => { + this.debuggers.forEach(adapter => { const items = (schema.properties['configurations'].items); const schemaAttributes = adapter.getSchemaAttributes(); if (schemaAttributes) { @@ -448,24 +463,24 @@ export class ConfigurationManager implements IConfigurationManager { return this.breakpointModeIdsSet.has(modeId); } - public getAdapter(type: string): Adapter { - return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop(); + public getAdapter(type: string): Debugger { + return this.debuggers.filter(adapter => strings.equalsIgnoreCase(adapter.type, type)).pop(); } - public guessAdapter(type?: string): TPromise { + public guessAdapter(type?: string): TPromise { if (type) { const adapter = this.getAdapter(type); return TPromise.as(adapter); } const editor = this.editorService.getActiveEditor(); - let candidates: Adapter[]; + let candidates: Debugger[]; if (editor) { const codeEditor = editor.getControl(); if (isCodeEditor(codeEditor)) { const model = codeEditor.getModel(); const language = model ? model.getLanguageIdentifier().language : undefined; - const adapters = this.adapters.filter(a => a.languages && a.languages.indexOf(language) >= 0); + const adapters = this.debuggers.filter(a => a.languages && a.languages.indexOf(language) >= 0); if (adapters.length === 1) { return TPromise.as(adapters[0]); } @@ -476,11 +491,11 @@ export class ConfigurationManager implements IConfigurationManager { } if (!candidates) { - candidates = this.adapters.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider); + candidates = this.debuggers.filter(a => a.hasInitialConfiguration() || a.hasConfigurationProvider); } return this.quickOpenService.pick([...candidates, { label: 'More...', separator: { border: true } }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) .then(picked => { - if (picked instanceof Adapter) { + if (picked instanceof Debugger) { return picked; } if (picked) { diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 04f3a0dfe6e..ab7d4f14eed 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -4,27 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as cp from 'child_process'; import * as net from 'net'; import { Event, Emitter } from 'vs/base/common/event'; -import * as platform from 'vs/base/common/platform'; 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 * as stdfork from 'vs/base/node/stdFork'; 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 * as debug from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; -import { V8Protocol } from 'vs/workbench/parts/debug/node/v8Protocol'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -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'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { StreamDebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol'; + export interface SessionExitedEvent extends debug.DebugEvent { body: { @@ -40,14 +37,46 @@ export interface SessionTerminatedEvent extends debug.DebugEvent { }; } -export class RawDebugSession extends V8Protocol implements debug.ISession { +export class SocketDebugAdapter extends StreamDebugAdapter { + + private socket: net.Socket; + + constructor(private host: string, private port: number) { + super(); + } + + startSession(): TPromise { + return new TPromise((c, e) => { + this.socket = net.createConnection(this.port, this.host, () => { + this.connect(this.socket, this.socket); + c(null); + }); + this.socket.on('error', (err: any) => { + e(err); + }); + this.socket.on('close', () => this._onExit.fire(0)); + }); + } + + stopSession(): TPromise { + if (this.socket !== null) { + this.socket.end(); + this.socket = undefined; + } + return void 0; + } +} + +export class RawDebugSession implements debug.ISession { + + private debugAdapter: debug.IDebugAdapter; public emittedStopped: boolean; public readyForBreakpoints: boolean; - private serverProcess: cp.ChildProcess; - private socket: net.Socket = null; - private cachedInitServer: TPromise; + //private serverProcess: cp.ChildProcess; + //private socket: net.Socket = null; + private cachedInitServerP: TPromise; private startTime: number; public disconnected: boolean; private sentPromises: TPromise[]; @@ -66,9 +95,9 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { private readonly _onDidEvent: Emitter; constructor( - id: string, + private id: string, private debugServerPort: number, - private adapter: Adapter, + private _debugger: Debugger, public customTelemetryService: ITelemetryService, public root: IWorkspaceFolder, @INotificationService private notificationService: INotificationService, @@ -78,7 +107,6 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { @IExternalTerminalService private nativeTerminalService: IExternalTerminalService, @IConfigurationService private configurationService: IConfigurationService ) { - super(id); this.emittedStopped = false; this.readyForBreakpoints = false; this.allThreadsContinued = true; @@ -96,6 +124,10 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { this._onDidEvent = new Emitter(); } + public getId(): string { + return this.id; + } + public get onDidInitialize(): Event { return this._onDidInitialize.event; } @@ -137,28 +169,49 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { } private initServer(): TPromise { - if (this.cachedInitServer) { - return this.cachedInitServer; + + if (this.cachedInitServerP) { + return this.cachedInitServerP; } - const serverPromise = this.debugServerPort ? this.connectServer(this.debugServerPort) : this.startServer(); - this.cachedInitServer = serverPromise.then(() => { + const startSessionP = this.startSession(); + + this.cachedInitServerP = startSessionP.then(() => { this.startTime = new Date().getTime(); }, err => { - this.cachedInitServer = null; + this.cachedInitServerP = null; return TPromise.wrapError(err); }); - return this.cachedInitServer; + return this.cachedInitServerP; + } + + private startSession(): TPromise { + + const debugAdapterP = this.debugServerPort + ? TPromise.as(new SocketDebugAdapter('127.0.0.1', this.debugServerPort)) + : this._debugger.createDebugAdapter(this.root, this.outputService); + + return debugAdapterP.then(debugAdapter => { + + this.debugAdapter = debugAdapter; + + this.debugAdapter.onError(err => this.onDapServerError(err)); + this.debugAdapter.onEvent(event => this.onDapEvent(event)); + this.debugAdapter.onRequest(request => this.dispatchRequest(request)); + this.debugAdapter.onExit(code => this.onServerExit()); + + return this.debugAdapter.startSession(); + }); } public custom(request: string, args: any): TPromise { return this.send(request, args); } - protected send(command: string, args: any, cancelOnDisconnect = true): TPromise { + private 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 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 ? debug.formatPII(error.format, true, error.variables) : errorMessage; @@ -199,8 +252,22 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { }); } - protected onEvent(event: debug.DebugEvent): void { - event.sessionId = this.getId(); + 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.readyForBreakpoints = true; @@ -317,7 +384,7 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { this.sentPromises = []; }, 1000); - if ((this.serverProcess || this.socket) && !this.disconnected) { + 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()); @@ -392,16 +459,24 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { return (new Date().getTime() - this.startTime) / 1000; } - protected dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void { + 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') { TerminalSupport.runInTerminal(this.terminalService, this.nativeTerminalService, this.configurationService, request.arguments, response).then(() => { - this.sendResponse(response); + this.debugAdapter.sendResponse(response); }, e => { response.success = false; response.message = e.message; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); }); } else if (request.command === 'handshake') { try { @@ -411,16 +486,16 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { response.body = { signature: sig }; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } catch (e) { response.success = false; response.message = e.message; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } } else { response.success = false; response.message = `unknown request '${request.command}'`; - this.sendResponse(response); + this.debugAdapter.sendResponse(response); } } @@ -436,111 +511,36 @@ export class RawDebugSession extends V8Protocol implements debug.ISession { }); } - 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(this.root).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; + if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { + this.debugAdapter.stopSession(); + this.cachedInitServerP = null; } - this.onEvent({ event: 'exit', type: 'event', seq: 0 }); - if (!this.serverProcess) { + this.onDapEvent({ event: 'exit', type: 'event', seq: 0 }); + if (/* !this.serverProcess */ this.debugAdapter instanceof SocketDebugAdapter) { 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; + return this.debugAdapter.stopSession(); } - protected onServerError(err: Error): void { - this.notificationService.error(nls.localize('stoppingDebugAdapter', "{0}. Stopping the debug adapter.", err.message)); + private onDapServerError(err: Error): void { + this.notificationService.error(err.message || err.toString()); this.stopServer().done(null, errors.onUnexpectedError); } private onServerExit(): void { - this.serverProcess = null; - this.cachedInitServer = null; + //this.serverProcess = null; + this.debugAdapter = null; + this.cachedInitServerP = null; if (!this.disconnected) { this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } - this.onEvent({ event: 'exit', type: 'event', seq: 0 }); + this.onDapEvent({ event: 'exit', type: 'event', seq: 0 }); } public dispose(): void { diff --git a/src/vs/workbench/parts/debug/node/DapPathConverter.ts b/src/vs/workbench/parts/debug/node/DapPathConverter.ts new file mode 100644 index 00000000000..5ecfb49f1b7 --- /dev/null +++ b/src/vs/workbench/parts/debug/node/DapPathConverter.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export function convertPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (toDA: boolean, source: DebugProtocol.Source | undefined) => void) { + switch (msg.type) { + case 'event': + const event = msg; + switch (event.event) { + case 'output': + fixSourcePaths(false, (event).body.source); + break; + case 'loadedSource': + fixSourcePaths(false, (event).body.source); + break; + case 'breakpoint': + fixSourcePaths(false, (event).body.breakpoint.source); + break; + default: + break; + } + break; + case 'request': + const request = msg; + switch (request.command) { + case 'setBreakpoints': + fixSourcePaths(true, (request.arguments).source); + break; + case 'source': + fixSourcePaths(true, (request.arguments).source); + break; + case 'gotoTargets': + fixSourcePaths(true, (request.arguments).source); + break; + default: + break; + } + break; + case 'response': + const response = msg; + switch (response.command) { + case 'stackTrace': + const r1 = response; + r1.body.stackFrames.forEach(frame => fixSourcePaths(false, frame.source)); + break; + case 'loadedSources': + const r2 = response; + r2.body.sources.forEach(source => fixSourcePaths(false, source)); + break; + case 'scopes': + const r3 = response; + r3.body.scopes.forEach(scope => fixSourcePaths(false, scope.source)); + break; + case 'setFunctionBreakpoints': + const r4 = response; + r4.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + break; + case 'setBreakpoints': + const r5 = response; + r5.body.breakpoints.forEach(bp => fixSourcePaths(false, bp.source)); + break; + default: + break; + } + break; + } +} + +export function convertToDAPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void) { + convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + if (toDA && source) { + fixSourcePaths(source); + } + }); +} + +export function convertToVSCPaths(msg: DebugProtocol.ProtocolMessage, fixSourcePaths: (source: DebugProtocol.Source) => void) { + convertPaths(msg, (toDA: boolean, source: DebugProtocol.Source | undefined) => { + if (!toDA && source) { + fixSourcePaths(source); + } + }); +} \ No newline at end of file diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index fc2f741c812..610922e647b 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -3,110 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; -import * as paths from 'vs/base/common/paths'; -import * as platform from 'vs/base/common/platform'; import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IRawAdapter, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IRawAdapter, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration } from 'vs/workbench/parts/debug/common/debug'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; +import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol'; -export class Adapter { + +export class Debugger { + + private _mergedExtensionDescriptions: IExtensionDescription[]; constructor(private configurationManager: IConfigurationManager, private rawAdapter: IRawAdapter, public extensionDescription: IExtensionDescription, @IConfigurationService private configurationService: IConfigurationService, @ICommandService private commandService: ICommandService ) { - if (rawAdapter.windows) { - rawAdapter.win = rawAdapter.windows; - } + this._mergedExtensionDescriptions = [extensionDescription]; } public hasConfigurationProvider = false; - public getAdapterExecutable(root: IWorkspaceFolder, verifyAgainstFS = true): TPromise { + public createDebugAdapter(root: IWorkspaceFolder, outputService: IOutputService): TPromise { + return this.getAdapterExecutable(root).then(adapterExecutable => { + const debugConfigs = this.configurationService.getValue('debug'); + if (debugConfigs.extensionHostDebugAdapter) { + return this.configurationManager.createDebugAdapter(this.rawAdapter.type, adapterExecutable); + } else { + return new DebugAdapter(this.rawAdapter.type, adapterExecutable, this._mergedExtensionDescriptions, outputService); + } + }); + } + + public getAdapterExecutable(root: IWorkspaceFolder): TPromise { return this.configurationManager.debugAdapterExecutable(root ? root.uri : undefined, this.rawAdapter.type).then(adapterExecutable => { if (adapterExecutable) { - return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS); + return adapterExecutable; } // try deprecated command based extension API if (this.rawAdapter.adapterExecutableCommand) { - return this.commandService.executeCommand(this.rawAdapter.adapterExecutableCommand, root ? root.uri.toString() : undefined).then(ad => { - return this.verifyAdapterDetails(ad, verifyAgainstFS); - }); + return this.commandService.executeCommand(this.rawAdapter.adapterExecutableCommand, root ? root.uri.toString() : undefined); } - // fallback: executable contribution specified in package.json - adapterExecutable = { - command: this.getProgram(), - args: this.getAttributeBasedOnPlatform('args') - }; - const runtime = this.getRuntime(); - if (runtime) { - const runtimeArgs = this.getAttributeBasedOnPlatform('runtimeArgs'); - adapterExecutable.args = (runtimeArgs || []).concat([adapterExecutable.command]).concat(adapterExecutable.args || []); - adapterExecutable.command = runtime; - } - return this.verifyAdapterDetails(adapterExecutable, verifyAgainstFS); + return TPromise.as(null); }); } - private verifyAdapterDetails(details: IAdapterExecutable, verifyAgainstFS: boolean): TPromise { - - if (details.command) { - if (verifyAgainstFS) { - if (path.isAbsolute(details.command)) { - return new TPromise((c, e) => { - fs.exists(details.command, exists => { - if (exists) { - c(details); - } else { - e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", details.command))); - } - }); - }); - } else { - // relative path - if (details.command.indexOf('/') < 0 && details.command.indexOf('\\') < 0) { - // no separators: command looks like a runtime name like 'node' or 'mono' - return TPromise.as(details); // TODO: check that the runtime is available on PATH - } - } - } else { - return TPromise.as(details); - } - } - - return TPromise.wrapError(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] }, - "Cannot determine executable for debug adapter '{0}'.", this.type))); - } - - private getRuntime(): string { - let runtime = this.getAttributeBasedOnPlatform('runtime'); - if (runtime && runtime.indexOf('./') === 0) { - runtime = paths.join(this.extensionDescription.extensionFolderPath, runtime); - } - return runtime; - } - - private getProgram(): string { - let program = this.getAttributeBasedOnPlatform('program'); - if (program) { - program = paths.join(this.extensionDescription.extensionFolderPath, program); - } - return program; - } - public get aiKey(): string { return this.rawAdapter.aiKey; } @@ -132,6 +83,10 @@ export class Adapter { } public merge(secondRawAdapter: IRawAdapter, extensionDescription: IExtensionDescription): void { + + // remember all ext descriptions that are the source of this debugger + this._mergedExtensionDescriptions.push(extensionDescription); + // Give priority to built in debug adapters if (extensionDescription.isBuiltin) { this.extensionDescription = extensionDescription; @@ -248,19 +203,4 @@ export class Adapter { return attributes; }); } - - private getAttributeBasedOnPlatform(key: string): any { - let result: any; - if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432') && this.rawAdapter.winx86) { - result = this.rawAdapter.winx86[key]; - } else if (platform.isWindows && this.rawAdapter.win) { - result = this.rawAdapter.win[key]; - } else if (platform.isMacintosh && this.rawAdapter.osx) { - result = this.rawAdapter.osx[key]; - } else if (platform.isLinux && this.rawAdapter.linux) { - result = this.rawAdapter.linux[key]; - } - - return result || this.rawAdapter[key]; - } } diff --git a/src/vs/workbench/parts/debug/node/v8Protocol.ts b/src/vs/workbench/parts/debug/node/v8Protocol.ts index efac7e46afa..483779763c8 100644 --- a/src/vs/workbench/parts/debug/node/v8Protocol.ts +++ b/src/vs/workbench/parts/debug/node/v8Protocol.ts @@ -3,68 +3,82 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as cp from 'child_process'; import * as stream from 'stream'; +import * as nls from 'vs/nls'; +import * as paths from 'vs/base/common/paths'; +import * as objects from 'vs/base/common/objects'; +import * as platform from 'vs/base/common/platform'; +import * as stdfork from 'vs/base/node/stdFork'; +import { Emitter, Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; -import { canceled } from 'vs/base/common/errors'; +import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import * as debug from 'vs/workbench/parts/debug/common/debug'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; -export abstract class V8Protocol { +/** + * Abstract implementation of the low level API for a debug adapter. + * Missing is how this API communicates with the debug adapter. + */ +export abstract class AbstractDebugAdapter implements debug.IDebugAdapter { - private static readonly TWO_CRLF = '\r\n\r\n'; - - private outputStream: stream.Writable; private sequence: number; private pendingRequests: Map void>; - private rawData: Buffer; - private contentLength: number; + private requestCallback: (request: DebugProtocol.Request) => void; + private eventCallback: (request: DebugProtocol.Event) => void; - constructor(private id: string) { + protected readonly _onError: Emitter; + protected readonly _onExit: Emitter; + + constructor() { this.sequence = 1; - this.contentLength = -1; this.pendingRequests = new Map void>(); - this.rawData = Buffer.allocUnsafe(0); + + this._onError = new Emitter(); + this._onExit = new Emitter(); } - public getId(): string { - return this.id; + abstract startSession(): TPromise; + abstract stopSession(): TPromise; + + public dispose(): void { } - protected abstract onServerError(err: Error): void; - protected abstract onEvent(event: DebugProtocol.Event): void; - protected abstract dispatchRequest(request: DebugProtocol.Request, response: DebugProtocol.Response): void; + abstract sendMessage(message: DebugProtocol.ProtocolMessage): void; - protected connect(readable: stream.Readable, writable: stream.Writable): void { - - this.outputStream = writable; - - readable.on('data', (data: Buffer) => { - this.rawData = Buffer.concat([this.rawData, data]); - this.handleData(); - }); + public get onError(): Event { + return this._onError.event; } - protected send(command: string, args: any): TPromise { - let errorCallback: (error: Error) => void; - return new TPromise((completeDispatch, errorDispatch) => { - errorCallback = errorDispatch; - this.doSend(command, args, (result: R) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } - }); - }, () => errorCallback(canceled())); + public get onExit(): Event { + return this._onExit.event; + } + + public onEvent(callback: (event: DebugProtocol.Event) => void) { + if (this.eventCallback) { + this._onError.fire(new Error(`attempt to set more than one 'Event' callback`)); + } + this.eventCallback = callback; + } + + public onRequest(callback: (request: DebugProtocol.Request) => void) { + if (this.requestCallback) { + this._onError.fire(new Error(`attempt to set more than one 'Request' callback`)); + } + this.requestCallback = callback; } public sendResponse(response: DebugProtocol.Response): void { if (response.seq > 0) { - console.error(`attempt to send more than one response for command ${response.command}`); + this._onError.fire(new Error(`attempt to send more than one response for command ${response.command}`)); } else { - this.sendMessage('response', response); + this.internalSend('response', response); } } - private doSend(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { + public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { const request: any = { command: command @@ -73,7 +87,7 @@ export abstract class V8Protocol { request.arguments = args; } - this.sendMessage('request', request); + this.internalSend('request', request); if (clb) { // store callback for this request @@ -81,19 +95,85 @@ export abstract class V8Protocol { } } - private sendMessage(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { + public acceptMessage(message: DebugProtocol.ProtocolMessage) { + switch (message.type) { + case 'event': + if (this.eventCallback) { + this.eventCallback(message); + } + break; + case 'request': + if (this.requestCallback) { + this.requestCallback(message); + } + break; + case 'response': + const response = message; + const clb = this.pendingRequests.get(response.request_seq); + if (clb) { + this.pendingRequests.delete(response.request_seq); + clb(response); + } + break; + } + } + + private internalSend(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { message.type = typ; message.seq = this.sequence++; - const json = JSON.stringify(message); - const length = Buffer.byteLength(json, 'utf8'); + this.sendMessage(message); + } +} - this.outputStream.write('Content-Length: ' + length.toString() + V8Protocol.TWO_CRLF, 'utf8'); - this.outputStream.write(json, 'utf8'); +/** + * An implementation that communicates via two streams with the debug adapter. + */ +export abstract class StreamDebugAdapter extends AbstractDebugAdapter { + + private static readonly TWO_CRLF = '\r\n\r\n'; + + private outputStream: stream.Writable; + private rawData: Buffer; + private contentLength: number; + + constructor() { + super(); } - private handleData(): void { + public connect(readable: stream.Readable, writable: stream.Writable): void { + + this.outputStream = writable; + this.rawData = Buffer.allocUnsafe(0); + this.contentLength = -1; + + readable.on('data', (data: Buffer) => this.handleData(data)); + + // readable.on('close', () => { + // this._emitEvent(new Event('close')); + // }); + // readable.on('error', (error) => { + // this._emitEvent(new Event('error', 'readable error: ' + (error && error.message))); + // }); + + // writable.on('error', (error) => { + // this._emitEvent(new Event('error', 'writable error: ' + (error && error.message))); + // }); + } + + public sendMessage(message: DebugProtocol.ProtocolMessage): void { + + if (this.outputStream) { + const json = JSON.stringify(message); + this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8'); + } + } + + private handleData(data: Buffer): void { + + this.rawData = Buffer.concat([this.rawData, data]); + while (true) { if (this.contentLength >= 0) { if (this.rawData.length >= this.contentLength) { @@ -101,55 +181,226 @@ export abstract class V8Protocol { this.rawData = this.rawData.slice(this.contentLength); this.contentLength = -1; if (message.length > 0) { - this.dispatch(message); + try { + this.acceptMessage(JSON.parse(message)); + } catch (e) { + this._onError.fire(new Error((e.message || e) + '\n' + message)); + } } continue; // there may be more complete messages to process } } else { - const s = this.rawData.toString('utf8', 0, this.rawData.length); - const idx = s.indexOf(V8Protocol.TWO_CRLF); + const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF); if (idx !== -1) { - const match = /Content-Length: (\d+)/.exec(s); - if (match && match[1]) { - this.contentLength = Number(match[1]); - this.rawData = this.rawData.slice(idx + V8Protocol.TWO_CRLF.length); - continue; // try to handle a complete message + const header = this.rawData.toString('utf8', 0, idx); + const lines = header.split('\r\n'); + for (const h of lines) { + const kvPair = h.split(/: +/); + if (kvPair[0] === 'Content-Length') { + this.contentLength = Number(kvPair[1]); + } } + this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length); + continue; } } break; } } +} - private dispatch(body: string): void { - try { - const rawData = JSON.parse(body); - switch (rawData.type) { - case 'event': - this.onEvent(rawData); - break; - case 'response': - const response = rawData; - const clb = this.pendingRequests.get(response.request_seq); - if (clb) { - this.pendingRequests.delete(response.request_seq); - clb(response); +/** + * An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout. +*/ +export class DebugAdapter extends StreamDebugAdapter { + + private _serverProcess: cp.ChildProcess; + + constructor(private _debugType: string, private _adapterExecutable: debug.IAdapterExecutable | null, extensionDescriptions: IExtensionDescription[], private _outputService?: IOutputService) { + super(); + + if (!this._adapterExecutable) { + this._adapterExecutable = DebugAdapter.platformAdapterExecutable(extensionDescriptions, this._debugType); + } + } + + startSession(): TPromise { + + return new TPromise((c, e) => { + + // verify executables + if (this._adapterExecutable.command) { + if (paths.isAbsolute(this._adapterExecutable.command)) { + if (!fs.existsSync(this._adapterExecutable.command)) { + e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", this._adapterExecutable.command))); } - break; - case 'request': - const request = rawData; - const resp: DebugProtocol.Response = { - type: 'response', - seq: 0, - command: request.command, - request_seq: request.seq, - success: true - }; - this.dispatchRequest(request, resp); - break; + } else { + // relative path + if (this._adapterExecutable.command.indexOf('/') < 0 && this._adapterExecutable.command.indexOf('\\') < 0) { + // no separators: command looks like a runtime name like 'node' or 'mono' + // TODO: check that the runtime is available on PATH + } + } + } else { + e(new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] }, + "Cannot determine executable for debug adapter '{0}'.", this._debugType))); } - } catch (e) { - this.onServerError(new Error((e.message || e) + '\n' + body)); + + if (this._adapterExecutable.command === 'node' /*&& this.outputService*/) { + if (Array.isArray(this._adapterExecutable.args) && this._adapterExecutable.args.length > 0) { + stdfork.fork(this._adapterExecutable.args[0], this._adapterExecutable.args.slice(1), {}, (err, child) => { + if (err) { + e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", this._adapterExecutable.args[0]))); + } + this._serverProcess = child; + c(null); + }); + } else { + e(new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."))); + } + } else { + this._serverProcess = cp.spawn(this._adapterExecutable.command, this._adapterExecutable.args); + c(null); + } + }).then(_ => { + this._serverProcess.on('error', (err: Error) => this._onError.fire(err)); + this._serverProcess.on('exit', (code: number, signal: string) => this._onExit.fire(code)); + + if (this._outputService) { + 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); + }, err => { + this._onError.fire(err); + }); + } + + stopSession(): 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) { + return 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'); + return TPromise.as(null); + } + } + + private static extract(dbg: debug.IRawAdapter, extensionFolderPath: string) { + if (!dbg) { + return undefined; + } + let x: debug.IRawAdapter = {}; + + if (dbg.runtime) { + if (dbg.runtime.indexOf('./') === 0) { // TODO + x.runtime = paths.join(extensionFolderPath, dbg.runtime); + } else { + x.runtime = dbg.runtime; + } + } + if (dbg.runtimeArgs) { + x.runtimeArgs = dbg.runtimeArgs; + } + if (dbg.program) { + if (!paths.isAbsolute(dbg.program)) { + x.program = paths.join(extensionFolderPath, dbg.program); + } else { + x.program = dbg.program; + } + } + if (dbg.args) { + x.args = dbg.args; + } + + if (dbg.win) { + x.win = DebugAdapter.extract(dbg.win, extensionFolderPath); + } + if (dbg.winx86) { + x.winx86 = DebugAdapter.extract(dbg.winx86, extensionFolderPath); + } + if (dbg.windows) { + x.windows = DebugAdapter.extract(dbg.windows, extensionFolderPath); + } + if (dbg.osx) { + x.osx = DebugAdapter.extract(dbg.osx, extensionFolderPath); + } + if (dbg.linux) { + x.linux = DebugAdapter.extract(dbg.linux, extensionFolderPath); + } + return x; + } + + static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): debug.IAdapterExecutable { + + let result: debug.IRawAdapter = {}; + + debugType = debugType.toLowerCase(); + + // merge all contributions into one + for (const ed of extensionDescriptions) { + if (ed.contributes) { + const debuggers = ed.contributes['debuggers']; + if (debuggers && debuggers.length > 0) { + const dbgs = debuggers.filter(d => d.type.toLowerCase() === debugType); + for (const dbg of dbgs) { + + // extract relevant attributes and make then absolute where needed + const dbg1 = DebugAdapter.extract(dbg, ed.extensionFolderPath); + + // merge + objects.mixin(result, dbg1, ed.isBuiltin); + } + } + } + } + + // select the right platform + let platformInfo: debug.IRawEnvAdapter; + if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) { + platformInfo = result.winx86; + } else if (platform.isWindows) { + platformInfo = result.win || result.windows; + } else if (platform.isMacintosh) { + platformInfo = result.osx; + } else if (platform.isLinux) { + platformInfo = result.linux; + } + platformInfo = platformInfo || result; + + // these are the relevant attributes + let program = platformInfo.program || result.program; + const args = platformInfo.args || result.args; + let runtime = platformInfo.runtime || result.runtime; + const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs; + + if (runtime) { + return { + command: runtime, + args: (runtimeArgs || []).concat([program]).concat(args || []) + }; + } else { + return { + command: program, + args: args || [] + }; } } } diff --git a/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts b/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts index bc4cfc87f6c..62a59b4741e 100644 --- a/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts +++ b/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts @@ -6,15 +6,17 @@ import * as assert from 'assert'; import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; -import { IRawAdapter, IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; -import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; +import { IAdapterExecutable, IConfigurationManager } from 'vs/workbench/parts/debug/common/debug'; +import { Debugger } from 'vs/workbench/parts/debug/node/debugAdapter'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import uri from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; +import { DebugAdapter } from 'vs/workbench/parts/debug/node/v8Protocol'; -suite('Debug - Adapter', () => { - let adapter: Adapter; +suite('Debug - Debugger', () => { + let _debugger: Debugger; + const extensionFolderPath = 'a/b/c/'; const rawAdapter = { type: 'mock', @@ -44,6 +46,73 @@ suite('Debug - Adapter', () => { } ] }; + + const extensionDescriptor0 = { + id: 'adapter', + name: 'myAdapter', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: extensionFolderPath, + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + rawAdapter + ] + } + }; + + const extensionDescriptor1 = { + id: 'extension1', + name: 'extension1', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: '/e1/b/c/', + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + { + type: 'mock', + runtime: 'runtime', + runtimeArgs: ['rarg'], + program: 'mockprogram', + args: ['parg'] + } + ] + } + }; + + const extensionDescriptor2 = { + id: 'extension2', + name: 'extension2', + version: '1.0.0', + publisher: 'vscode', + extensionFolderPath: '/e2/b/c/', + isBuiltin: false, + engines: null, + contributes: { + 'debuggers': [ + { + type: 'mock', + win: { + runtime: 'winRuntime', + program: 'winProgram' + }, + linux: { + runtime: 'linuxRuntime', + program: 'linuxProgram' + }, + osx: { + runtime: 'osxRuntime', + program: 'osxProgram' + } + } + ] + } + }; + + const configurationManager = { debugAdapterExecutable(folderUri: uri | undefined, type: string): TPromise { return TPromise.as(undefined); @@ -51,26 +120,25 @@ suite('Debug - Adapter', () => { }; setup(() => { - adapter = new Adapter(configurationManager, rawAdapter, { extensionFolderPath, id: 'adapter', name: 'myAdapter', version: '1.0.0', publisher: 'vscode', isBuiltin: false, engines: null }, - new TestConfigurationService(), null); + _debugger = new Debugger(configurationManager, rawAdapter, extensionDescriptor0, new TestConfigurationService(), null); }); teardown(() => { - adapter = null; + _debugger = null; }); test('attributes', () => { - assert.equal(adapter.type, rawAdapter.type); - assert.equal(adapter.label, rawAdapter.label); + assert.equal(_debugger.type, rawAdapter.type); + assert.equal(_debugger.label, rawAdapter.label); - return adapter.getAdapterExecutable(undefined, false).then(details => { - assert.equal(details.command, paths.join(extensionFolderPath, rawAdapter.program)); - assert.deepEqual(details.args, rawAdapter.args); - }); + const ae = DebugAdapter.platformAdapterExecutable([extensionDescriptor0], 'mock'); + + assert.equal(ae.command, paths.join(extensionFolderPath, rawAdapter.program)); + assert.deepEqual(ae.args, rawAdapter.args); }); test('schema attributes', () => { - const schemaAttribute = adapter.getSchemaAttributes()[0]; + const schemaAttribute = _debugger.getSchemaAttributes()[0]; assert.notDeepEqual(schemaAttribute, rawAdapter.configurationAttributes); Object.keys(rawAdapter.configurationAttributes.launch).forEach(key => { assert.deepEqual(schemaAttribute[key], rawAdapter.configurationAttributes.launch[key]); @@ -83,38 +151,13 @@ suite('Debug - Adapter', () => { assert.equal(!!schemaAttribute['properties']['preLaunchTask'], true); }); - test('merge', () => { + test('merge platform specific attributes', () => { - const da: IRawAdapter = { - type: 'mock', - win: { - runtime: 'winRuntime' - }, - linux: { - runtime: 'linuxRuntime' - }, - osx: { - runtime: 'osxRuntime' - }, - runtimeArgs: ['first arg'], - program: 'mockprogram', - args: ['arg'] - }; - adapter.merge(da, { - name: 'my name', - id: 'my_id', - version: '1.0', - publisher: 'mockPublisher', - isBuiltin: true, - extensionFolderPath: 'a/b/c/d', - engines: null - }); - - return adapter.getAdapterExecutable(undefined, false).then(details => { - assert.equal(details.command, platform.isLinux ? da.linux.runtime : platform.isMacintosh ? da.osx.runtime : da.win.runtime); - assert.deepEqual(details.args, da.runtimeArgs.concat(['a/b/c/d/mockprogram'].concat(da.args))); - }); + const ae2 = DebugAdapter.platformAdapterExecutable([extensionDescriptor1, extensionDescriptor2], 'mock'); + assert.equal(ae2.command, platform.isLinux ? 'linuxRuntime' : (platform.isMacintosh ? 'osxRuntime' : 'winRuntime')); + const xprogram = platform.isLinux ? 'linuxProgram' : (platform.isMacintosh ? 'osxProgram' : 'winProgram'); + assert.deepEqual(ae2.args, ['rarg', '/e2/b/c/' + xprogram, 'parg']); }); test('initial config file content', () => { @@ -134,7 +177,7 @@ suite('Debug - Adapter', () => { ' ]', '}'].join('\n'); - return adapter.getInitialConfigurationContent().then(content => { + return _debugger.getInitialConfigurationContent().then(content => { assert.equal(content, expected); }, err => assert.fail(err)); });