Skip to content
Permalink
1cdde3eb41
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
0 contributors

Users who have contributed to this file

361 lines (292 sloc) 7.5 KB
'use strict';
const path = require('path');
const childProcess = require('child_process');
const crossSpawn = require('cross-spawn');
const stripEof = require('strip-eof');
const npmRunPath = require('npm-run-path');
const isStream = require('is-stream');
const _getStream = require('get-stream');
const pFinally = require('p-finally');
const onExit = require('signal-exit');
const errname = require('./lib/errname');
const stdio = require('./lib/stdio');
const TEN_MEGABYTES = 1000 * 1000 * 10;
function handleArgs(cmd, args, opts) {
let parsed;
opts = Object.assign({
extendEnv: true,
env: {}
}, opts);
if (opts.extendEnv) {
opts.env = Object.assign({}, process.env, opts.env);
}
if (opts.__winShell === true) {
delete opts.__winShell;
parsed = {
command: cmd,
args,
options: opts,
file: cmd,
original: {
cmd,
args
}
};
} else {
parsed = crossSpawn._parse(cmd, args, opts);
}
opts = Object.assign({
maxBuffer: TEN_MEGABYTES,
buffer: true,
stripEof: true,
preferLocal: true,
localDir: parsed.options.cwd || process.cwd(),
encoding: 'utf8',
reject: true,
cleanup: true
}, parsed.options);
opts.stdio = stdio(opts);
if (opts.preferLocal) {
opts.env = npmRunPath.env(Object.assign({}, opts, {cwd: opts.localDir}));
}
if (opts.detached) {
// #115
opts.cleanup = false;
}
if (process.platform === 'win32' && path.basename(parsed.command) === 'cmd.exe') {
// #116
parsed.args.unshift('/q');
}
return {
cmd: parsed.command,
args: parsed.args,
opts,
parsed
};
}
function handleInput(spawned, input) {
if (input === null || input === undefined) {
return;
}
if (isStream(input)) {
input.pipe(spawned.stdin);
} else {
spawned.stdin.end(input);
}
}
function handleOutput(opts, val) {
if (val && opts.stripEof) {
val = stripEof(val);
}
return val;
}
function handleShell(fn, cmd, opts) {
let file = '/bin/sh';
let args = ['-c', cmd];
opts = Object.assign({}, opts);
if (process.platform === 'win32') {
opts.__winShell = true;
file = process.env.comspec || 'cmd.exe';
args = ['/s', '/c', `"${cmd}"`];
opts.windowsVerbatimArguments = true;
}
if (opts.shell) {
file = opts.shell;
delete opts.shell;
}
return fn(file, args, opts);
}
function getStream(process, stream, {encoding, buffer, maxBuffer}) {
if (!process[stream]) {
return null;
}
let ret;
if (!buffer) {
// TODO: Use `ret = util.promisify(stream.finished)(process[stream]);` when targeting Node.js 10
ret = new Promise((resolve, reject) => {
process[stream]
.once('end', resolve)
.once('error', reject);
});
} else if (encoding) {
ret = _getStream(process[stream], {
encoding,
maxBuffer
});
} else {
ret = _getStream.buffer(process[stream], {maxBuffer});
}
return ret.catch(err => {
err.stream = stream;
err.message = `${stream} ${err.message}`;
throw err;
});
}
function makeError(result, options) {
const {stdout, stderr} = result;
let err = result.error;
const {code, signal} = result;
const {parsed, joinedCmd} = options;
const timedOut = options.timedOut || false;
if (!err) {
let output = '';
if (Array.isArray(parsed.opts.stdio)) {
if (parsed.opts.stdio[2] !== 'inherit') {
output += output.length > 0 ? stderr : `\n${stderr}`;
}
if (parsed.opts.stdio[1] !== 'inherit') {
output += `\n${stdout}`;
}
} else if (parsed.opts.stdio !== 'inherit') {
output = `\n${stderr}${stdout}`;
}
err = new Error(`Command failed: ${joinedCmd}${output}`);
err.code = code < 0 ? errname(code) : code;
}
err.stdout = stdout;
err.stderr = stderr;
err.failed = true;
err.signal = signal || null;
err.cmd = joinedCmd;
err.timedOut = timedOut;
return err;
}
function joinCmd(cmd, args) {
let joinedCmd = cmd;
if (Array.isArray(args) && args.length > 0) {
joinedCmd += ' ' + args.join(' ');
}
return joinedCmd;
}
module.exports = (cmd, args, opts) => {
const parsed = handleArgs(cmd, args, opts);
const {encoding, buffer, maxBuffer} = parsed.opts;
const joinedCmd = joinCmd(cmd, args);
let spawned;
try {
spawned = childProcess.spawn(parsed.cmd, parsed.args, parsed.opts);
} catch (err) {
return Promise.reject(err);
}
let removeExitHandler;
if (parsed.opts.cleanup) {
removeExitHandler = onExit(() => {
spawned.kill();
});
}
let timeoutId = null;
let timedOut = false;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (removeExitHandler) {
removeExitHandler();
}
};
if (parsed.opts.timeout > 0) {
timeoutId = setTimeout(() => {
timeoutId = null;
timedOut = true;
spawned.kill(parsed.opts.killSignal);
}, parsed.opts.timeout);
}
const processDone = new Promise(resolve => {
spawned.on('exit', (code, signal) => {
cleanup();
resolve({code, signal});
});
spawned.on('error', err => {
cleanup();
resolve({error: err});
});
if (spawned.stdin) {
spawned.stdin.on('error', err => {
cleanup();
resolve({error: err});
});
}
});
function destroy() {
if (spawned.stdout) {
spawned.stdout.destroy();
}
if (spawned.stderr) {
spawned.stderr.destroy();
}
}
const handlePromise = () => pFinally(Promise.all([
processDone,
getStream(spawned, 'stdout', {encoding, buffer, maxBuffer}),
getStream(spawned, 'stderr', {encoding, buffer, maxBuffer})
]).then(arr => {
const result = arr[0];
result.stdout = arr[1];
result.stderr = arr[2];
if (result.error || result.code !== 0 || result.signal !== null) {
const err = makeError(result, {
joinedCmd,
parsed,
timedOut
});
// TODO: missing some timeout logic for killed
// https://github.com/nodejs/node/blob/master/lib/child_process.js#L203
// err.killed = spawned.killed || killed;
err.killed = err.killed || spawned.killed;
if (!parsed.opts.reject) {
return err;
}
throw err;
}
return {
stdout: handleOutput(parsed.opts, result.stdout),
stderr: handleOutput(parsed.opts, result.stderr),
code: 0,
failed: false,
killed: false,
signal: null,
cmd: joinedCmd,
timedOut: false
};
}), destroy);
crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed);
handleInput(spawned, parsed.opts.input);
spawned.then = (onfulfilled, onrejected) => handlePromise().then(onfulfilled, onrejected);
spawned.catch = onrejected => handlePromise().catch(onrejected);
return spawned;
};
// TODO: set `stderr: 'ignore'` when that option is implemented
module.exports.stdout = (...args) => module.exports(...args).then(x => x.stdout);
// TODO: set `stdout: 'ignore'` when that option is implemented
module.exports.stderr = (...args) => module.exports(...args).then(x => x.stderr);
module.exports.shell = (cmd, opts) => handleShell(module.exports, cmd, opts);
module.exports.sync = (cmd, args, opts) => {
const parsed = handleArgs(cmd, args, opts);
const joinedCmd = joinCmd(cmd, args);
if (isStream(parsed.opts.input)) {
throw new TypeError('The `input` option cannot be a stream in sync mode');
}
const result = childProcess.spawnSync(parsed.cmd, parsed.args, parsed.opts);
result.code = result.status;
if (result.error || result.status !== 0 || result.signal !== null) {
const err = makeError(result, {
joinedCmd,
parsed
});
if (!parsed.opts.reject) {
return err;
}
throw err;
}
return {
stdout: handleOutput(parsed.opts, result.stdout),
stderr: handleOutput(parsed.opts, result.stderr),
code: 0,
failed: false,
signal: null,
cmd: joinedCmd,
timedOut: false
};
};
module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);