| // 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 { Object, ObjectPrototype } = primordials; |
| |
| const { |
| promisify, |
| convertToValidSignal, |
| getSystemErrorName |
| } = require('internal/util'); |
| const { isArrayBufferView } = require('internal/util/types'); |
| const debug = require('internal/util/debuglog').debuglog('child_process'); |
| const { Buffer } = require('buffer'); |
| const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); |
| const { |
| ERR_INVALID_ARG_VALUE, |
| ERR_CHILD_PROCESS_IPC_REQUIRED, |
| ERR_CHILD_PROCESS_STDIO_MAXBUFFER, |
| ERR_INVALID_ARG_TYPE, |
| ERR_INVALID_OPT_VALUE, |
| ERR_OUT_OF_RANGE |
| } = require('internal/errors').codes; |
| const { clearTimeout, setTimeout } = require('timers'); |
| const { validateString, isInt32 } = require('internal/validators'); |
| const child_process = require('internal/child_process'); |
| const { |
| getValidStdio, |
| setupChannel, |
| ChildProcess |
| } = child_process; |
| |
| const MAX_BUFFER = 1024 * 1024; |
| |
| exports.ChildProcess = ChildProcess; |
| |
| function stdioStringToArray(option) { |
| switch (option) { |
| case 'ignore': |
| case 'pipe': |
| case 'inherit': |
| return [option, option, option, 'ipc']; |
| default: |
| throw new ERR_INVALID_OPT_VALUE('stdio', option); |
| } |
| } |
| |
| exports.fork = function fork(modulePath /* , args, options */) { |
| validateString(modulePath, 'modulePath'); |
| |
| // 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] === undefined || arguments[pos] === null)) { |
| pos++; |
| } |
| |
| if (pos < arguments.length && arguments[pos] != null) { |
| if (typeof arguments[pos] !== 'object') { |
| throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]); |
| } |
| |
| options = { ...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.includes('ipc')) { |
| throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio'); |
| } |
| |
| options.execPath = options.execPath || process.execPath; |
| options.shell = false; |
| |
| return spawn(options.execPath, args, options); |
| }; |
| |
| |
| exports._forkChild = function _forkChild(fd) { |
| // set process.send() |
| const p = new Pipe(PipeConstants.IPC); |
| 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 = { ...options }; |
| options.shell = typeof options.shell === 'string' ? options.shell : true; |
| |
| return { |
| file: command, |
| options: options, |
| callback: callback |
| }; |
| } |
| |
| |
| exports.exec = function exec(command, options, callback) { |
| const opts = normalizeExecArgs(command, options, callback); |
| return exports.execFile(opts.file, |
| opts.options, |
| opts.callback); |
| }; |
| |
| const customPromiseExecFunction = (orig) => { |
| return (...args) => { |
| return new Promise((resolve, reject) => { |
| orig(...args, (err, stdout, stderr) => { |
| if (err !== null) { |
| err.stdout = stdout; |
| err.stderr = stderr; |
| reject(err); |
| } else { |
| resolve({ stdout, stderr }); |
| } |
| }); |
| }); |
| }; |
| }; |
| |
| Object.defineProperty(exports.exec, promisify.custom, { |
| enumerable: false, |
| value: customPromiseExecFunction(exports.exec) |
| }); |
| |
| exports.execFile = function execFile(file /* , args, options, callback */) { |
| let args = []; |
| let callback; |
| let options; |
| |
| // Parse the optional positional parameters. |
| let 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 = 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 ERR_INVALID_ARG_VALUE('args', arguments[pos]); |
| } |
| |
| options = { |
| 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, |
| uid: options.uid, |
| shell: options.shell, |
| windowsHide: !!options.windowsHide, |
| windowsVerbatimArguments: !!options.windowsVerbatimArguments |
| }); |
| |
| var encoding; |
| const _stdout = []; |
| const _stderr = []; |
| if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) { |
| encoding = options.encoding; |
| } else { |
| 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 || |
| ( |
| child.stdout && |
| child.stdout._readableState && |
| child.stdout._readableState.encoding |
| )) { |
| stdout = _stdout.join(''); |
| } else { |
| stdout = Buffer.concat(_stdout); |
| } |
| if (encoding || |
| ( |
| child.stderr && |
| child.stderr._readableState && |
| child.stderr._readableState.encoding |
| )) { |
| stderr = _stderr.join(''); |
| } else { |
| stderr = Buffer.concat(_stderr); |
| } |
| |
| if (!ex && code === 0 && signal === null) { |
| callback(null, stdout, stderr); |
| return; |
| } |
| |
| if (args.length !== 0) |
| cmd += ` ${args.join(' ')}`; |
| |
| if (!ex) { |
| // eslint-disable-next-line no-restricted-syntax |
| ex = new Error('Command failed: ' + cmd + '\n' + stderr); |
| ex.killed = child.killed || killed; |
| ex.code = code < 0 ? getSystemErrorName(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) { |
| const encoding = child.stdout._readableState.encoding; |
| const length = encoding ? |
| Buffer.byteLength(chunk, encoding) : |
| chunk.length; |
| stdoutLen += length; |
| |
| if (stdoutLen > options.maxBuffer) { |
| const truncatedLen = options.maxBuffer - (stdoutLen - length); |
| _stdout.push(chunk.slice(0, truncatedLen)); |
| |
| ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout'); |
| kill(); |
| } else { |
| _stdout.push(chunk); |
| } |
| }); |
| } |
| |
| if (child.stderr) { |
| if (encoding) |
| child.stderr.setEncoding(encoding); |
| |
| child.stderr.on('data', function onChildStderr(chunk) { |
| const encoding = child.stderr._readableState.encoding; |
| const length = encoding ? |
| Buffer.byteLength(chunk, encoding) : |
| chunk.length; |
| stderrLen += length; |
| |
| if (stderrLen > options.maxBuffer) { |
| const truncatedLen = options.maxBuffer - (stderrLen - length); |
| _stderr.push(chunk.slice(0, truncatedLen)); |
| |
| ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stderr'); |
| kill(); |
| } else { |
| _stderr.push(chunk); |
| } |
| }); |
| } |
| |
| child.addListener('close', exithandler); |
| child.addListener('error', errorhandler); |
| |
| return child; |
| }; |
| |
| Object.defineProperty(exports.execFile, promisify.custom, { |
| enumerable: false, |
| value: customPromiseExecFunction(exports.execFile) |
| }); |
| |
| function normalizeSpawnArguments(file, args, options) { |
| validateString(file, 'file'); |
| |
| if (file.length === 0) |
| throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty'); |
| |
| if (Array.isArray(args)) { |
| args = args.slice(0); |
| } else if (args == null) { |
| args = []; |
| } else if (typeof args !== 'object') { |
| throw new ERR_INVALID_ARG_TYPE('args', 'object', args); |
| } else { |
| options = args; |
| args = []; |
| } |
| |
| if (options === undefined) |
| options = {}; |
| else if (options === null || typeof options !== 'object') |
| throw new ERR_INVALID_ARG_TYPE('options', 'object', options); |
| |
| // Validate the cwd, if present. |
| if (options.cwd != null && |
| typeof options.cwd !== 'string') { |
| throw new ERR_INVALID_ARG_TYPE('options.cwd', 'string', options.cwd); |
| } |
| |
| // Validate detached, if present. |
| if (options.detached != null && |
| typeof options.detached !== 'boolean') { |
| throw new ERR_INVALID_ARG_TYPE('options.detached', |
| 'boolean', 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 && |
| typeof options.argv0 !== 'string') { |
| throw new ERR_INVALID_ARG_TYPE('options.argv0', 'string', options.argv0); |
| } |
| |
| // Validate windowsHide, if present. |
| if (options.windowsHide != null && |
| typeof options.windowsHide !== 'boolean') { |
| throw new ERR_INVALID_ARG_TYPE('options.windowsHide', |
| 'boolean', options.windowsHide); |
| } |
| |
| // Validate windowsVerbatimArguments, if present. |
| if (options.windowsVerbatimArguments != null && |
| typeof options.windowsVerbatimArguments !== 'boolean') { |
| throw new ERR_INVALID_ARG_TYPE('options.windowsVerbatimArguments', |
| 'boolean', |
| options.windowsVerbatimArguments); |
| } |
| |
| // Make a shallow copy so we don't clobber the user's options object. |
| options = { ...options }; |
| |
| if (options.shell) { |
| const command = [file].concat(args).join(' '); |
| // 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 (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { |
| args = ['/d', '/s', '/c', `"${command}"`]; |
| options.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') { |
| args.unshift(options.argv0); |
| } else { |
| args.unshift(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. |
| if (process.env.NODE_V8_COVERAGE && |
| !ObjectPrototype.hasOwnProperty(options.env || {}, 'NODE_V8_COVERAGE')) { |
| env.NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE; |
| } |
| |
| // Prototype values are intentionally included. |
| for (const key in env) { |
| const value = env[key]; |
| if (value !== undefined) { |
| envPairs.push(`${key}=${value}`); |
| } |
| } |
| |
| return { |
| file: file, |
| args: args, |
| options: options, |
| envPairs: envPairs |
| }; |
| } |
| |
| |
| var spawn = exports.spawn = function spawn(file, args, options) { |
| const opts = normalizeSpawnArguments(file, args, options); |
| const child = new ChildProcess(); |
| |
| options = opts.options; |
| 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) { |
| const opts = normalizeSpawnArguments(file, args, options); |
| |
| const defaults = { |
| maxBuffer: MAX_BUFFER, |
| ...opts.options |
| }; |
| options = opts.options = Object.assign(defaults, 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 = getValidStdio(options.stdio || 'pipe', true).stdio; |
| |
| if (options.input) { |
| var 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 (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] = { ...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(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()}`; |
| // eslint-disable-next-line no-restricted-syntax |
| err = new Error(msg); |
| } |
| if (err) { |
| Object.assign(err, ret); |
| } |
| return err; |
| } |
| |
| |
| function execFileSync(command, args, options) { |
| const opts = normalizeSpawnArguments(command, args, options); |
| const inheritStderr = !opts.options.stdio; |
| |
| const ret = spawnSync(opts.file, opts.args.slice(1), opts.options); |
| |
| if (inheritStderr && ret.stderr) |
| process.stderr.write(ret.stderr); |
| |
| const err = checkExecSyncError(ret, opts.args, undefined); |
| |
| if (err) |
| throw err; |
| |
| return ret.stdout; |
| } |
| exports.execFileSync = execFileSync; |
| |
| |
| 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, 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 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); |
| } |
| } |