vscode/src/vs/base/node/processes.ts
2021-11-19 15:15:26 +09:00

529 lines
17 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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 { Stats } from 'fs';
import { IStringDictionary } from 'vs/base/common/collections';
import * as extpath from 'vs/base/common/extpath';
import { FileAccess } from 'vs/base/common/network';
import * as Objects from 'vs/base/common/objects';
import * as path from 'vs/base/common/path';
import * as Platform from 'vs/base/common/platform';
import * as process from 'vs/base/common/process';
import { CommandOptions, Executable, ForkOptions, Source, SuccessData, TerminateResponse, TerminateResponseCode } from 'vs/base/common/processes';
import * as Types from 'vs/base/common/types';
import { LineDecoder } from 'vs/base/node/decoder';
import * as pfs from 'vs/base/node/pfs';
import * as nls from 'vs/nls';
export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode };
export type ValueCallback<T> = (value: T | Promise<T>) => void;
export type ErrorCallback = (error?: any) => void;
export type ProgressCallback<T> = (progress: T) => void;
export interface LineData {
line: string;
source: Source;
}
function getWindowsCode(status: number): TerminateResponseCode {
switch (status) {
case 0:
return TerminateResponseCode.Success;
case 1:
return TerminateResponseCode.AccessDenied;
case 128:
return TerminateResponseCode.ProcessNotFound;
default:
return TerminateResponseCode.Unknown;
}
}
function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<TerminateResponse> {
if (Platform.isWindows) {
try {
const options: any = {
stdio: ['pipe', 'pipe', 'ignore']
};
if (cwd) {
options.cwd = cwd;
}
const killProcess = cp.execFile('taskkill', ['/T', '/F', '/PID', process.pid!.toString()], options);
return new Promise(resolve => {
killProcess.once('error', (err) => {
resolve({ success: false, error: err });
});
killProcess.once('exit', (code, signal) => {
if (code === 0) {
resolve({ success: true });
} else {
resolve({ success: false, code: code !== null ? code : TerminateResponseCode.Unknown });
}
});
});
} catch (err) {
return Promise.resolve({ success: false, error: err, code: err.status ? getWindowsCode(err.status) : TerminateResponseCode.Unknown });
}
} else if (Platform.isLinux || Platform.isMacintosh) {
try {
const cmd = FileAccess.asFileUri('vs/base/node/terminateProcess.sh', require).fsPath;
return new Promise(resolve => {
cp.execFile(cmd, [process.pid!.toString()], { encoding: 'utf8', shell: true } as cp.ExecFileOptions, (err, stdout, stderr) => {
if (err) {
resolve({ success: false, error: err });
} else {
resolve({ success: true });
}
});
});
} catch (err) {
return Promise.resolve({ success: false, error: err });
}
} else {
process.kill('SIGKILL');
}
return Promise.resolve({ success: true });
}
/**
* Remove dangerous environment variables that have caused crashes
* in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes)
*
* @param env The env object to change
*/
export function removeDangerousEnvVariables(env: NodeJS.ProcessEnv | undefined): void {
if (!env) {
return;
}
// Unset `DEBUG`, as an invalid value might lead to process crashes
// See https://github.com/microsoft/vscode/issues/130072
delete env['DEBUG'];
if (Platform.isMacintosh) {
// Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes
// See https://github.com/microsoft/vscode/issues/104525
// See https://github.com/microsoft/vscode/issues/105848
delete env['DYLD_LIBRARY_PATH'];
}
if (Platform.isLinux) {
// Unset `LD_PRELOAD`, as it might lead to process crashes
// See https://github.com/microsoft/vscode/issues/134177
delete env['LD_PRELOAD'];
}
}
export function getWindowsShell(env = process.env as Platform.IProcessEnvironment): string {
return env['comspec'] || 'cmd.exe';
}
export abstract class AbstractProcess<TProgressData> {
private cmd: string;
private args: string[];
private options: CommandOptions | ForkOptions;
protected shell: boolean;
private childProcess: cp.ChildProcess | null;
protected childProcessPromise: Promise<cp.ChildProcess> | null;
private pidResolve: ValueCallback<number> | undefined;
protected terminateRequested: boolean;
private static WellKnowCommands: IStringDictionary<boolean> = {
'ant': true,
'cmake': true,
'eslint': true,
'gradle': true,
'grunt': true,
'gulp': true,
'jake': true,
'jenkins': true,
'jshint': true,
'make': true,
'maven': true,
'msbuild': true,
'msc': true,
'nmake': true,
'npm': true,
'rake': true,
'tsc': true,
'xbuild': true
};
public constructor(executable: Executable);
public constructor(cmd: string, args: string[] | undefined, shell: boolean, options: CommandOptions | undefined);
public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean, arg4?: CommandOptions) {
if (arg2 !== undefined && arg3 !== undefined && arg4 !== undefined) {
this.cmd = <string>arg1;
this.args = arg2;
this.shell = arg3;
this.options = arg4;
} else {
const executable = <Executable>arg1;
this.cmd = executable.command;
this.shell = executable.isShellCommand;
this.args = executable.args.slice(0);
this.options = executable.options || {};
}
this.childProcess = null;
this.childProcessPromise = null;
this.terminateRequested = false;
if (this.options.env) {
const newEnv: IStringDictionary<string> = Object.create(null);
Object.keys(process.env).forEach((key) => {
newEnv[key] = process.env[key]!;
});
Object.keys(this.options.env).forEach((key) => {
newEnv[key] = this.options.env![key]!;
});
this.options.env = newEnv;
}
}
public getSanitizedCommand(): string {
let result = this.cmd.toLowerCase();
const index = result.lastIndexOf(path.sep);
if (index !== -1) {
result = result.substring(index + 1);
}
if (AbstractProcess.WellKnowCommands[result]) {
return result;
}
return 'other';
}
public start(pp: ProgressCallback<TProgressData>): Promise<SuccessData> {
if (Platform.isWindows && ((this.options && this.options.cwd && extpath.isUNC(this.options.cwd)) || !this.options && extpath.isUNC(process.cwd()))) {
return Promise.reject(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.')));
}
return this.useExec().then((useExec) => {
let cc: ValueCallback<SuccessData>;
let ee: ErrorCallback;
const result = new Promise<any>((c, e) => {
cc = c;
ee = e;
});
if (useExec) {
let cmd: string = this.cmd;
if (this.args) {
cmd = cmd + ' ' + this.args.join(' ');
}
this.childProcess = cp.exec(cmd, this.options, (error, stdout, stderr) => {
this.childProcess = null;
const err: any = error;
// This is tricky since executing a command shell reports error back in case the executed command return an
// error or the command didn't exist at all. So we can't blindly treat an error as a failed command. So we
// always parse the output and report success unless the job got killed.
if (err && err.killed) {
ee({ killed: this.terminateRequested, stdout: stdout.toString(), stderr: stderr.toString() });
} else {
this.handleExec(cc, pp, error, stdout as any, stderr as any);
}
});
} else {
let childProcess: cp.ChildProcess | null = null;
const closeHandler = (data: any) => {
this.childProcess = null;
this.childProcessPromise = null;
this.handleClose(data, cc, pp, ee);
const result: SuccessData = {
terminated: this.terminateRequested
};
if (Types.isNumber(data)) {
result.cmdCode = <number>data;
}
cc(result);
};
if (this.shell && Platform.isWindows) {
const options: any = Objects.deepClone(this.options);
options.windowsVerbatimArguments = true;
options.detached = false;
let quotedCommand: boolean = false;
let quotedArg: boolean = false;
const commandLine: string[] = [];
let quoted = this.ensureQuotes(this.cmd);
commandLine.push(quoted.value);
quotedCommand = quoted.quoted;
if (this.args) {
this.args.forEach((elem) => {
quoted = this.ensureQuotes(elem);
commandLine.push(quoted.value);
quotedArg = quotedArg && quoted.quoted;
});
}
const args: string[] = [
'/s',
'/c',
];
if (quotedCommand) {
if (quotedArg) {
args.push('"' + commandLine.join(' ') + '"');
} else if (commandLine.length > 1) {
args.push('"' + commandLine[0] + '"' + ' ' + commandLine.slice(1).join(' '));
} else {
args.push('"' + commandLine[0] + '"');
}
} else {
args.push(commandLine.join(' '));
}
childProcess = cp.spawn(getWindowsShell(), args, options);
} else {
if (this.cmd) {
childProcess = cp.spawn(this.cmd, this.args, this.options);
}
}
if (childProcess) {
this.childProcess = childProcess;
this.childProcessPromise = Promise.resolve(childProcess);
if (this.pidResolve) {
this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1);
this.pidResolve = undefined;
}
childProcess.on('error', (error: Error) => {
this.childProcess = null;
ee({ terminated: this.terminateRequested, error: error });
});
if (childProcess.pid) {
this.childProcess.on('close', closeHandler);
this.handleSpawn(childProcess, cc!, pp, ee!, true);
}
}
}
return result;
});
}
protected abstract handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, error: Error | null, stdout: Buffer, stderr: Buffer): void;
protected abstract handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, ee: ErrorCallback, sync: boolean): void;
protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, ee: ErrorCallback): void {
// Default is to do nothing.
}
private static readonly regexp = /^[^"].* .*[^"]/;
private ensureQuotes(value: string) {
if (AbstractProcess.regexp.test(value)) {
return {
value: '"' + value + '"', //`"${value}"`,
quoted: true
};
} else {
return {
value: value,
quoted: value.length > 0 && value[0] === '"' && value[value.length - 1] === '"'
};
}
}
public get pid(): Promise<number> {
if (this.childProcessPromise) {
return this.childProcessPromise.then(childProcess => childProcess.pid!, err => -1);
} else {
return new Promise<number>((resolve) => {
this.pidResolve = resolve;
});
}
}
public terminate(): Promise<TerminateResponse> {
if (!this.childProcessPromise) {
return Promise.resolve<TerminateResponse>({ success: true });
}
return this.childProcessPromise.then((childProcess) => {
this.terminateRequested = true;
return terminateProcess(childProcess, this.options.cwd).then(response => {
if (response.success) {
this.childProcess = null;
}
return response;
});
}, (err) => {
return { success: true };
});
}
private useExec(): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!this.shell || !Platform.isWindows) {
return resolve(false);
}
const cmdShell = cp.spawn(getWindowsShell(), ['/s', '/c']);
cmdShell.on('error', (error: Error) => {
return resolve(true);
});
cmdShell.on('exit', (data: any) => {
return resolve(false);
});
});
}
}
export class LineProcess extends AbstractProcess<LineData> {
private stdoutLineDecoder: LineDecoder | null;
private stderrLineDecoder: LineDecoder | null;
public constructor(executable: Executable);
public constructor(cmd: string, args: string[], shell: boolean, options: CommandOptions);
public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean | ForkOptions, arg4?: CommandOptions) {
super(<any>arg1, arg2, <any>arg3, arg4);
this.stdoutLineDecoder = null;
this.stderrLineDecoder = null;
}
protected handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, error: Error, stdout: Buffer, stderr: Buffer) {
[stdout, stderr].forEach((buffer: Buffer, index: number) => {
const lineDecoder = new LineDecoder();
const lines = lineDecoder.write(buffer);
lines.forEach((line) => {
pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
});
const line = lineDecoder.end();
if (line) {
pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
}
});
cc({ terminated: this.terminateRequested, error: error });
}
protected handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback, sync: boolean): void {
const stdoutLineDecoder = new LineDecoder();
const stderrLineDecoder = new LineDecoder();
childProcess.stdout!.on('data', (data: Buffer) => {
const lines = stdoutLineDecoder.write(data);
lines.forEach(line => pp({ line: line, source: Source.stdout }));
});
childProcess.stderr!.on('data', (data: Buffer) => {
const lines = stderrLineDecoder.write(data);
lines.forEach(line => pp({ line: line, source: Source.stderr }));
});
this.stdoutLineDecoder = stdoutLineDecoder;
this.stderrLineDecoder = stderrLineDecoder;
}
protected override handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback): void {
const stdoutLine = this.stdoutLineDecoder ? this.stdoutLineDecoder.end() : null;
if (stdoutLine) {
pp({ line: stdoutLine, source: Source.stdout });
}
const stderrLine = this.stderrLineDecoder ? this.stderrLineDecoder.end() : null;
if (stderrLine) {
pp({ line: stderrLine, source: Source.stderr });
}
}
}
export interface IQueuedSender {
send: (msg: any) => void;
}
// Wrapper around process.send() that will queue any messages if the internal node.js
// queue is filled with messages and only continue sending messages when the internal
// queue is free again to consume messages.
// On Windows we always wait for the send() method to return before sending the next message
// to workaround https://github.com/nodejs/node/issues/7657 (IPC can freeze process)
export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
let msgQueue: string[] = [];
let useQueue = false;
const send = function (msg: any): void {
if (useQueue) {
msgQueue.push(msg); // add to the queue if the process cannot handle more messages
return;
}
const result = childProcess.send(msg, (error: Error | null) => {
if (error) {
console.error(error); // unlikely to happen, best we can do is log this error
}
useQueue = false; // we are good again to send directly without queue
// now send all the messages that we have in our queue and did not send yet
if (msgQueue.length > 0) {
const msgQueueCopy = msgQueue.slice(0);
msgQueue = [];
msgQueueCopy.forEach(entry => send(entry));
}
});
if (!result || Platform.isWindows /* workaround https://github.com/nodejs/node/issues/7657 */) {
useQueue = true;
}
};
return { send };
}
export namespace win32 {
export async function findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
// If we have an absolute path then we take it.
if (path.isAbsolute(command)) {
return command;
}
if (cwd === undefined) {
cwd = process.cwd();
}
const dir = path.dirname(command);
if (dir !== '.') {
// We have a directory and the directory is relative (see above). Make the path absolute
// to the current working directory.
return path.join(cwd, command);
}
if (paths === undefined && Types.isString(process.env['PATH'])) {
paths = process.env['PATH'].split(path.delimiter);
}
// No PATH environment. Make path absolute to the cwd.
if (paths === undefined || paths.length === 0) {
return path.join(cwd, command);
}
async function fileExists(path: string): Promise<boolean> {
if (await pfs.Promises.exists(path)) {
let statValue: Stats | undefined;
try {
statValue = await pfs.Promises.stat(path);
} catch (e) {
if (e.message.startsWith('EACCES')) {
// it might be symlink
statValue = await pfs.Promises.lstat(path);
}
}
return statValue ? !statValue.isDirectory() : false;
}
return false;
}
// We have a simple file name. We get the path variable from the env
// and try to find the executable on the path.
for (let pathEntry of paths) {
// The path entry is absolute.
let fullPath: string;
if (path.isAbsolute(pathEntry)) {
fullPath = path.join(pathEntry, command);
} else {
fullPath = path.join(cwd, pathEntry, command);
}
if (await fileExists(fullPath)) {
return fullPath;
}
let withExtension = fullPath + '.com';
if (await fileExists(withExtension)) {
return withExtension;
}
withExtension = fullPath + '.exe';
if (await fileExists(withExtension)) {
return withExtension;
}
}
return path.join(cwd, command);
}
}