Path: blob/main/src/vs/platform/externalTerminal/node/externalTerminalService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as cp from 'child_process';6import { memoize } from '../../../base/common/decorators.js';7import { FileAccess } from '../../../base/common/network.js';8import * as path from '../../../base/common/path.js';9import * as env from '../../../base/common/platform.js';10import { sanitizeProcessEnvironment } from '../../../base/common/processes.js';11import * as pfs from '../../../base/node/pfs.js';12import * as processes from '../../../base/node/processes.js';13import * as nls from '../../../nls.js';14import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings, ITerminalForPlatform } from '../common/externalTerminal.js';15import { ITerminalEnvironment } from '../../terminal/common/terminal.js';1617const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");1819abstract class ExternalTerminalService {20public _serviceBrand: undefined;2122async getDefaultTerminalForPlatforms(): Promise<ITerminalForPlatform> {23return {24windows: WindowsExternalTerminalService.getDefaultTerminalWindows(),25linux: await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(),26osx: 'xterm'27};28}29}3031export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {32private static readonly CMD = 'cmd.exe';33private static _DEFAULT_TERMINAL_WINDOWS: string;3435public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {36return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);37}3839public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, command: string, cwd?: string): Promise<void> {40const exec = configuration.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();4142// Make the drive letter uppercase on Windows (see #9448)43if (cwd && cwd[1] === ':') {44cwd = cwd[0].toUpperCase() + cwd.substr(1);45}4647// cmder ignores the environment cwd and instead opts to always open in %USERPROFILE%48// unless otherwise specified49const basename = path.basename(exec, '.exe').toLowerCase();50if (basename === 'cmder') {51spawner.spawn(exec, cwd ? [cwd] : undefined);52return Promise.resolve(undefined);53}5455const cmdArgs = ['/c', 'start', '/wait'];56if (exec.indexOf(' ') >= 0) {57// The "" argument is the window title. Without this, exec doesn't work when the path58// contains spaces. #659059// Title is Execution Path. #22012960cmdArgs.push(exec);61}62cmdArgs.push(exec);63// Add starting directory parameter for Windows Terminal (see #90734)64if (basename === 'wt') {65cmdArgs.push('-d .');66}6768return new Promise<void>((c, e) => {69const env = getSanitizedEnvironment(process);70const child = spawner.spawn(command, cmdArgs, { cwd, env, detached: true });71child.on('error', e);72child.on('exit', () => c());73});74}7576public async runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {77const exec = 'windowsExec' in settings && settings.windowsExec ? settings.windowsExec : WindowsExternalTerminalService.getDefaultTerminalWindows();78const wt = await WindowsExternalTerminalService.getWtExePath();7980return new Promise<number | undefined>((resolve, reject) => {8182const title = `"${dir} - ${TERMINAL_TITLE}"`;83const command = `"${args.join('" "')}" & pause`; // use '|' to only pause on non-zero exit code8485// merge environment variables into a copy of the process.env86const env = Object.assign({}, getSanitizedEnvironment(process), envVars);8788// delete environment variables that have a null value89Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);9091const options: any = {92cwd: dir,93env: env,94windowsVerbatimArguments: true95};9697let spawnExec: string;98let cmdArgs: string[];99100if (path.basename(exec, '.exe') === 'wt') {101// Handle Windows Terminal specially; -d to set the cwd and run a cmd.exe instance102// inside it103spawnExec = exec;104cmdArgs = ['-d', '.', WindowsExternalTerminalService.CMD, '/c', command];105} else if (wt) {106// prefer to use the window terminal to spawn if it's available instead107// of start, since that allows ctrl+c handling (#81322)108spawnExec = wt;109cmdArgs = ['-d', '.', exec, '/c', command];110} else {111spawnExec = WindowsExternalTerminalService.CMD;112cmdArgs = ['/c', 'start', title, '/wait', exec, '/c', `"${command}"`];113}114115const cmd = cp.spawn(spawnExec, cmdArgs, options);116117cmd.on('error', err => {118reject(improveError(err));119});120121resolve(undefined);122});123}124125public static getDefaultTerminalWindows(): string {126if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) {127const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');128WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;129}130return WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS;131}132133@memoize134private static async getWtExePath() {135try {136return await processes.findExecutable('wt');137} catch {138return undefined;139}140}141}142143export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {144private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X145146public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {147return this.spawnTerminal(cp, configuration, cwd);148}149150public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {151152const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX;153154return new Promise<number | undefined>((resolve, reject) => {155156if (terminalApp === DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {157158// On OS X we launch an AppleScript that creates (or reuses) a Terminal window159// and then launches the program inside that window.160161const script = terminalApp === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';162const scriptpath = FileAccess.asFileUri(`vs/workbench/contrib/externalTerminal/node/${script}.scpt`).fsPath;163164const osaArgs = [165scriptpath,166'-t', title || TERMINAL_TITLE,167'-w', dir,168];169170for (const a of args) {171osaArgs.push('-a');172osaArgs.push(a);173}174175if (envVars) {176// merge environment variables into a copy of the process.env177const env = Object.assign({}, getSanitizedEnvironment(process), envVars);178179for (const key in env) {180const value = env[key];181if (value === null) {182osaArgs.push('-u');183osaArgs.push(key);184} else {185osaArgs.push('-e');186osaArgs.push(`${key}=${value}`);187}188}189}190191let stderr = '';192const osa = cp.spawn(MacExternalTerminalService.OSASCRIPT, osaArgs);193osa.on('error', err => {194reject(improveError(err));195});196osa.stderr.on('data', (data) => {197stderr += data.toString();198});199osa.on('exit', (code: number) => {200if (code === 0) { // OK201resolve(undefined);202} else {203if (stderr) {204const lines = stderr.split('\n', 1);205reject(new Error(lines[0]));206} else {207reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));208}209}210});211} else {212reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));213}214});215}216217spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {218const terminalApp = configuration.osxExec || DEFAULT_TERMINAL_OSX;219220return new Promise<void>((c, e) => {221const args = ['-a', terminalApp];222if (cwd) {223args.push(cwd);224}225const env = getSanitizedEnvironment(process);226const child = spawner.spawn('/usr/bin/open', args, { cwd, env });227child.on('error', e);228child.on('exit', () => c());229});230}231}232233export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {234235private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");236237public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {238return this.spawnTerminal(cp, configuration, cwd);239}240241public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {242243const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();244245return new Promise<number | undefined>((resolve, reject) => {246247const termArgs: string[] = [];248//termArgs.push('--title');249//termArgs.push(`"${TERMINAL_TITLE}"`);250execPromise.then(exec => {251if (exec.indexOf('gnome-terminal') >= 0) {252termArgs.push('-x');253} else {254termArgs.push('-e');255}256termArgs.push('bash');257termArgs.push('-c');258259const bashCommand = `${quote(args)}; echo; read -p "${LinuxExternalTerminalService.WAIT_MESSAGE}" -n1;`;260termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set...261262263// merge environment variables into a copy of the process.env264const env = Object.assign({}, getSanitizedEnvironment(process), envVars);265266// delete environment variables that have a null value267Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);268269const options: any = {270cwd: dir,271env: env272};273274let stderr = '';275const cmd = cp.spawn(exec, termArgs, options);276cmd.on('error', err => {277reject(improveError(err));278});279cmd.stderr.on('data', (data) => {280stderr += data.toString();281});282cmd.on('exit', (code: number) => {283if (code === 0) { // OK284resolve(undefined);285} else {286if (stderr) {287const lines = stderr.split('\n', 1);288reject(new Error(lines[0]));289} else {290reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));291}292}293});294});295});296}297298private static _DEFAULT_TERMINAL_LINUX_READY: Promise<string>;299300public static async getDefaultTerminalLinuxReady(): Promise<string> {301if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) {302if (!env.isLinux) {303LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = Promise.resolve('xterm');304} else {305const isDebian = await pfs.Promises.exists('/etc/debian_version');306LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(r => {307if (isDebian) {308r('x-terminal-emulator');309} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {310r('gnome-terminal');311} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {312r('konsole');313} else if (process.env.COLORTERM) {314r(process.env.COLORTERM);315} else if (process.env.TERM) {316r(process.env.TERM);317} else {318r('xterm');319}320});321}322}323return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY;324}325326spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {327const execPromise = configuration.linuxExec ? Promise.resolve(configuration.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();328329return new Promise<void>((c, e) => {330execPromise.then(exec => {331const env = getSanitizedEnvironment(process);332const child = spawner.spawn(exec, [], { cwd, env });333child.on('error', e);334child.on('exit', () => c());335});336});337}338}339340function getSanitizedEnvironment(process: NodeJS.Process) {341const env = { ...process.env };342sanitizeProcessEnvironment(env);343return env;344}345346/**347* tries to turn OS errors into more meaningful error messages348*/349function improveError(err: Error & { errno?: string; path?: string }): Error {350if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {351return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));352}353return err;354}355356/**357* Quote args if necessary and combine into a space separated string.358*/359function quote(args: string[]): string {360let r = '';361for (const a of args) {362if (a.indexOf(' ') >= 0) {363r += '"' + a + '"';364} else {365r += a;366}367r += ' ';368}369return r;370}371372373