diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 3be13501b7d..421cb44dafb 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, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable, ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, @@ -54,7 +54,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { this._debugAdapters = new Map(); - // register a default DA provider + // register a default EH DA provider debugService.getConfigurationManager().registerDebugAdapterProvider('*', { createDebugAdapter: (debugType, adapterInfo) => { const handle = this._debugAdaptersHandleCounter++; @@ -63,6 +63,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { return da; } }); + + // register a default EH terminal launcher + debugService.getConfigurationManager().registerEHTerminalLauncher(new ExtensionHostTerminalLauncher(this._proxy)); } public dispose(): void { @@ -281,3 +284,13 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter { return this._proxy.$stopDASession(this._handle); } } + +class ExtensionHostTerminalLauncher implements ITerminalLauncher { + + constructor(private _proxy: ExtHostDebugServiceShape) { + } + + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this._proxy.$runInTerminal(args, config); + } +} diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index f7a1a1d069a..59f2f90acda 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -24,7 +24,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; import { IConfigurationData, ConfigurationTarget, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; -import { IConfig, IAdapterExecutable } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IAdapterExecutable, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; @@ -786,6 +786,7 @@ export interface ISourceMultiBreakpointDto { } export interface ExtHostDebugServiceShape { + $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null): TPromise; $stopDASession(handle: number): TPromise; $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): TPromise; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 9cd1acc2719..b6fcdc55a47 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -19,7 +19,8 @@ import { generateUuid } from 'vs/base/common/uuid'; import { DebugAdapter, convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/node/debugAdapter'; 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'; +import { IAdapterExecutable, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { getTerminalLauncher } from 'vs/workbench/parts/debug/node/terminals'; export class ExtHostDebugService implements ExtHostDebugServiceShape { @@ -82,6 +83,10 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._debugAdapters = new Map(); } + public $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return getTerminalLauncher().runInTerminal(args, config); + } + public $startDASession(handle: number, debugType: string, adpaterExecutable: IAdapterExecutable | null): TPromise { const mythis = this; diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 69c7e554891..687eb04aacc 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -446,6 +446,25 @@ export interface IDebugConfigurationProvider { debugAdapterExecutable(folderUri: uri | undefined): TPromise; } +export interface ITerminalLauncher { + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; +} + +export interface ITerminalSettings { + external: { + windowsExec: string, + osxExec: string, + linuxExec: string + }; + integrated: { + shell: { + osx: string, + windows: string, + linux: string + } + }; +} + export interface IConfigurationManager { /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. @@ -479,6 +498,9 @@ export interface IConfigurationManager { registerDebugAdapterProvider(debugType: string, debugAdapterLauncher: IDebugAdapterProvider); createDebugAdapter(debugType: string, adapterExecutable: IAdapterExecutable | null): IDebugAdapter; + + registerEHTerminalLauncher(launcher: ITerminalLauncher): void; + runInTerminal(extensionHost: boolean, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; } export interface ILaunch { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index ce741cd3008..d722e8e68fa 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -26,7 +26,7 @@ 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, IDebuggerContribution, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugConfigurationProvider, IDebuggerContribution, ICompound, IDebugConfiguration, IConfig, IEnvConfig, IGlobalConfig, IConfigurationManager, ILaunch, IAdapterExecutable, IDebugAdapterProvider, IDebugAdapter, ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -34,6 +34,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { TerminalLauncher } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; // debuggers extension point export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { @@ -230,6 +231,8 @@ export class ConfigurationManager implements IConfigurationManager { private _onDidSelectConfigurationName = new Emitter(); private providers: IDebugConfigurationProvider[]; private debugAdapterProviders: Map; + private _terminalLauncher: ITerminalLauncher; + private _ehTerminalLauncher: ITerminalLauncher; constructor( @@ -315,6 +318,22 @@ export class ConfigurationManager implements IConfigurationManager { return dap.createDebugAdapter(debugType, adapterExecutable); } + public registerEHTerminalLauncher(launcher: ITerminalLauncher): void { + this._ehTerminalLauncher = launcher; + } + + public runInTerminal(extensionHost: boolean, args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + + if (extensionHost && this._ehTerminalLauncher) { + return this._ehTerminalLauncher.runInTerminal(args, config); + } else { + if (!this._terminalLauncher) { + this._terminalLauncher = this.instantiationService.createInstance(TerminalLauncher); + } + return this._terminalLauncher.runInTerminal(args, config); + } + } + private registerListeners(lifecycleService: ILifecycleService): void { debuggersExtPoint.setHandler((extensions) => { extensions.forEach(extension => { diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 86598c7d04f..80b235858b0 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -11,14 +11,10 @@ import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { 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 { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { 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/debugAdapter'; @@ -102,10 +98,7 @@ export class RawDebugSession implements debug.ISession { public root: IWorkspaceFolder, @INotificationService private notificationService: INotificationService, @ITelemetryService private telemetryService: ITelemetryService, - @IOutputService private outputService: IOutputService, - @ITerminalService private terminalService: ITerminalService, - @IExternalTerminalService private nativeTerminalService: IExternalTerminalService, - @IConfigurationService private configurationService: IConfigurationService + @IOutputService private outputService: IOutputService ) { this.emittedStopped = false; this.readyForBreakpoints = false; @@ -471,13 +464,14 @@ export class RawDebugSession implements debug.ISession { if (request.command === 'runInTerminal') { - TerminalSupport.runInTerminal(this.terminalService, this.nativeTerminalService, this.configurationService, request.arguments, response).then(() => { + this._debugger.runInTerminal(request.arguments).then(_ => { this.debugAdapter.sendResponse(response); - }, e => { + }, err => { response.success = false; - response.message = e.message; + response.message = err.message; this.debugAdapter.sendResponse(response); }); + } else if (request.command === 'handshake') { try { const vsda = require.__$__nodeRequire('vsda'); diff --git a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts index 084ff3264de..6e622753c83 100644 --- a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts +++ b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts @@ -8,41 +8,47 @@ import * as platform from 'vs/base/common/platform'; import * as cp from 'child_process'; import { IDisposable } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITerminalService, ITerminalInstance, ITerminalConfiguration } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance } from 'vs/workbench/parts/terminal/common/terminal'; import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; const enum ShellType { cmd, powershell, bash } -export class TerminalSupport { +export class TerminalLauncher implements ITerminalLauncher { - private static integratedTerminalInstance: ITerminalInstance; - private static terminalDisposedListener: IDisposable; + private integratedTerminalInstance: ITerminalInstance; + private terminalDisposedListener: IDisposable; - public static runInTerminal(terminalService: ITerminalService, nativeTerminalService: IExternalTerminalService, configurationService: IConfigurationService, args: DebugProtocol.RunInTerminalRequestArguments, response: DebugProtocol.RunInTerminalResponse): TPromise { + constructor( + @ITerminalService private terminalService: ITerminalService, + @IExternalTerminalService private nativeTerminalService: IExternalTerminalService + ) { + } + + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'external') { - return nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); + return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); } - if (!TerminalSupport.terminalDisposedListener) { + if (!this.terminalDisposedListener) { // React on terminal disposed and check if that is the debug terminal #12956 - TerminalSupport.terminalDisposedListener = terminalService.onInstanceDisposed(terminal => { - if (TerminalSupport.integratedTerminalInstance && TerminalSupport.integratedTerminalInstance.id === terminal.id) { - TerminalSupport.integratedTerminalInstance = null; + this.terminalDisposedListener = this.terminalService.onInstanceDisposed(terminal => { + if (this.integratedTerminalInstance && this.integratedTerminalInstance.id === terminal.id) { + this.integratedTerminalInstance = null; } }); } - let t = TerminalSupport.integratedTerminalInstance; + let t = this.integratedTerminalInstance; if ((t && this.isBusy(t)) || !t) { - t = terminalService.createInstance({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); - TerminalSupport.integratedTerminalInstance = t; + t = this.terminalService.createInstance({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); + this.integratedTerminalInstance = t; } - terminalService.setActiveInstance(t); - terminalService.showPanel(true); + this.terminalService.setActiveInstance(t); + this.terminalService.showPanel(true); - const command = this.prepareCommand(args, configurationService); + const command = this.prepareCommand(args, config); return new TPromise((resolve, error) => { setTimeout(_ => { @@ -52,7 +58,7 @@ export class TerminalSupport { }); } - private static isBusy(t: ITerminalInstance): boolean { + private isBusy(t: ITerminalInstance): boolean { if (t.processId) { try { // if shell has at least one child process, assume that shell is busy @@ -82,13 +88,13 @@ export class TerminalSupport { return true; } - private static prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, configurationService: IConfigurationService): string { + private prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string { let shellType: ShellType; // get the shell configuration for the current platform let shell: string; - const shell_config = (configurationService.getValue().terminal.integrated).shell; + const shell_config = config.integrated.shell; if (platform.isWindows) { shell = shell_config.windows; shellType = ShellType.cmd; @@ -187,7 +193,7 @@ export class TerminalSupport { command += `cd ${quote(args.cwd)} ; `; } if (args.env) { - command += 'env'; + command += 'envVars'; for (let key in args.env) { const value = args.env[key]; if (value === null) { @@ -206,4 +212,4 @@ export class TerminalSupport { return command; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index d23eb5bfcac..c6e420b6f45 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -9,14 +9,13 @@ import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings } 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/debugAdapter'; - export class Debugger { private _mergedExtensionDescriptions: IExtensionDescription[]; @@ -60,6 +59,12 @@ export class Debugger { }); } + public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise { + const debugConfigs = this.configurationService.getValue('debug'); + const config = this.configurationService.getValue('terminal'); + return this.configurationManager.runInTerminal(debugConfigs && debugConfigs.extensionHostDebugAdapter, args, config); + } + public get aiKey(): string { return this.debuggerContribution.aiKey; } diff --git a/src/vs/workbench/parts/debug/node/terminals.ts b/src/vs/workbench/parts/debug/node/terminals.ts new file mode 100644 index 00000000000..492aed9cba1 --- /dev/null +++ b/src/vs/workbench/parts/debug/node/terminals.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as cp from 'child_process'; +import * as nls from 'vs/nls'; +import * as env from 'vs/base/common/platform'; +import * as pfs from 'vs/base/node/pfs'; +import { assign } from 'vs/base/common/objects'; +import { TPromise } from 'vs/base/common/winjs.base'; +import uri from 'vs/base/common/uri'; +import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; + +const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); + +let terminalLauncher: ITerminalLauncher = undefined; + +export function getTerminalLauncher() { + if (!terminalLauncher) { + if (env.isWindows) { + terminalLauncher = new WinTerminalService(); + } else if (env.isMacintosh) { + terminalLauncher = new MacTerminalService(); + } else if (env.isLinux) { + terminalLauncher = new LinuxTerminalService(); + } + } + return terminalLauncher; +} + +let _DEFAULT_TERMINAL_LINUX_READY: TPromise = null; +export function getDefaultTerminalLinuxReady(): TPromise { + if (!_DEFAULT_TERMINAL_LINUX_READY) { + _DEFAULT_TERMINAL_LINUX_READY = new TPromise(c => { + if (env.isLinux) { + TPromise.join([pfs.exists('/etc/debian_version'), process.lazyEnv]).then(([isDebian]) => { + if (isDebian) { + c('x-terminal-emulator'); + } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { + c('gnome-terminal'); + } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { + c('konsole'); + } else if (process.env.COLORTERM) { + c(process.env.COLORTERM); + } else if (process.env.TERM) { + c(process.env.TERM); + } else { + c('xterm'); + } + }); + return; + } + + c('xterm'); + }, () => { }); + } + return _DEFAULT_TERMINAL_LINUX_READY; +} + +let _DEFAULT_TERMINAL_WINDOWS: string = null; +export function getDefaultTerminalWindows(): string { + if (!_DEFAULT_TERMINAL_WINDOWS) { + const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + _DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; + } + return _DEFAULT_TERMINAL_WINDOWS; +} + +abstract class TerminalLauncher implements ITerminalLauncher { + public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this.runInTerminal0(args.title, args.cwd, args.args, args.env, config); + } + runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, config): TPromise { + return void 0; + } +} + +class WinTerminalService extends TerminalLauncher { + + private static readonly CMD = 'cmd.exe'; + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const exec = configuration.external.windowsExec || getDefaultTerminalWindows(); + + return new TPromise((c, e) => { + + const title = `"${dir} - ${TERMINAL_TITLE}"`; + const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code + + const cmdArgs = [ + '/c', 'start', title, '/wait', exec, '/c', command + ]; + + // merge environment variables into a copy of the process.env + const env = assign({}, process.env, envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env, + windowsVerbatimArguments: true + }; + + const cmd = cp.spawn(WinTerminalService.CMD, cmdArgs, options); + cmd.on('error', e); + + c(null); + }); + } +} + +class MacTerminalService extends TerminalLauncher { + + private static readonly DEFAULT_TERMINAL_OSX = 'Terminal.app'; + private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const terminalApp = configuration.external.osxExec || MacTerminalService.DEFAULT_TERMINAL_OSX; + + return new TPromise((c, e) => { + + if (terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') { + + // On OS X we launch an AppleScript that creates (or reuses) a Terminal window + // and then launches the program inside that window. + + const script = terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper'; + const scriptpath = uri.parse(require.toUrl(`vs/workbench/parts/execution/electron-browser/${script}.scpt`)).fsPath; + + const osaArgs = [ + scriptpath, + '-t', title || TERMINAL_TITLE, + '-w', dir, + ]; + + for (let a of args) { + osaArgs.push('-a'); + osaArgs.push(a); + } + + if (envVars) { + for (let key in envVars) { + const value = envVars[key]; + if (value === null) { + osaArgs.push('-u'); + osaArgs.push(key); + } else { + osaArgs.push('-e'); + osaArgs.push(`${key}=${value}`); + } + } + } + + let stderr = ''; + const osa = cp.spawn(MacTerminalService.OSASCRIPT, osaArgs); + osa.on('error', e); + osa.stderr.on('data', (data) => { + stderr += data.toString(); + }); + osa.on('exit', (code: number) => { + if (code === 0) { // OK + c(null); + } else { + if (stderr) { + const lines = stderr.split('\n', 1); + e(new Error(lines[0])); + } else { + e(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code))); + } + } + }); + } else { + e(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp))); + } + }); + } +} + +class LinuxTerminalService extends TerminalLauncher { + + private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); + + public runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): TPromise { + + const terminalConfig = configuration.external; + const execPromise = terminalConfig.linuxExec ? TPromise.as(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady(); + + return new TPromise((c, e) => { + + let termArgs: string[] = []; + //termArgs.push('--title'); + //termArgs.push(`"${TERMINAL_TITLE}"`); + execPromise.then(exec => { + if (exec.indexOf('gnome-terminal') >= 0) { + termArgs.push('-x'); + } else { + termArgs.push('-e'); + } + termArgs.push('bash'); + termArgs.push('-c'); + + const bashCommand = `${quote(args)}; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`; + termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... + + // merge environment variables into a copy of the process.env + const env = assign({}, process.env, envVars); + + // delete environment variables that have a null value + Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); + + const options: any = { + cwd: dir, + env: env + }; + + let stderr = ''; + const cmd = cp.spawn(exec, termArgs, options); + cmd.on('error', e); + cmd.stderr.on('data', (data) => { + stderr += data.toString(); + }); + cmd.on('exit', (code: number) => { + if (code === 0) { // OK + c(null); + } else { + if (stderr) { + const lines = stderr.split('\n', 1); + e(new Error(lines[0])); + } else { + e(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code))); + } + } + }); + }); + }); + } +} + +/** + * Quote args if necessary and combine into a space separated string. + */ +function quote(args: string[]): string { + let r = ''; + for (let a of args) { + if (a.indexOf(' ') >= 0) { + r += '"' + a + '"'; + } else { + r += a; + } + r += ' '; + } + return r; +}