mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
556ebab30e
In PR [1], a bunch of properties were removed from the error thrown by execSync and execFileSync. It turns out that some of those were still supposed to be there, as the documentation states that the error contains the entire result from the spawnSync call. [1] https://github.com/nodejs/node/pull/13601 PR-URL: https://github.com/nodejs/node/pull/16060 Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
642 lines
17 KiB
JavaScript
642 lines
17 KiB
JavaScript
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
'use strict';
|
|
|
|
const util = require('util');
|
|
const { deprecate, convertToValidSignal } = require('internal/util');
|
|
const { isUint8Array } = require('internal/util/types');
|
|
const { createPromise,
|
|
promiseResolve, promiseReject } = process.binding('util');
|
|
const debug = util.debuglog('child_process');
|
|
const { Buffer } = require('buffer');
|
|
const { Pipe } = process.binding('pipe_wrap');
|
|
const { errname } = process.binding('uv');
|
|
const child_process = require('internal/child_process');
|
|
const {
|
|
_validateStdio,
|
|
setupChannel,
|
|
ChildProcess
|
|
} = child_process;
|
|
|
|
exports.ChildProcess = ChildProcess;
|
|
|
|
function stdioStringToArray(option) {
|
|
switch (option) {
|
|
case 'ignore':
|
|
case 'pipe':
|
|
case 'inherit':
|
|
return [option, option, option, 'ipc'];
|
|
default:
|
|
throw new TypeError('Incorrect value of stdio option: ' + option);
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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 (typeof options.stdio === 'string') {
|
|
options.stdio = stdioStringToArray(options.stdio);
|
|
} else 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 ? stdioStringToArray('pipe') :
|
|
stdioStringToArray('inherit');
|
|
} else if (options.stdio.indexOf('ipc') === -1) {
|
|
throw new TypeError('Forked processes must have an IPC channel');
|
|
}
|
|
|
|
options.execPath = options.execPath || process.execPath;
|
|
options.shell = false;
|
|
|
|
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 onNewListener(name) {
|
|
if (name === 'message' || name === 'disconnect') control.ref();
|
|
});
|
|
process.on('removeListener', function onRemoveListener(name) {
|
|
if (name === 'message' || name === 'disconnect') control.unref();
|
|
});
|
|
};
|
|
|
|
|
|
function normalizeExecArgs(command, options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
// 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);
|
|
};
|
|
|
|
const customPromiseExecFunction = (orig) => {
|
|
return (...args) => {
|
|
const promise = createPromise();
|
|
|
|
orig(...args, (err, stdout, stderr) => {
|
|
if (err !== null) {
|
|
err.stdout = stdout;
|
|
err.stderr = stderr;
|
|
promiseReject(promise, err);
|
|
} else {
|
|
promiseResolve(promise, { stdout, stderr });
|
|
}
|
|
});
|
|
return promise;
|
|
};
|
|
};
|
|
|
|
Object.defineProperty(exports.exec, util.promisify.custom, {
|
|
enumerable: false,
|
|
value: customPromiseExecFunction(exports.exec)
|
|
});
|
|
|
|
exports.execFile = function(file /*, args, options, callback*/) {
|
|
var args = [];
|
|
var 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') {
|
|
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 && pos < arguments.length && arguments[pos] != null) {
|
|
throw new TypeError('Incorrect value of args option');
|
|
}
|
|
|
|
// Validate the timeout, if present.
|
|
validateTimeout(options.timeout);
|
|
|
|
// Validate maxBuffer, if present.
|
|
validateMaxBuffer(options.maxBuffer);
|
|
|
|
options.killSignal = sanitizeKillSignal(options.killSignal);
|
|
|
|
var child = spawn(file, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
gid: options.gid,
|
|
uid: options.uid,
|
|
shell: options.shell,
|
|
windowsHide: !!options.windowsHide,
|
|
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;
|
|
|
|
var cmd = file;
|
|
|
|
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 && code === 0 && signal === null) {
|
|
callback(null, stdout, stderr);
|
|
return;
|
|
}
|
|
|
|
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 ? 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 delayedKill() {
|
|
kill();
|
|
timeoutId = null;
|
|
}, options.timeout);
|
|
}
|
|
|
|
if (child.stdout) {
|
|
if (encoding)
|
|
child.stdout.setEncoding(encoding);
|
|
|
|
child.stdout.on('data', function onChildStdout(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.on('data', function onChildStderr(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;
|
|
};
|
|
|
|
Object.defineProperty(exports.execFile, util.promisify.custom, {
|
|
enumerable: false,
|
|
value: customPromiseExecFunction(exports.execFile)
|
|
});
|
|
|
|
const _deprecatedCustomFds = deprecate(
|
|
function deprecateCustomFds(options) {
|
|
options.stdio = options.customFds.map(function mapCustomFds(fd) {
|
|
return fd === -1 ? 'pipe' : fd;
|
|
});
|
|
}, 'child_process: options.customFds option is deprecated. ' +
|
|
'Use options.stdio instead.', 'DEP0006');
|
|
|
|
function _convertCustomFds(options) {
|
|
if (options.customFds && !options.stdio) {
|
|
_deprecatedCustomFds(options);
|
|
}
|
|
}
|
|
|
|
function normalizeSpawnArguments(file, args, options) {
|
|
if (typeof file !== 'string' || file.length === 0)
|
|
throw new TypeError('"file" argument must be a non-empty string');
|
|
|
|
if (Array.isArray(args)) {
|
|
args = args.slice(0);
|
|
} else if (args !== undefined &&
|
|
(args === null || typeof args !== 'object')) {
|
|
throw new TypeError('Incorrect value of args option');
|
|
} else {
|
|
options = args;
|
|
args = [];
|
|
}
|
|
|
|
if (options === undefined)
|
|
options = {};
|
|
else if (options === null || typeof options !== 'object')
|
|
throw new TypeError('"options" argument must be an object');
|
|
|
|
// Validate the cwd, if present.
|
|
if (options.cwd != null &&
|
|
typeof options.cwd !== 'string') {
|
|
throw new TypeError('"cwd" must be a string');
|
|
}
|
|
|
|
// Validate detached, if present.
|
|
if (options.detached != null &&
|
|
typeof options.detached !== 'boolean') {
|
|
throw new TypeError('"detached" must be a boolean');
|
|
}
|
|
|
|
// Validate the uid, if present.
|
|
if (options.uid != null && !Number.isInteger(options.uid)) {
|
|
throw new TypeError('"uid" must be an integer');
|
|
}
|
|
|
|
// Validate the gid, if present.
|
|
if (options.gid != null && !Number.isInteger(options.gid)) {
|
|
throw new TypeError('"gid" must be an integer');
|
|
}
|
|
|
|
// Validate the shell, if present.
|
|
if (options.shell != null &&
|
|
typeof options.shell !== 'boolean' &&
|
|
typeof options.shell !== 'string') {
|
|
throw new TypeError('"shell" must be a boolean or string');
|
|
}
|
|
|
|
// Validate argv0, if present.
|
|
if (options.argv0 != null &&
|
|
typeof options.argv0 !== 'string') {
|
|
throw new TypeError('"argv0" must be a string');
|
|
}
|
|
|
|
// Validate windowsHide, if present.
|
|
if (options.windowsHide != null &&
|
|
typeof options.windowsHide !== 'boolean') {
|
|
throw new TypeError('"windowsHide" must be a boolean');
|
|
}
|
|
|
|
// Validate windowsVerbatimArguments, if present.
|
|
if (options.windowsVerbatimArguments != null &&
|
|
typeof options.windowsVerbatimArguments !== 'boolean') {
|
|
throw new TypeError('"windowsVerbatimArguments" must be a boolean');
|
|
}
|
|
|
|
// 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') {
|
|
if (typeof options.shell === 'string')
|
|
file = options.shell;
|
|
else
|
|
file = 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,
|
|
windowsHide: !!options.windowsHide,
|
|
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
|
|
detached: !!options.detached,
|
|
envPairs: opts.envPairs,
|
|
stdio: options.stdio,
|
|
uid: options.uid,
|
|
gid: options.gid
|
|
});
|
|
|
|
return child;
|
|
};
|
|
|
|
function spawnSync(/*file, args, options*/) {
|
|
var opts = normalizeSpawnArguments.apply(null, arguments);
|
|
|
|
var options = opts.options;
|
|
|
|
debug('spawnSync', opts.args, options);
|
|
|
|
// Validate the timeout, if present.
|
|
validateTimeout(options.timeout);
|
|
|
|
// Validate maxBuffer, if present.
|
|
validateMaxBuffer(options.maxBuffer);
|
|
|
|
options.file = opts.file;
|
|
options.args = opts.args;
|
|
options.envPairs = opts.envPairs;
|
|
|
|
// Validate and translate the kill signal, if present.
|
|
options.killSignal = sanitizeKillSignal(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 (var 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 (isUint8Array(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, Uint8Array or string not %s',
|
|
i,
|
|
typeof input));
|
|
}
|
|
}
|
|
}
|
|
|
|
return child_process.spawnSync(opts);
|
|
}
|
|
exports.spawnSync = spawnSync;
|
|
|
|
|
|
function checkExecSyncError(ret, args, cmd) {
|
|
var err;
|
|
if (ret.error) {
|
|
err = ret.error;
|
|
} else if (ret.status !== 0) {
|
|
var msg = 'Command failed: ';
|
|
msg += cmd || args.join(' ');
|
|
if (ret.stderr && ret.stderr.length > 0)
|
|
msg += '\n' + ret.stderr.toString();
|
|
err = new Error(msg);
|
|
}
|
|
if (err) {
|
|
Object.assign(err, ret);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
|
|
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, opts.args, undefined);
|
|
|
|
if (err)
|
|
throw err;
|
|
|
|
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);
|
|
|
|
if (inheritStderr && ret.stderr)
|
|
process.stderr.write(ret.stderr);
|
|
|
|
var err = checkExecSyncError(ret, opts.args, command);
|
|
|
|
if (err)
|
|
throw err;
|
|
|
|
return ret.stdout;
|
|
}
|
|
exports.execSync = execSync;
|
|
|
|
|
|
function validateTimeout(timeout) {
|
|
if (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) {
|
|
throw new TypeError('"timeout" must be an unsigned integer');
|
|
}
|
|
}
|
|
|
|
|
|
function validateMaxBuffer(maxBuffer) {
|
|
if (maxBuffer != null && !(typeof maxBuffer === 'number' && maxBuffer >= 0)) {
|
|
throw new TypeError('"maxBuffer" must be a positive number');
|
|
}
|
|
}
|
|
|
|
|
|
function sanitizeKillSignal(killSignal) {
|
|
if (typeof killSignal === 'string' || typeof killSignal === 'number') {
|
|
return convertToValidSignal(killSignal);
|
|
} else if (killSignal != null) {
|
|
throw new TypeError('"killSignal" must be a string or number');
|
|
}
|
|
}
|