diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index af2d76abc49..8c8d095db84 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, ITerminalSettings, IDebugAdapter, IDebugAdapterProvider } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IAdapterExecutable, ITerminalSettings, IDebugAdapter, IDebugAdapterProvider, ITerminalLauncher } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, @@ -18,6 +18,8 @@ import { AbstractDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter import * as paths from 'vs/base/common/paths'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/parts/debug/common/debugUtils'; +import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; +import { AbstractTerminalLauncher } from 'vs/workbench/parts/debug/electron-browser/terminalSupport'; @extHostNamedCustomer(MainContext.MainThreadDebugService) @@ -28,11 +30,13 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private _breakpointEventsActive: boolean; private _debugAdapters: Map; private _debugAdaptersHandleCounter = 1; + private _terminalLauncher: ITerminalLauncher; constructor( extHostContext: IExtHostContext, - @IDebugService private debugService: IDebugService + @IDebugService private debugService: IDebugService, + @ITerminalService private terminalService: ITerminalService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService); this._toDispose = []; @@ -73,7 +77,10 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { - return this._proxy.$runInTerminal(args, config); + if (!this._terminalLauncher) { + this._terminalLauncher = new ExtensionTerminalLauncher(this.terminalService, this._proxy); + } + return this._terminalLauncher.runInTerminal(args, config); } public dispose(): void { @@ -295,3 +302,25 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter { return this._proxy.$stopDASession(this._handle); } } + +export class ExtensionTerminalLauncher extends AbstractTerminalLauncher { + + constructor( + @ITerminalService terminalService: ITerminalService, + private _proxy: ExtHostDebugServiceShape + ) { + super(terminalService); + } + + protected runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this._proxy.$runInTerminal(args, config); + } + + protected isBusy(processId: number): TPromise { + return this._proxy.$isTerminalBusy(processId); + } + + protected prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this._proxy.$prepareCommandForTerminal(args, config); + } +} diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 80202809e70..4f757687281 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -831,6 +831,8 @@ export interface ISourceMultiBreakpointDto { export interface ExtHostDebugServiceShape { $substituteVariables(folder: UriComponents | undefined, config: IConfig): TPromise; $runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; + $isTerminalBusy(processId: number): TPromise; + $prepareCommandForTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise; $startDASession(handle: number, debugType: string, adapterExecutableInfo: IAdapterExecutable | null, debugPort: number): 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 5dfa0140e6a..a16c1525a5e 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -22,7 +22,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { IAdapterExecutable, ITerminalSettings, IDebuggerContribution, IConfig, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; -import { getTerminalLauncher } from 'vs/workbench/parts/debug/node/terminals'; +import { getTerminalLauncher, hasChildprocesses, prepareCommand } from 'vs/workbench/parts/debug/node/terminals'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -125,6 +125,14 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { return void 0; } + public $isTerminalBusy(processId: number): TPromise { + return asWinJsPromise(token => hasChildprocesses(processId)); + } + + public $prepareCommandForTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return asWinJsPromise(token => prepareCommand(args, config)); + } + public $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): TPromise { if (!this._variableResolver) { this._variableResolver = new ExtHostVariableResolverService(this._workspace, this._editorsService, this._configurationService); diff --git a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts index d6901928791..e9abd1de5f1 100644 --- a/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts +++ b/src/vs/workbench/parts/debug/electron-browser/terminalSupport.ts @@ -4,31 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -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 } from 'vs/workbench/parts/terminal/common/terminal'; import { ITerminalService as IExternalTerminalService } from 'vs/workbench/parts/execution/common/execution'; import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { hasChildprocesses, prepareCommand } from 'vs/workbench/parts/debug/node/terminals'; -const enum ShellType { cmd, powershell, bash } - -export class TerminalLauncher implements ITerminalLauncher { +export class AbstractTerminalLauncher implements ITerminalLauncher { private integratedTerminalInstance: ITerminalInstance; private terminalDisposedListener: IDisposable; - constructor( - @ITerminalService private terminalService: ITerminalService, - @IExternalTerminalService private nativeTerminalService: IExternalTerminalService - ) { + constructor(private terminalService: ITerminalService) { } - runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + async runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { if (args.kind === 'external') { - return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); + return this.runInExternalTerminal(args, config); } if (!this.terminalDisposedListener) { @@ -41,14 +35,14 @@ export class TerminalLauncher implements ITerminalLauncher { } let t = this.integratedTerminalInstance; - if ((t && this.isBusy(t)) || !t) { + if ((t && await this.isBusy(t.processId)) || !t) { t = this.terminalService.createTerminal({ name: args.title || nls.localize('debug.terminal.title', "debuggee") }); this.integratedTerminalInstance = t; } this.terminalService.setActiveInstance(t); this.terminalService.showPanel(true); - const command = this.prepareCommand(args, config); + const command = await this.prepareCommand(args, config); return new TPromise((resolve, error) => { setTimeout(_ => { @@ -58,158 +52,29 @@ export class TerminalLauncher implements ITerminalLauncher { }); } - private isBusy(t: ITerminalInstance): boolean { - if (t.processId) { - try { - // if shell has at least one child process, assume that shell is busy - if (platform.isWindows) { - const result = cp.spawnSync('wmic', ['process', 'get', 'ParentProcessId']); - if (result.stdout) { - const pids = result.stdout.toString().split('\r\n'); - if (!pids.some(p => parseInt(p) === t.processId)) { - return false; - } - } - } else { - const result = cp.spawnSync('/usr/bin/pgrep', ['-lP', String(t.processId)]); - if (result.stdout) { - const r = result.stdout.toString().trim(); - if (r.length === 0 || r.indexOf(' tmux') >= 0) { // ignore 'tmux'; see #43683 - return false; - } - } - } - } - catch (e) { - // silently ignore - } - } - // fall back to safe side - return true; + protected runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return void 0; } - private prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string { + protected isBusy(processId: number): TPromise { + return TPromise.as(hasChildprocesses(processId)); + } - let shellType: ShellType; - - // get the shell configuration for the current platform - let shell: string; - const shell_config = config.integrated.shell; - if (platform.isWindows) { - shell = shell_config.windows; - shellType = ShellType.cmd; - } else if (platform.isLinux) { - shell = shell_config.linux; - shellType = ShellType.bash; - } else if (platform.isMacintosh) { - shell = shell_config.osx; - shellType = ShellType.bash; - } - - // try to determine the shell type - shell = shell.trim().toLowerCase(); - if (shell.indexOf('powershell') >= 0) { - shellType = ShellType.powershell; - } else if (shell.indexOf('cmd.exe') >= 0) { - shellType = ShellType.cmd; - } else if (shell.indexOf('bash') >= 0) { - shellType = ShellType.bash; - } else if (shell.indexOf('git\\bin\\bash.exe') >= 0) { - shellType = ShellType.bash; - } - - let quote: (s: string) => string; - let command = ''; - - switch (shellType) { - - case ShellType.powershell: - - quote = (s: string) => { - s = s.replace(/\'/g, '\'\''); - return `'${s}'`; - //return s.indexOf(' ') >= 0 || s.indexOf('\'') >= 0 || s.indexOf('"') >= 0 ? `'${s}'` : s; - }; - - if (args.cwd) { - command += `cd '${args.cwd}'; `; - } - if (args.env) { - for (let key in args.env) { - const value = args.env[key]; - if (value === null) { - command += `Remove-Item env:${key}; `; - } else { - command += `\${env:${key}}='${value}'; `; - } - } - } - if (args.args && args.args.length > 0) { - const cmd = quote(args.args.shift()); - command += (cmd[0] === '\'') ? `& ${cmd} ` : `${cmd} `; - for (let a of args.args) { - command += `${quote(a)} `; - } - } - break; - - case ShellType.cmd: - - quote = (s: string) => { - s = s.replace(/\"/g, '""'); - return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0) ? `"${s}"` : s; - }; - - if (args.cwd) { - command += `cd ${quote(args.cwd)} && `; - } - if (args.env) { - command += 'cmd /C "'; - for (let key in args.env) { - const value = args.env[key]; - if (value === null) { - command += `set "${key}=" && `; - } else { - command += `set "${key}=${args.env[key]}" && `; - } - } - } - for (let a of args.args) { - command += `${quote(a)} `; - } - if (args.env) { - command += '"'; - } - break; - - case ShellType.bash: - - quote = (s: string) => { - s = s.replace(/\"/g, '\\"'); - return (s.indexOf(' ') >= 0 || s.indexOf('\\') >= 0) ? `"${s}"` : s; - }; - - if (args.cwd) { - command += `cd ${quote(args.cwd)} ; `; - } - if (args.env) { - command += 'env'; - for (let key in args.env) { - const value = args.env[key]; - if (value === null) { - command += ` -u "${key}"`; - } else { - command += ` "${key}=${value}"`; - } - } - command += ' '; - } - for (let a of args.args) { - command += `${quote(a)} `; - } - break; - } - - return command; + protected prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return TPromise.as(prepareCommand(args, config)); + } +} + +export class TerminalLauncher extends AbstractTerminalLauncher { + + constructor( + @ITerminalService terminalService: ITerminalService, + @IExternalTerminalService private nativeTerminalService: IExternalTerminalService + ) { + super(terminalService); + } + + runInExternalTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): TPromise { + return this.nativeTerminalService.runInTerminal(args.title, args.cwd, args.args, args.env || {}); } } diff --git a/src/vs/workbench/parts/debug/node/terminals.ts b/src/vs/workbench/parts/debug/node/terminals.ts index c0cfca5db35..a46ea642248 100644 --- a/src/vs/workbench/parts/debug/node/terminals.ts +++ b/src/vs/workbench/parts/debug/node/terminals.ts @@ -258,3 +258,161 @@ function quote(args: string[]): string { } return r; } + + +export function hasChildprocesses(processId: number): boolean { + if (processId) { + try { + // if shell has at least one child process, assume that shell is busy + if (env.isWindows) { + const result = cp.spawnSync('wmic', ['process', 'get', 'ParentProcessId']); + if (result.stdout) { + const pids = result.stdout.toString().split('\r\n'); + if (!pids.some(p => parseInt(p) === processId)) { + return false; + } + } + } else { + const result = cp.spawnSync('/usr/bin/pgrep', ['-lP', String(processId)]); + if (result.stdout) { + const r = result.stdout.toString().trim(); + if (r.length === 0 || r.indexOf(' tmux') >= 0) { // ignore 'tmux'; see #43683 + return false; + } + } + } + } + catch (e) { + // silently ignore + } + } + // fall back to safe side + return true; +} + +const enum ShellType { cmd, powershell, bash } + +export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string { + + let shellType: ShellType; + + // get the shell configuration for the current platform + let shell: string; + const shell_config = config.integrated.shell; + if (env.isWindows) { + shell = shell_config.windows; + shellType = ShellType.cmd; + } else if (env.isLinux) { + shell = shell_config.linux; + shellType = ShellType.bash; + } else if (env.isMacintosh) { + shell = shell_config.osx; + shellType = ShellType.bash; + } + + // try to determine the shell type + shell = shell.trim().toLowerCase(); + if (shell.indexOf('powershell') >= 0) { + shellType = ShellType.powershell; + } else if (shell.indexOf('cmd.exe') >= 0) { + shellType = ShellType.cmd; + } else if (shell.indexOf('bash') >= 0) { + shellType = ShellType.bash; + } else if (shell.indexOf('git\\bin\\bash.exe') >= 0) { + shellType = ShellType.bash; + } + + let quote: (s: string) => string; + let command = ''; + + switch (shellType) { + + case ShellType.powershell: + + quote = (s: string) => { + s = s.replace(/\'/g, '\'\''); + return `'${s}'`; + //return s.indexOf(' ') >= 0 || s.indexOf('\'') >= 0 || s.indexOf('"') >= 0 ? `'${s}'` : s; + }; + + if (args.cwd) { + command += `cd '${args.cwd}'; `; + } + if (args.env) { + for (let key in args.env) { + const value = args.env[key]; + if (value === null) { + command += `Remove-Item env:${key}; `; + } else { + command += `\${env:${key}}='${value}'; `; + } + } + } + if (args.args && args.args.length > 0) { + const cmd = quote(args.args.shift()); + command += (cmd[0] === '\'') ? `& ${cmd} ` : `${cmd} `; + for (let a of args.args) { + command += `${quote(a)} `; + } + } + break; + + case ShellType.cmd: + + quote = (s: string) => { + s = s.replace(/\"/g, '""'); + return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0) ? `"${s}"` : s; + }; + + if (args.cwd) { + command += `cd ${quote(args.cwd)} && `; + } + if (args.env) { + command += 'cmd /C "'; + for (let key in args.env) { + const value = args.env[key]; + if (value === null) { + command += `set "${key}=" && `; + } else { + command += `set "${key}=${args.env[key]}" && `; + } + } + } + for (let a of args.args) { + command += `${quote(a)} `; + } + if (args.env) { + command += '"'; + } + break; + + case ShellType.bash: + + quote = (s: string) => { + s = s.replace(/\"/g, '\\"'); + return (s.indexOf(' ') >= 0 || s.indexOf('\\') >= 0) ? `"${s}"` : s; + }; + + if (args.cwd) { + command += `cd ${quote(args.cwd)} ; `; + } + if (args.env) { + command += 'env'; + for (let key in args.env) { + const value = args.env[key]; + if (value === null) { + command += ` -u "${key}"`; + } else { + command += ` "${key}=${value}"`; + } + } + command += ' '; + } + for (let a of args.args) { + command += `${quote(a)} `; + } + break; + } + + return command; +} \ No newline at end of file