mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
49d1c366d8
checkExecSyncError() creates error objects for execSync() and execFileSync(). If the child process created stderr output, then it is attached to the end of the error message. However, stderr can be an empty Buffer object, which always passes the truthy check, leading to an extra newline in the error message. This commit adds a length check, which will work with both strings and Buffers. PR-URL: https://github.com/nodejs/node/pull/9343 Reviewed-By: Wyatt Preul <wpreul@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
536 lines
13 KiB
JavaScript
536 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const util = require('util');
|
|
const internalUtil = require('internal/util');
|
|
const debug = util.debuglog('child_process');
|
|
const constants = process.binding('constants').os.signals;
|
|
|
|
const uv = process.binding('uv');
|
|
const spawn_sync = process.binding('spawn_sync');
|
|
const Buffer = require('buffer').Buffer;
|
|
const Pipe = process.binding('pipe_wrap').Pipe;
|
|
const child_process = require('internal/child_process');
|
|
|
|
const errnoException = util._errnoException;
|
|
const _validateStdio = child_process._validateStdio;
|
|
const setupChannel = child_process.setupChannel;
|
|
const ChildProcess = exports.ChildProcess = child_process.ChildProcess;
|
|
|
|
exports.fork = function(modulePath /*, args, options*/) {
|
|
|
|
// Get options and args arguments.
|
|
var execArgv;
|
|
var options = {};
|
|
var args = [];
|
|
var pos = 1;
|
|
if (pos < arguments.length && Array.isArray(arguments[pos])) {
|
|
args = arguments[pos++];
|
|
}
|
|
|
|
if (pos < arguments.length && arguments[pos] != null) {
|
|
if (typeof arguments[pos] !== 'object') {
|
|
throw new TypeError('Incorrect value of args option');
|
|
} else {
|
|
options = util._extend({}, arguments[pos++]);
|
|
}
|
|
}
|
|
|
|
// Prepare arguments for fork:
|
|
execArgv = options.execArgv || process.execArgv;
|
|
|
|
if (execArgv === process.execArgv && process._eval != null) {
|
|
const index = execArgv.lastIndexOf(process._eval);
|
|
if (index > 0) {
|
|
// Remove the -e switch to avoid fork bombing ourselves.
|
|
execArgv = execArgv.slice();
|
|
execArgv.splice(index - 1, 2);
|
|
}
|
|
}
|
|
|
|
args = execArgv.concat([modulePath], args);
|
|
|
|
if (!Array.isArray(options.stdio)) {
|
|
// Use a separate fd=3 for the IPC channel. Inherit stdin, stdout,
|
|
// and stderr from the parent if silent isn't set.
|
|
options.stdio = options.silent ? ['pipe', 'pipe', 'pipe', 'ipc'] :
|
|
[0, 1, 2, 'ipc'];
|
|
} else if (options.stdio.indexOf('ipc') === -1) {
|
|
throw new TypeError('Forked processes must have an IPC channel');
|
|
}
|
|
|
|
options.execPath = options.execPath || process.execPath;
|
|
|
|
return spawn(options.execPath, args, options);
|
|
};
|
|
|
|
|
|
exports._forkChild = function(fd) {
|
|
// set process.send()
|
|
var p = new Pipe(true);
|
|
p.open(fd);
|
|
p.unref();
|
|
const control = setupChannel(process, p);
|
|
process.on('newListener', function(name) {
|
|
if (name === 'message' || name === 'disconnect') control.ref();
|
|
});
|
|
process.on('removeListener', function(name) {
|
|
if (name === 'message' || name === 'disconnect') control.unref();
|
|
});
|
|
};
|
|
|
|
|
|
function normalizeExecArgs(command /*, options, callback*/) {
|
|
let options;
|
|
let callback;
|
|
|
|
if (typeof arguments[1] === 'function') {
|
|
options = undefined;
|
|
callback = arguments[1];
|
|
} else {
|
|
options = arguments[1];
|
|
callback = arguments[2];
|
|
}
|
|
|
|
// Make a shallow copy so we don't clobber the user's options object.
|
|
options = Object.assign({}, options);
|
|
options.shell = typeof options.shell === 'string' ? options.shell : true;
|
|
|
|
return {
|
|
file: command,
|
|
options: options,
|
|
callback: callback
|
|
};
|
|
}
|
|
|
|
|
|
exports.exec = function(command /*, options, callback*/) {
|
|
var opts = normalizeExecArgs.apply(null, arguments);
|
|
return exports.execFile(opts.file,
|
|
opts.options,
|
|
opts.callback);
|
|
};
|
|
|
|
|
|
exports.execFile = function(file /*, args, options, callback*/) {
|
|
var args = [], callback;
|
|
var options = {
|
|
encoding: 'utf8',
|
|
timeout: 0,
|
|
maxBuffer: 200 * 1024,
|
|
killSignal: 'SIGTERM',
|
|
cwd: null,
|
|
env: null,
|
|
shell: false
|
|
};
|
|
|
|
// Parse the optional positional parameters.
|
|
var pos = 1;
|
|
if (pos < arguments.length && Array.isArray(arguments[pos])) {
|
|
args = arguments[pos++];
|
|
} else if (pos < arguments.length && arguments[pos] == null) {
|
|
pos++;
|
|
}
|
|
|
|
if (pos < arguments.length && typeof arguments[pos] === 'object') {
|
|
options = util._extend(options, arguments[pos++]);
|
|
} else if (pos < arguments.length && arguments[pos] == null) {
|
|
pos++;
|
|
}
|
|
|
|
if (pos < arguments.length && typeof arguments[pos] === 'function') {
|
|
callback = arguments[pos++];
|
|
}
|
|
|
|
if (!callback && arguments[pos] != null) {
|
|
throw new TypeError('Incorrect value of args option');
|
|
}
|
|
|
|
var child = spawn(file, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
gid: options.gid,
|
|
uid: options.uid,
|
|
shell: options.shell,
|
|
windowsVerbatimArguments: !!options.windowsVerbatimArguments
|
|
});
|
|
|
|
var encoding;
|
|
var _stdout;
|
|
var _stderr;
|
|
if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
|
|
encoding = options.encoding;
|
|
_stdout = '';
|
|
_stderr = '';
|
|
} else {
|
|
_stdout = [];
|
|
_stderr = [];
|
|
encoding = null;
|
|
}
|
|
var stdoutLen = 0;
|
|
var stderrLen = 0;
|
|
var killed = false;
|
|
var exited = false;
|
|
var timeoutId;
|
|
|
|
var ex = null;
|
|
|
|
function exithandler(code, signal) {
|
|
if (exited) return;
|
|
exited = true;
|
|
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
|
|
if (!callback) return;
|
|
|
|
// merge chunks
|
|
var stdout;
|
|
var stderr;
|
|
if (encoding) {
|
|
stdout = _stdout;
|
|
stderr = _stderr;
|
|
} else {
|
|
stdout = Buffer.concat(_stdout);
|
|
stderr = Buffer.concat(_stderr);
|
|
}
|
|
|
|
if (ex) {
|
|
// Will be handled later
|
|
} else if (code === 0 && signal === null) {
|
|
callback(null, stdout, stderr);
|
|
return;
|
|
}
|
|
|
|
var cmd = file;
|
|
if (args.length !== 0)
|
|
cmd += ' ' + args.join(' ');
|
|
|
|
if (!ex) {
|
|
ex = new Error('Command failed: ' + cmd + '\n' + stderr);
|
|
ex.killed = child.killed || killed;
|
|
ex.code = code < 0 ? uv.errname(code) : code;
|
|
ex.signal = signal;
|
|
}
|
|
|
|
ex.cmd = cmd;
|
|
callback(ex, stdout, stderr);
|
|
}
|
|
|
|
function errorhandler(e) {
|
|
ex = e;
|
|
|
|
if (child.stdout)
|
|
child.stdout.destroy();
|
|
|
|
if (child.stderr)
|
|
child.stderr.destroy();
|
|
|
|
exithandler();
|
|
}
|
|
|
|
function kill() {
|
|
if (child.stdout)
|
|
child.stdout.destroy();
|
|
|
|
if (child.stderr)
|
|
child.stderr.destroy();
|
|
|
|
killed = true;
|
|
try {
|
|
child.kill(options.killSignal);
|
|
} catch (e) {
|
|
ex = e;
|
|
exithandler();
|
|
}
|
|
}
|
|
|
|
if (options.timeout > 0) {
|
|
timeoutId = setTimeout(function() {
|
|
kill();
|
|
timeoutId = null;
|
|
}, options.timeout);
|
|
}
|
|
|
|
if (child.stdout) {
|
|
if (encoding)
|
|
child.stdout.setEncoding(encoding);
|
|
|
|
child.stdout.addListener('data', function(chunk) {
|
|
stdoutLen += encoding ? Buffer.byteLength(chunk, encoding) : chunk.length;
|
|
|
|
if (stdoutLen > options.maxBuffer) {
|
|
ex = new Error('stdout maxBuffer exceeded');
|
|
kill();
|
|
} else {
|
|
if (encoding)
|
|
_stdout += chunk;
|
|
else
|
|
_stdout.push(chunk);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (child.stderr) {
|
|
if (encoding)
|
|
child.stderr.setEncoding(encoding);
|
|
|
|
child.stderr.addListener('data', function(chunk) {
|
|
stderrLen += encoding ? Buffer.byteLength(chunk, encoding) : chunk.length;
|
|
|
|
if (stderrLen > options.maxBuffer) {
|
|
ex = new Error('stderr maxBuffer exceeded');
|
|
kill();
|
|
} else {
|
|
if (encoding)
|
|
_stderr += chunk;
|
|
else
|
|
_stderr.push(chunk);
|
|
}
|
|
});
|
|
}
|
|
|
|
child.addListener('close', exithandler);
|
|
child.addListener('error', errorhandler);
|
|
|
|
return child;
|
|
};
|
|
|
|
var _deprecatedCustomFds = internalUtil.deprecate(function(options) {
|
|
options.stdio = options.customFds.map(function(fd) {
|
|
return fd === -1 ? 'pipe' : fd;
|
|
});
|
|
}, 'child_process: options.customFds option is deprecated. ' +
|
|
'Use options.stdio instead.');
|
|
|
|
function _convertCustomFds(options) {
|
|
if (options.customFds && !options.stdio) {
|
|
_deprecatedCustomFds(options);
|
|
}
|
|
}
|
|
|
|
function normalizeSpawnArguments(file /*, args, options*/) {
|
|
var args, options;
|
|
|
|
if (Array.isArray(arguments[1])) {
|
|
args = arguments[1].slice(0);
|
|
options = arguments[2];
|
|
} else if (arguments[1] !== undefined &&
|
|
(arguments[1] === null || typeof arguments[1] !== 'object')) {
|
|
throw new TypeError('Incorrect value of args option');
|
|
} else {
|
|
args = [];
|
|
options = arguments[1];
|
|
}
|
|
|
|
if (options === undefined)
|
|
options = {};
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be an object');
|
|
|
|
// Make a shallow copy so we don't clobber the user's options object.
|
|
options = Object.assign({}, options);
|
|
|
|
if (options.shell) {
|
|
const command = [file].concat(args).join(' ');
|
|
|
|
if (process.platform === 'win32') {
|
|
file = typeof options.shell === 'string' ? options.shell :
|
|
process.env.comspec || 'cmd.exe';
|
|
args = ['/d', '/s', '/c', '"' + command + '"'];
|
|
options.windowsVerbatimArguments = true;
|
|
} else {
|
|
if (typeof options.shell === 'string')
|
|
file = options.shell;
|
|
else if (process.platform === 'android')
|
|
file = '/system/bin/sh';
|
|
else
|
|
file = '/bin/sh';
|
|
args = ['-c', command];
|
|
}
|
|
}
|
|
|
|
if (typeof options.argv0 === 'string') {
|
|
args.unshift(options.argv0);
|
|
} else {
|
|
args.unshift(file);
|
|
}
|
|
|
|
var env = options.env || process.env;
|
|
var envPairs = [];
|
|
|
|
for (var key in env) {
|
|
envPairs.push(key + '=' + env[key]);
|
|
}
|
|
|
|
_convertCustomFds(options);
|
|
|
|
return {
|
|
file: file,
|
|
args: args,
|
|
options: options,
|
|
envPairs: envPairs
|
|
};
|
|
}
|
|
|
|
|
|
var spawn = exports.spawn = function(/*file, args, options*/) {
|
|
var opts = normalizeSpawnArguments.apply(null, arguments);
|
|
var options = opts.options;
|
|
var child = new ChildProcess();
|
|
|
|
debug('spawn', opts.args, options);
|
|
|
|
child.spawn({
|
|
file: opts.file,
|
|
args: opts.args,
|
|
cwd: options.cwd,
|
|
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
|
|
detached: !!options.detached,
|
|
envPairs: opts.envPairs,
|
|
stdio: options.stdio,
|
|
uid: options.uid,
|
|
gid: options.gid
|
|
});
|
|
|
|
return child;
|
|
};
|
|
|
|
|
|
function lookupSignal(signal) {
|
|
if (typeof signal === 'number')
|
|
return signal;
|
|
|
|
if (!(signal in constants))
|
|
throw new Error('Unknown signal: ' + signal);
|
|
|
|
return constants[signal];
|
|
}
|
|
|
|
|
|
function spawnSync(/*file, args, options*/) {
|
|
var opts = normalizeSpawnArguments.apply(null, arguments);
|
|
|
|
var options = opts.options;
|
|
|
|
var i;
|
|
|
|
debug('spawnSync', opts.args, options);
|
|
|
|
options.file = opts.file;
|
|
options.args = opts.args;
|
|
options.envPairs = opts.envPairs;
|
|
|
|
if (options.killSignal)
|
|
options.killSignal = lookupSignal(options.killSignal);
|
|
|
|
options.stdio = _validateStdio(options.stdio || 'pipe', true).stdio;
|
|
|
|
if (options.input) {
|
|
var stdin = options.stdio[0] = util._extend({}, options.stdio[0]);
|
|
stdin.input = options.input;
|
|
}
|
|
|
|
// We may want to pass data in on any given fd, ensure it is a valid buffer
|
|
for (i = 0; i < options.stdio.length; i++) {
|
|
var input = options.stdio[i] && options.stdio[i].input;
|
|
if (input != null) {
|
|
var pipe = options.stdio[i] = util._extend({}, options.stdio[i]);
|
|
if (Buffer.isBuffer(input))
|
|
pipe.input = input;
|
|
else if (typeof input === 'string')
|
|
pipe.input = Buffer.from(input, options.encoding);
|
|
else
|
|
throw new TypeError(util.format(
|
|
'stdio[%d] should be Buffer or string not %s',
|
|
i,
|
|
typeof input));
|
|
}
|
|
}
|
|
|
|
var result = spawn_sync.spawn(options);
|
|
|
|
if (result.output && options.encoding && options.encoding !== 'buffer') {
|
|
for (i = 0; i < result.output.length; i++) {
|
|
if (!result.output[i])
|
|
continue;
|
|
result.output[i] = result.output[i].toString(options.encoding);
|
|
}
|
|
}
|
|
|
|
result.stdout = result.output && result.output[1];
|
|
result.stderr = result.output && result.output[2];
|
|
|
|
if (result.error) {
|
|
result.error = errnoException(result.error, 'spawnSync ' + opts.file);
|
|
result.error.path = opts.file;
|
|
result.error.spawnargs = opts.args.slice(1);
|
|
}
|
|
|
|
util._extend(result, opts);
|
|
|
|
return result;
|
|
}
|
|
exports.spawnSync = spawnSync;
|
|
|
|
|
|
function checkExecSyncError(ret) {
|
|
if (ret.error || ret.status !== 0) {
|
|
var err = ret.error;
|
|
ret.error = null;
|
|
|
|
if (!err) {
|
|
var msg = 'Command failed: ';
|
|
msg += ret.cmd || ret.args.join(' ');
|
|
if (ret.stderr && ret.stderr.length > 0)
|
|
msg += '\n' + ret.stderr.toString();
|
|
err = new Error(msg);
|
|
}
|
|
|
|
util._extend(err, ret);
|
|
return err;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
function execFileSync(/*command, args, options*/) {
|
|
var opts = normalizeSpawnArguments.apply(null, arguments);
|
|
var inheritStderr = !opts.options.stdio;
|
|
|
|
var ret = spawnSync(opts.file, opts.args.slice(1), opts.options);
|
|
|
|
if (inheritStderr && ret.stderr)
|
|
process.stderr.write(ret.stderr);
|
|
|
|
var err = checkExecSyncError(ret);
|
|
|
|
if (err)
|
|
throw err;
|
|
else
|
|
return ret.stdout;
|
|
}
|
|
exports.execFileSync = execFileSync;
|
|
|
|
|
|
function execSync(command /*, options*/) {
|
|
var opts = normalizeExecArgs.apply(null, arguments);
|
|
var inheritStderr = !opts.options.stdio;
|
|
|
|
var ret = spawnSync(opts.file, opts.options);
|
|
ret.cmd = command;
|
|
|
|
if (inheritStderr && ret.stderr)
|
|
process.stderr.write(ret.stderr);
|
|
|
|
var err = checkExecSyncError(ret);
|
|
|
|
if (err)
|
|
throw err;
|
|
else
|
|
return ret.stdout;
|
|
}
|
|
exports.execSync = execSync;
|