mirror of
https://github.com/classilla/tenfourfox.git
synced 2026-04-19 06:25:12 +00:00
330 lines
8.9 KiB
JavaScript
330 lines
8.9 KiB
JavaScript
/* 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) {
|
|
// 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 || '').split(' ')];
|
|
}
|
|
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;
|
|
}
|