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