src/vs/platform/externalTerminal/node/externalTerminalService.ts
399 LOC · 162 covered · 237 uncovered · 37 ranges · 12 concepts · 10 introducers · 11 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
externalTerminalService.ts ×18
* 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<ITerminalForPlatform> {
return {
windows: WindowsExternalTerminalService.getDefaultTerminalWindows(),
linux: await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(),
osx: DEFAULT_TERMINAL_OSX
};
}
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<void> {
return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);
}
public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, command: string, cwd?: string): Promise<void> {
const exec = configuration.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
externalTerminalService.ts ×3
// Make the drive letter uppercase on Windows (see #9448)
if (cwd && cwd[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') {
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);
}
// Add starting directory parameter for Windows Terminal (see #90734)
if (basename === 'wt') {
cmdArgs.push('-d .');
}
return new Promise<void>((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<number | undefined> {
const exec = settings.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
const wt = await WindowsExternalTerminalService.getWtExePath();
return new Promise<number | undefined>((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) {
externalTerminalService.ts ×1
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<void> {
return this.spawnTerminal(cp, configuration, cwd);
}
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX;
return new Promise<number | undefined>((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}`);
}
}
}
const osa = cp.spawn(MacExternalTerminalService.OSASCRIPT, osaArgs);
setupSpawnErrorHandling(osa, resolve, reject, terminalApp);
} else if (terminalApp === 'Ghostty.app') {
// Ghostty uses CLI flags directly instead of AppleScript like Mac Terminal and iTerm
// Note: -na is required (not just -a) because we need to spawn a new instance that
// receives our --args. With just -a, if Ghostty is already running, open will
// activate the existing instance and ignore --args entirely.
const env = Object.assign({}, getSanitizedEnvironment(process), envVars);
const openArgs = ['-na', 'Ghostty.app', '--args'];
openArgs.push('--working-directory=' + dir);
openArgs.push('--wait-after-command=true');
openArgs.push('-e', ...args);
const cmd = cp.spawn('/usr/bin/open', openArgs, { env });
setupSpawnErrorHandling(cmd, resolve, reject, terminalApp);
} else {
reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
}
});
}
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
const terminalApp = configuration.osxExec || DEFAULT_TERMINAL_OSX;
externalTerminalService.ts ×1
return new Promise<void>((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<void> {
return this.spawnTerminal(cp, configuration, cwd);
}
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
return new Promise<number | undefined>((resolve, reject) => {
execPromise.then(exec => {
const basename = path.basename(exec).toLowerCase();
if (basename === 'ghostty') {
const ghosttyArgs: string[] = [];
if (dir) {
ghosttyArgs.push(`--working-directory=${dir}`);
}
ghosttyArgs.push('--wait-after-command=true');
if (args.length) {
ghosttyArgs.push('-e', ...args);
}
LinuxExternalTerminalService.spawnTerminalWithEnv(exec, ghosttyArgs, dir, envVars, resolve, reject);
return;
}
const termArgs: string[] = [];
//termArgs.push('--title');
//termArgs.push(`"${TERMINAL_TITLE}"`);
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...
LinuxExternalTerminalService.spawnTerminalWithEnv(exec, termArgs, dir, envVars, resolve, reject);
});
});
}
private static spawnTerminalWithEnv(
exec: string,
args: string[],
dir: string,
envVars: ITerminalEnvironment,
resolve: (value: number | PromiseLike<number | undefined> | undefined) => void,
reject: (reason?: unknown) => void
): void {
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 cmd = cp.spawn(exec, args, { cwd: dir, env });
setupSpawnErrorHandling(cmd, resolve, reject, exec);
}
private static _DEFAULT_TERMINAL_LINUX_READY: Promise<string>;
public static async getDefaultTerminalLinuxReady(): Promise<string> {
if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) {
externalTerminalService.ts ×7
if (!env.isLinux) {
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = Promise.resolve('xterm');
const isDebian = await pfs.Promises.exists('/etc/debian_version');
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(r => {
if (isDebian) {
r('x-terminal-emulator');
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
externalTerminalService.ts ×7
r('gnome-terminal');
r('konsole');
r(process.env.COLORTERM);
r(process.env.TERM);
} else {
r('xterm');
}
}
}
return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY;
}
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
const execPromise = configuration.linuxExec ? Promise.resolve(configuration.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
externalTerminalService.ts ×1
return new Promise<void>((c, e) => {
execPromise.then(exec => {
const env = getSanitizedEnvironment(process);
const basename = path.basename(exec).toLowerCase();
const args = basename === 'ghostty' && cwd ? [`--working-directory=${cwd}`] : [];
const child = spawner.spawn(exec, args, { cwd, env });
child.on('error', e);
child.on('exit', () => c());
});
});
}
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;
}
/**
* Attaches error handling to a spawned child process for terminal launching.
*/
function setupSpawnErrorHandling(
cmd: cp.ChildProcess,
resolve: (value: number | PromiseLike<number | undefined> | undefined) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void,
terminalApp: string
): void {
let stderr = '';
cmd.on('error', err => {
reject(improveError(err));
});
cmd.stderr?.on('data', (data) => {
stderr += data.toString();
});
cmd.on('exit', (code: number) => {
if (code === 0) {
resolve(undefined);
} else {
if (stderr) {
const lines = stderr.split('\n', 1);
reject(new Error(lines[0]));
} else {
reject(new Error(nls.localize('terminal.launch.failed', "Launching '{0}' failed with exit code {1}", terminalApp, code)));
}
}
});
}
/**
* 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;
}