/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 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 { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/contrib/debug/common/debug'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); let terminalLauncher: ITerminalLauncher | undefined = 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: Promise | null = null; export function getDefaultTerminalLinuxReady(): Promise { if (!_DEFAULT_TERMINAL_LINUX_READY) { _DEFAULT_TERMINAL_LINUX_READY = new Promise(resolve => { if (env.isLinux) { Promise.all([pfs.exists('/etc/debian_version'), process.lazyEnv]).then(([isDebian]) => { if (isDebian) { resolve('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { resolve('gnome-terminal'); } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { resolve('konsole'); } else if (process.env.COLORTERM) { resolve(process.env.COLORTERM); } else if (process.env.TERM) { resolve(process.env.TERM); } else { resolve('xterm'); } }); return; } resolve('xterm'); }); } return _DEFAULT_TERMINAL_LINUX_READY; } let _DEFAULT_TERMINAL_WINDOWS: string | null = 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:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; } return _DEFAULT_TERMINAL_WINDOWS; } abstract class TerminalLauncher implements ITerminalLauncher { runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): Promise { return this.runInTerminal0(args.title!, args.cwd, args.args, args.env || {}, config); } abstract runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment | {}, config: ITerminalSettings): Promise; } class WinTerminalService extends TerminalLauncher { private static readonly CMD = 'cmd.exe'; runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise { const exec = configuration.external.windowsExec || getDefaultTerminalWindows(); return new Promise((resolve, reject) => { 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', err => { reject(improveError(err)); }); resolve(undefined); }); } } 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 runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise { const terminalApp = configuration.external.osxExec || MacTerminalService.DEFAULT_TERMINAL_OSX; return new Promise((resolve, reject) => { 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 = getPathFromAmdModule(require, `vs/workbench/contrib/externalTerminal/electron-browser/${script}.scpt`); 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', err => { reject(improveError(err)); }); osa.stderr.on('data', (data) => { stderr += data.toString(); }); osa.on('exit', (code: number) => { if (code === 0) { // OK resolve(undefined); } else { if (stderr) { const lines = stderr.split('\n', 1); reject(new Error(lines[0])); } else { reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code))); } } }); } else { reject(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..."); runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise { const terminalConfig = configuration.external; const execThenable: Promise = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady(); return new Promise((resolve, reject) => { let termArgs: string[] = []; //termArgs.push('--title'); //termArgs.push(`"${TERMINAL_TITLE}"`); execThenable.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', err => { reject(improveError(err)); }); cmd.stderr.on('data', (data) => { stderr += data.toString(); }); cmd.on('exit', (code: number) => { if (code === 0) { // OK resolve(undefined); } else { if (stderr) { const lines = stderr.split('\n', 1); reject(new Error(lines[0])); } else { reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code))); } } }); }); }); } } /** * tries to turn OS errors into more meaningful error messages */ function improveError(err: Error): Error { if (err['errno'] === 'ENOENT' && err['path']) { return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path'])); } return err; } /** * 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; } 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 || getDefaultShell(env.Platform.Windows); shellType = ShellType.cmd; } else if (env.isLinux) { shell = shell_config.linux || getDefaultShell(env.Platform.Linux); shellType = ShellType.bash; } else if (env.isMacintosh) { shell = shell_config.osx || getDefaultShell(env.Platform.Mac); shellType = ShellType.bash; } else { throw new Error('Unknown platform'); } // try to determine the shell type shell = shell.trim().toLowerCase(); if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 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) { let value = args.env[key]; if (value === null) { command += `set "${key}=" && `; } else { value = value.replace(/[\^\&]/g, s => `^${s}`); command += `set "${key}=${value}" && `; } } } 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; }; const hardQuote = (s: string) => { return /[^\w@%\/+=,.:^-]/.test(s) ? `'${s.replace(/'/g, '\'\\\'\'')}'` : 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 ${hardQuote(key)}`; } else { command += ` ${hardQuote(`${key}=${value}`)}`; } } command += ' '; } for (let a of args.args) { command += `${quote(a)} `; } break; } return command; }