index.js
  1  'use strict';
  2  const path = require('path');
  3  const childProcess = require('child_process');
  4  const crossSpawn = require('cross-spawn');
  5  const stripEof = require('strip-eof');
  6  const npmRunPath = require('npm-run-path');
  7  const isStream = require('is-stream');
  8  const _getStream = require('get-stream');
  9  const pFinally = require('p-finally');
 10  const onExit = require('signal-exit');
 11  const errname = require('./lib/errname');
 12  const stdio = require('./lib/stdio');
 13  
 14  const TEN_MEGABYTES = 1000 * 1000 * 10;
 15  
 16  function handleArgs(cmd, args, opts) {
 17  	let parsed;
 18  
 19  	opts = Object.assign({
 20  		extendEnv: true,
 21  		env: {}
 22  	}, opts);
 23  
 24  	if (opts.extendEnv) {
 25  		opts.env = Object.assign({}, process.env, opts.env);
 26  	}
 27  
 28  	if (opts.__winShell === true) {
 29  		delete opts.__winShell;
 30  		parsed = {
 31  			command: cmd,
 32  			args,
 33  			options: opts,
 34  			file: cmd,
 35  			original: {
 36  				cmd,
 37  				args
 38  			}
 39  		};
 40  	} else {
 41  		parsed = crossSpawn._parse(cmd, args, opts);
 42  	}
 43  
 44  	opts = Object.assign({
 45  		maxBuffer: TEN_MEGABYTES,
 46  		buffer: true,
 47  		stripEof: true,
 48  		preferLocal: true,
 49  		localDir: parsed.options.cwd || process.cwd(),
 50  		encoding: 'utf8',
 51  		reject: true,
 52  		cleanup: true
 53  	}, parsed.options);
 54  
 55  	opts.stdio = stdio(opts);
 56  
 57  	if (opts.preferLocal) {
 58  		opts.env = npmRunPath.env(Object.assign({}, opts, {cwd: opts.localDir}));
 59  	}
 60  
 61  	if (opts.detached) {
 62  		// #115
 63  		opts.cleanup = false;
 64  	}
 65  
 66  	if (process.platform === 'win32' && path.basename(parsed.command) === 'cmd.exe') {
 67  		// #116
 68  		parsed.args.unshift('/q');
 69  	}
 70  
 71  	return {
 72  		cmd: parsed.command,
 73  		args: parsed.args,
 74  		opts,
 75  		parsed
 76  	};
 77  }
 78  
 79  function handleInput(spawned, input) {
 80  	if (input === null || input === undefined) {
 81  		return;
 82  	}
 83  
 84  	if (isStream(input)) {
 85  		input.pipe(spawned.stdin);
 86  	} else {
 87  		spawned.stdin.end(input);
 88  	}
 89  }
 90  
 91  function handleOutput(opts, val) {
 92  	if (val && opts.stripEof) {
 93  		val = stripEof(val);
 94  	}
 95  
 96  	return val;
 97  }
 98  
 99  function handleShell(fn, cmd, opts) {
100  	let file = '/bin/sh';
101  	let args = ['-c', cmd];
102  
103  	opts = Object.assign({}, opts);
104  
105  	if (process.platform === 'win32') {
106  		opts.__winShell = true;
107  		file = process.env.comspec || 'cmd.exe';
108  		args = ['/s', '/c', `"${cmd}"`];
109  		opts.windowsVerbatimArguments = true;
110  	}
111  
112  	if (opts.shell) {
113  		file = opts.shell;
114  		delete opts.shell;
115  	}
116  
117  	return fn(file, args, opts);
118  }
119  
120  function getStream(process, stream, {encoding, buffer, maxBuffer}) {
121  	if (!process[stream]) {
122  		return null;
123  	}
124  
125  	let ret;
126  
127  	if (!buffer) {
128  		// TODO: Use `ret = util.promisify(stream.finished)(process[stream]);` when targeting Node.js 10
129  		ret = new Promise((resolve, reject) => {
130  			process[stream]
131  				.once('end', resolve)
132  				.once('error', reject);
133  		});
134  	} else if (encoding) {
135  		ret = _getStream(process[stream], {
136  			encoding,
137  			maxBuffer
138  		});
139  	} else {
140  		ret = _getStream.buffer(process[stream], {maxBuffer});
141  	}
142  
143  	return ret.catch(err => {
144  		err.stream = stream;
145  		err.message = `${stream} ${err.message}`;
146  		throw err;
147  	});
148  }
149  
150  function makeError(result, options) {
151  	const {stdout, stderr} = result;
152  
153  	let err = result.error;
154  	const {code, signal} = result;
155  
156  	const {parsed, joinedCmd} = options;
157  	const timedOut = options.timedOut || false;
158  
159  	if (!err) {
160  		let output = '';
161  
162  		if (Array.isArray(parsed.opts.stdio)) {
163  			if (parsed.opts.stdio[2] !== 'inherit') {
164  				output += output.length > 0 ? stderr : `\n${stderr}`;
165  			}
166  
167  			if (parsed.opts.stdio[1] !== 'inherit') {
168  				output += `\n${stdout}`;
169  			}
170  		} else if (parsed.opts.stdio !== 'inherit') {
171  			output = `\n${stderr}${stdout}`;
172  		}
173  
174  		err = new Error(`Command failed: ${joinedCmd}${output}`);
175  		err.code = code < 0 ? errname(code) : code;
176  	}
177  
178  	err.stdout = stdout;
179  	err.stderr = stderr;
180  	err.failed = true;
181  	err.signal = signal || null;
182  	err.cmd = joinedCmd;
183  	err.timedOut = timedOut;
184  
185  	return err;
186  }
187  
188  function joinCmd(cmd, args) {
189  	let joinedCmd = cmd;
190  
191  	if (Array.isArray(args) && args.length > 0) {
192  		joinedCmd += ' ' + args.join(' ');
193  	}
194  
195  	return joinedCmd;
196  }
197  
198  module.exports = (cmd, args, opts) => {
199  	const parsed = handleArgs(cmd, args, opts);
200  	const {encoding, buffer, maxBuffer} = parsed.opts;
201  	const joinedCmd = joinCmd(cmd, args);
202  
203  	let spawned;
204  	try {
205  		spawned = childProcess.spawn(parsed.cmd, parsed.args, parsed.opts);
206  	} catch (err) {
207  		return Promise.reject(err);
208  	}
209  
210  	let removeExitHandler;
211  	if (parsed.opts.cleanup) {
212  		removeExitHandler = onExit(() => {
213  			spawned.kill();
214  		});
215  	}
216  
217  	let timeoutId = null;
218  	let timedOut = false;
219  
220  	const cleanup = () => {
221  		if (timeoutId) {
222  			clearTimeout(timeoutId);
223  			timeoutId = null;
224  		}
225  
226  		if (removeExitHandler) {
227  			removeExitHandler();
228  		}
229  	};
230  
231  	if (parsed.opts.timeout > 0) {
232  		timeoutId = setTimeout(() => {
233  			timeoutId = null;
234  			timedOut = true;
235  			spawned.kill(parsed.opts.killSignal);
236  		}, parsed.opts.timeout);
237  	}
238  
239  	const processDone = new Promise(resolve => {
240  		spawned.on('exit', (code, signal) => {
241  			cleanup();
242  			resolve({code, signal});
243  		});
244  
245  		spawned.on('error', err => {
246  			cleanup();
247  			resolve({error: err});
248  		});
249  
250  		if (spawned.stdin) {
251  			spawned.stdin.on('error', err => {
252  				cleanup();
253  				resolve({error: err});
254  			});
255  		}
256  	});
257  
258  	function destroy() {
259  		if (spawned.stdout) {
260  			spawned.stdout.destroy();
261  		}
262  
263  		if (spawned.stderr) {
264  			spawned.stderr.destroy();
265  		}
266  	}
267  
268  	const handlePromise = () => pFinally(Promise.all([
269  		processDone,
270  		getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}),
271  		getStream(spawned, 'stderr', {encoding, buffer, maxBuffer})
272  	]).then(arr => {
273  		const result = arr[0];
274  		result.stdout = arr[1];
275  		result.stderr = arr[2];
276  
277  		if (result.error || result.code !== 0 || result.signal !== null) {
278  			const err = makeError(result, {
279  				joinedCmd,
280  				parsed,
281  				timedOut
282  			});
283  
284  			// TODO: missing some timeout logic for killed
285  			// https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
286  			// err.killed = spawned.killed || killed;
287  			err.killed = err.killed || spawned.killed;
288  
289  			if (!parsed.opts.reject) {
290  				return err;
291  			}
292  
293  			throw err;
294  		}
295  
296  		return {
297  			stdout: handleOutput(parsed.opts, result.stdout),
298  			stderr: handleOutput(parsed.opts, result.stderr),
299  			code: 0,
300  			failed: false,
301  			killed: false,
302  			signal: null,
303  			cmd: joinedCmd,
304  			timedOut: false
305  		};
306  	}), destroy);
307  
308  	crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed);
309  
310  	handleInput(spawned, parsed.opts.input);
311  
312  	spawned.then = (onfulfilled, onrejected) => handlePromise().then(onfulfilled, onrejected);
313  	spawned.catch = onrejected => handlePromise().catch(onrejected);
314  
315  	return spawned;
316  };
317  
318  // TODO: set `stderr: 'ignore'` when that option is implemented
319  module.exports.stdout = (...args) => module.exports(...args).then(x => x.stdout);
320  
321  // TODO: set `stdout: 'ignore'` when that option is implemented
322  module.exports.stderr = (...args) => module.exports(...args).then(x => x.stderr);
323  
324  module.exports.shell = (cmd, opts) => handleShell(module.exports, cmd, opts);
325  
326  module.exports.sync = (cmd, args, opts) => {
327  	const parsed = handleArgs(cmd, args, opts);
328  	const joinedCmd = joinCmd(cmd, args);
329  
330  	if (isStream(parsed.opts.input)) {
331  		throw new TypeError('The `input` option cannot be a stream in sync mode');
332  	}
333  
334  	const result = childProcess.spawnSync(parsed.cmd, parsed.args, parsed.opts);
335  	result.code = result.status;
336  
337  	if (result.error || result.status !== 0 || result.signal !== null) {
338  		const err = makeError(result, {
339  			joinedCmd,
340  			parsed
341  		});
342  
343  		if (!parsed.opts.reject) {
344  			return err;
345  		}
346  
347  		throw err;
348  	}
349  
350  	return {
351  		stdout: handleOutput(parsed.opts, result.stdout),
352  		stderr: handleOutput(parsed.opts, result.stderr),
353  		code: 0,
354  		failed: false,
355  		signal: null,
356  		cmd: joinedCmd,
357  		timedOut: false
358  	};
359  };
360  
361  module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);