/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as cp from 'child_process'; import { memoize } from '../../../base/common/decorators.js'; import { FileAccess } from '../../../base/common/network.js'; import * as path from '../../../base/common/path.js'; import * as env from '../../../base/common/platform.js'; import { sanitizeProcessEnvironment } from '../../../base/common/processes.js'; import * as pfs from '../../../base/node/pfs.js'; import * as processes from '../../../base/node/processes.js'; import * as nls from '../../../nls.js'; import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings, ITerminalForPlatform } from '../common/externalTerminal.js'; import { ITerminalEnvironment } from '../../terminal/common/terminal.js'; const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); abstract class ExternalTerminalService { public _serviceBrand: undefined; async getDefaultTerminalForPlatforms(): Promise { return { windows: WindowsExternalTerminalService.getDefaultTerminalWindows(), linux: await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(), osx: 'xterm' }; } } export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly CMD = 'cmd.exe'; private static _DEFAULT_TERMINAL_WINDOWS: string; public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd); } public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, command: string, cwd?: string): Promise { const exec = configuration.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); // Make the drive letter uppercase on Windows (see #9448) if (cwd && cwd[1] === ':') { cwd = cwd[0].toUpperCase() + cwd.substr(1); } // cmder ignores the environment cwd and instead opts to always open in %USERPROFILE% // unless otherwise specified const basename = path.basename(exec, '.exe').toLowerCase(); if (basename === 'cmder') { spawner.spawn(exec, cwd ? [cwd] : undefined); return Promise.resolve(undefined); } const cmdArgs = ['/c', 'start', '/wait']; if (exec.indexOf(' ') >= 0) { // The "" argument is the window title. Without this, exec doesn't work when the path // contains spaces. #6590 // Title is Execution Path. #220129 cmdArgs.push(exec); } cmdArgs.push(exec); // Add starting directory parameter for Windows Terminal (see #90734) if (basename === 'wt') { cmdArgs.push('-d .'); } return new Promise((c, e) => { const env = getSanitizedEnvironment(process); const child = spawner.spawn(command, cmdArgs, { cwd, env, detached: true }); child.on('error', e); child.on('exit', () => c()); }); } public async runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { const exec = settings.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows(); const wt = await WindowsExternalTerminalService.getWtExePath(); return new Promise((resolve, reject) => { const title = `"${dir} - ${TERMINAL_TITLE}"`; const command = `"${args.join('" "')}" & pause`; // use '|' to only pause on non-zero exit code // merge environment variables into a copy of the process.env const env = Object.assign({}, getSanitizedEnvironment(process), envVars); // delete environment variables that have a null value Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); const options = { cwd: dir, env: env, windowsVerbatimArguments: true }; let spawnExec: string; let cmdArgs: string[]; if (path.basename(exec, '.exe') === 'wt') { // Handle Windows Terminal specially; -d to set the cwd and run a cmd.exe instance // inside it spawnExec = exec; cmdArgs = ['-d', '.', WindowsExternalTerminalService.CMD, '/c', command]; } else if (wt) { // prefer to use the window terminal to spawn if it's available instead // of start, since that allows ctrl+c handling (#81322) spawnExec = wt; cmdArgs = ['-d', '.', exec, '/c', command]; } else { spawnExec = WindowsExternalTerminalService.CMD; cmdArgs = ['/c', 'start', title, '/wait', exec, '/c', `"${command}"`]; } const cmd = cp.spawn(spawnExec, cmdArgs, options); cmd.on('error', err => { reject(improveError(err)); }); resolve(undefined); }); } public static getDefaultTerminalWindows(): string { if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) { const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`; } return WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS; } @memoize private static async getWtExePath() { try { return await processes.findExecutable('wt'); } catch { return undefined; } } } export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX; return new Promise((resolve, reject) => { if (terminalApp === 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 === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper'; const scriptpath = FileAccess.asFileUri(`vs/workbench/contrib/externalTerminal/node/${script}.scpt`).fsPath; const osaArgs = [ scriptpath, '-t', title || TERMINAL_TITLE, '-w', dir, ]; for (const a of args) { osaArgs.push('-a'); osaArgs.push(a); } if (envVars) { // merge environment variables into a copy of the process.env const env = Object.assign({}, getSanitizedEnvironment(process), envVars); for (const key in env) { const value = env[key]; if (value === null) { osaArgs.push('-u'); osaArgs.push(key); } else { osaArgs.push('-e'); osaArgs.push(`${key}=${value}`); } } } let stderr = ''; const osa = cp.spawn(MacExternalTerminalService.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))); } }); } spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise { const terminalApp = configuration.osxExec || DEFAULT_TERMINAL_OSX; return new Promise((c, e) => { const args = ['-a', terminalApp]; if (cwd) { args.push(cwd); } const env = getSanitizedEnvironment(process); const child = spawner.spawn('/usr/bin/open', args, { cwd, env }); child.on('error', e); child.on('exit', () => c()); }); } } export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService { private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue..."); public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise { return this.spawnTerminal(cp, configuration, cwd); } public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise { const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); return new Promise((resolve, reject) => { const 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 "${LinuxExternalTerminalService.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 = Object.assign({}, getSanitizedEnvironment(process), envVars); // delete environment variables that have a null value Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]); const options = { 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))); } } }); }); }); } private static _DEFAULT_TERMINAL_LINUX_READY: Promise; public static async getDefaultTerminalLinuxReady(): Promise { if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) { if (!env.isLinux) { LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = Promise.resolve('xterm'); } else { const isDebian = await pfs.Promises.exists('/etc/debian_version'); LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(r => { if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { r('gnome-terminal'); } else if (process.env.DESKTOP_SESSION === 'kde-plasma') { r('konsole'); } else if (process.env.COLORTERM) { r(process.env.COLORTERM); } else if (process.env.TERM) { r(process.env.TERM); } else { r('xterm'); } }); } } return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY; } spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise { const execPromise = configuration.linuxExec ? Promise.resolve(configuration.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady(); return new Promise((c, e) => { execPromise.then(exec => { const env = getSanitizedEnvironment(process); const child = spawner.spawn(exec, [], { cwd, env }); child.on('error', e); child.on('exit', () => c()); }); }); } } function getSanitizedEnvironment(process: NodeJS.Process) { const env = { ...process.env }; sanitizeProcessEnvironment(env); return env; } /** * tries to turn OS errors into more meaningful error messages */ function improveError(err: Error & { errno?: string; path?: string }): 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 (const a of args) { if (a.indexOf(' ') >= 0) { r += '"' + a + '"'; } else { r += a; } r += ' '; } return r; }