diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 91f21245663..a1e78a9271d 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -308,6 +308,11 @@ export interface ICompound { configurations: string[]; } +export interface IAdapterExecutable { + command?: string; + args?: string[]; +} + export interface IRawEnvAdapter { type?: string; label?: string; @@ -318,6 +323,7 @@ export interface IRawEnvAdapter { } export interface IRawAdapter extends IRawEnvAdapter { + adapterExecutableCommand?: string; enableBreakpointsFor?: { languageIds: string[] }; configurationAttributes?: any; configurationSnippets?: IJSONSchemaSnippet[]; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index f6070468b38..abffb6c8724 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -75,6 +75,10 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE description: nls.localize('vscode.extension.contributes.debuggers.languages', "List of languages for which the debug extension could be considered the \"default debugger\"."), type: 'array' }, + adapterExecutableCommand: { + description: nls.localize('vscode.extension.contributes.debuggers.adapterExecutableCommand', "If specified VS Code will call this command to determine the executable path of the debug adapter and the argumnts to pass."), + type: 'string' + }, startSessionCommand: { description: nls.localize('vscode.extension.contributes.debuggers.startSessionCommand', "If specified VS Code will call this command for the \"debug\" or \"run\" actions targeted for this extension."), type: 'string' diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 04be8868b7e..6d18187f22f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -5,7 +5,6 @@ import nls = require('vs/nls'); import cp = require('child_process'); -import fs = require('fs'); import net = require('net'); import Event, { Emitter } from 'vs/base/common/event'; import platform = require('vs/base/common/platform'); @@ -431,11 +430,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { } private startServer(): TPromise { - if (!this.adapter.program) { - return TPromise.wrapError(new Error(nls.localize('noDebugAdapterExtensionInstalled', "No extension installed for '{0}' debugging.", this.adapter.type))); - } - - return this.getLaunchDetails().then(d => this.launchServer(d).then(() => { + return this.adapter.getAdapterExecutable().then(d => this.launchServer(d).then(() => { this.serverProcess.on('error', (err: Error) => this.onServerError(err)); this.serverProcess.on('exit', (code: number, signal: string) => this.onServerExit()); @@ -451,18 +446,18 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { })); } - private launchServer(launch: { command: string, argv: string[] }): TPromise { + private launchServer(launch: debug.IAdapterExecutable): TPromise { return new TPromise((c, e) => { if (launch.command === 'node') { - stdfork.fork(launch.argv[0], launch.argv.slice(1), {}, (err, child) => { + 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.argv[0]))); + e(new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", launch.args[0]))); } this.serverProcess = child; c(null); }); } else { - this.serverProcess = cp.spawn(launch.command, launch.argv, { + this.serverProcess = cp.spawn(launch.command, launch.args, { stdio: [ 'pipe', // stdin 'pipe', // stdout @@ -510,30 +505,6 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { return ret; } - private getLaunchDetails(): TPromise<{ command: string; argv: string[]; }> { - return new TPromise((c, e) => { - fs.exists(this.adapter.program, exists => { - if (exists) { - c(null); - } else { - e(new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' not found.", this.adapter.program))); - } - }); - }).then(() => { - if (this.adapter.runtime) { - return { - command: this.adapter.runtime, - argv: (this.adapter.runtimeArgs || []).concat([this.adapter.program]).concat(this.adapter.args || []) - }; - } - - return { - command: this.adapter.program, - argv: this.adapter.args || [] - }; - }); - } - 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); diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index dbedd8d6574..719f6fc4bb9 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -9,8 +9,9 @@ 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 fs = require('fs'); import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; -import { IRawAdapter } from 'vs/workbench/parts/debug/common/debug'; +import { IRawAdapter, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -28,34 +29,74 @@ export class Adapter { } } - public get runtime(): string { + public getAdapterExecutable(verifyAgainstFS = true): TPromise { + + if (this.rawAdapter.adapterExecutableCommand) { + return this.commandService.executeCommand(this.rawAdapter.adapterExecutableCommand).then(ad => { + return this.verifyAdapterDetails(ad, verifyAgainstFS); + }); + } + + const ad = { + command: this.getProgram(), + args: this.getAttributeBasedOnPlatform('args') + }; + const runtime = this.getRuntime(); + if (runtime) { + const runtimeArgs = this.getAttributeBasedOnPlatform('runtimeArgs'); + ad.args = (runtimeArgs || []).concat([ad.command]).concat(ad.args || []); + ad.command = runtime; + } + return this.verifyAdapterDetails(ad, verifyAgainstFS); + } + + private verifyAdapterDetails(details: IAdapterExecutable, verifyAgainstFS: boolean): TPromise { + + if (details.command) { + if (verifyAgainstFS) { + if (paths.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('debugAdapterCannotDetermineExecutable', "Cannot determine executable for debug adapter '{0}'.", details.command))); + } + + private getRuntime(): string { let runtime = this.getAttributeBasedOnPlatform('runtime'); if (runtime && runtime.indexOf('./') === 0) { runtime = this.configurationResolverService ? this.configurationResolverService.resolve(runtime) : runtime; runtime = paths.join(this.extensionDescription.extensionFolderPath, runtime); } - return runtime; } - public get program(): string { + private getProgram(): string { let program = this.getAttributeBasedOnPlatform('program'); if (program) { program = this.configurationResolverService ? this.configurationResolverService.resolve(program) : program; program = paths.join(this.extensionDescription.extensionFolderPath, program); } - return program; } - public get runtimeArgs(): string[] { - return this.getAttributeBasedOnPlatform('runtimeArgs'); - } - - public get args(): string[] { - return this.getAttributeBasedOnPlatform('args'); - } - public get aiKey(): string { return this.rawAdapter.aiKey; } 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 e91588da46a..af7c57e791f 100644 --- a/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts +++ b/src/vs/workbench/parts/debug/test/node/debugAdapter.test.ts @@ -8,6 +8,7 @@ import * as paths from 'vs/base/common/paths'; import * as platform from 'vs/base/common/platform'; import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import debug = require('vs/workbench/parts/debug/common/debug'); suite('Debug - Adapter', () => { let adapter: Adapter; @@ -17,15 +18,7 @@ suite('Debug - Adapter', () => { label: 'Mock Debug', enableBreakpointsFor: { 'languageIds': ['markdown'] }, program: './out/mock/mockDebug.js', - win: { - runtime: 'winRuntime' - }, - linux: { - runtime: 'linuxRuntime' - }, - osx: { - runtime: 'osxRuntime' - }, + args: ['arg1', 'arg2'], configurationAttributes: { launch: { required: ['program'], @@ -61,8 +54,11 @@ suite('Debug - Adapter', () => { test('attributes', () => { assert.equal(adapter.type, rawAdapter.type); assert.equal(adapter.label, rawAdapter.label); - assert.equal(adapter.program, paths.join(extensionFolderPath, rawAdapter.program)); - assert.equal(adapter.runtime, platform.isLinux ? rawAdapter.linux.runtime : platform.isMacintosh ? rawAdapter.osx.runtime : rawAdapter.win.runtime); + + return adapter.getAdapterExecutable(false).then(details => { + assert.equal(details.command, paths.join(extensionFolderPath, rawAdapter.program)); + assert.deepEqual(details.args, rawAdapter.args); + }); }); test('schema attributes', () => { @@ -80,23 +76,37 @@ suite('Debug - Adapter', () => { }); test('merge', () => { - const runtimeArgs = ['first arg']; - adapter.merge({ - type: 'mock', - runtimeArgs, - program: 'mockprogram' - }, { - name: 'my name', - id: 'my_id', - version: '1.0', - publisher: 'mockPublisher', - isBuiltin: true, - extensionFolderPath: 'a/b/c/d', - engines: null - }); - assert.deepEqual(adapter.runtimeArgs, runtimeArgs); - assert.equal(adapter.program, 'a/b/c/d/mockprogram'); + const da: debug.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(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))); + }); }); test('initial config file content', () => {