processes.ts 14.6 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 'path';
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';
}

E
Erich Gamma 已提交
75 76 77 78 79 80
export abstract class AbstractProcess<TProgressData> {
	private cmd: string;
	private args: string[];
	private options: CommandOptions | ForkOptions;
	protected shell: boolean;

M
Matt Bierner 已提交
81
	private childProcess: cp.ChildProcess | null;
D
Dirk Baeumer 已提交
82 83
	protected childProcessPromise: Promise<cp.ChildProcess> | null;
	private pidResolve?: ValueCallback<number>;
J
Johannes Rieken 已提交
84
	protected terminateRequested: boolean;
E
Erich Gamma 已提交
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107

	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 已提交
108
	public constructor(cmd: string, args: string[] | undefined, shell: boolean, options: CommandOptions | undefined);
D
Dirk Baeumer 已提交
109 110
	public constructor(arg1: string | Executable, arg2?: string[], arg3?: boolean, arg4?: CommandOptions) {
		if (arg2 !== void 0 && arg3 !== void 0 && arg4 !== void 0) {
E
Erich Gamma 已提交
111 112
			this.cmd = <string>arg1;
			this.args = arg2;
D
Dirk Baeumer 已提交
113
			this.shell = arg3;
E
Erich Gamma 已提交
114 115
			this.options = arg4;
		} else {
P
Pascal Borreli 已提交
116 117 118 119 120
			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 已提交
121 122 123 124 125 126 127 128
		}

		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 已提交
129
				newEnv[key] = process.env[key]!;
E
Erich Gamma 已提交
130 131
			});
			Object.keys(this.options.env).forEach((key) => {
M
Matt Bierner 已提交
132
				newEnv[key] = this.options.env![key]!;
E
Erich Gamma 已提交
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
			});
			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 已提交
150
	public start(pp: ProgressCallback<TProgressData>): Promise<SuccessData> {
M
Matt Bierner 已提交
151
		if (Platform.isWindows && ((this.options && this.options.cwd && TPath.isUNC(this.options.cwd)) || !this.options && TPath.isUNC(process.cwd()))) {
D
Dirk Baeumer 已提交
152
			return Promise.reject(new Error(nls.localize('TaskRunner.UNC', 'Can\'t execute a shell command on a UNC drive.')));
E
Erich Gamma 已提交
153 154
		}
		return this.useExec().then((useExec) => {
D
Dirk Baeumer 已提交
155
			let cc: ValueCallback<SuccessData>;
E
Erich Gamma 已提交
156
			let ee: ErrorCallback;
D
Dirk Baeumer 已提交
157
			let result = new Promise<any>((c, e) => {
E
Erich Gamma 已提交
158 159 160 161 162 163 164 165 166
				cc = c;
				ee = e;
			});

			if (useExec) {
				let cmd: string = this.cmd;
				if (this.args) {
					cmd = cmd + ' ' + this.args.join(' ');
				}
A
Alex Dima 已提交
167
				this.childProcess = cp.exec(cmd, this.options, (error, stdout, stderr) => {
E
Erich Gamma 已提交
168
					this.childProcess = null;
J
Johannes Rieken 已提交
169
					let err: any = error;
E
Erich Gamma 已提交
170 171 172 173 174 175
					// 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 已提交
176
						this.handleExec(cc, pp, error, stdout as any, stderr as any);
E
Erich Gamma 已提交
177 178 179
					}
				});
			} else {
M
Matt Bierner 已提交
180
				let childProcess: cp.ChildProcess | null = null;
E
Erich Gamma 已提交
181 182 183 184 185 186 187
				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 已提交
188
					if (Types.isNumber(data)) {
E
Erich Gamma 已提交
189 190 191
						result.cmdCode = <number>data;
					}
					cc(result);
B
Benjamin Pasero 已提交
192
				};
E
Erich Gamma 已提交
193
				if (this.shell && Platform.isWindows) {
J
Johannes Rieken 已提交
194
					let options: any = Objects.deepClone(this.options);
E
Erich Gamma 已提交
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
					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 已提交
216
							args.push('"' + commandLine.join(' ') + '"');
E
Erich Gamma 已提交
217 218 219 220 221 222 223 224
						} 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 已提交
225
					childProcess = cp.spawn(getWindowsShell(), args, options);
E
Erich Gamma 已提交
226 227
				} else {
					if (this.cmd) {
A
Alex Dima 已提交
228
						childProcess = cp.spawn(this.cmd, this.args, this.options);
E
Erich Gamma 已提交
229 230 231 232
					}
				}
				if (childProcess) {
					this.childProcess = childProcess;
D
Dirk Baeumer 已提交
233
					this.childProcessPromise = Promise.resolve(childProcess);
234 235 236 237
					if (this.pidResolve) {
						this.pidResolve(Types.isNumber(childProcess.pid) ? childProcess.pid : -1);
						this.pidResolve = undefined;
					}
J
Johannes Rieken 已提交
238
					childProcess.on('error', (error: Error) => {
E
Erich Gamma 已提交
239
						this.childProcess = null;
J
Johannes Rieken 已提交
240
						ee({ terminated: this.terminateRequested, error: error });
E
Erich Gamma 已提交
241 242 243
					});
					if (childProcess.pid) {
						this.childProcess.on('close', closeHandler);
M
Matt Bierner 已提交
244
						this.handleSpawn(childProcess, cc!, pp, ee!, true);
E
Erich Gamma 已提交
245 246 247 248 249 250 251
					}
				}
			}
			return result;
		});
	}

D
Dirk Baeumer 已提交
252 253
	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 已提交
254

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

259
	private static readonly regexp = /^[^"].* .*[^"]/;
E
Erich Gamma 已提交
260
	private ensureQuotes(value: string) {
J
Johannes Rieken 已提交
261 262 263 264 265 266 267 268 269 270 271
		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 已提交
272 273
	}

D
Dirk Baeumer 已提交
274
	public get pid(): Promise<number> {
275 276 277
		if (this.childProcessPromise) {
			return this.childProcessPromise.then(childProcess => childProcess.pid, err => -1);
		} else {
D
Dirk Baeumer 已提交
278
			return new Promise<number>((resolve) => {
279 280 281
				this.pidResolve = resolve;
			});
		}
E
Erich Gamma 已提交
282 283
	}

D
Dirk Baeumer 已提交
284
	public terminate(): Promise<TerminateResponse> {
E
Erich Gamma 已提交
285
		if (!this.childProcessPromise) {
D
Dirk Baeumer 已提交
286
			return Promise.resolve<TerminateResponse>({ success: true });
E
Erich Gamma 已提交
287 288 289 290 291 292 293 294 295
		}
		return this.childProcessPromise.then((childProcess) => {
			this.terminateRequested = true;
			let result = terminateProcess(childProcess, this.options.cwd);
			if (result.success) {
				this.childProcess = null;
			}
			return result;
		}, (err) => {
296
			return { success: true };
E
Erich Gamma 已提交
297 298 299
		});
	}

D
Dirk Baeumer 已提交
300 301
	private useExec(): Promise<boolean> {
		return new Promise<boolean>((c, e) => {
E
Erich Gamma 已提交
302 303 304
			if (!this.shell || !Platform.isWindows) {
				c(false);
			}
A
Alex Dima 已提交
305
			let cmdShell = cp.spawn(getWindowsShell(), ['/s', '/c']);
J
Johannes Rieken 已提交
306
			cmdShell.on('error', (error: Error) => {
E
Erich Gamma 已提交
307 308
				c(true);
			});
J
Johannes Rieken 已提交
309
			cmdShell.on('exit', (data: any) => {
E
Erich Gamma 已提交
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
				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 已提交
327
	protected handleExec(cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, error: Error, stdout: Buffer, stderr: Buffer) {
J
Johannes Rieken 已提交
328
		[stdout, stderr].forEach((buffer: Buffer, index: number) => {
E
Erich Gamma 已提交
329 330 331
			let lineDecoder = new LineDecoder();
			let lines = lineDecoder.write(buffer);
			lines.forEach((line) => {
J
Johannes Rieken 已提交
332
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
E
Erich Gamma 已提交
333 334 335
			});
			let line = lineDecoder.end();
			if (line) {
J
Johannes Rieken 已提交
336
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
E
Erich Gamma 已提交
337 338 339 340 341
			}
		});
		cc({ terminated: this.terminateRequested, error: error });
	}

D
Dirk Baeumer 已提交
342
	protected handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback, sync: boolean): void {
E
Erich Gamma 已提交
343 344
		this.stdoutLineDecoder = new LineDecoder();
		this.stderrLineDecoder = new LineDecoder();
J
Johannes Rieken 已提交
345
		childProcess.stdout.on('data', (data: Buffer) => {
E
Erich Gamma 已提交
346 347 348
			let lines = this.stdoutLineDecoder.write(data);
			lines.forEach(line => pp({ line: line, source: Source.stdout }));
		});
J
Johannes Rieken 已提交
349
		childProcess.stderr.on('data', (data: Buffer) => {
E
Erich Gamma 已提交
350 351 352 353 354
			let lines = this.stderrLineDecoder.write(data);
			lines.forEach(line => pp({ line: line, source: Source.stderr }));
		});
	}

D
Dirk Baeumer 已提交
355
	protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback): void {
E
Erich Gamma 已提交
356 357 358 359 360 361 362 363
		[this.stdoutLineDecoder.end(), this.stderrLineDecoder.end()].forEach((line, index) => {
			if (line) {
				pp({ line: line, source: index === 0 ? Source.stdout : Source.stderr });
			}
		});
	}
}

B
Benjamin Pasero 已提交
364
export interface IQueuedSender {
B
Benjamin Pasero 已提交
365 366 367
	send: (msg: any) => void;
}

B
Benjamin Pasero 已提交
368 369 370 371 372
// 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)
373
export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
374
	let msgQueue: string[] = [];
B
Benjamin Pasero 已提交
375
	let useQueue = false;
B
Benjamin Pasero 已提交
376 377

	const send = function (msg: any): void {
B
Benjamin Pasero 已提交
378 379
		if (useQueue) {
			msgQueue.push(msg); // add to the queue if the process cannot handle more messages
B
Benjamin Pasero 已提交
380 381 382
			return;
		}

383
		let result = childProcess.send(msg, (error: Error) => {
B
Benjamin Pasero 已提交
384 385 386 387
			if (error) {
				console.error(error); // unlikely to happen, best we can do is log this error
			}

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

B
Benjamin Pasero 已提交
390 391 392 393 394
			// 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 已提交
395 396 397
			}
		});

B
Benjamin Pasero 已提交
398 399
		if (!result || Platform.isWindows /* workaround https://github.com/nodejs/node/issues/7657 */) {
			useQueue = true;
B
Benjamin Pasero 已提交
400 401 402 403
		}
	};

	return { send };
404
}
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452

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;
		}
		if (cwd === void 0) {
			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);
		}
		if (paths === void 0 && Types.isString(process.env.PATH)) {
			paths = process.env.PATH.split(path.delimiter);
		}
		// No PATH environment. Make path absolute to the cwd.
		if (paths === void 0 || paths.length === 0) {
			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);
	}
}