UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

986 lines (859 loc) 26.7 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/child_process.js import * as __hoisted_internal_events_abort_listener__ from "nstdlib/lib/internal/events/abort_listener"; import { convertToValidSignal, createDeferredPromise, getSystemErrorName, kEmptyObject, promisify, } from "nstdlib/lib/internal/util"; import { isArrayBufferView } from "nstdlib/lib/internal/util/types"; import { Buffer } from "nstdlib/lib/buffer"; import { Pipe, constants as PipeConstants, } from "nstdlib/stub/binding/pipe_wrap"; import { AbortError, codes as __codes__, genericNodeError, } from "nstdlib/lib/internal/errors"; import { clearTimeout, setTimeout } from "nstdlib/lib/timers"; import { getValidatedPath } from "nstdlib/lib/internal/fs/utils"; import { isInt32, validateAbortSignal, validateArray, validateBoolean, validateFunction, validateObject, validateString, } from "nstdlib/lib/internal/validators"; import * as child_process from "nstdlib/lib/internal/child_process"; // 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. const { ERR_CHILD_PROCESS_IPC_REQUIRED, ERR_CHILD_PROCESS_STDIO_MAXBUFFER, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE, } = __codes__; const { getValidStdio, setupChannel, ChildProcess, stdioStringToArray } = child_process; const MAX_BUFFER = 1024 * 1024; 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" && !Array.isArray(args)) { options = args; args = []; } else { validateArray(args, "args"); } if (options != null) { validateObject(options, "options"); } options = { __proto__: null, ...options, shell: false }; options.execPath = 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 = Array.prototype.lastIndexOf.call(execArgv, process._eval); if (index > 0) { // Remove the -e switch to avoid fork bombing ourselves. execArgv = Array.prototype.slice.call(execArgv); Array.prototype.splice.call(execArgv, index - 1, 2); } } args = [...execArgv, modulePath, ...args]; if (typeof options.stdio === "string") { options.stdio = stdioStringToArray(options.stdio, "ipc"); } 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 = stdioStringToArray( options.silent ? "pipe" : "inherit", "ipc", ); } else if (!Array.prototype.includes.call(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 (...args) => { const { promise, resolve, reject } = createDeferredPromise(); promise.child = orig(...args, (err, stdout, stderr) => { if (err !== null) { err.stdout = stdout; err.stderr = stderr; reject(err); } else { resolve({ stdout, stderr }); } }); return promise; }; }; Object.defineProperty(exec, promisify.custom, { __proto__: null, enumerable: false, value: customPromiseExecFunction(exec), }); function normalizeExecFileArgs(file, args, options, callback) { if (Array.isArray(args)) { args = Array.prototype.slice.call(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; } if (args == null) { args = []; } if (typeof options === "function") { callback = options; } else if (options != null) { validateObject(options, "options"); } if (options == null) { 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 && child.stdout.readableEncoding)) { stdout = Array.prototype.join.call(_stdout, ""); } else { stdout = Buffer.concat(_stdout); } if (encoding || (child.stderr && child.stderr.readableEncoding)) { stderr = Array.prototype.join.call(_stderr, ""); } else { stderr = Buffer.concat(_stderr); } if (!ex && code === 0 && signal === null) { callback(null, stdout, stderr); return; } if (args?.length) cmd += ` ${Array.prototype.join.call(args, " ")}`; if (!ex) { 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) { Array.prototype.push.call(_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); Array.prototype.push.call(_stdout, slice(chunk, 0, truncatedLen)); ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stdout"); kill(); } else { Array.prototype.push.call(_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) { Array.prototype.push.call(_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); Array.prototype.push.call(_stderr, chunk.slice(0, truncatedLen)); ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER("stderr"); kill(); } else { Array.prototype.push.call(_stderr, chunk); } }); } child.addListener("close", exithandler); child.addListener("error", errorhandler); return child; } Object.defineProperty(execFile, promisify.custom, { __proto__: null, enumerable: false, value: customPromiseExecFunction(execFile), }); function copyProcessEnvToEnv(env, name, optionEnv) { if ( process.env[name] && (!optionEnv || !Object.prototype.hasOwnProperty.call(optionEnv, name)) ) { env[name] = process.env[name]; } } 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 (Array.isArray(args)) { args = Array.prototype.slice.call(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 && !isInt32(options.uid)) { throw new ERR_INVALID_ARG_TYPE("options.uid", "int32", options.uid); } // Validate the gid, if present. if (options.gid != null && !isInt32(options.gid)) { throw new ERR_INVALID_ARG_TYPE("options.gid", "int32", 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"); const command = Array.prototype.join.call([file, ...args], " "); // 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 ( RegExp.prototype.exec.call(/^(?:.*\\)?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") { Array.prototype.unshift.call(args, options.argv0); } else { Array.prototype.unshift.call(args, file); } const env = options.env || process.env; const 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); } let envKeys = []; // Prototype values are intentionally included. for (const key in env) { Array.prototype.push.call(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 Set(); envKeys = Array.prototype.filter.call( Array.prototype.sort.call(envKeys), (key) => { const uppercaseKey = String.prototype.toUpperCase.call(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}']`); Array.prototype.push.call(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, }; } 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 */ } 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); child.once("exit", () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }); } if (options.signal) { const signal = options.signal; if (signal.aborted) { process.nextTick(onAbortListener); } else { addAbortListener ??= __hoisted_internal_events_abort_listener__.addAbortListener; const disposable = addAbortListener(signal, onAbortListener); child.once("exit", disposable[Symbol.for("nodejs.dispose")]); } 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; * stderr: Buffer | string; * status: number | null; * signal: string | null; * error: Error; * }} */ function spawnSync(file, args, options) { options = { __proto__: null, maxBuffer: MAX_BUFFER, ...normalizeSpawnArguments(file, args, options), }; { /* debug */ } // 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] && 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; Object.assign(err, ret); } else if (ret.status !== 0) { let msg = "Command failed: "; msg += cmd || Array.prototype.join.call(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} */ 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]; Array.prototype.push.apply(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} */ 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" && String.prototype.includes.call(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 && !(Number.isInteger(timeout) && timeout >= 0)) { throw new ERR_OUT_OF_RANGE("timeout", "an unsigned integer", timeout); } } function validateMaxBuffer(maxBuffer) { if (maxBuffer != null && !(typeof maxBuffer === "number" && maxBuffer >= 0)) { throw new ERR_OUT_OF_RANGE( "options.maxBuffer", "a positive number", maxBuffer, ); } } 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, ); } } export { _forkChild }; export { ChildProcess }; export { exec }; export { execFile }; export { execFileSync }; export { execSync }; export { fork }; export { spawn }; export { spawnSync };