/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; module.metadata = { 'stability': 'experimental' }; var { Ci } = require('chrome'); var subprocess = require('./child_process/subprocess'); var { EventTarget } = require('../event/target'); var { Stream } = require('../io/stream'); var { on, emit, off } = require('../event/core'); var { Class } = require('../core/heritage'); var { platform } = require('../system'); var { isFunction, isArray } = require('../lang/type'); var { delay } = require('../lang/functional'); var { merge } = require('../util/object'); var { setTimeout, clearTimeout } = require('../timers'); var isWindows = platform.indexOf('win') === 0; var processes = new WeakMap(); /** * The `Child` class wraps a subprocess command, exposes * the stdio streams, and methods to manipulate the subprocess */ var Child = Class({ implements: [EventTarget], initialize: function initialize (options) { let child = this; let proc; this.killed = false; this.exitCode = undefined; this.signalCode = undefined; this.stdin = Stream(); this.stdout = Stream(); this.stderr = Stream(); try { proc = subprocess.call({ command: options.file, arguments: options.cmdArgs, environment: serializeEnv(options.env), workdir: options.cwd, charset: options.encoding, stdout: data => emit(child.stdout, 'data', data), stderr: data => emit(child.stderr, 'data', data), stdin: stream => { child.stdin.on('data', pumpStdin); child.stdin.on('end', function closeStdin () { child.stdin.off('data', pumpStdin); child.stdin.off('end', closeStdin); stream.close(); }); function pumpStdin (data) { stream.write(data); } }, done: function (result, error) { if (error) return handleError(error); // Only emit if child is not killed; otherwise, // the `kill` method will handle this if (!child.killed) { child.exitCode = result.exitCode; child.signalCode = null; // If process exits with < 0, there was an error if (child.exitCode < 0) { handleError(new Error('Process exited with exit code ' + child.exitCode)); } else { // Also do 'exit' event as there's not much of // a difference in our implementation as we're not using // node streams emit(child, 'exit', child.exitCode, child.signalCode); } // Emit 'close' event with exit code and signal, // which is `null`, as it was not a killed process emit(child, 'close', child.exitCode, child.signalCode); } } }); processes.set(child, proc); } catch (e) { // Delay the error handling so an error handler can be set // during the same tick that the Child was created delay(() => handleError(e)); } // `handleError` is called when process could not even // be spawned function handleError (e) { // If error is an nsIObject, make a fresh error object // so we're not exposing nsIObjects, and we can modify it // with additional process information, like node let error = e; if (e instanceof Ci.nsISupports) { error = new Error(e.message, e.filename, e.lineNumber); } emit(child, 'error', error); child.exitCode = -1; child.signalCode = null; emit(child, 'close', child.exitCode, child.signalCode); } }, kill: function kill (signal) { let proc = processes.get(this); proc.kill(signal); this.killed = true; this.exitCode = null; this.signalCode = signal; emit(this, 'exit', this.exitCode, this.signalCode); emit(this, 'close', this.exitCode, this.signalCode); }, get pid() { return processes.get(this, {}).pid || -1; } }); function spawn (file, ...args) { let cmdArgs = []; // Default options let options = { cwd: null, env: null, encoding: 'UTF-8' }; if (args[1]) { merge(options, args[1]); cmdArgs = args[0]; } else { if (isArray(args[0])) cmdArgs = args[0]; else merge(options, args[0]); } if ('gid' in options) console.warn('`gid` option is not yet supported for `child_process`'); if ('uid' in options) console.warn('`uid` option is not yet supported for `child_process`'); if ('detached' in options) console.warn('`detached` option is not yet supported for `child_process`'); options.file = file; options.cmdArgs = cmdArgs; return Child(options); } exports.spawn = spawn; /** * exec(command, options, callback) */ function exec (cmd, ...args) { let file, cmdArgs, callback, options = {}; if (isFunction(args[0])) callback = args[0]; else { merge(options, args[0]); callback = args[1]; } if (isWindows) { file = 'C:\\Windows\\System32\\cmd.exe'; cmdArgs = ['/S/C', cmd || '']; } else { file = '/bin/sh'; cmdArgs = ['-c', cmd]; } // Undocumented option from node being able to specify shell if (options && options.shell) file = options.shell; return execFile(file, cmdArgs, options, callback); } exports.exec = exec; /** * execFile (file, args, options, callback) */ function execFile (file, ...args) { let cmdArgs = [], callback; // Default options let options = { cwd: null, env: null, encoding: 'utf8', timeout: 0, maxBuffer: 204800, //200 KB (200*1024 bytes) killSignal: 'SIGTERM' }; if (isFunction(args[args.length - 1])) callback = args[args.length - 1]; if (isArray(args[0])) { cmdArgs = args[0]; merge(options, args[1]); } else if (!isFunction(args[0])) merge(options, args[0]); let child = spawn(file, cmdArgs, options); let exited = false; let stdout = ''; let stderr = ''; let error = null; let timeoutId = null; child.stdout.setEncoding(options.encoding); child.stderr.setEncoding(options.encoding); on(child.stdout, 'data', pumpStdout); on(child.stderr, 'data', pumpStderr); on(child, 'close', exitHandler); on(child, 'error', errorHandler); if (options.timeout > 0) { setTimeout(() => { kill(); timeoutId = null; }, options.timeout); } function exitHandler (code, signal) { // Return if exitHandler called previously, occurs // when multiple maxBuffer errors thrown and attempt to kill multiple // times if (exited) return; exited = true; if (!isFunction(callback)) return; if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (!error && (code !== 0 || signal !== null)) error = createProcessError(new Error('Command failed: ' + stderr), { code: code, signal: signal, killed: !!child.killed }); callback(error, stdout, stderr); off(child.stdout, 'data', pumpStdout); off(child.stderr, 'data', pumpStderr); off(child, 'close', exitHandler); off(child, 'error', errorHandler); } function errorHandler (e) { error = e; exitHandler(); } function kill () { try { child.kill(options.killSignal); } catch (e) { // In the scenario where the kill signal happens when // the process is already closing, just abort the kill fail if (/library is not open/.test(e)) return; error = e; exitHandler(-1, options.killSignal); } } function pumpStdout (data) { stdout += data; if (stdout.length > options.maxBuffer) { error = new Error('stdout maxBuffer exceeded'); kill(); } } function pumpStderr (data) { stderr += data; if (stderr.length > options.maxBuffer) { error = new Error('stderr maxBuffer exceeded'); kill(); } } return child; } exports.execFile = execFile; exports.fork = function fork () { throw new Error("child_process#fork is not currently supported"); }; function serializeEnv (obj) { return Object.keys(obj || {}).map(prop => prop + '=' + obj[prop]); } function createProcessError (err, options = {}) { // If code and signal look OK, this was probably a failure // attempting to spawn the process (like ENOENT in node) -- use // the code from the error message if (!options.code && !options.signal) { let match = err.message.match(/(NS_ERROR_\w*)/); if (match && match.length > 1) err.code = match[1]; else { // If no good error message found, use the passed in exit code; // this occurs when killing a process that's already closing, // where we want both a valid exit code (0) and the error err.code = options.code != null ? options.code : null; } } else err.code = options.code != null ? options.code : null; err.signal = options.signal || null; err.killed = options.killed || false; return err; }