processes.ts 15.9 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/path';
7
import * as fs from 'fs';
A
Alex Ross 已提交
8
import { promisify } from 'util';
E
Erich Gamma 已提交
9
import * as cp from 'child_process';
10
import * as nls from 'vs/nls';
E
Erich Gamma 已提交
11 12 13
import * as Types from 'vs/base/common/types';
import { IStringDictionary } from 'vs/base/common/collections';
import * as Objects from 'vs/base/common/objects';
B
Benjamin Pasero 已提交
14
import * as extpath from 'vs/base/common/extpath';
E
Erich Gamma 已提交
15 16
import * as Platform from 'vs/base/common/platform';
import { LineDecoder } from 'vs/base/node/decoder';
D
Dirk Baeumer 已提交
17
import { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode, Executable } from 'vs/base/common/processes';
18
import { getPathFromAmdModule } from 'vs/base/common/amd';
D
Dirk Baeumer 已提交
19
export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode };
E
Erich Gamma 已提交
20

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

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

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

43
function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<TerminateResponse> {
E
Erich Gamma 已提交
44 45
	if (Platform.isWindows) {
		try {
46
			const options: any = {
E
Erich Gamma 已提交
47 48 49
				stdio: ['pipe', 'pipe', 'ignore']
			};
			if (cwd) {
B
Benjamin Pasero 已提交
50
				options.cwd = cwd;
E
Erich Gamma 已提交
51
			}
52 53 54 55 56 57 58 59 60 61 62 63 64
			const killProcess = cp.execFile('taskkill', ['/T', '/F', '/PID', process.pid.toString()], options);
			return new Promise((resolve, reject) => {
				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 });
					}
				});
			});
E
Erich Gamma 已提交
65
		} catch (err) {
66
			return Promise.resolve({ success: false, error: err, code: err.status ? getWindowsCode(err.status) : TerminateResponseCode.Unknown });
E
Erich Gamma 已提交
67 68 69
		}
	} else if (Platform.isLinux || Platform.isMacintosh) {
		try {
70
			const cmd = getPathFromAmdModule(require, 'vs/base/node/terminateProcess.sh');
71
			return new Promise((resolve, reject) => {
72 73 74
				cp.execFile(cmd, [process.pid.toString()], { encoding: 'utf8', shell: true } as cp.ExecFileOptions, (err, stdout, stderr) => {
					if (err) {
						resolve({ success: false, error: err });
75
					} else {
76
						resolve({ success: true });
77 78 79
					}
				});
			});
E
Erich Gamma 已提交
80
		} catch (err) {
81
			return Promise.resolve({ success: false, error: err });
E
Erich Gamma 已提交
82 83 84 85
		}
	} else {
		process.kill('SIGKILL');
	}
86
	return Promise.resolve({ success: true });
E
Erich Gamma 已提交
87 88
}

89 90 91 92
export function getWindowsShell(): string {
	return process.env['comspec'] || 'cmd.exe';
}

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

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

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

		this.childProcess = null;
142
		this.childProcessPromise = null;
E
Erich Gamma 已提交
143 144 145
		this.terminateRequested = false;

		if (this.options.env) {
146
			const newEnv: IStringDictionary<string> = Object.create(null);
E
Erich Gamma 已提交
147
			Object.keys(process.env).forEach((key) => {
M
Matt Bierner 已提交
148
				newEnv[key] = process.env[key]!;
E
Erich Gamma 已提交
149 150
			});
			Object.keys(this.options.env).forEach((key) => {
M
Matt Bierner 已提交
151
				newEnv[key] = this.options.env![key]!;
E
Erich Gamma 已提交
152 153 154 155 156 157 158
			});
			this.options.env = newEnv;
		}
	}

	public getSanitizedCommand(): string {
		let result = this.cmd.toLowerCase();
159
		const index = result.lastIndexOf(path.sep);
E
Erich Gamma 已提交
160 161 162 163 164 165 166 167 168
		if (index !== -1) {
			result = result.substring(index + 1);
		}
		if (AbstractProcess.WellKnowCommands[result]) {
			return result;
		}
		return 'other';
	}

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

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

D
Dirk Baeumer 已提交
271 272
	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 已提交
273

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

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

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

D
Dirk Baeumer 已提交
303
	public terminate(): Promise<TerminateResponse> {
E
Erich Gamma 已提交
304
		if (!this.childProcessPromise) {
D
Dirk Baeumer 已提交
305
			return Promise.resolve<TerminateResponse>({ success: true });
E
Erich Gamma 已提交
306 307 308
		}
		return this.childProcessPromise.then((childProcess) => {
			this.terminateRequested = true;
309 310 311 312 313 314
			return terminateProcess(childProcess, this.options.cwd).then(response => {
				if (response.success) {
					this.childProcess = null;
				}
				return response;
			});
E
Erich Gamma 已提交
315
		}, (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
			if (!this.shell || !Platform.isWindows) {
323
				return c(false);
E
Erich Gamma 已提交
324
			}
325
			const cmdShell = cp.spawn(getWindowsShell(), ['/s', '/c']);
J
Johannes Rieken 已提交
326
			cmdShell.on('error', (error: Error) => {
327
				return c(true);
E
Erich Gamma 已提交
328
			});
J
Johannes Rieken 已提交
329
			cmdShell.on('exit', (data: any) => {
330
				return c(false);
E
Erich Gamma 已提交
331 332 333 334 335 336 337
			});
		});
	}
}

export class LineProcess extends AbstractProcess<LineData> {

338 339
	private stdoutLineDecoder: LineDecoder | null;
	private stderrLineDecoder: LineDecoder | null;
E
Erich Gamma 已提交
340 341 342 343 344

	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);
345 346 347

		this.stdoutLineDecoder = null;
		this.stderrLineDecoder = null;
E
Erich Gamma 已提交
348 349
	}

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

D
Dirk Baeumer 已提交
365
	protected handleSpawn(childProcess: cp.ChildProcess, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback, sync: boolean): void {
366 367
		const stdoutLineDecoder = new LineDecoder();
		const stderrLineDecoder = new LineDecoder();
B
Benjamin Pasero 已提交
368
		childProcess.stdout!.on('data', (data: Buffer) => {
369
			const lines = stdoutLineDecoder.write(data);
E
Erich Gamma 已提交
370 371
			lines.forEach(line => pp({ line: line, source: Source.stdout }));
		});
B
Benjamin Pasero 已提交
372
		childProcess.stderr!.on('data', (data: Buffer) => {
373
			const lines = stderrLineDecoder.write(data);
E
Erich Gamma 已提交
374 375
			lines.forEach(line => pp({ line: line, source: Source.stderr }));
		});
376 377 378

		this.stdoutLineDecoder = stdoutLineDecoder;
		this.stderrLineDecoder = stderrLineDecoder;
E
Erich Gamma 已提交
379 380
	}

D
Dirk Baeumer 已提交
381
	protected handleClose(data: any, cc: ValueCallback<SuccessData>, pp: ProgressCallback<LineData>, ee: ErrorCallback): void {
382 383 384 385 386 387 388 389
		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 });
		}
E
Erich Gamma 已提交
390 391 392
	}
}

B
Benjamin Pasero 已提交
393
export interface IQueuedSender {
B
Benjamin Pasero 已提交
394 395 396
	send: (msg: any) => void;
}

B
Benjamin Pasero 已提交
397 398 399 400 401
// 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)
402
export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender {
403
	let msgQueue: string[] = [];
B
Benjamin Pasero 已提交
404
	let useQueue = false;
B
Benjamin Pasero 已提交
405 406

	const send = function (msg: any): void {
B
Benjamin Pasero 已提交
407 408
		if (useQueue) {
			msgQueue.push(msg); // add to the queue if the process cannot handle more messages
B
Benjamin Pasero 已提交
409 410 411
			return;
		}

412
		const result = childProcess.send(msg, (error: Error) => {
B
Benjamin Pasero 已提交
413 414 415 416
			if (error) {
				console.error(error); // unlikely to happen, best we can do is log this error
			}

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

B
Benjamin Pasero 已提交
419 420 421 422 423
			// 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 已提交
424 425 426
			}
		});

B
Benjamin Pasero 已提交
427 428
		if (!result || Platform.isWindows /* workaround https://github.com/nodejs/node/issues/7657 */) {
			useQueue = true;
B
Benjamin Pasero 已提交
429 430 431 432
		}
	};

	return { send };
433
}
434 435

export namespace win32 {
A
Alex Ross 已提交
436
	export async function findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
437 438 439 440
		// If we have an absolute path then we take it.
		if (path.isAbsolute(command)) {
			return command;
		}
R
Rob Lourens 已提交
441
		if (cwd === undefined) {
442 443
			cwd = process.cwd();
		}
444
		const dir = path.dirname(command);
445 446 447 448 449
		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 已提交
450
		if (paths === undefined && Types.isString(process.env.PATH)) {
451 452 453
			paths = process.env.PATH.split(path.delimiter);
		}
		// No PATH environment. Make path absolute to the cwd.
R
Rob Lourens 已提交
454
		if (paths === undefined || paths.length === 0) {
455 456
			return path.join(cwd, command);
		}
457 458 459 460 461 462 463 464

		async function fileExists(path: string): Promise<boolean> {
			if (await promisify(fs.exists)(path)) {
				return !((await promisify(fs.stat)(path)).isDirectory);
			}
			return false;
		}

465 466 467 468 469 470 471 472 473 474
		// 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);
			}
475
			if (await fileExists(fullPath)) {
476 477 478
				return fullPath;
			}
			let withExtension = fullPath + '.com';
479
			if (await fileExists(withExtension)) {
480 481 482
				return withExtension;
			}
			withExtension = fullPath + '.exe';
483
			if (await fileExists(withExtension)) {
484 485 486 487 488
				return withExtension;
			}
		}
		return path.join(cwd, command);
	}
489
}