mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: https://github.com/nodejs/node/pull/66559 Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
1152 lines
31 KiB
JavaScript
1152 lines
31 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 {
|
|
ArrayIsArray,
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeIncludes,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeLastIndexOf,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypePushApply,
|
|
ArrayPrototypeSlice,
|
|
ArrayPrototypeSort,
|
|
ArrayPrototypeSplice,
|
|
ArrayPrototypeUnshift,
|
|
ObjectAssign,
|
|
ObjectDefineProperty,
|
|
ObjectPrototypeHasOwnProperty,
|
|
PromiseWithResolvers,
|
|
RegExpPrototypeExec,
|
|
RegExpPrototypeSymbolReplace,
|
|
SafeSet,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeIndexOf,
|
|
StringPrototypeSlice,
|
|
StringPrototypeToUpperCase,
|
|
SymbolDispose,
|
|
} = primordials;
|
|
|
|
const {
|
|
assignFunctionName,
|
|
convertToValidSignal,
|
|
getSystemErrorName,
|
|
kEmptyObject,
|
|
promisify,
|
|
} = require('internal/util');
|
|
const { isArrayBufferView } = require('internal/util/types');
|
|
let debug = require('internal/util/debuglog').debuglog(
|
|
'child_process',
|
|
(fn) => {
|
|
debug = fn;
|
|
},
|
|
);
|
|
const { Buffer } = require('buffer');
|
|
const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap');
|
|
|
|
const {
|
|
AbortError,
|
|
codes: {
|
|
ERR_CHILD_PROCESS_IPC_REQUIRED,
|
|
ERR_CHILD_PROCESS_STDIO_MAXBUFFER,
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
},
|
|
genericNodeError,
|
|
} = require('internal/errors');
|
|
const { clearTimeout, setTimeout } = require('timers');
|
|
const { getValidatedPath } = require('internal/fs/utils');
|
|
const { getOptionValue } = require('internal/options');
|
|
const {
|
|
validateAbortSignal,
|
|
validateArray,
|
|
validateBoolean,
|
|
validateFunction,
|
|
validateInteger,
|
|
validateInt32,
|
|
validateNumber,
|
|
validateObject,
|
|
validateString,
|
|
} = require('internal/validators');
|
|
const child_process = require('internal/child_process');
|
|
const {
|
|
getValidStdio,
|
|
setupChannel,
|
|
ChildProcess,
|
|
stdioStringToArray,
|
|
} = child_process;
|
|
|
|
const { getEnvPairs } = internalBinding('process_wrap');
|
|
|
|
const MAX_BUFFER = 1024 * 1024;
|
|
|
|
const permission = require('internal/process/permission');
|
|
|
|
const isZOS = process.platform === 'os390';
|
|
let addAbortListener;
|
|
|
|
/**
|
|
* Spawns a new Node.js process + fork.
|
|
* @param {string|URL} modulePath
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* detached?: boolean;
|
|
* env?: Record<string, string>;
|
|
* execPath?: string;
|
|
* execArgv?: string[];
|
|
* gid?: number;
|
|
* serialization?: string;
|
|
* signal?: AbortSignal;
|
|
* killSignal?: string | number;
|
|
* silent?: boolean;
|
|
* stdio?: Array | string;
|
|
* uid?: number;
|
|
* windowsVerbatimArguments?: boolean;
|
|
* timeout?: number;
|
|
* }} [options]
|
|
* @returns {ChildProcess}
|
|
*/
|
|
function fork(modulePath, args = [], options) {
|
|
modulePath = getValidatedPath(modulePath, 'modulePath');
|
|
|
|
// Get options and args arguments.
|
|
let execArgv;
|
|
|
|
if (args == null) {
|
|
args = [];
|
|
} else if (typeof args === 'object' && !ArrayIsArray(args)) {
|
|
options = args;
|
|
args = [];
|
|
} else {
|
|
validateArray(args, 'args');
|
|
}
|
|
|
|
if (options != null) {
|
|
validateObject(options, 'options');
|
|
}
|
|
options = { __proto__: null, ...options, shell: false };
|
|
options.execPath ||= process.execPath;
|
|
validateArgumentNullCheck(options.execPath, 'options.execPath');
|
|
|
|
// Prepare arguments for fork:
|
|
execArgv = options.execArgv || process.execArgv;
|
|
validateArgumentsNullCheck(execArgv, 'options.execArgv');
|
|
|
|
if (execArgv === process.execArgv && process._eval != null) {
|
|
const index = ArrayPrototypeLastIndexOf(execArgv, process._eval);
|
|
if (index > 0) {
|
|
// Remove the -e switch to avoid fork bombing ourselves.
|
|
execArgv = ArrayPrototypeSlice(execArgv);
|
|
ArrayPrototypeSplice(execArgv, index - 1, 2);
|
|
}
|
|
}
|
|
|
|
args = [...execArgv, modulePath, ...args];
|
|
|
|
if (typeof options.stdio === 'string') {
|
|
options.stdio = stdioStringToArray(options.stdio, 'ipc');
|
|
} else if (!ArrayIsArray(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 = stdioStringToArray(
|
|
options.silent ? 'pipe' : 'inherit',
|
|
'ipc');
|
|
} else if (!ArrayPrototypeIncludes(options.stdio, 'ipc')) {
|
|
throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio');
|
|
}
|
|
|
|
return spawn(options.execPath, args, options);
|
|
}
|
|
|
|
function _forkChild(fd, serializationMode) {
|
|
// set process.send()
|
|
const p = new Pipe(PipeConstants.IPC);
|
|
p.open(fd);
|
|
p.unref();
|
|
const control = setupChannel(process, p, serializationMode);
|
|
process.on('newListener', function onNewListener(name) {
|
|
if (name === 'message' || name === 'disconnect') control.refCounted();
|
|
});
|
|
process.on('removeListener', function onRemoveListener(name) {
|
|
if (name === 'message' || name === 'disconnect') control.unrefCounted();
|
|
});
|
|
}
|
|
|
|
function normalizeExecArgs(command, options, callback) {
|
|
validateString(command, 'command');
|
|
validateArgumentNullCheck(command, 'command');
|
|
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
// Make a shallow copy so we don't clobber the user's options object.
|
|
options = { __proto__: null, ...options };
|
|
options.shell = typeof options.shell === 'string' ? options.shell : true;
|
|
|
|
return {
|
|
file: command,
|
|
options: options,
|
|
callback: callback,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Spawns a shell executing the given command.
|
|
* @param {string} command
|
|
* @param {{
|
|
* cmd?: string;
|
|
* env?: Record<string, string>;
|
|
* encoding?: string;
|
|
* shell?: string;
|
|
* signal?: AbortSignal;
|
|
* timeout?: number;
|
|
* maxBuffer?: number;
|
|
* killSignal?: string | number;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* windowsHide?: boolean;
|
|
* }} [options]
|
|
* @param {(
|
|
* error?: Error,
|
|
* stdout?: string | Buffer,
|
|
* stderr?: string | Buffer
|
|
* ) => any} [callback]
|
|
* @returns {ChildProcess}
|
|
*/
|
|
function exec(command, options, callback) {
|
|
const opts = normalizeExecArgs(command, options, callback);
|
|
return module.exports.execFile(opts.file,
|
|
opts.options,
|
|
opts.callback);
|
|
}
|
|
|
|
const customPromiseExecFunction = (orig) => {
|
|
return assignFunctionName(orig.name, function(...args) {
|
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
|
|
promise.child = orig(...args, (err, stdout, stderr) => {
|
|
if (err !== null) {
|
|
err.stdout = stdout;
|
|
err.stderr = stderr;
|
|
reject(err);
|
|
} else {
|
|
resolve({ stdout, stderr });
|
|
}
|
|
});
|
|
|
|
return promise;
|
|
});
|
|
};
|
|
|
|
ObjectDefineProperty(exec, promisify.custom, {
|
|
__proto__: null,
|
|
enumerable: false,
|
|
value: customPromiseExecFunction(exec),
|
|
});
|
|
|
|
function normalizeExecFileArgs(file, args, options, callback) {
|
|
if (ArrayIsArray(args)) {
|
|
args = ArrayPrototypeSlice(args);
|
|
} else if (args != null && typeof args === 'object') {
|
|
callback = options;
|
|
options = args;
|
|
args = null;
|
|
} else if (typeof args === 'function') {
|
|
callback = args;
|
|
options = null;
|
|
args = null;
|
|
}
|
|
|
|
args ??= [];
|
|
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
} else if (options != null) {
|
|
validateObject(options, 'options');
|
|
}
|
|
|
|
options ??= kEmptyObject;
|
|
|
|
if (callback != null) {
|
|
validateFunction(callback, 'callback');
|
|
}
|
|
|
|
// Validate argv0, if present.
|
|
if (options.argv0 != null) {
|
|
validateString(options.argv0, 'options.argv0');
|
|
validateArgumentNullCheck(options.argv0, 'options.argv0');
|
|
}
|
|
|
|
return { file, args, options, callback };
|
|
}
|
|
|
|
/**
|
|
* Spawns the specified file as a shell.
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* env?: Record<string, string>;
|
|
* encoding?: string;
|
|
* timeout?: number;
|
|
* maxBuffer?: number;
|
|
* killSignal?: string | number;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* windowsHide?: boolean;
|
|
* windowsVerbatimArguments?: boolean;
|
|
* shell?: boolean | string;
|
|
* signal?: AbortSignal;
|
|
* }} [options]
|
|
* @param {(
|
|
* error?: Error,
|
|
* stdout?: string | Buffer,
|
|
* stderr?: string | Buffer
|
|
* ) => any} [callback]
|
|
* @returns {ChildProcess}
|
|
*/
|
|
function execFile(file, args, options, callback) {
|
|
({ file, args, options, callback } = normalizeExecFileArgs(file, args, options, callback));
|
|
|
|
options = {
|
|
__proto__: null,
|
|
encoding: 'utf8',
|
|
timeout: 0,
|
|
maxBuffer: MAX_BUFFER,
|
|
killSignal: 'SIGTERM',
|
|
cwd: null,
|
|
env: null,
|
|
shell: false,
|
|
...options,
|
|
};
|
|
|
|
// Validate the timeout, if present.
|
|
validateTimeout(options.timeout);
|
|
|
|
// Validate maxBuffer, if present.
|
|
validateMaxBuffer(options.maxBuffer);
|
|
|
|
options.killSignal = sanitizeKillSignal(options.killSignal);
|
|
|
|
const child = spawn(file, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
gid: options.gid,
|
|
shell: options.shell,
|
|
signal: options.signal,
|
|
uid: options.uid,
|
|
windowsHide: !!options.windowsHide,
|
|
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
|
|
});
|
|
|
|
let encoding;
|
|
const _stdout = [];
|
|
const _stderr = [];
|
|
if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
|
|
encoding = options.encoding;
|
|
} else {
|
|
encoding = null;
|
|
}
|
|
let stdoutLen = 0;
|
|
let stderrLen = 0;
|
|
let killed = false;
|
|
let exited = false;
|
|
let timeoutId;
|
|
|
|
let ex = null;
|
|
|
|
let cmd = file;
|
|
|
|
function exithandler(code, signal) {
|
|
if (exited) return;
|
|
exited = true;
|
|
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
|
|
if (!callback) return;
|
|
|
|
// merge chunks
|
|
let stdout;
|
|
let stderr;
|
|
if (encoding ||
|
|
(
|
|
child.stdout?.readableEncoding
|
|
)) {
|
|
stdout = ArrayPrototypeJoin(_stdout, '');
|
|
} else {
|
|
stdout = Buffer.concat(_stdout);
|
|
}
|
|
if (encoding ||
|
|
(
|
|
child.stderr?.readableEncoding
|
|
)) {
|
|
stderr = ArrayPrototypeJoin(_stderr, '');
|
|
} else {
|
|
stderr = Buffer.concat(_stderr);
|
|
}
|
|
|
|
if (!ex && code === 0 && signal === null) {
|
|
callback(null, stdout, stderr);
|
|
return;
|
|
}
|
|
|
|
if (args?.length)
|
|
cmd += ` ${ArrayPrototypeJoin(args, ' ')}`;
|
|
|
|
ex ||= genericNodeError(`Command failed: ${cmd}\n${stderr}`, {
|
|
code: code < 0 ? getSystemErrorName(code) : code,
|
|
killed: child.killed || killed,
|
|
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) {
|
|
// Do not need to count the length
|
|
if (options.maxBuffer === Infinity) {
|
|
ArrayPrototypePush(_stdout, chunk);
|
|
return;
|
|
}
|
|
const encoding = child.stdout.readableEncoding;
|
|
const length = encoding ?
|
|
Buffer.byteLength(chunk, encoding) :
|
|
chunk.length;
|
|
const slice = encoding ? StringPrototypeSlice :
|
|
(buf, ...args) => buf.slice(...args);
|
|
stdoutLen += length;
|
|
|
|
if (stdoutLen > options.maxBuffer) {
|
|
const truncatedLen = options.maxBuffer - (stdoutLen - length);
|
|
ArrayPrototypePush(_stdout, slice(chunk, 0, truncatedLen));
|
|
|
|
ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout');
|
|
kill();
|
|
} else {
|
|
ArrayPrototypePush(_stdout, chunk);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (child.stderr) {
|
|
if (encoding)
|
|
child.stderr.setEncoding(encoding);
|
|
|
|
child.stderr.on('data', function onChildStderr(chunk) {
|
|
// Do not need to count the length
|
|
if (options.maxBuffer === Infinity) {
|
|
ArrayPrototypePush(_stderr, chunk);
|
|
return;
|
|
}
|
|
const encoding = child.stderr.readableEncoding;
|
|
const length = encoding ?
|
|
Buffer.byteLength(chunk, encoding) :
|
|
chunk.length;
|
|
stderrLen += length;
|
|
|
|
if (stderrLen > options.maxBuffer) {
|
|
const truncatedLen = options.maxBuffer - (stderrLen - length);
|
|
ArrayPrototypePush(_stderr,
|
|
chunk.slice(0, truncatedLen));
|
|
|
|
ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stderr');
|
|
kill();
|
|
} else {
|
|
ArrayPrototypePush(_stderr, chunk);
|
|
}
|
|
});
|
|
}
|
|
|
|
child.addListener('close', exithandler);
|
|
child.addListener('error', errorhandler);
|
|
|
|
return child;
|
|
}
|
|
|
|
ObjectDefineProperty(execFile, promisify.custom, {
|
|
__proto__: null,
|
|
enumerable: false,
|
|
value: customPromiseExecFunction(execFile),
|
|
});
|
|
|
|
function copyProcessEnvToEnv(env, name, optionEnv) {
|
|
if (process.env[name] &&
|
|
(!optionEnv ||
|
|
!ObjectPrototypeHasOwnProperty(optionEnv, name))) {
|
|
env[name] = process.env[name];
|
|
}
|
|
}
|
|
|
|
let permissionModelFlagsToCopy;
|
|
|
|
function getPermissionModelFlagsToCopy() {
|
|
if (permissionModelFlagsToCopy === undefined) {
|
|
permissionModelFlagsToCopy = [...permission.availableFlags(), '--permission'];
|
|
}
|
|
return permissionModelFlagsToCopy;
|
|
}
|
|
|
|
function hasPermissionFlagInEnv(nodeOptions) {
|
|
// Parse NODE_OPTIONS into individual tokens and check if any token
|
|
// is an actual --permission or --permission-audit flag. We use exact
|
|
// token matching rather than substring matching to avoid false positives
|
|
// when unrelated option values contain '--permission' (e.g.,
|
|
// --title=--permission).
|
|
if (!nodeOptions) return false;
|
|
const tokens = nodeOptions.split(/\s+/);
|
|
return tokens.some((token) =>
|
|
token === '--permission' ||
|
|
token.startsWith('--permission=') ||
|
|
token === '--permission-audit' ||
|
|
token.startsWith('--permission-audit='),
|
|
);
|
|
}
|
|
|
|
function copyPermissionModelFlagsToEnv(env, key, args) {
|
|
// Do not override if permission was already passed to file
|
|
if (args.includes('--permission') || args.includes('--permission-audit') ||
|
|
hasPermissionFlagInEnv(env[key])) {
|
|
return;
|
|
}
|
|
|
|
// Enforcing the permission model removed the variables --allow-env does not
|
|
// grant access to at startup, so everything left in the environment a child
|
|
// inherits is accessible. A child can never see more than its parent had.
|
|
const allowAllEnv = !permission.isAuditMode();
|
|
|
|
const flagsToCopy = getPermissionModelFlagsToCopy();
|
|
const copiedFlags = new SafeSet();
|
|
for (const arg of process.execArgv) {
|
|
if (allowAllEnv && arg.startsWith('--allow-env')) {
|
|
continue;
|
|
}
|
|
for (const flag of flagsToCopy) {
|
|
if (arg.startsWith(flag)) {
|
|
copiedFlags.add(flag);
|
|
env[key] = `${env[key] ? env[key] + ' ' + arg : arg}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flags set through --config-file are not part of process.execArgv.
|
|
for (const flag of flagsToCopy) {
|
|
if (copiedFlags.has(flag) || (allowAllEnv && flag === '--allow-env')) {
|
|
continue;
|
|
}
|
|
const value = getOptionValue(flag);
|
|
if (value === true) {
|
|
env[key] = `${env[key] ? env[key] + ' ' : ''}${flag}`;
|
|
} else if (ArrayIsArray(value)) {
|
|
for (const item of value) {
|
|
// Values may contain spaces or quotes (e.g. the implicitly allowed
|
|
// entry point), so quote them as NODE_OPTIONS expects.
|
|
const quoted = RegExpPrototypeSymbolReplace(/["\\]/g, item, '\\$&');
|
|
env[key] = `${env[key] ? env[key] + ' ' : ''}"${flag}=${quoted}"`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allowAllEnv) {
|
|
env[key] = `${env[key] ? env[key] + ' ' : ''}--allow-env=*`;
|
|
}
|
|
}
|
|
|
|
let emittedDEP0190Already = false;
|
|
function normalizeSpawnArguments(file, args, options) {
|
|
validateString(file, 'file');
|
|
validateArgumentNullCheck(file, 'file');
|
|
|
|
if (file.length === 0)
|
|
throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty');
|
|
|
|
if (ArrayIsArray(args)) {
|
|
args = ArrayPrototypeSlice(args);
|
|
} else if (args == null) {
|
|
args = [];
|
|
} else if (typeof args !== 'object') {
|
|
throw new ERR_INVALID_ARG_TYPE('args', 'object', args);
|
|
} else {
|
|
options = args;
|
|
args = [];
|
|
}
|
|
|
|
validateArgumentsNullCheck(args, 'args');
|
|
|
|
if (options === undefined)
|
|
options = kEmptyObject;
|
|
else
|
|
validateObject(options, 'options');
|
|
|
|
options = { __proto__: null, ...options };
|
|
let cwd = options.cwd;
|
|
|
|
// Validate the cwd, if present.
|
|
if (cwd != null) {
|
|
cwd = getValidatedPath(cwd, 'options.cwd');
|
|
}
|
|
|
|
// Validate detached, if present.
|
|
if (options.detached != null) {
|
|
validateBoolean(options.detached, 'options.detached');
|
|
}
|
|
|
|
// Validate the uid, if present.
|
|
if (options.uid != null) {
|
|
validateInt32(options.uid, 'options.uid');
|
|
}
|
|
|
|
// Validate the gid, if present.
|
|
if (options.gid != null) {
|
|
validateInt32(options.gid, 'options.gid');
|
|
}
|
|
|
|
// Validate the shell, if present.
|
|
if (options.shell != null &&
|
|
typeof options.shell !== 'boolean' &&
|
|
typeof options.shell !== 'string') {
|
|
throw new ERR_INVALID_ARG_TYPE('options.shell',
|
|
['boolean', 'string'], options.shell);
|
|
}
|
|
|
|
// Validate argv0, if present.
|
|
if (options.argv0 != null) {
|
|
validateString(options.argv0, 'options.argv0');
|
|
validateArgumentNullCheck(options.argv0, 'options.argv0');
|
|
}
|
|
|
|
// Validate windowsHide, if present.
|
|
if (options.windowsHide != null) {
|
|
validateBoolean(options.windowsHide, 'options.windowsHide');
|
|
}
|
|
|
|
// Validate windowsVerbatimArguments, if present.
|
|
let { windowsVerbatimArguments } = options;
|
|
if (windowsVerbatimArguments != null) {
|
|
validateBoolean(windowsVerbatimArguments,
|
|
'options.windowsVerbatimArguments');
|
|
}
|
|
|
|
if (options.shell) {
|
|
validateArgumentNullCheck(options.shell, 'options.shell');
|
|
if (args.length > 0 && !emittedDEP0190Already) {
|
|
process.emitWarning(
|
|
'Passing args to a child process with shell option true can lead to security ' +
|
|
'vulnerabilities, as the arguments are not escaped, only concatenated.',
|
|
'DeprecationWarning',
|
|
'DEP0190');
|
|
emittedDEP0190Already = true;
|
|
}
|
|
|
|
const command = args.length > 0 ? `${file} ${ArrayPrototypeJoin(args, ' ')}` : file;
|
|
// Set the shell, switches, and commands.
|
|
if (process.platform === 'win32') {
|
|
if (typeof options.shell === 'string')
|
|
file = options.shell;
|
|
else
|
|
file = process.env.comspec || 'cmd.exe';
|
|
// '/d /s /c' is used only for cmd.exe.
|
|
if (RegExpPrototypeExec(/^(?:.*\\)?cmd(?:\.exe)?$/i, file) !== null) {
|
|
args = ['/d', '/s', '/c', `"${command}"`];
|
|
windowsVerbatimArguments = true;
|
|
} else {
|
|
args = ['-c', command];
|
|
}
|
|
} 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') {
|
|
ArrayPrototypeUnshift(args, options.argv0);
|
|
} else {
|
|
ArrayPrototypeUnshift(args, file);
|
|
}
|
|
|
|
let envPairs;
|
|
if (!options.env && !permission.isEnabled()) {
|
|
// Default: the child inherits this process's environment. Take it as
|
|
// 'KEY=value' strings in one native pass over the environment block
|
|
// instead of copying process.env (one interceptor round trip and one
|
|
// getenv() scan per variable). Everything copyProcessEnvToEnv() would
|
|
// propagate below is part of it by definition, and entries of the real
|
|
// environment block cannot contain null bytes.
|
|
envPairs = getEnvPairs();
|
|
if (process.platform === 'win32') {
|
|
envPairs = dedupeWindowsEnvPairs(envPairs);
|
|
}
|
|
} else {
|
|
// Shallow copy to guarantee changes won't impact process.env
|
|
const env = options.env || { ...process.env };
|
|
envPairs = [];
|
|
|
|
// process.env.NODE_V8_COVERAGE always propagates, making it possible to
|
|
// collect coverage for programs that spawn with white-listed environment.
|
|
copyProcessEnvToEnv(env, 'NODE_V8_COVERAGE', options.env);
|
|
|
|
if (isZOS) {
|
|
// The following environment variables must always propagate if set.
|
|
copyProcessEnvToEnv(env, '_BPXK_AUTOCVT', options.env);
|
|
copyProcessEnvToEnv(env, '_CEE_RUNOPTS', options.env);
|
|
copyProcessEnvToEnv(env, '_TAG_REDIR_ERR', options.env);
|
|
copyProcessEnvToEnv(env, '_TAG_REDIR_IN', options.env);
|
|
copyProcessEnvToEnv(env, '_TAG_REDIR_OUT', options.env);
|
|
copyProcessEnvToEnv(env, 'STEPLIB', options.env);
|
|
copyProcessEnvToEnv(env, 'LIBPATH', options.env);
|
|
copyProcessEnvToEnv(env, '_EDC_SIG_DFLT', options.env);
|
|
copyProcessEnvToEnv(env, '_EDC_SUSV3', options.env);
|
|
}
|
|
|
|
if (permission.isEnabled()) {
|
|
copyPermissionModelFlagsToEnv(env, 'NODE_OPTIONS', args);
|
|
}
|
|
|
|
let envKeys = [];
|
|
// Prototype values are intentionally included.
|
|
for (const key in env) {
|
|
ArrayPrototypePush(envKeys, key);
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
// On Windows env keys are case insensitive. Filter out duplicates,
|
|
// keeping only the first one (in lexicographic order)
|
|
const sawKey = new SafeSet();
|
|
envKeys = ArrayPrototypeFilter(
|
|
ArrayPrototypeSort(envKeys),
|
|
(key) => {
|
|
const uppercaseKey = StringPrototypeToUpperCase(key);
|
|
if (sawKey.has(uppercaseKey)) {
|
|
return false;
|
|
}
|
|
sawKey.add(uppercaseKey);
|
|
return true;
|
|
},
|
|
);
|
|
}
|
|
|
|
for (const key of envKeys) {
|
|
const value = env[key];
|
|
if (value !== undefined) {
|
|
validateArgumentNullCheck(key, `options.env['${key}']`);
|
|
validateArgumentNullCheck(value, `options.env['${key}']`);
|
|
ArrayPrototypePush(envPairs, `${key}=${value}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
// Make a shallow copy so we don't clobber the user's options object.
|
|
__proto__: null,
|
|
...options,
|
|
args,
|
|
cwd,
|
|
detached: !!options.detached,
|
|
envPairs,
|
|
file,
|
|
windowsHide: !!options.windowsHide,
|
|
windowsVerbatimArguments: !!windowsVerbatimArguments,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Windows environment variable names are case-insensitive: keep only the
|
|
* first entry for each name in lexicographic order of the names, exactly like
|
|
* the key filtering applied to a user-supplied `options.env`.
|
|
* @param {string[]} envPairs 'KEY=value' strings
|
|
* @returns {string[]}
|
|
*/
|
|
function dedupeWindowsEnvPairs(envPairs) {
|
|
const keyed = [];
|
|
for (let i = 0; i < envPairs.length; i++) {
|
|
const pair = envPairs[i];
|
|
// Names never start with '=' here (hidden variables are not enumerated),
|
|
// so the first '=' ends the name.
|
|
const eq = StringPrototypeIndexOf(pair, '=');
|
|
ArrayPrototypePush(keyed, { key: eq === -1 ? pair : StringPrototypeSlice(pair, 0, eq), pair });
|
|
}
|
|
ArrayPrototypeSort(keyed, (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
const sawKey = new SafeSet();
|
|
const result = [];
|
|
for (let i = 0; i < keyed.length; i++) {
|
|
const uppercaseKey = StringPrototypeToUpperCase(keyed[i].key);
|
|
if (sawKey.has(uppercaseKey)) continue;
|
|
sawKey.add(uppercaseKey);
|
|
ArrayPrototypePush(result, keyed[i].pair);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function abortChildProcess(child, killSignal, reason) {
|
|
if (!child)
|
|
return;
|
|
try {
|
|
if (child.kill(killSignal)) {
|
|
child.emit('error', new AbortError(undefined, { cause: reason }));
|
|
}
|
|
} catch (err) {
|
|
child.emit('error', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawns a new process using the given `file`.
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* env?: Record<string, string>;
|
|
* argv0?: string;
|
|
* stdio?: Array | string;
|
|
* detached?: boolean;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* serialization?: string;
|
|
* shell?: boolean | string;
|
|
* windowsVerbatimArguments?: boolean;
|
|
* windowsHide?: boolean;
|
|
* signal?: AbortSignal;
|
|
* timeout?: number;
|
|
* killSignal?: string | number;
|
|
* }} [options]
|
|
* @returns {ChildProcess}
|
|
*/
|
|
function spawn(file, args, options) {
|
|
options = normalizeSpawnArguments(file, args, options);
|
|
validateTimeout(options.timeout);
|
|
validateAbortSignal(options.signal, 'options.signal');
|
|
const killSignal = sanitizeKillSignal(options.killSignal);
|
|
const child = new ChildProcess();
|
|
|
|
debug('spawn', options);
|
|
child.spawn(options);
|
|
|
|
if (options.timeout > 0) {
|
|
let timeoutId = setTimeout(() => {
|
|
if (timeoutId) {
|
|
try {
|
|
child.kill(killSignal);
|
|
} catch (err) {
|
|
child.emit('error', err);
|
|
}
|
|
timeoutId = null;
|
|
}
|
|
}, options.timeout);
|
|
|
|
// 'close' always fires after 'exit', or after 'error' on a spawn failure.
|
|
child.once('close', () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (options.signal) {
|
|
const signal = options.signal;
|
|
if (signal.aborted) {
|
|
process.nextTick(onAbortListener);
|
|
} else {
|
|
addAbortListener ??= require('internal/events/abort_listener').addAbortListener;
|
|
const disposable = addAbortListener(signal, onAbortListener);
|
|
child.once('exit', disposable[SymbolDispose]);
|
|
}
|
|
|
|
function onAbortListener() {
|
|
abortChildProcess(child, killSignal, options.signal.reason);
|
|
}
|
|
}
|
|
|
|
return child;
|
|
}
|
|
|
|
/**
|
|
* Spawns a new process synchronously using the given `file`.
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* input?: string | Buffer | TypedArray | DataView;
|
|
* argv0?: string;
|
|
* stdio?: string | Array;
|
|
* env?: Record<string, string>;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* timeout?: number;
|
|
* killSignal?: string | number;
|
|
* maxBuffer?: number;
|
|
* encoding?: string;
|
|
* shell?: boolean | string;
|
|
* windowsVerbatimArguments?: boolean;
|
|
* windowsHide?: boolean;
|
|
* }} [options]
|
|
* @returns {{
|
|
* pid: number;
|
|
* output: Array;
|
|
* stdout: Buffer | string | null;
|
|
* stderr: Buffer | string | null;
|
|
* status: number | null;
|
|
* signal: string | null;
|
|
* error: Error;
|
|
* }}
|
|
*/
|
|
function spawnSync(file, args, options) {
|
|
options = {
|
|
__proto__: null,
|
|
maxBuffer: MAX_BUFFER,
|
|
...normalizeSpawnArguments(file, args, options),
|
|
};
|
|
|
|
debug('spawnSync', options);
|
|
|
|
// Validate the timeout, if present.
|
|
validateTimeout(options.timeout);
|
|
|
|
// Validate maxBuffer, if present.
|
|
validateMaxBuffer(options.maxBuffer);
|
|
|
|
// Validate and translate the kill signal, if present.
|
|
options.killSignal = sanitizeKillSignal(options.killSignal);
|
|
|
|
options.stdio = getValidStdio(options.stdio || 'pipe', true).stdio;
|
|
|
|
if (options.input) {
|
|
const stdin = options.stdio[0] = { ...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 (let i = 0; i < options.stdio.length; i++) {
|
|
const input = options.stdio[i]?.input;
|
|
if (input != null) {
|
|
const pipe = options.stdio[i] = { ...options.stdio[i] };
|
|
if (isArrayBufferView(input)) {
|
|
pipe.input = input;
|
|
} else if (typeof input === 'string') {
|
|
pipe.input = Buffer.from(input, options.encoding);
|
|
} else {
|
|
throw new ERR_INVALID_ARG_TYPE(`options.stdio[${i}]`,
|
|
['Buffer',
|
|
'TypedArray',
|
|
'DataView',
|
|
'string'],
|
|
input);
|
|
}
|
|
}
|
|
}
|
|
|
|
return child_process.spawnSync(options);
|
|
}
|
|
|
|
|
|
function checkExecSyncError(ret, args, cmd) {
|
|
let err;
|
|
if (ret.error) {
|
|
err = ret.error;
|
|
ObjectAssign(err, ret);
|
|
} else if (ret.status !== 0) {
|
|
let msg = 'Command failed: ';
|
|
msg += cmd || ArrayPrototypeJoin(args, ' ');
|
|
if (ret.stderr && ret.stderr.length > 0)
|
|
msg += `\n${ret.stderr.toString()}`;
|
|
err = genericNodeError(msg, ret);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Spawns a file as a shell synchronously.
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* input?: string | Buffer | TypedArray | DataView;
|
|
* stdio?: string | Array;
|
|
* env?: Record<string, string>;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* timeout?: number;
|
|
* killSignal?: string | number;
|
|
* maxBuffer?: number;
|
|
* encoding?: string;
|
|
* windowsHide?: boolean;
|
|
* shell?: boolean | string;
|
|
* }} [options]
|
|
* @returns {Buffer | string | null}
|
|
*/
|
|
function execFileSync(file, args, options) {
|
|
({ file, args, options } = normalizeExecFileArgs(file, args, options));
|
|
|
|
const inheritStderr = !options.stdio;
|
|
const ret = spawnSync(file, args, options);
|
|
|
|
if (inheritStderr && ret.stderr)
|
|
process.stderr.write(ret.stderr);
|
|
|
|
const errArgs = [options.argv0 || file];
|
|
ArrayPrototypePushApply(errArgs, args);
|
|
const err = checkExecSyncError(ret, errArgs);
|
|
|
|
if (err)
|
|
throw err;
|
|
|
|
return ret.stdout;
|
|
}
|
|
|
|
/**
|
|
* Spawns a shell executing the given `command` synchronously.
|
|
* @param {string} command
|
|
* @param {{
|
|
* cwd?: string | URL;
|
|
* input?: string | Buffer | TypedArray | DataView;
|
|
* stdio?: string | Array;
|
|
* env?: Record<string, string>;
|
|
* shell?: string;
|
|
* uid?: number;
|
|
* gid?: number;
|
|
* timeout?: number;
|
|
* killSignal?: string | number;
|
|
* maxBuffer?: number;
|
|
* encoding?: string;
|
|
* windowsHide?: boolean;
|
|
* }} [options]
|
|
* @returns {Buffer | string | null}
|
|
*/
|
|
function execSync(command, options) {
|
|
const opts = normalizeExecArgs(command, options, null);
|
|
const inheritStderr = !opts.options.stdio;
|
|
|
|
const ret = spawnSync(opts.file, opts.options);
|
|
|
|
if (inheritStderr && ret.stderr)
|
|
process.stderr.write(ret.stderr);
|
|
|
|
const err = checkExecSyncError(ret, undefined, command);
|
|
|
|
if (err)
|
|
throw err;
|
|
|
|
return ret.stdout;
|
|
}
|
|
|
|
|
|
function validateArgumentNullCheck(arg, propName) {
|
|
if (typeof arg === 'string' && StringPrototypeIncludes(arg, '\u0000')) {
|
|
throw new ERR_INVALID_ARG_VALUE(propName, arg, 'must be a string without null bytes');
|
|
}
|
|
}
|
|
|
|
|
|
function validateArgumentsNullCheck(args, propName) {
|
|
for (let i = 0; i < args.length; ++i) {
|
|
validateArgumentNullCheck(args[i], `${propName}[${i}]`);
|
|
}
|
|
}
|
|
|
|
|
|
function validateTimeout(timeout) {
|
|
if (timeout != null) {
|
|
validateInteger(timeout, 'timeout', 0);
|
|
}
|
|
}
|
|
|
|
|
|
function validateMaxBuffer(maxBuffer) {
|
|
if (maxBuffer != null) {
|
|
validateNumber(maxBuffer, 'options.maxBuffer', 0);
|
|
}
|
|
}
|
|
|
|
|
|
function sanitizeKillSignal(killSignal) {
|
|
if (typeof killSignal === 'string' || typeof killSignal === 'number') {
|
|
return convertToValidSignal(killSignal);
|
|
} else if (killSignal != null) {
|
|
throw new ERR_INVALID_ARG_TYPE('options.killSignal',
|
|
['string', 'number'],
|
|
killSignal);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
_forkChild,
|
|
ChildProcess,
|
|
exec,
|
|
execFile,
|
|
execFileSync,
|
|
execSync,
|
|
fork,
|
|
spawn,
|
|
spawnSync,
|
|
};
|