@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
301 lines (300 loc) • 10.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 { FileAccess } from '@sussudio/base/common/network.mjs';
import * as path from '@sussudio/base/common/path.mjs';
import * as env from '@sussudio/base/common/platform.mjs';
import { sanitizeProcessEnvironment } from '@sussudio/base/common/processes.mjs';
import * as pfs from '@sussudio/base/node/pfs.mjs';
import * as processes from '@sussudio/base/node/processes.mjs';
import * as nls from 'vscode-nls.mjs';
import { DEFAULT_TERMINAL_OSX } from '../common/externalTerminal.mjs';
const TERMINAL_TITLE = nls.localize('console.title', 'VS Code Console');
class ExternalTerminalService {
_serviceBrand;
async getDefaultTerminalForPlatforms() {
return {
windows: WindowsExternalTerminalService.getDefaultTerminalWindows(),
linux: await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(),
osx: 'xterm',
};
}
}
export class WindowsExternalTerminalService extends ExternalTerminalService {
static CMD = 'cmd.exe';
static _DEFAULT_TERMINAL_WINDOWS;
openTerminal(configuration, cwd) {
return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);
}
spawnTerminal(spawner, configuration, command, cwd) {
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).toLowerCase();
if (basename === 'cmder' || basename === 'cmder.exe') {
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
cmdArgs.push('""');
}
cmdArgs.push(exec);
// Add starting directory parameter for Windows Terminal (see #90734)
if (basename === 'wt' || basename === 'wt.exe') {
cmdArgs.push('-d .');
}
return new Promise((c, e) => {
const env = getSanitizedEnvironment(process);
const child = spawner.spawn(command, cmdArgs, { cwd, env });
child.on('error', e);
child.on('exit', () => c());
});
}
runInTerminal(title, dir, args, envVars, settings) {
const exec =
'windowsExec' in settings && settings.windowsExec
? settings.windowsExec
: WindowsExternalTerminalService.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 = 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,
};
const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options);
cmd.on('error', (err) => {
reject(improveError(err));
});
resolve(undefined);
});
}
static getDefaultTerminalWindows() {
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;
}
}
export class MacExternalTerminalService extends ExternalTerminalService {
static OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X
openTerminal(configuration, cwd) {
return this.spawnTerminal(cp, configuration, cwd);
}
runInTerminal(title, dir, args, envVars, settings) {
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) => {
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, configuration, cwd) {
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 {
static WAIT_MESSAGE = nls.localize('press.any.key', 'Press any key to continue...');
openTerminal(configuration, cwd) {
return this.spawnTerminal(cp, configuration, cwd);
}
runInTerminal(title, dir, args, envVars, settings) {
const execPromise = settings.linuxExec
? Promise.resolve(settings.linuxExec)
: LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
return new Promise((resolve, reject) => {
const termArgs = [];
//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) => {
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)));
}
}
});
});
});
}
static _DEFAULT_TERMINAL_LINUX_READY;
static async getDefaultTerminalLinuxReady() {
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, configuration, cwd) {
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) {
const env = { ...process.env };
sanitizeProcessEnvironment(env);
return env;
}
/**
* tries to turn OS errors into more meaningful error messages
*/
function improveError(err) {
if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {
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) {
let r = '';
for (const a of args) {
if (a.indexOf(' ') >= 0) {
r += '"' + a + '"';
} else {
r += a;
}
r += ' ';
}
return r;
}