summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/system/child_process.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-10 02:51:36 -0500
commit37d5300335d81cecbecc99812747a657588c63eb (patch)
tree765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/system/child_process.js
parentb2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff)
parent4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff)
downloadUXP-37d5300335d81cecbecc99812747a657588c63eb.tar
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz
UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz
UXP-37d5300335d81cecbecc99812747a657588c63eb.zip
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/system/child_process.js')
-rw-r--r--toolkit/jetpack/sdk/system/child_process.js332
1 files changed, 332 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/system/child_process.js b/toolkit/jetpack/sdk/system/child_process.js
new file mode 100644
index 000000000..8ea1f4f80
--- /dev/null
+++ b/toolkit/jetpack/sdk/system/child_process.js
@@ -0,0 +1,332 @@
+/* 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;
+}