processes.ts 15.1 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as path from 'vs/base/common/paths.node';
7
import * as fs from 'fs';
E
Erich Gamma 已提交
8
import * as cp from 'child_process';
9
import * as nls from 'vs/nls';
E
Erich Gamma 已提交
10 11 12 13 14 15
import * as Types from 'vs/base/common/types';
import { IStringDictionary } from 'vs/base/common/collections';
import * as Objects from 'vs/base/common/objects';
import * as TPath from 'vs/base/common/paths';
import * as Platform from 'vs/base/common/platform';
import { LineDecoder } from 'vs/base/node/decoder';
D
Dirk Baeumer 已提交
16
import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode, Executable } from 'vs/base/common/processes';
17
import { getPathFromAmdModule } from 'vs/base/common/amd';
D
Dirk Baeumer 已提交
18
export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode };
E
Erich Gamma 已提交
19

J
Johannes Rieken 已提交
20
export type ValueCallback<T> = (value?: T | Promise<T>) => void;
D
Dirk Baeumer 已提交
21 22
export type ErrorCallback = (error?: any) => void;
export type ProgressCallback<T> = (progress: T) => void;
J
Joao Moreno 已提交
23

E
Erich Gamma 已提交
24 25 26 27 28
export interface LineData {
	line: string;
	source: Source;
}

D
Dirk Baeumer 已提交
29 30 31 32 33 34 35 36 37 38 39 40 41
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;
	}
}

A
Alex Dima 已提交
42
export function terminateProcess(process: cp.ChildProcess, cwd?: string): TerminateResponse {
E
Erich Gamma 已提交
43 44
	if (Platform.isWindows) {
		try {
J
Johannes Rieken 已提交
45
			let options: any = {
E
Erich Gamma 已提交
46 47 48
				stdio: ['pipe', 'pipe', 'ignore']
			};
			if (cwd) {
B
Benjamin Pasero 已提交
49
				options.cwd = cwd;
E
Erich Gamma 已提交
50
			}
D
Dirk Baeumer 已提交
51
			cp.execFileSync('taskkill', ['/T', '/F', '/PID', process.pid.toString()], options);
E
Erich Gamma 已提交
52
		} catch (err) {
J
Johannes Rieken 已提交
53
			return { success: false, error: err, code: err.status ? getWindowsCode(err.status) : TerminateResponseCode.Unknown };
E
Erich Gamma 已提交
54 55 56
		}
	} else if (Platform.isLinux || Platform.isMacintosh) {
		try {
57
			let cmd = getPathFromAmdModule(require, 'vs/base/node/terminateProcess.sh');
D
Dirk Baeumer 已提交
58
			let result = cp.spawnSync(cmd, [process.pid.toString()]);
E
Erich Gamma 已提交
59 60 61 62 63 64 65 66 67 68 69 70
			if (result.error) {
				return { success: false, error: result.error };
			}
		} catch (err) {
			return { success: false, error: err };
		}
	} else {
		process.kill('SIGKILL');
	}
	return { success: true };
}

71 72 73 74
export function getWindowsShell(): string {
	return process.env['comspec'] || 'cmd.exe';
}

75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
/**
 * Sanitizes a VS Code process environment by removing all Electron/VS Code-related values.
 */
export function sanitizeProcessEnvironment(env: Platform.IProcessEnvironment): void {
	const keysToRemove = [
		/^ELECTRON_.+$/,
		/^GOOGLE_API_KEY$/,
		/^VSCODE_.+$/
	];
	const envKeys = Object.keys(env);
	envKeys.forEach(envKey => {
		for (let i = 0; i < keysToRemove.length; i++) {
			if (envKey.search(keysToRemove[i]) !== -1) {
				delete env[envKey];
				break;
			}
		}
	});
}

E
Erich Gamma 已提交
95 96 97 98 99 100
export abstract class AbstractProcess<TProgressData> {
	private cmd: string;
	private args: string[];
	private options: CommandOptions | ForkOptions;
	protected shell: boolean;

M
Matt Bierner 已提交
101
	private childProcess: cp.ChildProcess | null;
D
Dirk Baeumer 已提交
102 103
	protected childProcessPromise: Promise<cp.ChildProcess> | null;
	private pidResolve?: ValueCallback<number>;
J
Johannes Rieken 已提交
104
	protected terminateRequested: boolean;
E
Erich Gamma 已提交
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127

	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);
M
Matt Bierner 已提交
128
	public constructor(cmd: string, args: string[] | undefined, shell: boolean, options: CommandOptions | undefined);
D
Dirk Baeumer 已提交
129
	public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean, arg4?: CommandOptions) {
R
Rob Lourens 已提交
130
		if (arg2 !== undefined && arg3 !== undefined && arg4 !== undefined) {
E
Erich Gamma 已提交
131 132
			this.cmd = <string>arg1;
			this.args = arg2;
D
Dirk Baeumer 已提交
133
			this.shell = arg3;
E
Erich Gamma 已提交
134 135
			this.options = arg4;
		} else {
P
Pascal Borreli 已提交
136 137 138 139 140
			let executable = <Executable>arg1;
			this.cmd = executable.command;
			this.shell = executable.isShellCommand;
			this.args = executable.args.slice(0);
			this.options = executable.options || {};
E
Erich Gamma 已提交
141 142 143 144 145 146 147 148
		}

		this.childProcess = null;
		this.terminateRequested = false;

		if (this.options.env) {
			let newEnv: IStringDictionary<string> = Object.create(null);
			Object.keys(process.env).forEach((key) => {
M
Matt Bierner 已提交
149
				newEnv[key] = process.env[key]!;
E
Erich Gamma 已提交
150 151
			});
			Object.keys(this.options.env).forEach((key) => {
M
Matt Bierner 已提交
152
				newEnv[key] = this.options.env![key]!;
E
Erich Gamma 已提交
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
			});
			this.options.env = newEnv;
		}
	}

	public getSanitizedCommand(): string {
		let result = this.cmd.toLowerCase();
		let index = result.lastIndexOf(path.sep);
		if (index !== -1) {
			result = result.substring(index + 1);
		}
		if (AbstractProcess.WellKnowCommands[result]) {
			return result;
		}
		return 'other';
	}

D
Dirk Baeumer 已提交
170
	public start(pp: ProgressCallback<TProgressData>): Promise<SuccessData> {
M
Matt Bierner 已提交
171
		if (Platform.isWindows && ((this.options && this.options.cwd && TPath.isUNC(this.options.cwd)) || !this.options && TPath.isUNC(process.cwd()))) {
D
Dirk Baeumer 已提交
172
			return Promise.reject(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.')));
E
Erich Gamma 已提交
173 174
		}
		return this.useExec().then((useExec) => {
D
Dirk Baeumer 已提交
175
			let cc: ValueCallback<SuccessData>;
E
Erich Gamma 已提交
176
			let ee: ErrorCallback;
D
Dirk Baeumer 已提交
177
			let result = new Promise<any>((c, e) => {
E
Erich Gamma 已提交
178 179 180 181 182 183 184 185 186
				cc = c;
				ee = e;
			});

			if (useExec) {
				let cmd: string = this.cmd;
				if (this.args) {
					cmd = cmd + ' ' + this.args.join(' ');
				}
A
Alex Dima 已提交
187
				this.childProcess = cp.exec(cmd, this.options, (error, stdout, stderr) => {
E
Erich Gamma 已提交
188
					this.childProcess = null;
J
Johannes Rieken 已提交
189
					let err: any = error;
E
Erich Gamma 已提交
190 191 192 193 194 195
					// 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 {
J
Joao Moreno 已提交
196
						this.handleExec(cc, pp, error, stdout as any, stderr as any);
E
Erich Gamma 已提交
197 198 199
					}
				});
			} else {
M
Matt Bierner 已提交
200
				let childProcess: cp.ChildProcess | null = null;
E
Erich Gamma 已提交
201 202 203 204 205 206 207
				let closeHandler = (data: any) => {
					this.childProcess = null;
					this.childProcessPromise = null;
					this.handleClose(data, cc, pp, ee);
					let result: SuccessData = {
						terminated: this.terminateRequested
					};
J
Johannes Rieken 已提交
208
					if (Types.isNumber(data)) {
E
Erich Gamma 已提交
209 210 211
						result.cmdCode = <number>data;
					}
					cc(result);
B
Benjamin Pasero 已提交
212
				};
E
Erich Gamma 已提交
213
				if (this.shell && Platform.isWindows) {
J
Johannes Rieken 已提交
214
					let options: any = Objects.deepClone(this.options);
E
Erich Gamma 已提交
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
					options.windowsVerbatimArguments = true;
					options.detached = false;
					let quotedCommand: boolean = false;
					let quotedArg: boolean = false;
					let 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;
						});
					}
					let args: string[] = [
						'/s',
						'/c',
					];
					if (quotedCommand) {
						if (quotedArg) {
B
Benjamin Pasero 已提交
236
							args.push('"' + commandLine.join(' ') + '"');
E
Erich Gamma 已提交
237 238 239 240 241 242 243 244
						} else if (commandLine.length > 1) {
							args.push('"' + commandLine[0] + '"' + ' ' + commandLine.slice(1).join(' '));
						} else {
							args.push('"' + commandLine[0] + '"');
						}
					} else {
						args.push(commandLine.join(' '));
					}
A
Alex Dima 已提交
245
					childProcess = cp.spawn(getWindowsShell(), args, options);
E
Erich Gamma 已提交
246 247
				} else {
					if (this.cmd) {
A
Alex Dima 已提交
248
						childProcess = cp.spawn(this.cmd, this.args, this.options);
E
Erich Gamma 已提交
249 250 251 252
					}
				}
				if (childProcess) {
					this.childProcess = childProcess;
D
Dirk Baeumer 已提交
253
					this.childProcessPromise = Promise.resolve(childProcess);
254 255 256 257
					if (this.pidResolve) {
						this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1);
						this.pidResolve = undefined;
					}
J
Johannes Rieken 已提交
258
					childProcess.on('error', (error: Error) => {
E
Erich Gamma 已提交
259
						this.childProcess = null;
J
Johannes Rieken 已提交
260
						ee({ terminated: this.terminateRequested, error: error });
E
Erich Gamma 已提交
261 262 263
					});
					if (childProcess.pid) {
						this.childProcess.on('close', closeHandler);
M
Matt Bierner 已提交
264
						this.handleSpawn(childProcess, cc!, pp, ee!, true);
E
Erich Gamma 已提交
265 266 267 268 269 270 271
					}
				}
			}
			return result;
		});
	}

D
Dirk Baeumer 已提交
272 273
	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;
E
Erich Gamma 已提交
274

D
Dirk Baeumer 已提交
275
	protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<TProgressData>, ee: ErrorCallback): void {
E
Erich Gamma 已提交
276 277 278
		// Default is to do nothing.
	}

279
	private static readonly regexp = /^[^"].* .*[^"]/;
E
Erich Gamma 已提交
280
	private ensureQuotes(value: string) {
J
Johannes Rieken 已提交
281 282 283 284 285 286 287 288 289 290 291
		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] === '"'
			};
		}
E
Erich Gamma 已提交
292 293
	}

D
Dirk Baeumer 已提交
294
	public get pid(): Promise<number> {
295 296 297
		if (this.childProcessPromise) {
			return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1);
		} else {
D
Dirk Baeumer 已提交
298
			return new Promise<number>((resolve) => {
299 300 301
				this.pidResolve = resolve;
			});
		}
E
Erich Gamma 已提交
302 303
	}

D
Dirk Baeumer 已提交
304
	public terminate(): Promise<TerminateResponse> {
E
Erich Gamma 已提交
305
		if (!this.childProcessPromise) {
D
Dirk Baeumer 已提交
306
			return Promise.resolve<TerminateResponse>({ success: true });
E
Erich Gamma 已提交
307 308 309 310 311 312 313 314 315
		}
		return this.childProcessPromise.then((childProcess) => {
			this.terminateRequested = true;
			let result = terminateProcess(childProcess, this.options.cwd);
			if (result.success) {
				this.childProcess = null;
			}
			return result;
		}, (err) => {
316
			return { success: true };
E
Erich Gamma 已提交
317 318 319
		});
	}

D
Dirk Baeumer 已提交
320 321
	private useExec(): Promise<boolean> {
		return new Promise<boolean>((c, e) => {
E
Erich Gamma 已提交
322 323 324
			if (!this.shell || !Platform.isWindows) {
				c(false);
			}
A
Alex Dima 已提交
325
			let cmdShell = cp.spawn(getWindowsShell(), ['/s', '/c']);
J
Johannes Rieken 已提交
326
			cmdShell.on('error', (error: Error) => {
E
Erich Gamma 已提交
327 328
				c(true);
			});
J
Johannes Rieken 已提交
329
			cmdShell.on('exit', (data: any) => {
E
Erich Gamma 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
				c(false);
			});
		});
	}
}

export class LineProcess extends AbstractProcess<LineData> {

	private stdoutLineDecoder: LineDecoder;
	private stderrLineDecoder: LineDecoder;

	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);
	}

D
Dirk Baeumer 已提交
347
	protected handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, error: Error, stdout: Buffer, stderr: Buffer) {
J
Johannes Rieken 已提交
348
		[stdout, stderr].forEach((buffer: Buffer, index: number) => {
E
Erich Gamma 已提交
349 350 351
			let lineDecoder = new LineDecoder();
			let lines = lineDecoder.write(buffer);
			lines.forEach((line) => {
J
Johannes Rieken 已提交
352
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
E
Erich Gamma 已提交
353 354 355
			});
			let line = lineDecoder.end();
			if (line) {
J
Johannes Rieken 已提交
356
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
E
Erich Gamma 已提交
357 358 359 360 361
			}
		});
		cc({ terminated: this.terminateRequested, error: error });
	}

D
Dirk Baeumer 已提交
362
	protected handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback, sync: boolean): void {
E
Erich Gamma 已提交
363 364
		this.stdoutLineDecoder = new LineDecoder();
		this.stderrLineDecoder = new LineDecoder();
J
Johannes Rieken 已提交
365
		childProcess.stdout.on('data', (data: Buffer) => {
E
Erich Gamma 已提交
366 367 368
			let lines = this.stdoutLineDecoder.write(data);
			lines.forEach(line => pp({ line: line, source: Source.stdout }));
		});
J
Johannes Rieken 已提交
369
		childProcess.stderr.on('data', (data: Buffer) => {
E
Erich Gamma 已提交
370 371 372 373 374
			let lines = this.stderrLineDecoder.write(data);
			lines.forEach(line => pp({ line: line, source: Source.stderr }));
		});
	}

D
Dirk Baeumer 已提交
375
	protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback): void {
E
Erich Gamma 已提交
376 377 378 379 380 381 382 383
		[this.stdoutLineDecoder.end(), this.stderrLineDecoder.end()].forEach((line, index) => {
			if (line) {
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
			}
		});
	}
}

B
Benjamin Pasero 已提交
384
export interface IQueuedSender {
B
Benjamin Pasero 已提交
385 386 387
	send: (msg: any) => void;
}

B
Benjamin Pasero 已提交
388 389 390 391 392
// 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)
393
export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
394
	let msgQueue: string[] = [];
B
Benjamin Pasero 已提交
395
	let useQueue = false;
B
Benjamin Pasero 已提交
396 397

	const send = function (msg: any): void {
B
Benjamin Pasero 已提交
398 399
		if (useQueue) {
			msgQueue.push(msg); // add to the queue if the process cannot handle more messages
B
Benjamin Pasero 已提交
400 401 402
			return;
		}

403
		let result = childProcess.send(msg, (error: Error) => {
B
Benjamin Pasero 已提交
404 405 406 407
			if (error) {
				console.error(error); // unlikely to happen, best we can do is log this error
			}

B
Benjamin Pasero 已提交
408
			useQueue = false; // we are good again to send directly without queue
B
Benjamin Pasero 已提交
409

B
Benjamin Pasero 已提交
410 411 412 413 414
			// 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));
B
Benjamin Pasero 已提交
415 416 417
			}
		});

B
Benjamin Pasero 已提交
418 419
		if (!result || Platform.isWindows /* workaround https://github.com/nodejs/node/issues/7657 */) {
			useQueue = true;
B
Benjamin Pasero 已提交
420 421 422 423
		}
	};

	return { send };
424
}
425 426 427 428 429 430 431

export namespace win32 {
	export function findExecutable(command: string, cwd?: string, paths?: string[]): string {
		// If we have an absolute path then we take it.
		if (path.isAbsolute(command)) {
			return command;
		}
R
Rob Lourens 已提交
432
		if (cwd === undefined) {
433 434 435 436 437 438 439 440
			cwd = process.cwd();
		}
		let 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);
		}
R
Rob Lourens 已提交
441
		if (paths === undefined && Types.isString(process.env.PATH)) {
442 443 444
			paths = process.env.PATH.split(path.delimiter);
		}
		// No PATH environment. Make path absolute to the cwd.
R
Rob Lourens 已提交
445
		if (paths === undefined || paths.length === 0) {
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
			return path.join(cwd, command);
		}
		// 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 (fs.existsSync(fullPath)) {
				return fullPath;
			}
			let withExtension = fullPath + '.com';
			if (fs.existsSync(withExtension)) {
				return withExtension;
			}
			withExtension = fullPath + '.exe';
			if (fs.existsSync(withExtension)) {
				return withExtension;
			}
		}
		return path.join(cwd, command);
	}
}