Bug 1269501: Part 3 - Add new Subprocess IPC module. r=aswan r=mhowell rs=bsmedberg

MozReview-Commit-ID: 6vl5xBTBXiF

--HG--
extra : rebase_source : 00a8096fe6f1ba79d41a242c8a324285daf4d605
This commit is contained in:
Kris Maglione 2016-05-25 18:29:05 -07:00
parent 1f20564e4e
commit 43154def49
22 changed files with 4156 additions and 0 deletions

View File

@ -106,6 +106,10 @@ if 'Android' != CONFIG['OS_TARGET']:
EXTRA_JS_MODULES += [
'LightweightThemeConsumer.jsm',
]
DIRS += [
'subprocess',
]
else:
DEFINES['ANDROID'] = True

View File

@ -0,0 +1,26 @@
{
"extends": "../../components/extensions/.eslintrc",
"env": {
"worker": true,
},
"globals": {
"ChromeWorker": false,
"Components": false,
"LIBC": true,
"Library": true,
"OS": false,
"Services": false,
"SubprocessConstants": true,
"ctypes": false,
"debug": true,
"dump": false,
"libc": true,
"unix": true,
},
"rules": {
"no-console": 0,
},
}

View File

@ -0,0 +1,163 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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/. */
/*
* These modules are loosely based on the subprocess.jsm module created
* by Jan Gerber and Patrick Brunschwig, though the implementation
* differs drastically.
*/
"use strict";
let EXPORTED_SYMBOLS = ["Subprocess"];
/* exported Subprocess */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
if (AppConstants.platform == "win") {
XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
"resource://gre/modules/subprocess/subprocess_win.jsm");
} else {
XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
"resource://gre/modules/subprocess/subprocess_unix.jsm");
}
/**
* Allows for creation of and communication with OS-level sub-processes.
* @namespace
*/
var Subprocess = {
/**
* Launches a process, and returns a handle to it.
*
* @param {object} options
* An object describing the process to launch.
*
* @param {string} options.command
* The full path of the execuable to launch. Relative paths are not
* accepted, and `$PATH` is not searched.
*
* If a path search is necessary, the {@link Subprocess.pathSearch} method may
* be used to map a bare executable name to a full path.
*
* @param {string[]} [options.arguments]
* A list of strings to pass as arguments to the process.
*
* @param {object} [options.environment]
* An object containing a key and value for each environment variable
* to pass to the process. Only the object's own, enumerable properties
* are added to the environment.
*
* @param {boolean} [options.environmentAppend]
* If true, append the environment variables passed in `environment` to
* the existing set of environment variables. Otherwise, the values in
* 'environment' constitute the entire set of environment variables
* passed to the new process.
*
* @param {string} [options.stderr]
* Defines how the process's stderr output is handled. One of:
*
* - `"ignore"`: (default) The process's standard error is not redirected.
* - `"stdout"`: The process's stderr is merged with its stdout.
* - `"pipe"`: The process's stderr is redirected to a pipe, which can be read
* from via its `stderr` property.
*
* @param {string} [options.workdir]
* The working directory in which to launch the new process.
*
* @returns {Promise<Process>}
*
* @rejects {Error}
* May be rejected with an Error object if the process can not be
* launched. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_BAD_EXECUTABLE: The given command could not
* be found, or the file that it references is not executable.
*
* Note that if the process is successfully launched, but exits with
* a non-zero exit code, the promise will still resolve successfully.
*/
call(options) {
options = Object.assign({}, options);
options.stderr = options.stderr || "ignore";
options.workdir = options.workdir || null;
let environment = {};
if (!options.environment || options.environmentAppend) {
environment = this.getEnvironment();
}
if (options.environment) {
Object.assign(environment, options.environment);
}
options.environment = Object.keys(environment)
.map(key => `${key}=${environment[key]}`);
options.arguments = Array.from(options.arguments || []);
return Promise.resolve(SubprocessImpl.isExecutableFile(options.command)).then(isExecutable => {
if (!isExecutable) {
let error = new Error(`File at path "${options.command}" does not exist, or is not executable`);
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
throw error;
}
options.arguments.unshift(options.command);
return SubprocessImpl.call(options);
});
},
/**
* Returns an object with a key-value pair for every variable in the process's
* current environment.
*
* @returns {object}
*/
getEnvironment() {
let environment = Object.create(null);
for (let [k, v] of SubprocessImpl.getEnvironment()) {
environment[k] = v;
}
return environment;
},
/**
* Searches for the given executable file in the system executable
* file paths as specified by the PATH environment variable.
*
* On Windows, if the unadorned filename cannot be found, the
* extensions in the semicolon-separated list in the PATHSEP
* environment variable are successively appended to the original
* name and searched for in turn.
*
* @param {string} bin
* The name of the executable to find.
* @param {object} [environment]
* An object containing a key for each environment variable to be used
* in the search. If not provided, full the current process environment
* is used.
* @returns {Promise<string>}
*/
pathSearch(command, environment = this.getEnvironment()) {
// Promise.resolve lets us get around returning one of the Promise.jsm
// pseudo-promises returned by Task.jsm.
let path = SubprocessImpl.pathSearch(command, environment);
return Promise.resolve(path);
},
};
Object.assign(Subprocess, SubprocessConstants);
Object.freeze(Subprocess);

View File

@ -0,0 +1,227 @@
.. _Subprocess:
=================
Supbrocess Module
=================
The Subprocess module allows a caller to spawn a native host executable, and
communicate with it asynchronously over its standard input and output pipes.
Processes are launched asynchronously ``Subprocess.call`` method, based
on the properties of a single options object. The method returns a promise
which resolves, once the process has successfully launched, to a ``Process``
object, which can be used to communicate with and control the process.
A simple Hello World invocation, which writes a message to a process, reads it
back, logs it, and waits for the process to exit looks something like:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/cat",
});
proc.stdin.write("Hello World!");
let result = await proc.stdout.readString();
console.log(result);
proc.stdin.close();
let {exitCode} = await proc.wait();
Input and Output Redirection
============================
Communication with the child process happens entirely via one-way pipes tied
to its standard input, standard output, and standard error file descriptors.
While standard input and output are always redirected to pipes, standard error
is inherited from the parent process by default. Standard error can, however,
optionally be either redirected to its own pipe or merged into the standard
output pipe.
The module is designed primarily for use with processes following a strict
IO protocol, with predictable message sizes. Its read operations, therefore,
either complete after reading the exact amount of data specified, or do not
complete at all. For cases where this is not desirable, ``read()`` and
``readString`` may be called without any length argument, and will return a
chunk of data of an arbitrary size.
Process and Pipe Lifecycles
===========================
Once the process exits, any buffered data from its output pipes may still be
read until the pipe is explicitly closed. Unless the pipe is explicitly
closed, however, any pending buffered data *must* be read from the pipe, or
the resources associated with the pipe will not be freed.
Beyond this, no explicit cleanup is required for either processes or their
pipes. So long as the caller ensures that the process exits, and there is no
pending input to be read on its ``stdout`` or ``stderr`` pipes, all resources
will be freed automatically.
The preferred way to ensure that a process exits is to close its input pipe
and wait for it to exit gracefully. Processes which haven't exited gracefully
by shutdown time, however, must be forcibly terminated:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/usr/bin/subprocess.py",
});
// Kill the process if it hasn't gracefully exited by shutdown time.
let blocker = () => proc.kill();
AsyncShutdown.profileBeforeChange.addBlocker(
"Subprocess: Killing hung process",
blocker);
proc.wait().then(() => {
// Remove the shutdown blocker once we've exited.
AsyncShutdown.profileBeforeChange.removeBlocker(blocker);
// Close standard output, in case there's any buffered data we haven't read.
proc.stdout.close();
});
// Send a message to the process, and close stdin, so the process knows to
// exit.
proc.stdin.write(message);
proc.stdin.close();
In the simpler case of a short-running process which takes no input, and exits
immediately after producing output, it's generally enough to simply read its
output stream until EOF:
.. code-block:: javascript
let proc = await Subprocess.call({
command: await Subprocess.pathSearch("ifconfig"),
});
// Read all of the process output.
let result = "";
let string;
while ((string = await proc.stdout.readString())) {
result += string;
}
console.log(result);
// The output pipe is closed and no buffered data remains to be read.
// This means the process has exited, and no further cleanup is necessary.
Bidirectional IO
================
When performing bidirectional IO, special care needs to be taken to avoid
deadlocks. While all IO operations in the Subprocess API are asynchronous,
careless ordering of operations can still lead to a state where both processes
are blocked on a read or write operation at the same time. For example,
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/cat",
});
let size = 1024 * 1024;
await proc.stdin.write(new ArrayBuffer(size));
let result = await proc.stdout.read(size);
The code attempts to write 1MB of data to an input pipe, and then read it back
from the output pipe. Because the data is big enough to fill both the input
and output pipe buffers, though, and because the code waits for the write
operation to complete before attempting any reads, the ``cat`` process will
block trying to write to its output indefinitely, and never finish reading the
data from its standard input.
In order to avoid the deadlock, we need to avoid blocking on the write
operation:
.. code-block:: javascript
let size = 1024 * 1024;
proc.stdin.write(new ArrayBuffer(size));
let result = await proc.stdout.read(size);
There is no silver bullet to avoiding deadlocks in this type of situation,
though. Any input operations that depend on output operations, or vice versa,
have the possibility of triggering deadlocks, and need to be thought out
carefully.
Arguments
=========
Arguments may be passed to the process in the form an array of strings.
Arguments are never split, or subjected to any sort of shell expansion, so the
target process will receive the exact arguments array as passed to
``Subprocess.call``. Argument 0 will always be the full path to the
executable, as passed via the ``command`` argument:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/sh",
arguments: ["-c", "echo -n $0"],
});
let output = await proc.stdout.readString();
assert(output === "/bin/sh");
Process Environment
===================
By default, the process is launched with the same environment variables and
working directory as the parent process, but either can be changed if
necessary. The working directory may be changed simply by passing a
``workdir`` option:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/pwd",
workdir: "/tmp",
});
let output = await proc.stdout.readString();
assert(output === "/tmp\n");
The process's environment variables can be changed using the ``environment``
and ``environmentAppend`` options. By default, passing an ``environment``
object replaces the process's entire environment with the properties in that
object:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/pwd",
environment: {FOO: "BAR"},
});
let output = await proc.stdout.readString();
assert(output === "FOO=BAR\n");
In order to add variables to, or change variables from, the current set of
environment variables, the ``environmentAppend`` object must be passed in
addition:
.. code-block:: javascript
let proc = await Subprocess.call({
command: "/bin/pwd",
environment: {FOO: "BAR"},
environmentAppend: true,
});
let output = "";
while ((string = await proc.stdout.readString())) {
output += string;
}
assert(output.includes("FOO=BAR\n"));

View File

@ -0,0 +1,32 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_JS_MODULES += [
'Subprocess.jsm',
]
EXTRA_JS_MODULES.subprocess += [
'subprocess_common.jsm',
'subprocess_shared.js',
'subprocess_worker_common.js',
]
if CONFIG['OS_TARGET'] == 'WINNT':
EXTRA_JS_MODULES.subprocess += [
'subprocess_shared_win.js',
'subprocess_win.jsm',
'subprocess_worker_win.js',
]
else:
EXTRA_JS_MODULES.subprocess += [
'subprocess_shared_unix.js',
'subprocess_unix.jsm',
'subprocess_worker_unix.js',
]
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
SPHINX_TREES['toolkit_modules/subprocess'] = ['docs']

View File

@ -0,0 +1,681 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* eslint-disable mozilla/balanced-listeners */
/* exported BaseProcess, PromiseWorker */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.importGlobalProperties(["TextDecoder"]);
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
var EXPORTED_SYMBOLS = ["BaseProcess", "PromiseWorker", "SubprocessConstants"];
const BUFFER_SIZE = 4096;
let nextResponseId = 0;
/**
* Wraps a ChromeWorker so that messages sent to it return a promise which
* resolves when the message has been received and the operation it triggers is
* complete.
*/
class PromiseWorker extends ChromeWorker {
constructor(url) {
super(url);
this.listeners = new Map();
this.pendingResponses = new Map();
this.addListener("failure", this.onFailure.bind(this));
this.addListener("success", this.onSuccess.bind(this));
this.addListener("debug", this.onDebug.bind(this));
this.addEventListener("message", this.onmessage);
}
/**
* Adds a listener for the given message from the worker. Any message received
* from the worker with a `data.msg` property matching the given `msg`
* parameter are passed to the given listener.
*
* @param {string} msg
* The message to listen for.
* @param {function(Event)} listener
* The listener to call when matching messages are received.
*/
addListener(msg, listener) {
if (!this.listeners.has(msg)) {
this.listeners.set(msg, new Set());
}
this.listeners.get(msg).add(listener);
}
/**
* Removes the given message listener.
*
* @param {string} msg
* The message to stop listening for.
* @param {function(Event)} listener
* The listener to remove.
*/
removeListener(msg, listener) {
let listeners = this.listeners.get(msg);
if (listeners) {
listeners.delete(listener);
if (!listeners.size) {
this.listeners.delete(msg);
}
}
}
onmessage(event) {
let {msg} = event.data;
let listeners = this.listeners.get(msg) || new Set();
for (let listener of listeners) {
try {
listener(event.data);
} catch (e) {
Cu.reportError(e);
}
}
}
/**
* Called when a message sent to the worker has failed, and rejects its
* corresponding promise.
*
* @private
*/
onFailure({msgId, error}) {
this.pendingResponses.get(msgId).reject(error);
this.pendingResponses.delete(msgId);
}
/**
* Called when a message sent to the worker has succeeded, and resolves its
* corresponding promise.
*
* @private
*/
onSuccess({msgId, data}) {
this.pendingResponses.get(msgId).resolve(data);
this.pendingResponses.delete(msgId);
}
onDebug({message}) {
dump(`Worker debug: ${message}\n`);
}
/**
* Calls the given method in the worker, and returns a promise which resolves
* or rejects when the method has completed.
*
* @param {string} method
* The name of the method to call.
* @param {Array} args
* The arguments to pass to the method.
* @param {Array} [transferList]
* A list of objects to transfer to the worker, rather than cloning.
* @returns {Promise}
*/
call(method, args, transferList = []) {
let msgId = nextResponseId++;
return new Promise((resolve, reject) => {
this.pendingResponses.set(msgId, {resolve, reject});
let message = {
msg: method,
msgId,
args,
};
this.postMessage(message, transferList);
});
}
}
/**
* Represents an input or output pipe connected to a subprocess.
*
* @property {integer} fd
* The file descriptor number of the pipe on the child process's side.
* @readonly
*/
class Pipe {
/**
* @param {Process} process
* The child process that this pipe is connected to.
* @param {integer} fd
* The file descriptor number of the pipe on the child process's side.
* @param {integer} id
* The internal ID of the pipe, which ties it to the corresponding Pipe
* object on the Worker side.
*/
constructor(process, fd, id) {
this.id = id;
this.fd = fd;
this.processId = process.id;
this.worker = process.worker;
/**
* @property {boolean} closed
* True if the file descriptor has been closed, and can no longer
* be read from or written to. Pending IO operations may still
* complete, but new operations may not be initiated.
* @readonly
*/
this.closed = false;
}
/**
* Closes the end of the pipe which belongs to this process.
*
* @param {boolean} force
* If true, the pipe is closed immediately, regardless of any pending
* IO operations. If false, the pipe is closed after any existing
* pending IO operations have completed.
* @returns {Promise<object>}
* Resolves to an object with no properties once the pipe has been
* closed.
*/
close(force = false) {
this.closed = true;
return this.worker.call("close", [this.id, force]);
}
}
/**
* Represents an output-only pipe, to which data may be written.
*/
class OutputPipe extends Pipe {
constructor(...args) {
super(...args);
this.encoder = new TextEncoder();
}
/**
* Writes the given data to the stream.
*
* When given an array buffer or typed array, ownership of the buffer is
* transferred to the IO worker, and it may no longer be used from this
* thread.
*
* @param {ArrayBuffer|TypedArray|string} buffer
* Data to write to the stream.
* @returns {Promise<object>}
* Resolves to an object with a `bytesWritten` property, containing
* the number of bytes successfully written, once the operation has
* completed.
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* all of the data in `buffer` could be written to it.
*/
write(buffer) {
if (typeof buffer === "string") {
buffer = this.encoder.encode(buffer);
}
if (Cu.getClassName(buffer, true) !== "ArrayBuffer") {
if (buffer.byteLength === buffer.buffer.byteLength) {
buffer = buffer.buffer;
} else {
buffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
}
let args = [this.id, buffer];
return this.worker.call("write", args, [buffer]);
}
}
/**
* Represents an input-only pipe, from which data may be read.
*/
class InputPipe extends Pipe {
constructor(...args) {
super(...args);
this.buffers = [];
/**
* @property {integer} dataAvailable
* The number of readable bytes currently stored in the input
* buffer.
* @readonly
*/
this.dataAvailable = 0;
this.decoder = new TextDecoder();
this.pendingReads = [];
this._pendingBufferRead = null;
this.fillBuffer();
}
/**
* @property {integer} bufferSize
* The current size of the input buffer. This varies depending on
* the size of pending read operations.
* @readonly
*/
get bufferSize() {
if (this.pendingReads.length) {
return Math.max(this.pendingReads[0].length, BUFFER_SIZE);
}
return BUFFER_SIZE;
}
/**
* Attempts to fill the input buffer.
*
* @private
*/
fillBuffer() {
let dataWanted = this.bufferSize - this.dataAvailable;
if (!this._pendingBufferRead && dataWanted > 0) {
this._pendingBufferRead = this._read(dataWanted);
this._pendingBufferRead.then((result) => {
this._pendingBufferRead = null;
if (result) {
this.onInput(result.buffer);
this.fillBuffer();
}
});
}
}
_read(size) {
let args = [this.id, size];
return this.worker.call("read", args).catch(e => {
this.closed = true;
for (let {length, resolve, reject} of this.pendingReads.splice(0)) {
if (length === null && e.errorCode === SubprocessConstants.ERROR_END_OF_FILE) {
resolve(new ArrayBuffer(0));
} else {
reject(e);
}
}
});
}
/**
* Adds the given data to the end of the input buffer.
*
* @private
*/
onInput(buffer) {
this.buffers.push(buffer);
this.dataAvailable += buffer.byteLength;
this.checkPendingReads();
}
/**
* Checks the topmost pending read operations and fulfills as many as can be
* filled from the current input buffer.
*
* @private
*/
checkPendingReads() {
this.fillBuffer();
let reads = this.pendingReads;
while (reads.length && this.dataAvailable &&
reads[0].length <= this.dataAvailable) {
let pending = this.pendingReads.shift();
let length = pending.length || this.dataAvailable;
let result;
let byteLength = this.buffers[0].byteLength;
if (byteLength == length) {
result = this.buffers.shift();
} else if (byteLength > length) {
let buffer = this.buffers[0];
this.buffers[0] = buffer.slice(length);
result = ArrayBuffer.transfer(buffer, length);
} else {
result = ArrayBuffer.transfer(this.buffers.shift(), length);
let u8result = new Uint8Array(result);
while (byteLength < length) {
let buffer = this.buffers[0];
let u8buffer = new Uint8Array(buffer);
let remaining = length - byteLength;
if (buffer.byteLength <= remaining) {
this.buffers.shift();
u8result.set(u8buffer, byteLength);
} else {
this.buffers[0] = buffer.slice(remaining);
u8result.set(u8buffer.subarray(0, remaining), byteLength);
}
byteLength += Math.min(buffer.byteLength, remaining);
}
}
this.dataAvailable -= result.byteLength;
pending.resolve(result);
}
}
/**
* Reads exactly `length` bytes of binary data from the input stream, or, if
* length is not provided, reads the first chunk of data to become available.
* In the latter case, returns an empty array buffer on end of file.
*
* The read operation will not complete until enough data is available to
* fulfill the request. If the pipe closes without enough available data to
* fulfill the read, the operation fails, and any remaining buffered data is
* lost.
*
* @param {integer} [length]
* The number of bytes to read.
* @returns {Promise<ArrayBuffer>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
read(length = null) {
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
throw new RangeError("Length must be a non-negative integer");
}
if (length == 0) {
return Promise.resolve(new ArrayBuffer(0));
}
return new Promise((resolve, reject) => {
this.pendingReads.push({length, resolve, reject});
this.checkPendingReads();
});
}
/**
* Reads exactly `length` bytes from the input stream, and parses them as
* UTF-8 JSON data.
*
* @param {integer} length
* The number of bytes to read.
* @returns {Promise<object>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
* - Subprocess.ERROR_INVALID_JSON: The data read from the pipe
* could not be parsed as a valid JSON string.
*/
readJSON(length) {
if (!Number.isInteger(length) || length <= 0) {
throw new RangeError("Length must be a positive integer");
}
return this.readString(length).then(string => {
try {
return JSON.parse(string);
} catch (e) {
e.errorCode = SubprocessConstants.ERROR_INVALID_JSON;
throw e;
}
});
}
/**
* Reads a chunk of UTF-8 data from the input stream, and converts it to a
* JavaScript string.
*
* If `length` is provided, reads exactly `length` bytes. Otherwise, reads the
* first chunk of data to become available, and returns an empty string on end
* of file. In the latter case, the chunk is decoded in streaming mode, and
* any incomplete UTF-8 sequences at the end of a chunk are returned at the
* start of a subsequent read operation.
*
* @param {integer} [length]
* The number of bytes to read.
* @param {object} [options]
* An options object as expected by TextDecoder.decode.
* @returns {Promise<string>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
readString(length = null, options = {stream: length === null}) {
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
throw new RangeError("Length must be a non-negative integer");
}
return this.read(length).then(buffer => {
return this.decoder.decode(buffer, options);
});
}
/**
* Reads 4 bytes from the input stream, and parses them as an unsigned
* integer, in native byte order.
*
* @returns {Promise<integer>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
readUint32() {
return this.read(4).then(buffer => {
return new Uint32Array(buffer)[0];
});
}
}
/**
* @class Process
* @extends BaseProcess
*/
/**
* Represents a currently-running process, and allows interaction with it.
*/
class BaseProcess {
/**
* @param {PromiseWorker} worker
* The worker instance which owns the process.
* @param {integer} processId
* The internal ID of the Process object, which ties it to the
* corresponding process on the Worker side.
* @param {integer[]} fds
* An array of internal Pipe IDs, one for each standard file descriptor
* in the child process.
* @param {integer} pid
* The operating system process ID of the process.
*/
constructor(worker, processId, fds, pid) {
this.id = processId;
this.worker = worker;
/**
* @property {integer} pid
* The process ID of the process, assigned by the operating system.
* @readonly
*/
this.pid = pid;
this.exitCode = null;
this.exitPromise = new Promise(resolve => {
this.worker.call("wait", [this.id]).then(({exitCode}) => {
resolve(Object.freeze({exitCode}));
this.exitCode = exitCode;
});
});
if (fds[0] !== undefined) {
/**
* @property {OutputPipe} stdin
* A Pipe object which allows writing to the process's standard
* input.
* @readonly
*/
this.stdin = new OutputPipe(this, 0, fds[0]);
}
if (fds[1] !== undefined) {
/**
* @property {InputPipe} stdout
* A Pipe object which allows reading from the process's standard
* output.
* @readonly
*/
this.stdout = new InputPipe(this, 1, fds[1]);
}
if (fds[2] !== undefined) {
/**
* @property {InputPipe} [stderr]
* An optional Pipe object which allows reading from the
* process's standard error output.
* @readonly
*/
this.stderr = new InputPipe(this, 2, fds[2]);
}
}
/**
* Spawns a process, and resolves to a BaseProcess instance on success.
*
* @param {object} options
* An options object as passed to `Subprocess.call`.
*
* @returns {Promise<BaseProcess>}
*/
static create(options) {
let worker = this.getWorker();
return worker.call("spawn", [options]).then(({processId, fds, pid}) => {
return new this(worker, processId, fds, pid);
});
}
static get WORKER_URL() {
throw new Error("Not implemented");
}
/**
* Gets the current subprocess worker, or spawns a new one if it does not
* currently exist.
*
* @returns {PromiseWorker}
*/
static getWorker() {
if (!this._worker) {
this._worker = new PromiseWorker(this.WORKER_URL);
}
return this._worker;
}
/**
* Kills the process.
*
* @param {integer} [timeout=300]
* A timeout, in milliseconds, after which the process will be forcibly
* killed. On platforms which support it, the process will be sent
* a `SIGTERM` signal immediately, so that it has a chance to terminate
* gracefully, and a `SIGKILL` signal if it hasn't exited within
* `timeout` milliseconds. On other platforms (namely Windows), the
* process will be forcibly terminated immediately.
*
* @returns {Promise<object>}
* Resolves to an object with an `exitCode` property when the process
* has exited.
*/
kill(timeout = 300) {
// If the process has already exited, don't bother sending a signal.
if (this.exitCode != null) {
return this.wait();
}
let force = timeout <= 0;
this.worker.call("kill", [this.id, force]);
if (!force) {
setTimeout(() => {
if (this.exitCode == null) {
this.worker.call("kill", [this.id, true]);
}
}, timeout);
}
return this.wait();
}
/**
* Returns a promise which resolves to the process's exit code, once it has
* exited.
*
* @returns {Promise<object>}
* Resolves to an object with an `exitCode` property, containing the
* process's exit code, once the process has exited.
*
* On Unix-like systems, a negative exit code indicates that the
* process was killed by a signal whose signal number is the absolute
* value of the error code. On Windows, an exit code of -9 indicates
* that the process was killed via the {@linkcode BaseProcess#kill kill()}
* method.
*/
wait() {
return this.exitPromise;
}
}

View File

@ -0,0 +1,94 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported Library, SubprocessConstants */
if (!ArrayBuffer.transfer) {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/transfer
*/
ArrayBuffer.transfer = function(buffer, size = buffer.byteLength) {
let u8out = new Uint8Array(size);
let u8buffer = new Uint8Array(buffer, 0, Math.min(size, buffer.byteLength));
u8out.set(u8buffer);
return u8out.buffer;
};
}
var libraries = {};
class Library {
constructor(name, names, definitions) {
if (name in libraries) {
return libraries[name];
}
for (let name of names) {
try {
if (!this.library) {
this.library = ctypes.open(name);
}
} catch (e) {
// Ignore errors until we've tried all the options.
}
}
if (!this.library) {
throw new Error("Could not load libc");
}
libraries[name] = this;
for (let symbol of Object.keys(definitions)) {
this.declare(symbol, ...definitions[symbol]);
}
}
declare(name, ...args) {
Object.defineProperty(this, name, {
configurable: true,
get() {
Object.defineProperty(this, name, {
configurable: true,
value: this.library.declare(name, ...args),
});
return this[name];
},
});
}
}
/**
* Holds constants which apply to various Subprocess operations.
* @namespace
* @lends Subprocess
*/
const SubprocessConstants = {
/**
* @property {integer} ERROR_END_OF_FILE
* The operation failed because the end of the file was reached.
* @constant
*/
ERROR_END_OF_FILE: 0xff7a0001,
/**
* @property {integer} ERROR_INVALID_JSON
* The operation failed because an invalid JSON was encountered.
* @constant
*/
ERROR_INVALID_JSON: 0xff7a0002,
/**
* @property {integer} ERROR_BAD_EXECUTABLE
* The operation failed because the given file did not exist, or
* could not be executed.
* @constant
*/
ERROR_BAD_EXECUTABLE: 0xff7a0003,
};
Object.freeze(SubprocessConstants);

View File

@ -0,0 +1,157 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported libc */
const LIBC = OS.Constants.libc;
const LIBC_CHOICES = ["libc.so", "libSystem.B.dylib", "a.out"];
const unix = {
pid_t: ctypes.int32_t,
pollfd: new ctypes.StructType("pollfd", [
{"fd": ctypes.int},
{"events": ctypes.short},
{"revents": ctypes.short},
]),
posix_spawn_file_actions_t: ctypes.uint8_t.array(
LIBC.OSFILE_SIZEOF_POSIX_SPAWN_FILE_ACTIONS_T),
WEXITSTATUS(status) {
return (status >> 8) & 0xff;
},
WTERMSIG(status) {
return status & 0x7f;
},
};
var libc = new Library("libc", LIBC_CHOICES, {
environ: [ctypes.char.ptr.ptr],
// Darwin-only.
_NSGetEnviron: [
ctypes.default_abi,
ctypes.char.ptr.ptr.ptr,
],
chdir: [
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr, /* path */
],
close: [
ctypes.default_abi,
ctypes.int,
ctypes.int, /* fildes */
],
fcntl: [
ctypes.default_abi,
ctypes.int,
ctypes.int, /* fildes */
ctypes.int, /* cmd */
ctypes.int, /* ... */
],
getcwd: [
ctypes.default_abi,
ctypes.char.ptr,
ctypes.char.ptr, /* buf */
ctypes.size_t, /* size */
],
kill: [
ctypes.default_abi,
ctypes.int,
unix.pid_t, /* pid */
ctypes.int, /* signal */
],
pipe: [
ctypes.default_abi,
ctypes.int,
ctypes.int.array(2), /* pipefd */
],
poll: [
ctypes.default_abi,
ctypes.int,
unix.pollfd.array(), /* fds */
ctypes.unsigned_int, /* nfds */
ctypes.int, /* timeout */
],
posix_spawn: [
ctypes.default_abi,
ctypes.int,
unix.pid_t.ptr, /* pid */
ctypes.char.ptr, /* path */
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
ctypes.voidptr_t, /* attrp */
ctypes.char.ptr.ptr, /* argv */
ctypes.char.ptr.ptr, /* envp */
],
posix_spawn_file_actions_addclose: [
ctypes.default_abi,
ctypes.int,
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
ctypes.int, /* fildes */
],
posix_spawn_file_actions_adddup2: [
ctypes.default_abi,
ctypes.int,
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
ctypes.int, /* fildes */
ctypes.int, /* newfildes */
],
posix_spawn_file_actions_destroy: [
ctypes.default_abi,
ctypes.int,
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
],
posix_spawn_file_actions_init: [
ctypes.default_abi,
ctypes.int,
unix.posix_spawn_file_actions_t.ptr, /* file_actions */
],
read: [
ctypes.default_abi,
ctypes.ssize_t,
ctypes.int, /* fildes */
ctypes.char.ptr, /* buf */
ctypes.size_t, /* nbyte */
],
waitpid: [
ctypes.default_abi,
unix.pid_t,
unix.pid_t, /* pid */
ctypes.int.ptr, /* status */
ctypes.int, /* options */
],
write: [
ctypes.default_abi,
ctypes.ssize_t,
ctypes.int, /* fildes */
ctypes.char.ptr, /* buf */
ctypes.size_t, /* nbyte */
],
});
unix.Fd = function(fd) {
return ctypes.CDataFinalizer(ctypes.int(fd), libc.close);
};

View File

@ -0,0 +1,346 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported LIBC, Win, createPipe, libc */
const LIBC = OS.Constants.libc;
const Win = OS.Constants.Win;
const LIBC_CHOICES = ["kernel32.dll"];
const win32 = {
// On Windows 64, winapi_abi is an alias for default_abi.
WINAPI: ctypes.winapi_abi,
BYTE: ctypes.uint8_t,
WORD: ctypes.uint16_t,
DWORD: ctypes.uint32_t,
UINT: ctypes.unsigned_int,
UCHAR: ctypes.unsigned_char,
BOOL: ctypes.bool,
HANDLE: ctypes.voidptr_t,
PVOID: ctypes.voidptr_t,
LPVOID: ctypes.voidptr_t,
CHAR: ctypes.char,
WCHAR: ctypes.jschar,
ULONG_PTR: ctypes.uintptr_t,
};
Object.assign(win32, {
LPSTR: win32.CHAR.ptr,
LPWSTR: win32.WCHAR.ptr,
LPBYTE: win32.BYTE.ptr,
LPDWORD: win32.DWORD.ptr,
LPHANDLE: win32.HANDLE.ptr,
});
Object.assign(win32, {
LPCSTR: win32.LPSTR,
LPCWSTR: win32.LPWSTR,
LPCVOID: win32.LPVOID,
});
Object.assign(win32, {
CREATE_NEW_CONSOLE: 0x00000010,
CREATE_UNICODE_ENVIRONMENT: 0x00000400,
CREATE_NO_WINDOW: 0x08000000,
STARTF_USESTDHANDLES: 0x0100,
DUPLICATE_CLOSE_SOURCE: 0x01,
DUPLICATE_SAME_ACCESS: 0x02,
ERROR_HANDLE_EOF: 38,
ERROR_BROKEN_PIPE: 109,
FILE_FLAG_OVERLAPPED: 0x40000000,
PIPE_TYPE_BYTE: 0x00,
PIPE_ACCESS_INBOUND: 0x01,
PIPE_ACCESS_OUTBOUND: 0x02,
PIPE_ACCESS_DUPLEX: 0x03,
PIPE_WAIT: 0x00,
PIPE_NOWAIT: 0x01,
STILL_ACTIVE: 259,
// These constants are 32-bit unsigned integers, but Windows defines
// them as negative integers cast to an unsigned type.
STD_INPUT_HANDLE: -10 + 0x100000000,
STD_OUTPUT_HANDLE: -11 + 0x100000000,
STD_ERROR_HANDLE: -12 + 0x100000000,
WAIT_TIMEOUT: 0x00000102,
WAIT_FAILED: 0xffffffff,
});
Object.assign(win32, {
OVERLAPPED: new ctypes.StructType("OVERLAPPED", [
{"Internal": win32.ULONG_PTR},
{"InternalHigh": win32.ULONG_PTR},
{"Offset": win32.DWORD},
{"OffsetHigh": win32.DWORD},
{"hEvent": win32.HANDLE},
]),
PROCESS_INFORMATION: new ctypes.StructType("PROCESS_INFORMATION", [
{"hProcess": win32.HANDLE},
{"hThread": win32.HANDLE},
{"dwProcessId": win32.DWORD},
{"dwThreadId": win32.DWORD},
]),
SECURITY_ATTRIBUTES: new ctypes.StructType("SECURITY_ATTRIBUTES", [
{"nLength": win32.DWORD},
{"lpSecurityDescriptor": win32.LPVOID},
{"bInheritHandle": win32.BOOL},
]),
STARTUPINFOW: new ctypes.StructType("STARTUPINFOW", [
{"cb": win32.DWORD},
{"lpReserved": win32.LPWSTR},
{"lpDesktop": win32.LPWSTR},
{"lpTitle": win32.LPWSTR},
{"dwX": win32.DWORD},
{"dwY": win32.DWORD},
{"dwXSize": win32.DWORD},
{"dwYSize": win32.DWORD},
{"dwXCountChars": win32.DWORD},
{"dwYCountChars": win32.DWORD},
{"dwFillAttribute": win32.DWORD},
{"dwFlags": win32.DWORD},
{"wShowWindow": win32.WORD},
{"cbReserved2": win32.WORD},
{"lpReserved2": win32.LPBYTE},
{"hStdInput": win32.HANDLE},
{"hStdOutput": win32.HANDLE},
{"hStdError": win32.HANDLE},
]),
});
var libc = new Library("libc", LIBC_CHOICES, {
CloseHandle: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hObject */
],
CreateEventW: [
win32.WINAPI,
win32.HANDLE,
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpEventAttributes */
win32.BOOL, /* bManualReset */
win32.BOOL, /* bInitialState */
win32.LPWSTR, /* lpName */
],
CreateFileW: [
win32.WINAPI,
win32.HANDLE,
win32.LPWSTR, /* lpFileName */
win32.DWORD, /* dwDesiredAccess */
win32.DWORD, /* dwShareMode */
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpSecurityAttributes */
win32.DWORD, /* dwCreationDisposition */
win32.DWORD, /* dwFlagsAndAttributes */
win32.HANDLE, /* opt hTemplateFile */
],
CreateNamedPipeW: [
win32.WINAPI,
win32.HANDLE,
win32.LPWSTR, /* lpName */
win32.DWORD, /* dwOpenMode */
win32.DWORD, /* dwPipeMode */
win32.DWORD, /* nMaxInstances */
win32.DWORD, /* nOutBufferSize */
win32.DWORD, /* nInBufferSize */
win32.DWORD, /* nDefaultTimeOut */
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpSecurityAttributes */
],
CreatePipe: [
win32.WINAPI,
win32.BOOL,
win32.LPHANDLE, /* out hReadPipe */
win32.LPHANDLE, /* out hWritePipe */
win32.SECURITY_ATTRIBUTES.ptr, /* opt lpPipeAttributes */
win32.DWORD, /* nSize */
],
CreateProcessW: [
win32.WINAPI,
win32.BOOL,
win32.LPCWSTR, /* lpApplicationName */
win32.LPWSTR, /* lpCommandLine */
win32.SECURITY_ATTRIBUTES.ptr, /* lpProcessAttributes */
win32.SECURITY_ATTRIBUTES.ptr, /* lpThreadAttributes */
win32.BOOL, /* bInheritHandle */
win32.DWORD, /* dwCreationFlags */
win32.LPVOID, /* opt lpEnvironment */
win32.LPCWSTR, /* opt lpCurrentDirectory */
win32.STARTUPINFOW.ptr, /* lpStartupInfo */
win32.PROCESS_INFORMATION.ptr, /* out lpProcessInformation */
],
DuplicateHandle: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hSourceProcessHandle */
win32.HANDLE, /* hSourceHandle */
win32.HANDLE, /* hTargetProcessHandle */
win32.LPHANDLE, /* out lpTargetHandle */
win32.DWORD, /* dwDesiredAccess */
win32.BOOL, /* bInheritHandle */
win32.DWORD, /* dwOptions */
],
FreeEnvironmentStringsW: [
win32.WINAPI,
win32.BOOL,
win32.LPCWSTR, /* lpszEnvironmentBlock */
],
GetCurrentProcess: [
win32.WINAPI,
win32.HANDLE,
],
GetCurrentProcessId: [
win32.WINAPI,
win32.DWORD,
],
GetEnvironmentStringsW: [
win32.WINAPI,
win32.LPCWSTR,
],
GetExitCodeProcess: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hProcess */
win32.LPDWORD, /* lpExitCode */
],
GetLastError: [
win32.WINAPI,
win32.DWORD,
],
GetOverlappedResult: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hFile */
win32.OVERLAPPED.ptr, /* lpOverlapped */
win32.LPDWORD, /* lpNumberOfBytesTransferred */
win32.BOOL, /* bWait */
],
GetStdHandle: [
win32.WINAPI,
win32.HANDLE,
win32.DWORD, /* nStdHandle */
],
ReadFile: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hFile */
win32.LPVOID, /* out lpBuffer */
win32.DWORD, /* nNumberOfBytesToRead */
win32.LPDWORD, /* opt out lpNumberOfBytesRead */
win32.OVERLAPPED.ptr, /* opt in/out lpOverlapped */
],
TerminateProcess: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hProcess */
win32.UINT, /* uExitCode */
],
WaitForMultipleObjects: [
win32.WINAPI,
win32.DWORD,
win32.DWORD, /* nCount */
win32.HANDLE.ptr, /* hHandles */
win32.BOOL, /* bWaitAll */
win32.DWORD, /* dwMilliseconds */
],
WaitForSingleObject: [
win32.WINAPI,
win32.DWORD,
win32.HANDLE, /* hHandle */
win32.BOOL, /* bWaitAll */
win32.DWORD, /* dwMilliseconds */
],
WriteFile: [
win32.WINAPI,
win32.BOOL,
win32.HANDLE, /* hFile */
win32.LPCVOID, /* lpBuffer */
win32.DWORD, /* nNumberOfBytesToRead */
win32.LPDWORD, /* opt out lpNumberOfBytesWritten */
win32.OVERLAPPED.ptr, /* opt in/out lpOverlapped */
],
});
let nextNamedPipeId = 0;
win32.Handle = function(handle) {
return ctypes.CDataFinalizer(win32.HANDLE(handle), libc.CloseHandle);
};
win32.createPipe = function(secAttr, readFlags = 0, writeFlags = 0, size = 0) {
readFlags |= win32.PIPE_ACCESS_INBOUND;
writeFlags |= Win.FILE_ATTRIBUTE_NORMAL;
if (size == 0) {
size = 4096;
}
let pid = libc.GetCurrentProcessId();
let pipeName = String.raw`\\.\Pipe\SubProcessPipe.${pid}.${nextNamedPipeId++}`;
let readHandle = libc.CreateNamedPipeW(
pipeName, readFlags,
win32.PIPE_TYPE_BYTE | win32.PIPE_WAIT,
1, /* number of connections */
size, /* output buffer size */
size, /* input buffer size */
0, /* timeout */
secAttr.address());
if (readHandle == Win.INVALID_HANDLE_VALUE) {
return [];
}
let writeHandle = libc.CreateFileW(
pipeName, Win.GENERIC_WRITE, 0, secAttr.address(),
Win.OPEN_EXISTING, writeFlags, 0);
if (writeHandle == Win.INVALID_HANDLE_VALUE) {
libc.CloseHandle(readHandle);
return [];
}
return [win32.Handle(readHandle),
win32.Handle(writeHandle)];
};

View File

@ -0,0 +1,120 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* eslint-disable mozilla/balanced-listeners */
/* exported SubprocessImpl */
/* globals BaseProcess */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
var EXPORTED_SYMBOLS = ["SubprocessImpl"];
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared_unix.js", this);
class Process extends BaseProcess {
static get WORKER_URL() {
return "resource://gre/modules/subprocess/subprocess_worker_unix.js";
}
}
var SubprocessUnix = {
Process,
call(options) {
return Process.create(options);
},
* getEnvironment() {
let environ;
if (OS.Constants.Sys.Name == "Darwin") {
environ = libc._NSGetEnviron().contents;
} else {
environ = libc.environ;
}
for (let envp = environ; !envp.contents.isNull(); envp = envp.increment()) {
let str = envp.contents.readString();
let idx = str.indexOf("=");
if (idx >= 0) {
yield [str.slice(0, idx),
str.slice(idx + 1)];
}
}
},
isExecutableFile: Task.async(function* isExecutable(path) {
if (!OS.Path.split(path).absolute) {
return false;
}
try {
let info = yield OS.File.stat(path);
// FIXME: We really want access(path, X_OK) here, but OS.File does not
// support it.
return !info.isDir && (info.unixMode & 0o111);
} catch (e) {
return false;
}
}),
/**
* Searches for the given executable file in the system executable
* file paths as specified by the PATH environment variable.
*
* On Windows, if the unadorned filename cannot be found, the
* extensions in the semicolon-separated list in the PATHEXT
* environment variable are successively appended to the original
* name and searched for in turn.
*
* @param {string} bin
* The name of the executable to find.
* @param {object} environment
* An object containing a key for each environment variable to be used
* in the search.
* @returns {Promise<string>}
*/
pathSearch: Task.async(function* (bin, environment) {
let split = OS.Path.split(bin);
if (split.absolute) {
if (yield this.isExecutableFile(bin)) {
return bin;
}
let error = new Error(`File at path "${bin}" does not exist, or is not executable`);
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
throw error;
}
let dirs = [];
if (environment.PATH) {
dirs = environment.PATH.split(":");
}
for (let dir of dirs) {
let path = OS.Path.join(dir, bin);
if (yield this.isExecutableFile(path)) {
return path;
}
}
let error = new Error(`Executable not found: ${bin}`);
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
throw error;
}),
};
var SubprocessImpl = SubprocessUnix;

View File

@ -0,0 +1,138 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* eslint-disable mozilla/balanced-listeners */
/* exported SubprocessImpl */
/* globals BaseProcess */
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
var EXPORTED_SYMBOLS = ["SubprocessImpl"];
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared.js", this);
Services.scriptloader.loadSubScript("resource://gre/modules/subprocess/subprocess_shared_win.js", this);
class Process extends BaseProcess {
static get WORKER_URL() {
return "resource://gre/modules/subprocess/subprocess_worker_win.js";
}
}
var SubprocessWin = {
Process,
call(options) {
return Process.create(options);
},
* getEnvironment() {
let env = libc.GetEnvironmentStringsW();
try {
for (let p = env, q = env; ; p = p.increment()) {
if (p.contents == "\0") {
if (String(p) == String(q)) {
break;
}
let str = q.readString();
q = p.increment();
let idx = str.indexOf("=");
if (idx == 0) {
idx = str.indexOf("=", 1);
}
if (idx >= 0) {
yield [str.slice(0, idx), str.slice(idx + 1)];
}
}
}
} finally {
libc.FreeEnvironmentStringsW(env);
}
},
isExecutableFile: Task.async(function* (path) {
if (!OS.Path.split(path).absolute) {
return false;
}
try {
let info = yield OS.File.stat(path);
return !(info.isDir || info.isSymlink);
} catch (e) {
return false;
}
}),
/**
* Searches for the given executable file in the system executable
* file paths as specified by the PATH environment variable.
*
* On Windows, if the unadorned filename cannot be found, the
* extensions in the semicolon-separated list in the PATHEXT
* environment variable are successively appended to the original
* name and searched for in turn.
*
* @param {string} bin
* The name of the executable to find.
* @param {object} environment
* An object containing a key for each environment variable to be used
* in the search.
* @returns {Promise<string>}
*/
pathSearch: Task.async(function* (bin, environment) {
let split = OS.Path.split(bin);
if (split.absolute) {
if (yield this.isExecutableFile(bin)) {
return bin;
}
let error = new Error(`File at path "${bin}" does not exist, or is not a normal file`);
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
throw error;
}
let dirs = [];
let exts = [];
if (environment.PATH) {
dirs = environment.PATH.split(";");
}
if (environment.PATHEXT) {
exts = environment.PATHEXT.split(";");
}
for (let dir of dirs) {
let path = OS.Path.join(dir, bin);
if (yield this.isExecutableFile(path)) {
return path;
}
for (let ext of exts) {
let file = path + ext;
if (yield this.isExecutableFile(file)) {
return file;
}
}
}
let error = new Error(`Executable not found: ${bin}`);
error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
throw error;
}),
};
var SubprocessImpl = SubprocessWin;

View File

@ -0,0 +1,193 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported BasePipe, BaseProcess, debug */
/* globals Process, io */
function debug(message) {
self.postMessage({msg: "debug", message});
}
class BasePipe {
constructor() {
this.closing = false;
this.closed = false;
this.closedPromise = new Promise(resolve => {
this.resolveClosed = resolve;
});
this.pending = [];
}
shiftPending() {
let result = this.pending.shift();
if (this.closing && this.pending.length == 0) {
this.close();
}
return result;
}
}
let nextProcessId = 0;
class BaseProcess {
constructor(options) {
this.id = nextProcessId++;
this.exitCode = null;
this.exitPromise = new Promise(resolve => {
this.resolveExit = resolve;
});
this.exitPromise.then(() => {
// The input file descriptors will be closed after poll
// reports that their input buffers are empty. If we close
// them now, we may lose output.
this.pipes[0].close(true);
});
this.pid = null;
this.pipes = [];
this.stringArrays = [];
this.spawn(options);
}
/**
* Creates a null-terminated array of pointers to null-terminated C-strings,
* and returns it.
*
* @param {string[]} strings
* The strings to convert into a C string array.
*
* @returns {ctypes.char.ptr.array()}
*/
stringArray(strings) {
let result = ctypes.char.ptr.array(strings.length + 1)();
let cstrings = strings.map(str => ctypes.char.array()(str));
for (let [i, cstring] of cstrings.entries()) {
result[i] = cstring;
}
// Char arrays used in char arg and environment vectors must be
// explicitly kept alive in a JS object, or they will be reaped
// by the GC if it runs before our process is started.
this.stringArrays.push(cstrings);
return result;
}
}
let requests = {
close(pipeId, force = false) {
let pipe = io.getPipe(pipeId);
return pipe.close(force).then(() => ({data: {}}));
},
spawn(options) {
let process = new Process(options);
let processId = process.id;
io.addProcess(process);
let fds = process.pipes.map(pipe => pipe.id);
return {data: {processId, fds, pid: process.pid}};
},
kill(processId, force = false) {
let process = io.getProcess(processId);
process.kill(force ? 9 : 15);
return {data: {}};
},
wait(processId) {
let process = io.getProcess(processId);
process.wait();
return process.exitPromise.then(exitCode => {
io.cleanupProcess(process);
return {data: {exitCode}};
});
},
read(pipeId, count) {
let pipe = io.getPipe(pipeId);
return pipe.read(count).then(buffer => {
return {data: {buffer}};
});
},
write(pipeId, buffer) {
let pipe = io.getPipe(pipeId);
return pipe.write(buffer).then(bytesWritten => {
return {data: {bytesWritten}};
});
},
getOpenFiles() {
return {data: new Set(io.pipes.keys())};
},
getProcesses() {
let data = new Map(Array.from(io.processes.values(),
proc => [proc.id, proc.pid]));
return {data};
},
};
onmessage = event => {
let {msg, msgId, args} = event.data;
new Promise(resolve => {
resolve(requests[msg](...args));
}).then(result => {
let response = {
msg: "success",
msgId,
data: result.data,
};
self.postMessage(response, result.transfer || []);
}).catch(error => {
if (error instanceof Error) {
error = {
message: error.message,
fileName: error.fileName,
lineNumber: error.lineNumber,
column: error.column,
stack: error.stack,
errorCode: error.errorCode,
};
}
self.postMessage({
msg: "failure",
msgId,
error,
});
}).catch(error => {
console.error(error);
self.postMessage({
msg: "failure",
msgId,
error: {},
});
});
};

View File

@ -0,0 +1,539 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported Process */
/* globals BaseProcess, BasePipe */
importScripts("resource://gre/modules/subprocess/subprocess_shared.js",
"resource://gre/modules/subprocess/subprocess_shared_unix.js",
"resource://gre/modules/subprocess/subprocess_worker_common.js");
const POLL_INTERVAL = 50;
const POLL_TIMEOUT = 0;
let io;
let nextPipeId = 0;
class Pipe extends BasePipe {
constructor(process, fd) {
super();
this.process = process;
this.fd = fd;
this.id = nextPipeId++;
}
get pollEvents() {
throw new Error("Not implemented");
}
/**
* Closes the file descriptor.
*
* @param {boolean} [force=false]
* If true, the file descriptor is closed immediately. If false, the
* file descriptor is closed after all current pending IO operations
* have completed.
*
* @returns {Promise<void>}
* Resolves when the file descriptor has been closed.
*/
close(force = false) {
if (!force && this.pending.length) {
this.closing = true;
return this.closedPromise;
}
for (let {reject} of this.pending) {
let error = new Error("File closed");
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
reject(error);
}
this.pending.length = 0;
if (!this.closed) {
this.fd.dispose();
this.closed = true;
this.resolveClosed();
io.pipes.delete(this.id);
io.updatePollFds();
}
return this.closedPromise;
}
/**
* Called when an error occurred while polling our file descriptor.
*/
onError() {
this.close(true);
this.process.wait();
}
}
class InputPipe extends Pipe {
/**
* A bit mask of poll() events which we currently wish to be notified of on
* this file descriptor.
*/
get pollEvents() {
if (this.pending.length) {
return LIBC.POLLIN;
}
return 0;
}
/**
* Asynchronously reads at most `length` bytes of binary data from the file
* descriptor into an ArrayBuffer of the same size. Returns a promise which
* resolves when the operation is complete.
*
* @param {integer} length
* The number of bytes to read.
*
* @returns {Promise<ArrayBuffer>}
*/
read(length) {
if (this.closing || this.closed) {
throw new Error("Attempt to read from closed pipe");
}
return new Promise((resolve, reject) => {
this.pending.push({resolve, reject, length});
io.updatePollFds();
});
}
/**
* Synchronously reads at most `count` bytes of binary data into an
* ArrayBuffer, and returns that buffer. If no data can be read without
* blocking, returns null instead.
*
* @param {integer} count
* The number of bytes to read.
*
* @returns {ArrayBuffer|null}
*/
readBuffer(count) {
let buffer = new ArrayBuffer(count);
let read = +libc.read(this.fd, buffer, buffer.byteLength);
if (read < 0 && ctypes.errno != LIBC.EAGAIN) {
this.onError();
}
if (read <= 0) {
return null;
}
if (read < buffer.byteLength) {
return ArrayBuffer.transfer(buffer, read);
}
return buffer;
}
/**
* Called when one of the IO operations matching the `pollEvents` mask may be
* performed without blocking.
*/
onReady() {
let reads = this.pending;
while (reads.length) {
let {resolve, length} = reads[0];
let buffer = this.readBuffer(length);
if (buffer) {
this.shiftPending();
resolve(buffer);
} else {
break;
}
}
if (reads.length == 0) {
io.updatePollFds();
}
}
}
class OutputPipe extends Pipe {
/**
* A bit mask of poll() events which we currently wish to be notified of on
* this file discriptor.
*/
get pollEvents() {
if (this.pending.length) {
return LIBC.POLLOUT;
}
return 0;
}
/**
* Asynchronously writes the given buffer to our file descriptor, and returns
* a promise which resolves when the operation is complete.
*
* @param {ArrayBuffer} buffer
* The buffer to write.
*
* @returns {Promise<integer>}
* Resolves to the number of bytes written when the operation is
* complete.
*/
write(buffer) {
if (this.closing || this.closed) {
throw new Error("Attempt to write to closed pipe");
}
return new Promise((resolve, reject) => {
this.pending.push({resolve, reject, buffer, length: buffer.byteLength});
io.updatePollFds();
});
}
/**
* Attempts to synchronously write the given buffer to our file descriptor.
* Writes only as many bytes as can be written without blocking, and returns
* the number of byes successfully written.
*
* Closes the file descriptor if an IO error occurs.
*
* @param {ArrayBuffer} buffer
* The buffer to write.
*
* @returns {integer}
* The number of bytes successfully written.
*/
writeBuffer(buffer) {
let bytesWritten = libc.write(this.fd, buffer, buffer.byteLength);
if (bytesWritten < 0 && ctypes.errno != LIBC.EAGAIN) {
this.onError();
}
return bytesWritten;
}
/**
* Called when one of the IO operations matching the `pollEvents` mask may be
* performed without blocking.
*/
onReady() {
let writes = this.pending;
while (writes.length) {
let {buffer, resolve, length} = writes[0];
let written = this.writeBuffer(buffer);
if (written == buffer.byteLength) {
resolve(length);
this.shiftPending();
} else if (written > 0) {
writes[0].buffer = buffer.slice(written);
} else {
break;
}
}
if (writes.length == 0) {
io.updatePollFds();
}
}
}
class Process extends BaseProcess {
/**
* Each Process object opens an additional pipe from the target object, which
* will be automatically closed when the process exits, but otherwise
* carries no data.
*
* This property contains a bit mask of poll() events which we wish to be
* notified of on this descriptor. We're not expecting any input from this
* pipe, but we need to poll for input until the process exits in order to be
* notified when the pipe closes.
*/
get pollEvents() {
if (this.exitCode === null) {
return LIBC.POLLIN;
}
return 0;
}
/**
* Kills the process with the given signal.
*
* @param {integer} signal
*/
kill(signal) {
libc.kill(this.pid, signal);
this.wait();
}
/**
* Initializes the IO pipes for use as standard input, output, and error
* descriptors in the spawned process.
*
* @returns {unix.Fd[]}
* The array of file descriptors belonging to the spawned process.
*/
initPipes(options) {
let stderr = options.stderr;
let our_pipes = [];
let their_pipes = new Map();
let pipe = input => {
let fds = ctypes.int.array(2)();
let res = libc.pipe(fds);
if (res == -1) {
throw new Error("Unable to create pipe");
}
fds = Array.from(fds, unix.Fd);
if (input) {
fds.reverse();
}
if (input) {
our_pipes.push(new InputPipe(this, fds[1]));
} else {
our_pipes.push(new OutputPipe(this, fds[1]));
}
libc.fcntl(fds[0], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
libc.fcntl(fds[1], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
libc.fcntl(fds[1], LIBC.F_SETFL, LIBC.O_NONBLOCK);
return fds[0];
};
their_pipes.set(0, pipe(false));
their_pipes.set(1, pipe(true));
if (stderr == "pipe") {
their_pipes.set(2, pipe(true));
} else if (stderr == "stdout") {
their_pipes.set(2, their_pipes.get(1));
}
// Create an additional pipe that we can use to monitor for process exit.
their_pipes.set(3, pipe(true));
this.fd = our_pipes.pop().fd;
this.pipes = our_pipes;
return their_pipes;
}
spawn(options) {
let {command, arguments: args} = options;
let argv = this.stringArray(args);
let envp = this.stringArray(options.environment);
let actions = unix.posix_spawn_file_actions_t();
let actionsp = actions.address();
let fds = this.initPipes(options);
let cwd;
try {
if (options.workdir) {
cwd = ctypes.char.array(LIBC.PATH_MAX)();
libc.getcwd(cwd, cwd.length);
if (libc.chdir(options.workdir) < 0) {
throw new Error(`Unable to change working directory to ${options.workdir}`);
}
}
libc.posix_spawn_file_actions_init(actionsp);
for (let [i, fd] of fds.entries()) {
libc.posix_spawn_file_actions_adddup2(actionsp, fd, i);
}
let pid = unix.pid_t();
let rv = libc.posix_spawn(pid.address(), command, actionsp, null, argv, envp);
if (rv != 0) {
for (let pipe of this.pipes) {
pipe.close();
}
throw new Error(`Failed to execute command "${command}"`);
}
this.pid = pid.value;
} finally {
libc.posix_spawn_file_actions_destroy(actionsp);
this.stringArrays.length = 0;
if (cwd) {
libc.chdir(cwd);
}
for (let fd of new Set(fds.values())) {
fd.dispose();
}
}
}
/**
* Called when input is available on our sentinel file descriptor.
*
* @see pollEvents
*/
onReady() {
// We're not actually expecting any input on this pipe. If we get any, we
// can't poll the pipe any further without reading it.
if (this.wait() == undefined) {
this.kill(9);
}
}
/**
* Called when an error occurred while polling our sentinel file descriptor.
*
* @see pollEvents
*/
onError() {
this.wait();
}
/**
* Attempts to wait for the process's exit status, without blocking. If
* successful, resolves the `exitPromise` to the process's exit value.
*
* @returns {integer|null}
* The process's exit status, if it has already exited.
*/
wait() {
if (this.exitCode !== null) {
return this.exitCode;
}
let status = ctypes.int();
let res = libc.waitpid(this.pid, status.address(), LIBC.WNOHANG);
if (res == this.pid) {
let sig = unix.WTERMSIG(status.value);
if (sig) {
this.exitCode = -sig;
} else {
this.exitCode = unix.WEXITSTATUS(status.value);
}
this.fd.dispose();
this.resolveExit(this.exitCode);
return this.exitCode;
}
}
}
io = {
pollFds: null,
pollHandlers: null,
pipes: new Map(),
processes: new Map(),
interval: null,
getPipe(pipeId) {
let pipe = this.pipes.get(pipeId);
if (!pipe) {
let error = new Error("File closed");
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
throw error;
}
return pipe;
},
getProcess(processId) {
let process = this.processes.get(processId);
if (!process) {
throw new Error(`Invalid process ID: ${processId}`);
}
return process;
},
updatePollFds() {
let handlers = [...this.pipes.values(),
...this.processes.values()];
handlers = handlers.filter(handler => handler.pollEvents);
let pollfds = unix.pollfd.array(handlers.length)();
for (let [i, handler] of handlers.entries()) {
let pollfd = pollfds[i];
pollfd.fd = handler.fd;
pollfd.events = handler.pollEvents;
pollfd.revents = 0;
}
this.pollFds = pollfds;
this.pollHandlers = handlers;
if (pollfds.length && !this.interval) {
this.interval = setInterval(this.poll.bind(this), POLL_INTERVAL);
} else if (!pollfds.length && this.interval) {
clearInterval(this.interval);
this.interval = null;
}
},
poll() {
let handlers = this.pollHandlers;
let pollfds = this.pollFds;
let count = libc.poll(pollfds, pollfds.length, POLL_TIMEOUT);
for (let i = 0; count && i < pollfds.length; i++) {
let pollfd = pollfds[i];
if (pollfd.revents) {
count--;
let handler = handlers[i];
try {
if (pollfd.revents & handler.pollEvents) {
handler.onReady();
}
if (pollfd.revents & (LIBC.POLLERR | LIBC.POLLHUP | LIBC.POLLNVAL)) {
handler.onError();
}
} catch (e) {
console.error(e);
debug(`Worker error: ${e} :: ${e.stack}`);
handler.onError();
}
pollfd.revents = 0;
}
}
},
addProcess(process) {
this.processes.set(process.id, process);
for (let pipe of process.pipes) {
this.pipes.set(pipe.id, pipe);
}
},
cleanupProcess(process) {
this.processes.delete(process.id);
},
};

View File

@ -0,0 +1,594 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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";
/* exported Process */
/* globals BaseProcess, BasePipe, win32 */
importScripts("resource://gre/modules/subprocess/subprocess_shared.js",
"resource://gre/modules/subprocess/subprocess_shared_win.js",
"resource://gre/modules/subprocess/subprocess_worker_common.js");
const POLL_INTERVAL = 50;
const POLL_TIMEOUT = 0;
// The exit code that we send when we forcibly terminate a process.
const TERMINATE_EXIT_CODE = 0x7f;
let io;
let nextPipeId = 0;
class Pipe extends BasePipe {
constructor(process, origHandle) {
super();
let handle = win32.HANDLE();
let curProc = libc.GetCurrentProcess();
libc.DuplicateHandle(curProc, origHandle, curProc, handle.address(),
0, false /* inheritable */, win32.DUPLICATE_SAME_ACCESS);
origHandle.dispose();
this.id = nextPipeId++;
this.process = process;
this.handle = win32.Handle(handle);
let event = libc.CreateEventW(null, false, false, null);
this.overlapped = win32.OVERLAPPED();
this.overlapped.hEvent = event;
this._event = win32.Handle(event);
this.buffer = null;
}
get event() {
if (this.pending.length) {
return this._event;
}
return null;
}
maybeClose() {}
/**
* Closes the file handle.
*
* @param {boolean} [force=false]
* If true, the file handle is closed immediately. If false, the
* file handle is closed after all current pending IO operations
* have completed.
*
* @returns {Promise<void>}
* Resolves when the file handle has been closed.
*/
close(force = false) {
if (!force && this.pending.length) {
this.closing = true;
return this.closedPromise;
}
for (let {reject} of this.pending) {
let error = new Error("File closed");
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
reject(error);
}
this.pending.length = 0;
this.buffer = null;
if (!this.closed) {
this.handle.dispose();
this._event.dispose();
io.pipes.delete(this.handle);
this.handle = null;
this.closed = true;
this.resolveClosed();
io.updatePollEvents();
}
return this.closedPromise;
}
/**
* Called when an error occurred while attempting an IO operation on our file
* handle.
*/
onError() {
this.close(true);
}
}
class InputPipe extends Pipe {
/**
* Queues the next chunk of data to be read from the pipe if, and only if,
* there is no IO operation currently pending.
*/
readNext() {
if (this.buffer === null) {
this.readBuffer(this.pending[0].length);
}
}
/**
* Closes the pipe if there is a pending read operation with no more
* buffered data to be read.
*/
maybeClose() {
if (this.buffer) {
let read = win32.DWORD();
let ok = libc.GetOverlappedResult(
this.handle, this.overlapped.address(),
read.address(), false);
if (!ok) {
this.onError();
}
}
}
/**
* Asynchronously reads at most `length` bytes of binary data from the file
* descriptor into an ArrayBuffer of the same size. Returns a promise which
* resolves when the operation is complete.
*
* @param {integer} length
* The number of bytes to read.
*
* @returns {Promise<ArrayBuffer>}
*/
read(length) {
if (this.closing || this.closed) {
throw new Error("Attempt to read from closed pipe");
}
return new Promise((resolve, reject) => {
this.pending.push({resolve, reject, length});
this.readNext();
});
}
/**
* Initializes an overlapped IO read operation to read exactly `count` bytes
* into a new ArrayBuffer, which is stored in the `buffer` property until the
* operation completes.
*
* @param {integer} count
* The number of bytes to read.
*/
readBuffer(count) {
this.buffer = new ArrayBuffer(count);
let ok = libc.ReadFile(this.handle, this.buffer, count,
null, this.overlapped.address());
if (!ok && (!this.process.handle || libc.GetLastError())) {
this.onError();
} else {
io.updatePollEvents();
}
}
/**
* Called when our pending overlapped IO operation has completed, whether
* successfully or in failure.
*/
onReady() {
let read = win32.DWORD();
let ok = libc.GetOverlappedResult(
this.handle, this.overlapped.address(),
read.address(), false);
read = read.value;
if (!ok) {
this.onError();
} else if (read > 0) {
let buffer = this.buffer;
this.buffer = null;
let {resolve} = this.shiftPending();
if (read == buffer.byteLength) {
resolve(buffer);
} else {
resolve(ArrayBuffer.transfer(buffer, read));
}
if (this.pending.length) {
this.readNext();
} else {
io.updatePollEvents();
}
}
}
}
class OutputPipe extends Pipe {
/**
* Queues the next chunk of data to be written to the pipe if, and only if,
* there is no IO operation currently pending.
*/
writeNext() {
if (this.buffer === null) {
this.writeBuffer(this.pending[0].buffer);
}
}
/**
* Asynchronously writes the given buffer to our file descriptor, and returns
* a promise which resolves when the operation is complete.
*
* @param {ArrayBuffer} buffer
* The buffer to write.
*
* @returns {Promise<integer>}
* Resolves to the number of bytes written when the operation is
* complete.
*/
write(buffer) {
if (this.closing || this.closed) {
throw new Error("Attempt to write to closed pipe");
}
return new Promise((resolve, reject) => {
this.pending.push({resolve, reject, buffer});
this.writeNext();
});
}
/**
* Initializes an overapped IO read operation to write the data in `buffer` to
* our file descriptor.
*
* @param {ArrayBuffer} buffer
* The buffer to write.
*/
writeBuffer(buffer) {
this.buffer = buffer;
let ok = libc.WriteFile(this.handle, buffer, buffer.byteLength,
null, this.overlapped.address());
if (!ok && libc.GetLastError()) {
this.onError();
} else {
io.updatePollEvents();
}
}
/**
* Called when our pending overlapped IO operation has completed, whether
* successfully or in failure.
*/
onReady() {
let written = win32.DWORD();
let ok = libc.GetOverlappedResult(
this.handle, this.overlapped.address(),
written.address(), false);
written = written.value;
if (!ok || written != this.buffer.byteLength) {
this.onError();
} else if (written > 0) {
let {resolve} = this.shiftPending();
this.buffer = null;
resolve(written);
if (this.pending.length) {
this.writeNext();
} else {
io.updatePollEvents();
}
}
}
}
class Process extends BaseProcess {
constructor(...args) {
super(...args);
this.killed = false;
}
/**
* Returns our process handle for use as an event in a WaitForMultipleObjects
* call.
*/
get event() {
return this.handle;
}
/**
* Forcibly terminates the process.
*/
kill() {
this.killed = true;
libc.TerminateProcess(this.handle, TERMINATE_EXIT_CODE);
}
/**
* Initializes the IO pipes for use as standard input, output, and error
* descriptors in the spawned process.
*
* @returns {win32.Handle[]}
* The array of file handles belonging to the spawned process.
*/
initPipes({stderr}) {
let our_pipes = [];
let their_pipes = [];
let secAttr = new win32.SECURITY_ATTRIBUTES();
secAttr.nLength = win32.SECURITY_ATTRIBUTES.size;
secAttr.bInheritHandle = true;
let pipe = input => {
if (input) {
let handles = win32.createPipe(secAttr, win32.FILE_FLAG_OVERLAPPED);
our_pipes.push(new InputPipe(this, handles[0]));
return handles[1];
} else {
let handles = win32.createPipe(secAttr, 0, win32.FILE_FLAG_OVERLAPPED);
our_pipes.push(new OutputPipe(this, handles[1]));
return handles[0];
}
};
their_pipes[0] = pipe(false);
their_pipes[1] = pipe(true);
if (stderr == "pipe") {
their_pipes[2] = pipe(true);
} else {
let srcHandle;
if (stderr == "stdout") {
srcHandle = their_pipes[1];
} else {
srcHandle = libc.GetStdHandle(win32.STD_ERROR_HANDLE);
}
let handle = win32.HANDLE();
let curProc = libc.GetCurrentProcess();
let ok = libc.DuplicateHandle(curProc, srcHandle, curProc, handle.address(),
0, true /* inheritable */,
win32.DUPLICATE_SAME_ACCESS);
their_pipes[2] = ok && win32.Handle(handle);
}
if (!their_pipes.every(handle => handle)) {
throw new Error("Failed to create pipe");
}
this.pipes = our_pipes;
return their_pipes;
}
/**
* Creates a null-separated, null-terminated string list.
*/
stringList(strings) {
// Remove empty strings, which would terminate the list early.
strings = strings.filter(string => string);
let string = strings.join("\0") + "\0\0";
return win32.WCHAR.array()(string);
}
/**
* Quotes a string for use as a single command argument, using Windows quoting
* conventions.
*
* @see https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx
*/
quoteString(str) {
if (!/[\s"]/.test(str)) {
return str;
}
let escaped = str.replace(/(\\*)("|$)/g, (m0, m1, m2) => {
if (m2) {
m2 = `\\${m2}`;
}
return `${m1}${m1}${m2}`;
});
return `"${escaped}"`;
}
spawn(options) {
let {command, arguments: args} = options;
args = args.map(arg => this.quoteString(arg));
let envp = this.stringList(options.environment);
let handles = this.initPipes(options);
let processFlags = win32.CREATE_NO_WINDOW
| win32.CREATE_UNICODE_ENVIRONMENT;
let startupInfo = new win32.STARTUPINFOW();
startupInfo.cb = win32.STARTUPINFOW.size;
startupInfo.dwFlags = win32.STARTF_USESTDHANDLES;
startupInfo.hStdInput = handles[0];
startupInfo.hStdOutput = handles[1];
startupInfo.hStdError = handles[2];
let procInfo = new win32.PROCESS_INFORMATION();
let ok = libc.CreateProcessW(
command, args.join(" "),
null, /* Security attributes */
null, /* Thread security attributes */
true, /* Inherits handles */
processFlags, envp, options.workdir,
startupInfo.address(),
procInfo.address());
for (let handle of new Set(handles)) {
handle.dispose();
}
if (!ok) {
for (let pipe of this.pipes) {
pipe.close();
}
throw new Error("Failed to create process");
}
libc.CloseHandle(procInfo.hThread);
this.handle = win32.Handle(procInfo.hProcess);
this.pid = procInfo.dwProcessId;
}
/**
* Called when our process handle is signaled as active, meaning the process
* has exited.
*/
onReady() {
this.wait();
}
/**
* Attempts to wait for the process's exit status, without blocking. If
* successful, resolves the `exitPromise` to the process's exit value.
*
* @returns {integer|null}
* The process's exit status, if it has already exited.
*/
wait() {
if (this.exitCode !== null) {
return this.exitCode;
}
let status = win32.DWORD();
let ok = libc.GetExitCodeProcess(this.handle, status.address());
if (ok && status.value != win32.STILL_ACTIVE) {
let exitCode = status.value;
if (this.killed && exitCode == TERMINATE_EXIT_CODE) {
// If we forcibly terminated the process, return the force kill exit
// code that we return on other platforms.
exitCode = -9;
}
this.resolveExit(exitCode);
this.exitCode = exitCode;
this.handle.dispose();
this.handle = null;
for (let pipe of this.pipes) {
pipe.maybeClose();
}
io.updatePollEvents();
return exitCode;
}
}
}
io = {
events: null,
eventHandlers: null,
pipes: new Map(),
processes: new Map(),
interval: null,
getPipe(pipeId) {
let pipe = this.pipes.get(pipeId);
if (!pipe) {
let error = new Error("File closed");
error.errorCode = SubprocessConstants.ERROR_END_OF_FILE;
throw error;
}
return pipe;
},
getProcess(processId) {
let process = this.processes.get(processId);
if (!process) {
throw new Error(`Invalid process ID: ${processId}`);
}
return process;
},
updatePollEvents() {
let handlers = [...this.pipes.values(),
...this.processes.values()];
handlers = handlers.filter(handler => handler.event);
this.eventHandlers = handlers;
let handles = handlers.map(handler => handler.event);
this.events = win32.HANDLE.array()(handles);
if (handles.length && !this.interval) {
this.interval = setInterval(this.poll.bind(this), POLL_INTERVAL);
} else if (!handlers.length && this.interval) {
clearInterval(this.interval);
this.interval = null;
}
},
poll() {
for (;;) {
let events = this.events;
let handlers = this.eventHandlers;
let result = libc.WaitForMultipleObjects(events.length, events,
false, POLL_TIMEOUT);
if (result < handlers.length) {
try {
handlers[result].onReady();
} catch (e) {
console.error(e);
debug(`Worker error: ${e} :: ${e.stack}`);
handlers[result].onError();
}
} else {
break;
}
}
},
addProcess(process) {
this.processes.set(process.id, process);
for (let pipe of process.pipes) {
this.pipes.set(pipe.id, pipe);
}
},
cleanupProcess(process) {
this.processes.delete(process.id);
},
};

View File

@ -0,0 +1,3 @@
{
"extends": "../../../../../testing/xpcshell/xpcshell.eslintrc",
}

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python2
from __future__ import print_function
import os
import signal
import struct
import sys
def output(line):
sys.stdout.write(struct.pack('@I', len(line)))
sys.stdout.write(line)
sys.stdout.flush()
def echo_loop():
while True:
line = sys.stdin.readline()
if not line:
break
output(line)
cmd = sys.argv[1]
if cmd == 'echo':
echo_loop()
elif cmd == 'exit':
sys.exit(int(sys.argv[2]))
elif cmd == 'env':
for var in sys.argv[2:]:
output(os.environ.get(var, ''))
elif cmd == 'pwd':
output(os.path.abspath(os.curdir))
elif cmd == 'print_args':
for arg in sys.argv[2:]:
output(arg)
elif cmd == 'ignore_sigterm':
signal.signal(signal.SIGTERM, signal.SIG_IGN)
output('Ready')
while True:
signal.pause()
elif cmd == 'print':
sys.stdout.write(sys.argv[2])
sys.stderr.write(sys.argv[3])

View File

@ -0,0 +1,14 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
"resource://gre/modules/Subprocess.jsm");

View File

@ -0,0 +1,676 @@
"use strict";
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
let PYTHON;
let PYTHON_BIN;
let PYTHON_DIR;
const TEST_SCRIPT = do_get_file("data_test_script.py").path;
let read = pipe => {
return pipe.readUint32().then(count => {
return pipe.readString(count);
});
};
let readAll = Task.async(function* (pipe) {
let result = [];
let string;
while ((string = yield pipe.readString())) {
result.push(string);
}
return result.join("");
});
add_task(function* setup() {
PYTHON = yield Subprocess.pathSearch(env.get("PYTHON"));
PYTHON_BIN = OS.Path.basename(PYTHON);
PYTHON_DIR = OS.Path.dirname(PYTHON);
});
add_task(function* test_subprocess_io() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
Assert.throws(() => { proc.stdout.read(-1); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.read(1.1); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.read(Infinity); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.read(NaN); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.readString(-1); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.readString(1.1); },
/non-negative integer/);
Assert.throws(() => { proc.stdout.readJSON(-1); },
/positive integer/);
Assert.throws(() => { proc.stdout.readJSON(0); },
/positive integer/);
Assert.throws(() => { proc.stdout.readJSON(1.1); },
/positive integer/);
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let outputPromise = read(proc.stdout);
yield new Promise(resolve => setTimeout(resolve, 100));
let [output] = yield Promise.all([
outputPromise,
proc.stdin.write(LINE1),
]);
equal(output, LINE1, "Got expected output");
// Make sure it succeeds whether the write comes before or after the
// read.
let inputPromise = proc.stdin.write(LINE2);
yield new Promise(resolve => setTimeout(resolve, 100));
[output] = yield Promise.all([
read(proc.stdout),
inputPromise,
]);
equal(output, LINE2, "Got expected output");
let JSON_BLOB = {foo: {bar: "baz"}};
inputPromise = proc.stdin.write(JSON.stringify(JSON_BLOB) + "\n");
output = yield proc.stdout.readUint32().then(count => {
return proc.stdout.readJSON(count);
});
Assert.deepEqual(output, JSON_BLOB, "Got expected JSON output");
yield proc.stdin.close();
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_large_io() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
const LINE = "I'm a leaf on the wind.\n";
const BUFFER_SIZE = 4096;
// Create a message that's ~3/4 the input buffer size.
let msg = Array(BUFFER_SIZE * .75 / 16 | 0).fill("0123456789abcdef").join("") + "\n";
// This sequence of writes and reads crosses several buffer size
// boundaries, and causes some branches of the read buffer code to be
// exercised which are not exercised by other tests.
proc.stdin.write(msg);
proc.stdin.write(msg);
proc.stdin.write(LINE);
let output = yield read(proc.stdout);
equal(output, msg, "Got the expected output");
output = yield read(proc.stdout);
equal(output, msg, "Got the expected output");
output = yield read(proc.stdout);
equal(output, LINE, "Got the expected output");
proc.stdin.close();
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_huge() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
// This should be large enough to fill most pipe input/output buffers.
const MESSAGE_SIZE = 1024 * 16;
let msg = Array(MESSAGE_SIZE).fill("0123456789abcdef").join("") + "\n";
proc.stdin.write(msg);
let output = yield read(proc.stdout);
equal(output, msg, "Got the expected output");
proc.stdin.close();
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_stderr_default() {
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
});
equal(proc.stderr, undefined, "There should be no stderr pipe by default");
let stdout = yield readAll(proc.stdout);
equal(stdout, LINE1, "Got the expected stdout output");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_stderr_pipe() {
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
stderr: "pipe",
});
let [stdout, stderr] = yield Promise.all([
readAll(proc.stdout),
readAll(proc.stderr),
]);
equal(stdout, LINE1, "Got the expected stdout output");
equal(stderr, LINE2, "Got the expected stderr output");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_stderr_merged() {
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
stderr: "stdout",
});
equal(proc.stderr, undefined, "There should be no stderr pipe by default");
let stdout = yield readAll(proc.stdout);
equal(stdout, LINE1 + LINE2, "Got the expected merged stdout output");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_read_after_exit() {
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
stderr: "pipe",
});
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Process exited with expected code");
let [stdout, stderr] = yield Promise.all([
readAll(proc.stdout),
readAll(proc.stderr),
]);
equal(stdout, LINE1, "Got the expected stdout output");
equal(stderr, LINE2, "Got the expected stderr output");
});
add_task(function* test_subprocess_lazy_close_output() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
const LINE1 = "I'm a leaf on the wind.\n";
const LINE2 = "Watch how I soar.\n";
let writePromises = [
proc.stdin.write(LINE1),
proc.stdin.write(LINE2),
];
let closedPromise = proc.stdin.close();
let output1 = yield read(proc.stdout);
let output2 = yield read(proc.stdout);
yield Promise.all([...writePromises, closedPromise]);
equal(output1, LINE1, "Got expected output");
equal(output2, LINE2, "Got expected output");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_lazy_close_input() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
let readPromise = proc.stdout.readUint32();
let closedPromise = proc.stdout.close();
const LINE = "I'm a leaf on the wind.\n";
proc.stdin.write(LINE);
proc.stdin.close();
let len = yield readPromise;
equal(len, LINE.length);
yield closedPromise;
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_force_close() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
let readPromise = proc.stdout.readUint32();
let closedPromise = proc.stdout.close(true);
yield Assert.rejects(
readPromise,
function(e) {
equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
"Got the expected error code");
return /File closed/.test(e.message);
},
"Promise should be rejected when file is closed");
yield closedPromise;
yield proc.stdin.close();
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_eof() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
let readPromise = proc.stdout.readUint32();
yield proc.stdin.close();
yield Assert.rejects(
readPromise,
function(e) {
equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
"Got the expected error code");
return /File closed/.test(e.message);
},
"Promise should be rejected on EOF");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_invalid_json() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
const LINE = "I'm a leaf on the wind.\n";
proc.stdin.write(LINE);
proc.stdin.close();
let count = yield proc.stdout.readUint32();
let readPromise = proc.stdout.readJSON(count);
yield Assert.rejects(
readPromise,
function(e) {
equal(e.errorCode, Subprocess.ERROR_INVALID_JSON,
"Got the expected error code");
return /SyntaxError/.test(e);
},
"Promise should be rejected on EOF");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_wait() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "exit", "42"],
});
let {exitCode} = yield proc.wait();
equal(exitCode, 42, "Got expected exit code");
});
add_task(function* test_subprocess_pathSearch() {
let promise = Subprocess.call({
command: PYTHON_BIN,
arguments: ["-u", TEST_SCRIPT, "exit", "13"],
environment: {
PATH: PYTHON_DIR,
},
});
Assert.rejects(
promise,
function(error) {
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
},
"Subprocess.call should fail for a bad executable");
});
add_task(function* test_subprocess_workdir() {
let procDir = yield OS.File.getCurrentDirectory();
let tmpDir = OS.Constants.Path.tmpDir;
notEqual(procDir, tmpDir,
"Current process directory must not be the current temp directory");
function* pwd(options) {
let proc = yield Subprocess.call(Object.assign({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "pwd"],
}, options));
let pwd = read(proc.stdout);
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
return pwd;
}
let dir = yield pwd({});
equal(dir, procDir, "Process should normally launch in current process directory");
dir = yield pwd({workdir: tmpDir});
equal(dir, tmpDir, "Process should launch in the directory specified in `workdir`");
dir = yield OS.File.getCurrentDirectory();
equal(dir, procDir, "`workdir` should not change the working directory of the current process");
});
add_task(function* test_subprocess_term() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
// Windows does not support killing processes gracefully, so they will
// always exit with -9 there.
let retVal = AppConstants.platform == "win" ? -9 : -15;
// Kill gracefully with the default timeout of 300ms.
let {exitCode} = yield proc.kill();
equal(exitCode, retVal, "Got expected exit code");
({exitCode} = yield proc.wait());
equal(exitCode, retVal, "Got expected exit code");
});
add_task(function* test_subprocess_kill() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "echo"],
});
// Force kill with no gracefull termination timeout.
let {exitCode} = yield proc.kill(0);
equal(exitCode, -9, "Got expected exit code");
({exitCode} = yield proc.wait());
equal(exitCode, -9, "Got expected exit code");
});
add_task(function* test_subprocess_kill_timeout() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "ignore_sigterm"],
});
// Wait for the process to set up its signal handler and tell us it's
// ready.
let msg = yield read(proc.stdout);
equal(msg, "Ready", "Process is ready");
// Kill gracefully with the default timeout of 300ms.
// Expect a force kill after 300ms, since the process traps SIGTERM.
const TIMEOUT = 300;
let startTime = Date.now();
let {exitCode} = yield proc.kill(TIMEOUT);
// Graceful termination is not supported on Windows, so don't bother
// testing the timeout there.
if (AppConstants.platform != "win") {
let diff = Date.now() - startTime;
ok(diff >= TIMEOUT, `Process was killed after ${diff}ms (expected ~${TIMEOUT}ms)`);
}
equal(exitCode, -9, "Got expected exit code");
({exitCode} = yield proc.wait());
equal(exitCode, -9, "Got expected exit code");
});
add_task(function* test_subprocess_arguments() {
let args = [
String.raw`C:\Program Files\Company\Program.exe`,
String.raw`\\NETWORK SHARE\Foo Directory${"\\"}`,
String.raw`foo bar baz`,
String.raw`"foo bar baz"`,
String.raw`foo " bar`,
String.raw`Thing \" with "" "\" \\\" \\\\" quotes\\" \\`,
];
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "print_args", ...args],
});
for (let [i, arg] of args.entries()) {
let val = yield read(proc.stdout);
equal(val, arg, `Got correct value for args[${i}]`);
}
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_environment() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
environment: {
FOO: "BAR",
},
});
let path = yield read(proc.stdout);
let foo = yield read(proc.stdout);
equal(path, "", "Got expected $PATH value");
equal(foo, "BAR", "Got expected $FOO value");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_subprocess_environmentAppend() {
let proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
environmentAppend: true,
environment: {
FOO: "BAR",
},
});
let path = yield read(proc.stdout);
let foo = yield read(proc.stdout);
equal(path, env.get("PATH"), "Got expected $PATH value");
equal(foo, "BAR", "Got expected $FOO value");
let {exitCode} = yield proc.wait();
equal(exitCode, 0, "Got expected exit code");
proc = yield Subprocess.call({
command: PYTHON,
arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
environmentAppend: true,
});
path = yield read(proc.stdout);
foo = yield read(proc.stdout);
equal(path, env.get("PATH"), "Got expected $PATH value");
equal(foo, "", "Got expected $FOO value");
({exitCode} = yield proc.wait());
equal(exitCode, 0, "Got expected exit code");
});
add_task(function* test_bad_executable() {
// Test with a non-executable file.
let textFile = do_get_file("data_text_file.txt").path;
let promise = Subprocess.call({
command: textFile,
arguments: [],
});
Assert.rejects(
promise,
function(error) {
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
},
"Subprocess.call should fail for a bad executable");
// Test with a nonexistent file.
promise = Subprocess.call({
command: textFile + ".doesNotExist",
arguments: [],
});
Assert.rejects(
promise,
function(error) {
return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
},
"Subprocess.call should fail for a bad executable");
});
add_task(function* test_cleanup() {
let {SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
let worker = SubprocessImpl.Process.getWorker();
let openFiles = yield worker.call("getOpenFiles", []);
let processes = yield worker.call("getProcesses", []);
equal(openFiles.size, 0, "No remaining open files");
equal(processes.size, 0, "No remaining processes");
});

View File

@ -0,0 +1,17 @@
"use strict";
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
add_task(function* test_getEnvironment() {
env.set("FOO", "BAR");
let environment = Subprocess.getEnvironment();
equal(environment.FOO, "BAR");
equal(environment.PATH, env.get("PATH"));
env.set("FOO", null);
environment = Subprocess.getEnvironment();
equal(environment.FOO || "", "");
});

View File

@ -0,0 +1,73 @@
"use strict";
let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
const PYTHON = env.get("PYTHON");
const PYTHON_BIN = OS.Path.basename(PYTHON);
const PYTHON_DIR = OS.Path.dirname(PYTHON);
const DOES_NOT_EXIST = OS.Path.join(OS.Constants.Path.tmpDir,
"ThisPathDoesNotExist");
const PATH_SEP = AppConstants.platform == "win" ? ";" : ":";
add_task(function* test_pathSearchAbsolute() {
let env = {};
let path = yield Subprocess.pathSearch(PYTHON, env);
equal(path, PYTHON, "Full path resolves even with no PATH.");
env.PATH = "";
path = yield Subprocess.pathSearch(PYTHON, env);
equal(path, PYTHON, "Full path resolves even with empty PATH.");
yield Assert.rejects(
Subprocess.pathSearch(DOES_NOT_EXIST, env),
function(e) {
equal(e.errorCode, Subprocess.ERROR_BAD_EXECUTABLE,
"Got the expected error code");
return /File at path .* does not exist, or is not (executable|a normal file)/.test(e.message);
},
"Absolute path should throw for a nonexistent execuable");
});
add_task(function* test_pathSearchRelative() {
let env = {};
yield Assert.rejects(
Subprocess.pathSearch(PYTHON_BIN, env),
function(e) {
equal(e.errorCode, Subprocess.ERROR_BAD_EXECUTABLE,
"Got the expected error code");
return /Executable not found:/.test(e.message);
},
"Relative path should not be found when PATH is missing");
env.PATH = [DOES_NOT_EXIST, PYTHON_DIR].join(PATH_SEP);
let path = yield Subprocess.pathSearch(PYTHON_BIN, env);
equal(path, PYTHON, "Correct executable should be found in the path");
});
add_task({
skip_if: () => AppConstants.platform != "win",
}, function* test_pathSearch_PATHEXT() {
ok(PYTHON_BIN.endsWith(".exe"), "Python executable must end with .exe");
const python_bin = PYTHON_BIN.slice(0, -4);
let env = {
PATH: PYTHON_DIR,
PATHEXT: [".com", ".exe", ".foobar"].join(";"),
};
let path = yield Subprocess.pathSearch(python_bin, env);
equal(path, PYTHON, "Correct executable should be found in the path, with guessed extension");
});
// IMPORTANT: Do not add any tests beyond this point without removing
// the `skip_if` condition from the previous task, or it will prevent
// all succeeding tasks from running when it does not match.

View File

@ -0,0 +1,13 @@
[DEFAULT]
head = head.js
tail =
firefox-appdir = browser
skip-if = os == 'android'
subprocess = true
support-files =
data_text_file.txt
data_test_script.py
[test_subprocess.js]
[test_subprocess_getEnvironment.js]
[test_subprocess_pathSearch.js]