1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23 24const { 25 ArrayIsArray, 26 Error, 27 NumberIsInteger, 28 ObjectAssign, 29 ObjectDefineProperty, 30 ObjectPrototypeHasOwnProperty, 31 Promise, 32} = primordials; 33 34const { 35 promisify, 36 convertToValidSignal, 37 getSystemErrorName 38} = require('internal/util'); 39const { isArrayBufferView } = require('internal/util/types'); 40let debug = require('internal/util/debuglog').debuglog( 41 'child_process', 42 (fn) => { 43 debug = fn; 44 } 45); 46const { Buffer } = require('buffer'); 47const { Pipe, constants: PipeConstants } = internalBinding('pipe_wrap'); 48const { 49 ERR_INVALID_ARG_VALUE, 50 ERR_CHILD_PROCESS_IPC_REQUIRED, 51 ERR_CHILD_PROCESS_STDIO_MAXBUFFER, 52 ERR_INVALID_ARG_TYPE, 53 ERR_OUT_OF_RANGE 54} = require('internal/errors').codes; 55const { clearTimeout, setTimeout } = require('timers'); 56const { validateString, isInt32 } = require('internal/validators'); 57const child_process = require('internal/child_process'); 58const { 59 getValidStdio, 60 setupChannel, 61 ChildProcess, 62 stdioStringToArray 63} = child_process; 64 65const MAX_BUFFER = 1024 * 1024; 66 67function fork(modulePath /* , args, options */) { 68 validateString(modulePath, 'modulePath'); 69 70 // Get options and args arguments. 71 let execArgv; 72 let options = {}; 73 let args = []; 74 let pos = 1; 75 if (pos < arguments.length && ArrayIsArray(arguments[pos])) { 76 args = arguments[pos++]; 77 } 78 79 if (pos < arguments.length && 80 (arguments[pos] === undefined || arguments[pos] === null)) { 81 pos++; 82 } 83 84 if (pos < arguments.length && arguments[pos] != null) { 85 if (typeof arguments[pos] !== 'object') { 86 throw new ERR_INVALID_ARG_VALUE(`arguments[${pos}]`, arguments[pos]); 87 } 88 89 options = { ...arguments[pos++] }; 90 } 91 92 // Prepare arguments for fork: 93 execArgv = options.execArgv || process.execArgv; 94 95 if (execArgv === process.execArgv && process._eval != null) { 96 const index = execArgv.lastIndexOf(process._eval); 97 if (index > 0) { 98 // Remove the -e switch to avoid fork bombing ourselves. 99 execArgv = execArgv.slice(); 100 execArgv.splice(index - 1, 2); 101 } 102 } 103 104 args = execArgv.concat([modulePath], args); 105 106 if (typeof options.stdio === 'string') { 107 options.stdio = stdioStringToArray(options.stdio, 'ipc'); 108 } else if (!ArrayIsArray(options.stdio)) { 109 // Use a separate fd=3 for the IPC channel. Inherit stdin, stdout, 110 // and stderr from the parent if silent isn't set. 111 options.stdio = stdioStringToArray( 112 options.silent ? 'pipe' : 'inherit', 113 'ipc'); 114 } else if (!options.stdio.includes('ipc')) { 115 throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio'); 116 } 117 118 options.execPath = options.execPath || process.execPath; 119 options.shell = false; 120 121 return spawn(options.execPath, args, options); 122} 123 124function _forkChild(fd, serializationMode) { 125 // set process.send() 126 const p = new Pipe(PipeConstants.IPC); 127 p.open(fd); 128 p.unref(); 129 const control = setupChannel(process, p, serializationMode); 130 process.on('newListener', function onNewListener(name) { 131 if (name === 'message' || name === 'disconnect') control.ref(); 132 }); 133 process.on('removeListener', function onRemoveListener(name) { 134 if (name === 'message' || name === 'disconnect') control.unref(); 135 }); 136} 137 138function normalizeExecArgs(command, options, callback) { 139 if (typeof options === 'function') { 140 callback = options; 141 options = undefined; 142 } 143 144 // Make a shallow copy so we don't clobber the user's options object. 145 options = { ...options }; 146 options.shell = typeof options.shell === 'string' ? options.shell : true; 147 148 return { 149 file: command, 150 options: options, 151 callback: callback 152 }; 153} 154 155 156function exec(command, options, callback) { 157 const opts = normalizeExecArgs(command, options, callback); 158 return module.exports.execFile(opts.file, 159 opts.options, 160 opts.callback); 161} 162 163const customPromiseExecFunction = (orig) => { 164 return (...args) => { 165 let resolve; 166 let reject; 167 const promise = new Promise((res, rej) => { 168 resolve = res; 169 reject = rej; 170 }); 171 172 promise.child = orig(...args, (err, stdout, stderr) => { 173 if (err !== null) { 174 err.stdout = stdout; 175 err.stderr = stderr; 176 reject(err); 177 } else { 178 resolve({ stdout, stderr }); 179 } 180 }); 181 182 return promise; 183 }; 184}; 185 186ObjectDefineProperty(exec, promisify.custom, { 187 enumerable: false, 188 value: customPromiseExecFunction(exec) 189}); 190 191function execFile(file /* , args, options, callback */) { 192 let args = []; 193 let callback; 194 let options; 195 196 // Parse the optional positional parameters. 197 let pos = 1; 198 if (pos < arguments.length && ArrayIsArray(arguments[pos])) { 199 args = arguments[pos++]; 200 } else if (pos < arguments.length && arguments[pos] == null) { 201 pos++; 202 } 203 204 if (pos < arguments.length && typeof arguments[pos] === 'object') { 205 options = arguments[pos++]; 206 } else if (pos < arguments.length && arguments[pos] == null) { 207 pos++; 208 } 209 210 if (pos < arguments.length && typeof arguments[pos] === 'function') { 211 callback = arguments[pos++]; 212 } 213 214 if (!callback && pos < arguments.length && arguments[pos] != null) { 215 throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]); 216 } 217 218 options = { 219 encoding: 'utf8', 220 timeout: 0, 221 maxBuffer: MAX_BUFFER, 222 killSignal: 'SIGTERM', 223 cwd: null, 224 env: null, 225 shell: false, 226 ...options 227 }; 228 229 // Validate the timeout, if present. 230 validateTimeout(options.timeout); 231 232 // Validate maxBuffer, if present. 233 validateMaxBuffer(options.maxBuffer); 234 235 options.killSignal = sanitizeKillSignal(options.killSignal); 236 237 const child = spawn(file, args, { 238 cwd: options.cwd, 239 env: options.env, 240 gid: options.gid, 241 uid: options.uid, 242 shell: options.shell, 243 windowsHide: !!options.windowsHide, 244 windowsVerbatimArguments: !!options.windowsVerbatimArguments 245 }); 246 247 let encoding; 248 const _stdout = []; 249 const _stderr = []; 250 if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) { 251 encoding = options.encoding; 252 } else { 253 encoding = null; 254 } 255 let stdoutLen = 0; 256 let stderrLen = 0; 257 let killed = false; 258 let exited = false; 259 let timeoutId; 260 261 let ex = null; 262 263 let cmd = file; 264 265 function exithandler(code, signal) { 266 if (exited) return; 267 exited = true; 268 269 if (timeoutId) { 270 clearTimeout(timeoutId); 271 timeoutId = null; 272 } 273 274 if (!callback) return; 275 276 // merge chunks 277 let stdout; 278 let stderr; 279 if (encoding || 280 ( 281 child.stdout && 282 child.stdout.readableEncoding 283 )) { 284 stdout = _stdout.join(''); 285 } else { 286 stdout = Buffer.concat(_stdout); 287 } 288 if (encoding || 289 ( 290 child.stderr && 291 child.stderr.readableEncoding 292 )) { 293 stderr = _stderr.join(''); 294 } else { 295 stderr = Buffer.concat(_stderr); 296 } 297 298 if (!ex && code === 0 && signal === null) { 299 callback(null, stdout, stderr); 300 return; 301 } 302 303 if (args.length !== 0) 304 cmd += ` ${args.join(' ')}`; 305 306 if (!ex) { 307 // eslint-disable-next-line no-restricted-syntax 308 ex = new Error('Command failed: ' + cmd + '\n' + stderr); 309 ex.killed = child.killed || killed; 310 ex.code = code < 0 ? getSystemErrorName(code) : code; 311 ex.signal = signal; 312 } 313 314 ex.cmd = cmd; 315 callback(ex, stdout, stderr); 316 } 317 318 function errorhandler(e) { 319 ex = e; 320 321 if (child.stdout) 322 child.stdout.destroy(); 323 324 if (child.stderr) 325 child.stderr.destroy(); 326 327 exithandler(); 328 } 329 330 function kill() { 331 if (child.stdout) 332 child.stdout.destroy(); 333 334 if (child.stderr) 335 child.stderr.destroy(); 336 337 killed = true; 338 try { 339 child.kill(options.killSignal); 340 } catch (e) { 341 ex = e; 342 exithandler(); 343 } 344 } 345 346 if (options.timeout > 0) { 347 timeoutId = setTimeout(function delayedKill() { 348 kill(); 349 timeoutId = null; 350 }, options.timeout); 351 } 352 353 if (child.stdout) { 354 if (encoding) 355 child.stdout.setEncoding(encoding); 356 357 child.stdout.on('data', function onChildStdout(chunk) { 358 const encoding = child.stdout.readableEncoding; 359 const length = encoding ? 360 Buffer.byteLength(chunk, encoding) : 361 chunk.length; 362 stdoutLen += length; 363 364 if (stdoutLen > options.maxBuffer) { 365 const truncatedLen = options.maxBuffer - (stdoutLen - length); 366 _stdout.push(chunk.slice(0, truncatedLen)); 367 368 ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout'); 369 kill(); 370 } else { 371 _stdout.push(chunk); 372 } 373 }); 374 } 375 376 if (child.stderr) { 377 if (encoding) 378 child.stderr.setEncoding(encoding); 379 380 child.stderr.on('data', function onChildStderr(chunk) { 381 const encoding = child.stderr.readableEncoding; 382 const length = encoding ? 383 Buffer.byteLength(chunk, encoding) : 384 chunk.length; 385 stderrLen += length; 386 387 if (stderrLen > options.maxBuffer) { 388 const truncatedLen = options.maxBuffer - (stderrLen - length); 389 _stderr.push(chunk.slice(0, truncatedLen)); 390 391 ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stderr'); 392 kill(); 393 } else { 394 _stderr.push(chunk); 395 } 396 }); 397 } 398 399 child.addListener('close', exithandler); 400 child.addListener('error', errorhandler); 401 402 return child; 403} 404 405ObjectDefineProperty(execFile, promisify.custom, { 406 enumerable: false, 407 value: customPromiseExecFunction(execFile) 408}); 409 410function normalizeSpawnArguments(file, args, options) { 411 validateString(file, 'file'); 412 413 if (file.length === 0) 414 throw new ERR_INVALID_ARG_VALUE('file', file, 'cannot be empty'); 415 416 if (ArrayIsArray(args)) { 417 args = args.slice(0); 418 } else if (args == null) { 419 args = []; 420 } else if (typeof args !== 'object') { 421 throw new ERR_INVALID_ARG_TYPE('args', 'object', args); 422 } else { 423 options = args; 424 args = []; 425 } 426 427 if (options === undefined) 428 options = {}; 429 else if (options === null || typeof options !== 'object') 430 throw new ERR_INVALID_ARG_TYPE('options', 'object', options); 431 432 // Validate the cwd, if present. 433 if (options.cwd != null && 434 typeof options.cwd !== 'string') { 435 throw new ERR_INVALID_ARG_TYPE('options.cwd', 'string', options.cwd); 436 } 437 438 // Validate detached, if present. 439 if (options.detached != null && 440 typeof options.detached !== 'boolean') { 441 throw new ERR_INVALID_ARG_TYPE('options.detached', 442 'boolean', options.detached); 443 } 444 445 // Validate the uid, if present. 446 if (options.uid != null && !isInt32(options.uid)) { 447 throw new ERR_INVALID_ARG_TYPE('options.uid', 'int32', options.uid); 448 } 449 450 // Validate the gid, if present. 451 if (options.gid != null && !isInt32(options.gid)) { 452 throw new ERR_INVALID_ARG_TYPE('options.gid', 'int32', options.gid); 453 } 454 455 // Validate the shell, if present. 456 if (options.shell != null && 457 typeof options.shell !== 'boolean' && 458 typeof options.shell !== 'string') { 459 throw new ERR_INVALID_ARG_TYPE('options.shell', 460 ['boolean', 'string'], options.shell); 461 } 462 463 // Validate argv0, if present. 464 if (options.argv0 != null && 465 typeof options.argv0 !== 'string') { 466 throw new ERR_INVALID_ARG_TYPE('options.argv0', 'string', options.argv0); 467 } 468 469 // Validate windowsHide, if present. 470 if (options.windowsHide != null && 471 typeof options.windowsHide !== 'boolean') { 472 throw new ERR_INVALID_ARG_TYPE('options.windowsHide', 473 'boolean', options.windowsHide); 474 } 475 476 // Validate windowsVerbatimArguments, if present. 477 if (options.windowsVerbatimArguments != null && 478 typeof options.windowsVerbatimArguments !== 'boolean') { 479 throw new ERR_INVALID_ARG_TYPE('options.windowsVerbatimArguments', 480 'boolean', 481 options.windowsVerbatimArguments); 482 } 483 484 // Make a shallow copy so we don't clobber the user's options object. 485 options = { ...options }; 486 487 if (options.shell) { 488 const command = [file].concat(args).join(' '); 489 // Set the shell, switches, and commands. 490 if (process.platform === 'win32') { 491 if (typeof options.shell === 'string') 492 file = options.shell; 493 else 494 file = process.env.comspec || 'cmd.exe'; 495 // '/d /s /c' is used only for cmd.exe. 496 if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { 497 args = ['/d', '/s', '/c', `"${command}"`]; 498 options.windowsVerbatimArguments = true; 499 } else { 500 args = ['-c', command]; 501 } 502 } else { 503 if (typeof options.shell === 'string') 504 file = options.shell; 505 else if (process.platform === 'android') 506 file = '/system/bin/sh'; 507 else 508 file = '/bin/sh'; 509 args = ['-c', command]; 510 } 511 } 512 513 if (typeof options.argv0 === 'string') { 514 args.unshift(options.argv0); 515 } else { 516 args.unshift(file); 517 } 518 519 const env = options.env || process.env; 520 const envPairs = []; 521 522 // process.env.NODE_V8_COVERAGE always propagates, making it possible to 523 // collect coverage for programs that spawn with white-listed environment. 524 if (process.env.NODE_V8_COVERAGE && 525 !ObjectPrototypeHasOwnProperty(options.env || {}, 'NODE_V8_COVERAGE')) { 526 env.NODE_V8_COVERAGE = process.env.NODE_V8_COVERAGE; 527 } 528 529 // Prototype values are intentionally included. 530 for (const key in env) { 531 const value = env[key]; 532 if (value !== undefined) { 533 envPairs.push(`${key}=${value}`); 534 } 535 } 536 537 return { 538 file: file, 539 args: args, 540 options: options, 541 envPairs: envPairs 542 }; 543} 544 545 546function spawn(file, args, options) { 547 const opts = normalizeSpawnArguments(file, args, options); 548 const child = new ChildProcess(); 549 550 options = opts.options; 551 debug('spawn', opts.args, options); 552 553 child.spawn({ 554 file: opts.file, 555 args: opts.args, 556 cwd: options.cwd, 557 windowsHide: !!options.windowsHide, 558 windowsVerbatimArguments: !!options.windowsVerbatimArguments, 559 detached: !!options.detached, 560 envPairs: opts.envPairs, 561 stdio: options.stdio, 562 uid: options.uid, 563 gid: options.gid, 564 serialization: options.serialization, 565 }); 566 567 return child; 568} 569 570function spawnSync(file, args, options) { 571 const opts = normalizeSpawnArguments(file, args, options); 572 573 const defaults = { 574 maxBuffer: MAX_BUFFER, 575 ...opts.options 576 }; 577 options = opts.options = defaults; 578 579 debug('spawnSync', opts.args, options); 580 581 // Validate the timeout, if present. 582 validateTimeout(options.timeout); 583 584 // Validate maxBuffer, if present. 585 validateMaxBuffer(options.maxBuffer); 586 587 options.file = opts.file; 588 options.args = opts.args; 589 options.envPairs = opts.envPairs; 590 591 // Validate and translate the kill signal, if present. 592 options.killSignal = sanitizeKillSignal(options.killSignal); 593 594 options.stdio = getValidStdio(options.stdio || 'pipe', true).stdio; 595 596 if (options.input) { 597 const stdin = options.stdio[0] = { ...options.stdio[0] }; 598 stdin.input = options.input; 599 } 600 601 // We may want to pass data in on any given fd, ensure it is a valid buffer 602 for (let i = 0; i < options.stdio.length; i++) { 603 const input = options.stdio[i] && options.stdio[i].input; 604 if (input != null) { 605 const pipe = options.stdio[i] = { ...options.stdio[i] }; 606 if (isArrayBufferView(input)) { 607 pipe.input = input; 608 } else if (typeof input === 'string') { 609 pipe.input = Buffer.from(input, options.encoding); 610 } else { 611 throw new ERR_INVALID_ARG_TYPE(`options.stdio[${i}]`, 612 ['Buffer', 613 'TypedArray', 614 'DataView', 615 'string'], 616 input); 617 } 618 } 619 } 620 621 return child_process.spawnSync(opts); 622} 623 624 625function checkExecSyncError(ret, args, cmd) { 626 let err; 627 if (ret.error) { 628 err = ret.error; 629 } else if (ret.status !== 0) { 630 let msg = 'Command failed: '; 631 msg += cmd || args.join(' '); 632 if (ret.stderr && ret.stderr.length > 0) 633 msg += `\n${ret.stderr.toString()}`; 634 // eslint-disable-next-line no-restricted-syntax 635 err = new Error(msg); 636 } 637 if (err) { 638 ObjectAssign(err, ret); 639 } 640 return err; 641} 642 643 644function execFileSync(command, args, options) { 645 const opts = normalizeSpawnArguments(command, args, options); 646 const inheritStderr = !opts.options.stdio; 647 648 const ret = spawnSync(opts.file, opts.args.slice(1), opts.options); 649 650 if (inheritStderr && ret.stderr) 651 process.stderr.write(ret.stderr); 652 653 const err = checkExecSyncError(ret, opts.args, undefined); 654 655 if (err) 656 throw err; 657 658 return ret.stdout; 659} 660 661 662function execSync(command, options) { 663 const opts = normalizeExecArgs(command, options, null); 664 const inheritStderr = !opts.options.stdio; 665 666 const ret = spawnSync(opts.file, opts.options); 667 668 if (inheritStderr && ret.stderr) 669 process.stderr.write(ret.stderr); 670 671 const err = checkExecSyncError(ret, opts.args, command); 672 673 if (err) 674 throw err; 675 676 return ret.stdout; 677} 678 679 680function validateTimeout(timeout) { 681 if (timeout != null && !(NumberIsInteger(timeout) && timeout >= 0)) { 682 throw new ERR_OUT_OF_RANGE('timeout', 'an unsigned integer', timeout); 683 } 684} 685 686 687function validateMaxBuffer(maxBuffer) { 688 if (maxBuffer != null && !(typeof maxBuffer === 'number' && maxBuffer >= 0)) { 689 throw new ERR_OUT_OF_RANGE('options.maxBuffer', 690 'a positive number', 691 maxBuffer); 692 } 693} 694 695 696function sanitizeKillSignal(killSignal) { 697 if (typeof killSignal === 'string' || typeof killSignal === 'number') { 698 return convertToValidSignal(killSignal); 699 } else if (killSignal != null) { 700 throw new ERR_INVALID_ARG_TYPE('options.killSignal', 701 ['string', 'number'], 702 killSignal); 703 } 704} 705 706module.exports = { 707 _forkChild, 708 ChildProcess, 709 exec, 710 execFile, 711 execFileSync, 712 execSync, 713 fork, 714 spawn, 715 spawnSync 716}; 717