summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/subprocess
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/subprocess')
-rw-r--r--toolkit/modules/subprocess/.eslintrc.js13
-rw-r--r--toolkit/modules/subprocess/Subprocess.sys.mjs198
-rw-r--r--toolkit/modules/subprocess/docs/index.rst226
-rw-r--r--toolkit/modules/subprocess/moz.build35
-rw-r--r--toolkit/modules/subprocess/subprocess_common.sys.mjs711
-rw-r--r--toolkit/modules/subprocess/subprocess_shared.js108
-rw-r--r--toolkit/modules/subprocess/subprocess_shared_unix.js116
-rw-r--r--toolkit/modules/subprocess/subprocess_shared_win.js532
-rw-r--r--toolkit/modules/subprocess/subprocess_unix.sys.mjs203
-rw-r--r--toolkit/modules/subprocess/subprocess_unix.worker.js611
-rw-r--r--toolkit/modules/subprocess/subprocess_win.sys.mjs173
-rw-r--r--toolkit/modules/subprocess/subprocess_win.worker.js785
-rw-r--r--toolkit/modules/subprocess/subprocess_worker_common.js211
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/data_test_script.py59
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/data_text_file.txt0
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/head.js13
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/test_subprocess.js870
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js15
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js87
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/xpcshell.toml20
20 files changed, 4986 insertions, 0 deletions
diff --git a/toolkit/modules/subprocess/.eslintrc.js b/toolkit/modules/subprocess/.eslintrc.js
new file mode 100644
index 0000000000..7640781589
--- /dev/null
+++ b/toolkit/modules/subprocess/.eslintrc.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ extends: "../../components/extensions/.eslintrc.js",
+
+ rules: {
+ "no-console": "off",
+ },
+};
diff --git a/toolkit/modules/subprocess/Subprocess.sys.mjs b/toolkit/modules/subprocess/Subprocess.sys.mjs
new file mode 100644
index 0000000000..ffbeb0acbb
--- /dev/null
+++ b/toolkit/modules/subprocess/Subprocess.sys.mjs
@@ -0,0 +1,198 @@
+/* -*- 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.
+ */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { SubprocessConstants } from "resource://gre/modules/subprocess/subprocess_common.sys.mjs";
+
+const lazy = {};
+
+if (AppConstants.platform == "win") {
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SubprocessImpl: "resource://gre/modules/subprocess/subprocess_win.sys.mjs",
+ });
+} else {
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SubprocessImpl: "resource://gre/modules/subprocess/subprocess_unix.sys.mjs",
+ });
+}
+
+function encodeEnvVar(name, value) {
+ if (typeof name === "string" && typeof value === "string") {
+ return `${name}=${value}`;
+ }
+
+ let encoder = new TextEncoder();
+ function encode(val) {
+ return typeof val === "string" ? encoder.encode(val) : val;
+ }
+
+ return Uint8Array.of(...encode(name), ...encode("="), ...encode(value), 0);
+}
+
+function platformSupportsDisclaimedSpawn() {
+ return AppConstants.isPlatformAndVersionAtLeast("macosx", 18);
+}
+
+/**
+ * Allows for creation of and communication with OS-level sub-processes.
+ *
+ * @namespace
+ */
+export 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 executable 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. Values that are `=== null` are ignored. 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. Values that are `=== null` are removed
+ * from the environment. 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.
+ *
+ * @param {boolean} [options.disclaim]
+ * macOS-specific option for 10.14+ OS versions. If true, enables a
+ * macOS-specific process launch option allowing the parent process to
+ * disclaim responsibility for the child process with respect to privacy/
+ * security permission prompts and decisions. This option is ignored on
+ * platforms that do not support it.
+ *
+ * @returns {Promise<Process>}
+ *
+ * @throws {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;
+ options.disclaim = options.disclaim || false;
+
+ let environment = {};
+ if (!options.environment || options.environmentAppend) {
+ environment = this.getEnvironment();
+ }
+
+ if (options.environment) {
+ Object.assign(environment, options.environment);
+ }
+
+ options.environment = Object.entries(environment)
+ .map(([key, val]) => (val !== null ? encodeEnvVar(key, val) : null))
+ .filter(s => s);
+
+ options.arguments = Array.from(options.arguments || []);
+
+ if (options.disclaim && !platformSupportsDisclaimedSpawn()) {
+ options.disclaim = false;
+ }
+
+ return Promise.resolve(
+ lazy.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 lazy.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 lazy.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} command
+ * 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 = lazy.SubprocessImpl.pathSearch(command, environment);
+ return Promise.resolve(path);
+ },
+};
+
+Object.assign(Subprocess, SubprocessConstants);
+Object.freeze(Subprocess);
+
+export function getSubprocessImplForTest() {
+ return lazy.SubprocessImpl;
+}
diff --git a/toolkit/modules/subprocess/docs/index.rst b/toolkit/modules/subprocess/docs/index.rst
new file mode 100644
index 0000000000..983fe30d4d
--- /dev/null
+++ b/toolkit/modules/subprocess/docs/index.rst
@@ -0,0 +1,226 @@
+.. _Subprocess:
+
+=================
+Subprocess 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"));
diff --git a/toolkit/modules/subprocess/moz.build b/toolkit/modules/subprocess/moz.build
new file mode 100644
index 0000000000..8c79d7f20c
--- /dev/null
+++ b/toolkit/modules/subprocess/moz.build
@@ -0,0 +1,35 @@
+# -*- Mode: python; 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.sys.mjs",
+]
+
+EXTRA_JS_MODULES.subprocess += [
+ "subprocess_common.sys.mjs",
+ "subprocess_shared.js",
+ "subprocess_worker_common.js",
+]
+
+if CONFIG["OS_TARGET"] == "WINNT":
+ EXTRA_JS_MODULES.subprocess += [
+ "subprocess_shared_win.js",
+ "subprocess_win.sys.mjs",
+ "subprocess_win.worker.js",
+ ]
+else:
+ EXTRA_JS_MODULES.subprocess += [
+ "subprocess_shared_unix.js",
+ "subprocess_unix.sys.mjs",
+ "subprocess_unix.worker.js",
+ ]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"]
+
+SPHINX_TREES["toolkit_modules/subprocess"] = ["docs"]
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
diff --git a/toolkit/modules/subprocess/subprocess_common.sys.mjs b/toolkit/modules/subprocess/subprocess_common.sys.mjs
new file mode 100644
index 0000000000..aa3a28fb7b
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_common.sys.mjs
@@ -0,0 +1,711 @@
+/* -*- 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/. */
+
+/* eslint-disable mozilla/balanced-listeners */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+var obj = {};
+Services.scriptloader.loadSubScript(
+ "resource://gre/modules/subprocess/subprocess_shared.js",
+ obj
+);
+
+const { ArrayBuffer_transfer } = obj;
+export const SubprocessConstants = obj.SubprocessConstants;
+
+const BUFFER_SIZE = 32768;
+
+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.
+ */
+export class PromiseWorker extends ChromeWorker {
+ constructor(url) {
+ super(url);
+
+ this.listeners = new Map();
+ this.pendingResponses = new Map();
+
+ this.addListener("close", this.onClose.bind(this));
+ 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);
+
+ this.shutdown = this.shutdown.bind(this);
+ lazy.AsyncShutdown.webWorkersShutdown.addBlocker(
+ "Subprocess.sys.mjs: Shut down IO worker",
+ this.shutdown
+ );
+ }
+
+ onClose() {
+ lazy.AsyncShutdown.webWorkersShutdown.removeBlocker(this.shutdown);
+ }
+
+ shutdown() {
+ return this.call("shutdown", []);
+ }
+
+ /**
+ * 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) {
+ console.error(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.
+ *
+ * @throws {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.
+ *
+ * @param {ArrayBuffer} buffer
+ * An input buffer to append to the current buffered input.
+ * @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>}
+ *
+ * @throws {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>}
+ *
+ * @throws {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>}
+ *
+ * @throws {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>}
+ *
+ * @throws {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
+ * @augments BaseProcess
+ */
+
+/**
+ * Represents a currently-running process, and allows interaction with it.
+ */
+export 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");
+ }
+
+ static get WorkerClass() {
+ return PromiseWorker;
+ }
+
+ /**
+ * 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 this.WorkerClass(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) {
+ lazy.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;
+ }
+}
diff --git a/toolkit/modules/subprocess/subprocess_shared.js b/toolkit/modules/subprocess/subprocess_shared.js
new file mode 100644
index 0000000000..d59a14a351
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_shared.js
@@ -0,0 +1,108 @@
+/* -*- 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 ArrayBuffer_transfer, Library, SubprocessConstants */
+
+// ctypes is either already available in the chrome worker scope, or defined
+// in scope via loadSubScript.
+/* global ctypes */
+
+/**
+ * Returns a new ArrayBuffer whose contents have been taken from the `buffer`'s
+ * data and then is either truncated or zero-extended by `size`. If `size` is
+ * undefined, the `byteLength` of the `buffer` is used. This operation leaves
+ * `buffer` in a detached state.
+ *
+ * @param {ArrayBuffer} buffer
+ * @param {integer} [size = buffer.byteLength]
+ * @returns {ArrayBuffer}
+ */
+var 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 = {};
+
+var Library = 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
+ */
+var 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,
+ /**
+ * @property {integer} ERROR_INVALID_OPTION
+ * The operation failed because an invalid option was provided.
+ * @constant
+ */
+ ERROR_INVALID_OPTION: 0xff7a0004,
+};
+
+Object.freeze(SubprocessConstants);
diff --git a/toolkit/modules/subprocess/subprocess_shared_unix.js b/toolkit/modules/subprocess/subprocess_shared_unix.js
new file mode 100644
index 0000000000..345e5122e4
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_shared_unix.js
@@ -0,0 +1,116 @@
+/* -*- 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, libc */
+
+// ctypes is either already available in the chrome worker scope, or defined
+// in scope via loadSubScript.
+/* global ctypes */
+
+// This file is loaded into the same scope as subprocess_shared.js.
+/* import-globals-from subprocess_shared.js */
+
+var LIBC = ChromeUtils.getLibcConstants();
+
+const LIBC_CHOICES = ["a.out"];
+
+const unix = {
+ pid_t: ctypes.int32_t,
+
+ pollfd: new ctypes.StructType("pollfd", [
+ { fd: ctypes.int },
+ { events: ctypes.short },
+ { revents: ctypes.short },
+ ]),
+
+ 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],
+
+ setenv: [
+ ctypes.default_abi,
+ ctypes.int,
+ ctypes.char.ptr,
+ ctypes.char.ptr,
+ ctypes.int,
+ ],
+
+ 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 */,
+ ],
+
+ 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);
+};
diff --git a/toolkit/modules/subprocess/subprocess_shared_win.js b/toolkit/modules/subprocess/subprocess_shared_win.js
new file mode 100644
index 0000000000..964e647128
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_shared_win.js
@@ -0,0 +1,532 @@
+/* -*- 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 createPipe, libc, win32 */
+
+// ctypes is either already available in the chrome worker scope, or defined
+// in scope via loadSubScript.
+/* global ctypes */
+
+// This file is loaded into the same scope as subprocess_shared.js.
+/* import-globals-from subprocess_shared.js */
+
+const LIBC_CHOICES = ["kernel32.dll"];
+
+var win32 = {
+ // On Windows 64, winapi_abi is an alias for default_abi.
+ WINAPI: ctypes.winapi_abi,
+
+ VOID: ctypes.void_t,
+
+ BYTE: ctypes.uint8_t,
+ WORD: ctypes.uint16_t,
+ DWORD: ctypes.uint32_t,
+ LONG: ctypes.long,
+ LARGE_INTEGER: ctypes.int64_t,
+ ULONGLONG: ctypes.uint64_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,
+
+ SIZE_T: ctypes.size_t,
+ PSIZE_T: ctypes.size_t.ptr,
+};
+
+Object.assign(win32, {
+ DWORD_PTR: win32.ULONG_PTR,
+
+ LPSTR: win32.CHAR.ptr,
+ LPWSTR: win32.WCHAR.ptr,
+
+ LPBYTE: win32.BYTE.ptr,
+ LPDWORD: win32.DWORD.ptr,
+ LPHANDLE: win32.HANDLE.ptr,
+
+ // This is an opaque type.
+ PROC_THREAD_ATTRIBUTE_LIST: ctypes.char.array(),
+ LPPROC_THREAD_ATTRIBUTE_LIST: ctypes.char.ptr,
+});
+
+Object.assign(win32, {
+ LPCSTR: win32.LPSTR,
+ LPCWSTR: win32.LPWSTR,
+ LPCVOID: win32.LPVOID,
+});
+
+Object.assign(win32, {
+ INVALID_HANDLE_VALUE: ctypes.cast(ctypes.int64_t(-1), win32.HANDLE),
+ NULL_HANDLE_VALUE: ctypes.cast(ctypes.uintptr_t(0), win32.HANDLE),
+
+ CREATE_SUSPENDED: 0x00000004,
+ CREATE_NEW_CONSOLE: 0x00000010,
+ CREATE_UNICODE_ENVIRONMENT: 0x00000400,
+ CREATE_NO_WINDOW: 0x08000000,
+ CREATE_BREAKAWAY_FROM_JOB: 0x01000000,
+ EXTENDED_STARTUPINFO_PRESENT: 0x00080000,
+
+ STARTF_USESTDHANDLES: 0x0100,
+
+ DUPLICATE_CLOSE_SOURCE: 0x01,
+ DUPLICATE_SAME_ACCESS: 0x02,
+
+ ERROR_HANDLE_EOF: 38,
+ ERROR_BROKEN_PIPE: 109,
+ ERROR_INSUFFICIENT_BUFFER: 122,
+
+ FILE_ATTRIBUTE_NORMAL: 0x00000080,
+ FILE_FLAG_OVERLAPPED: 0x40000000,
+
+ GENERIC_WRITE: 0x40000000,
+
+ OPEN_EXISTING: 0x00000003,
+
+ 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,
+
+ PROC_THREAD_ATTRIBUTE_HANDLE_LIST: 0x00020002,
+
+ JobObjectBasicLimitInformation: 2,
+ JobObjectExtendedLimitInformation: 9,
+
+ JOB_OBJECT_LIMIT_BREAKAWAY_OK: 0x00000800,
+
+ // 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, {
+ JOBOBJECT_BASIC_LIMIT_INFORMATION: new ctypes.StructType(
+ "JOBOBJECT_BASIC_LIMIT_INFORMATION",
+ [
+ { PerProcessUserTimeLimit: win32.LARGE_INTEGER },
+ { PerJobUserTimeLimit: win32.LARGE_INTEGER },
+ { LimitFlags: win32.DWORD },
+ { MinimumWorkingSetSize: win32.SIZE_T },
+ { MaximumWorkingSetSize: win32.SIZE_T },
+ { ActiveProcessLimit: win32.DWORD },
+ { Affinity: win32.ULONG_PTR },
+ { PriorityClass: win32.DWORD },
+ { SchedulingClass: win32.DWORD },
+ ]
+ ),
+
+ IO_COUNTERS: new ctypes.StructType("IO_COUNTERS", [
+ { ReadOperationCount: win32.ULONGLONG },
+ { WriteOperationCount: win32.ULONGLONG },
+ { OtherOperationCount: win32.ULONGLONG },
+ { ReadTransferCount: win32.ULONGLONG },
+ { WriteTransferCount: win32.ULONGLONG },
+ { OtherTransferCount: win32.ULONGLONG },
+ ]),
+});
+
+Object.assign(win32, {
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION: new ctypes.StructType(
+ "JOBOBJECT_EXTENDED_LIMIT_INFORMATION",
+ [
+ { BasicLimitInformation: win32.JOBOBJECT_BASIC_LIMIT_INFORMATION },
+ { IoInfo: win32.IO_COUNTERS },
+ { ProcessMemoryLimit: win32.SIZE_T },
+ { JobMemoryLimit: win32.SIZE_T },
+ { PeakProcessMemoryUsed: win32.SIZE_T },
+ { PeakJobMemoryUsed: win32.SIZE_T },
+ ]
+ ),
+
+ 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 },
+ ]),
+});
+
+Object.assign(win32, {
+ STARTUPINFOEXW: new ctypes.StructType("STARTUPINFOEXW", [
+ { StartupInfo: win32.STARTUPINFOW },
+ { lpAttributeList: win32.LPPROC_THREAD_ATTRIBUTE_LIST },
+ ]),
+});
+
+var libc = new Library("libc", LIBC_CHOICES, {
+ AssignProcessToJobObject: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.HANDLE /* hJob */,
+ win32.HANDLE /* hProcess */,
+ ],
+
+ 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 */,
+ ],
+
+ CreateJobObjectW: [
+ win32.WINAPI,
+ win32.HANDLE,
+ win32.SECURITY_ATTRIBUTES.ptr /* opt lpJobAttributes */,
+ win32.LPWSTR /* lpName */,
+ ],
+
+ 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 */,
+ ],
+
+ CreateSemaphoreW: [
+ win32.WINAPI,
+ win32.HANDLE,
+ win32.SECURITY_ATTRIBUTES.ptr /* opt lpSemaphoreAttributes */,
+ win32.LONG /* lInitialCount */,
+ win32.LONG /* lMaximumCount */,
+ win32.LPCWSTR /* opt lpName */,
+ ],
+
+ DeleteProcThreadAttributeList: [
+ win32.WINAPI,
+ win32.VOID,
+ win32.LPPROC_THREAD_ATTRIBUTE_LIST /* in/out lpAttributeList */,
+ ],
+
+ 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 */,
+ ],
+
+ 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 */],
+
+ InitializeProcThreadAttributeList: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.LPPROC_THREAD_ATTRIBUTE_LIST /* out opt lpAttributeList */,
+ win32.DWORD /* dwAttributeCount */,
+ win32.DWORD /* dwFlags */,
+ win32.PSIZE_T /* in/out lpSize */,
+ ],
+
+ 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 */,
+ ],
+
+ ReleaseSemaphore: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.HANDLE /* hSemaphore */,
+ win32.LONG /* lReleaseCount */,
+ win32.LONG.ptr /* opt out lpPreviousCount */,
+ ],
+
+ ResumeThread: [win32.WINAPI, win32.DWORD, win32.HANDLE /* hThread */],
+
+ SetInformationJobObject: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.HANDLE /* hJob */,
+ ctypes.int /* JobObjectInfoClass */,
+ win32.LPVOID /* lpJobObjectInfo */,
+ win32.DWORD /* cbJobObjectInfoLengt */,
+ ],
+
+ TerminateJobObject: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.HANDLE /* hJob */,
+ win32.UINT /* uExitCode */,
+ ],
+
+ TerminateProcess: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.HANDLE /* hProcess */,
+ win32.UINT /* uExitCode */,
+ ],
+
+ UpdateProcThreadAttribute: [
+ win32.WINAPI,
+ win32.BOOL,
+ win32.LPPROC_THREAD_ATTRIBUTE_LIST /* in/out lpAttributeList */,
+ win32.DWORD /* dwFlags */,
+ win32.DWORD_PTR /* Attribute */,
+ win32.PVOID /* lpValue */,
+ win32.SIZE_T /* cbSize */,
+ win32.PVOID /* out opt lpPreviousValue */,
+ win32.PSIZE_T /* opt lpReturnSize */,
+ ],
+
+ 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 |= win32.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()
+ );
+
+ let isInvalid = handle =>
+ String(handle) == String(win32.INVALID_HANDLE_VALUE);
+
+ if (isInvalid(readHandle)) {
+ return [];
+ }
+
+ let writeHandle = libc.CreateFileW(
+ pipeName,
+ win32.GENERIC_WRITE,
+ 0,
+ secAttr.address(),
+ win32.OPEN_EXISTING,
+ writeFlags,
+ null
+ );
+
+ if (isInvalid(writeHandle)) {
+ libc.CloseHandle(readHandle);
+ return [];
+ }
+
+ return [win32.Handle(readHandle), win32.Handle(writeHandle)];
+};
+
+win32.createThreadAttributeList = function (handles) {
+ try {
+ void libc.InitializeProcThreadAttributeList;
+ void libc.DeleteProcThreadAttributeList;
+ void libc.UpdateProcThreadAttribute;
+ } catch (e) {
+ // This is only supported in Windows Vista and later.
+ return null;
+ }
+
+ let size = win32.SIZE_T();
+ if (
+ !libc.InitializeProcThreadAttributeList(null, 1, 0, size.address()) &&
+ ctypes.winLastError != win32.ERROR_INSUFFICIENT_BUFFER
+ ) {
+ return null;
+ }
+
+ let attrList = win32.PROC_THREAD_ATTRIBUTE_LIST(size.value);
+
+ if (!libc.InitializeProcThreadAttributeList(attrList, 1, 0, size.address())) {
+ return null;
+ }
+
+ let ok = libc.UpdateProcThreadAttribute(
+ attrList,
+ 0,
+ win32.PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ handles,
+ handles.constructor.size,
+ null,
+ null
+ );
+
+ if (!ok) {
+ libc.DeleteProcThreadAttributeList(attrList);
+ return null;
+ }
+
+ return attrList;
+};
diff --git a/toolkit/modules/subprocess/subprocess_unix.sys.mjs b/toolkit/modules/subprocess/subprocess_unix.sys.mjs
new file mode 100644
index 0000000000..59b5873af2
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_unix.sys.mjs
@@ -0,0 +1,203 @@
+/* -*- 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/. */
+
+import {
+ BaseProcess,
+ PromiseWorker,
+} from "resource://gre/modules/subprocess/subprocess_common.sys.mjs";
+
+import { ctypes } from "resource://gre/modules/ctypes.sys.mjs";
+
+var obj = { ctypes };
+Services.scriptloader.loadSubScript(
+ "resource://gre/modules/subprocess/subprocess_shared.js",
+ obj
+);
+Services.scriptloader.loadSubScript(
+ "resource://gre/modules/subprocess/subprocess_shared_unix.js",
+ obj
+);
+
+const { SubprocessConstants, LIBC } = obj;
+
+// libc is exported for tests.
+export var libc = obj.libc;
+
+class UnixPromiseWorker extends PromiseWorker {
+ constructor(...args) {
+ super(...args);
+
+ let fds = ctypes.int.array(2)();
+ let res = libc.pipe(fds);
+ if (res == -1) {
+ throw new Error("Unable to create pipe");
+ }
+
+ this.signalFd = fds[1];
+
+ libc.fcntl(fds[0], LIBC.F_SETFL, LIBC.O_NONBLOCK);
+ libc.fcntl(fds[0], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
+ libc.fcntl(fds[1], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
+
+ this.call("init", [{ signalFd: fds[0] }]);
+ }
+
+ closePipe() {
+ if (this.signalFd) {
+ libc.close(this.signalFd);
+ this.signalFd = null;
+ }
+ }
+
+ onClose() {
+ this.closePipe();
+ super.onClose();
+ }
+
+ signalWorker() {
+ libc.write(this.signalFd, new ArrayBuffer(1), 1);
+ }
+
+ postMessage(...args) {
+ this.signalWorker();
+ return super.postMessage(...args);
+ }
+}
+
+class Process extends BaseProcess {
+ static get WORKER_URL() {
+ return "resource://gre/modules/subprocess/subprocess_unix.worker.js";
+ }
+
+ static get WorkerClass() {
+ return UnixPromiseWorker;
+ }
+}
+
+// Convert a null-terminated char pointer into a sized char array, and then
+// convert that into a JS typed array.
+// The resulting array will not be null-terminated.
+function ptrToUint8Array(input) {
+ let { cast, uint8_t } = ctypes;
+
+ let len = 0;
+ for (
+ let ptr = cast(input, uint8_t.ptr);
+ ptr.contents;
+ ptr = ptr.increment()
+ ) {
+ len++;
+ }
+
+ let aryPtr = cast(input, uint8_t.array(len).ptr);
+ return new Uint8Array(aryPtr.contents);
+}
+
+var SubprocessUnix = {
+ Process,
+
+ call(options) {
+ return Process.create(options);
+ },
+
+ *getEnvironment() {
+ let environ;
+ if (Services.appinfo.OS === "Darwin") {
+ environ = libc._NSGetEnviron().contents;
+ } else {
+ environ = libc.environ;
+ }
+
+ const EQUAL = "=".charCodeAt(0);
+ let decoder = new TextDecoder("utf-8", { fatal: true });
+
+ function decode(array) {
+ try {
+ return decoder.decode(array);
+ } catch (e) {
+ return array;
+ }
+ }
+
+ for (
+ let envp = environ;
+ !envp.isNull() && !envp.contents.isNull();
+ envp = envp.increment()
+ ) {
+ let buf = ptrToUint8Array(envp.contents);
+
+ for (let i = 0; i < buf.length; i++) {
+ if (buf[i] == EQUAL) {
+ yield [decode(buf.subarray(0, i)), decode(buf.subarray(i + 1))];
+ break;
+ }
+ }
+ }
+ },
+
+ isExecutableFile: async function isExecutable(path) {
+ if (!PathUtils.isAbsolute(path)) {
+ return false;
+ }
+
+ try {
+ let info = await IOUtils.stat(path);
+
+ // FIXME: We really want access(path, X_OK) here, but IOUtils does not
+ // support it.
+ return info.type !== "directory" && info.permissions & 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>}
+ */
+ async pathSearch(bin, environment) {
+ if (PathUtils.isAbsolute(bin)) {
+ if (await 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 (typeof environment.PATH === "string") {
+ dirs = environment.PATH.split(":");
+ }
+
+ for (let dir of dirs) {
+ let path = PathUtils.join(dir, bin);
+
+ if (await this.isExecutableFile(path)) {
+ return path;
+ }
+ }
+ let error = new Error(`Executable not found: ${bin}`);
+ error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
+ throw error;
+ },
+};
+
+export var SubprocessImpl = SubprocessUnix;
diff --git a/toolkit/modules/subprocess/subprocess_unix.worker.js b/toolkit/modules/subprocess/subprocess_unix.worker.js
new file mode 100644
index 0000000000..85632d2398
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_unix.worker.js
@@ -0,0 +1,611 @@
+/* -*- 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 */
+
+/* import-globals-from subprocess_shared.js */
+/* import-globals-from subprocess_shared_unix.js */
+/* import-globals-from subprocess_worker_common.js */
+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_TIMEOUT = 5000;
+
+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.
+ *
+ * @returns {boolean}
+ * True if any data was successfully read.
+ */
+ onReady() {
+ let result = false;
+ let reads = this.pending;
+ while (reads.length) {
+ let { resolve, length } = reads[0];
+
+ let buffer = this.readBuffer(length);
+ if (buffer) {
+ result = true;
+ this.shiftPending();
+ resolve(buffer);
+ } else {
+ break;
+ }
+ }
+
+ if (!reads.length) {
+ io.updatePollFds();
+ }
+ return result;
+ }
+}
+
+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) {
+ io.updatePollFds();
+ }
+ }
+}
+
+class Signal {
+ constructor(fd) {
+ this.fd = fd;
+ }
+
+ cleanup() {
+ libc.close(this.fd);
+ this.fd = null;
+ }
+
+ get pollEvents() {
+ return LIBC.POLLIN;
+ }
+
+ /**
+ * Called when an error occurred while polling our file descriptor.
+ */
+ onError() {
+ io.shutdown();
+ }
+
+ /**
+ * Called when one of the IO operations matching the `pollEvents` mask may be
+ * performed without blocking.
+ */
+ onReady() {
+ let buffer = new ArrayBuffer(16);
+ let count = +libc.read(this.fd, buffer, buffer.byteLength);
+ if (count > 0) {
+ io.messageCount += count;
+ }
+ }
+}
+
+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.
+ *
+ * @param {object} options
+ * The Subprocess options object for this 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 fds = this.initPipes(options);
+
+ let launchOptions = {
+ environment: options.environment,
+ disclaim: options.disclaim,
+ fdMap: [],
+ };
+
+ // Check for truthiness to avoid chdir("null")
+ if (options.workdir) {
+ launchOptions.workdir = options.workdir;
+ }
+
+ for (let [dst, src] of fds.entries()) {
+ launchOptions.fdMap.push({ src, dst });
+ }
+
+ try {
+ this.pid = IOUtils.launchProcess(options.arguments, launchOptions);
+ } finally {
+ 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 there's a failure here and we get any errno other than EINTR, it
+ // means that the process has been reaped by another thread (most likely
+ // the nspr process wait thread), and its actual exit status is not
+ // available to us. In that case, we have to assume success.
+ if (res == 0 || (res == -1 && ctypes.errno == LIBC.EINTR)) {
+ return null;
+ }
+
+ let sig = unix.WTERMSIG(status.value);
+ if (sig) {
+ this.exitCode = -sig;
+ } else {
+ this.exitCode = unix.WEXITSTATUS(status.value);
+ }
+
+ this.fd.dispose();
+ io.updatePollFds();
+ this.resolveExit(this.exitCode);
+ return this.exitCode;
+ }
+}
+
+io = {
+ pollFds: null,
+ pollHandlers: null,
+
+ pipes: new Map(),
+
+ processes: new Map(),
+
+ messageCount: 0,
+
+ running: true,
+
+ polling: false,
+
+ init(details) {
+ this.signal = new Signal(details.signalFd);
+ this.updatePollFds();
+
+ setTimeout(this.loop.bind(this), 0);
+ },
+
+ shutdown() {
+ if (this.running) {
+ this.running = false;
+
+ this.signal.cleanup();
+ this.signal = null;
+
+ self.postMessage({ msg: "close" });
+ self.close();
+ }
+ },
+
+ 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.signal,
+ ...this.pipes.values(),
+ ...this.processes.values(),
+ ];
+
+ handlers = handlers.filter(handler => handler.pollEvents);
+
+ // Our poll loop is only useful if we've got at least 1 thing to poll other than our own
+ // signal.
+ if (handlers.length == 1) {
+ this.polling = false;
+ } else if (!this.polling && this.running) {
+ // Restart the poll loop if necessary:
+ setTimeout(this.loop.bind(this), 0);
+ this.polling = true;
+ }
+
+ 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;
+ },
+
+ loop() {
+ this.poll();
+ if (this.running && this.polling) {
+ setTimeout(this.loop.bind(this), 0);
+ }
+ },
+
+ poll() {
+ let handlers = this.pollHandlers;
+ let pollfds = this.pollFds;
+
+ let timeout = this.messageCount > 0 ? 0 : POLL_TIMEOUT;
+ let count = libc.poll(pollfds, pollfds.length, timeout);
+
+ for (let i = 0; count && i < pollfds.length; i++) {
+ let pollfd = pollfds[i];
+ if (pollfd.revents) {
+ count--;
+
+ let handler = handlers[i];
+ try {
+ let success = false;
+ if (pollfd.revents & handler.pollEvents) {
+ success = handler.onReady();
+ }
+ // Only call the error handler in this iteration if we didn't also
+ // have a success. This is necessary because Linux systems set POLLHUP
+ // on a pipe when it's closed but there's still buffered data to be
+ // read, and Darwin sets POLLIN and POLLHUP on a closed pipe, even
+ // when there's no data to be read.
+ if (
+ !success &&
+ 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);
+ },
+};
diff --git a/toolkit/modules/subprocess/subprocess_win.sys.mjs b/toolkit/modules/subprocess/subprocess_win.sys.mjs
new file mode 100644
index 0000000000..baf8640235
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_win.sys.mjs
@@ -0,0 +1,173 @@
+/* -*- 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/. */
+
+import {
+ BaseProcess,
+ PromiseWorker,
+} from "resource://gre/modules/subprocess/subprocess_common.sys.mjs";
+
+import { ctypes } from "resource://gre/modules/ctypes.sys.mjs";
+
+var obj = { ctypes };
+Services.scriptloader.loadSubScript(
+ "resource://gre/modules/subprocess/subprocess_shared.js",
+ obj
+);
+Services.scriptloader.loadSubScript(
+ "resource://gre/modules/subprocess/subprocess_shared_win.js",
+ obj
+);
+
+const { SubprocessConstants } = obj;
+
+// libc and win32 are exported for tests.
+export const libc = obj.libc;
+export const win32 = obj.win32;
+
+class WinPromiseWorker extends PromiseWorker {
+ constructor(...args) {
+ super(...args);
+
+ this.signalEvent = libc.CreateSemaphoreW(null, 0, 32, null);
+
+ this.call("init", [
+ {
+ comspec: Services.env.get("COMSPEC"),
+ signalEvent: String(
+ ctypes.cast(this.signalEvent, ctypes.uintptr_t).value
+ ),
+ },
+ ]);
+ }
+
+ signalWorker() {
+ libc.ReleaseSemaphore(this.signalEvent, 1, null);
+ }
+
+ postMessage(...args) {
+ this.signalWorker();
+ return super.postMessage(...args);
+ }
+}
+
+class Process extends BaseProcess {
+ static get WORKER_URL() {
+ return "resource://gre/modules/subprocess/subprocess_win.worker.js";
+ }
+
+ static get WorkerClass() {
+ return WinPromiseWorker;
+ }
+}
+
+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);
+ }
+ },
+
+ async isExecutableFile(path) {
+ if (!PathUtils.isAbsolute(path)) {
+ return false;
+ }
+
+ try {
+ let info = await IOUtils.stat(path);
+ // On Windows, a FileType of "other" indicates it is a reparse point
+ // (i.e., a link).
+ return info.type !== "directory" && info.type !== "other";
+ } 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>}
+ */
+ async pathSearch(bin, environment) {
+ if (PathUtils.isAbsolute(bin)) {
+ if (await 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 = PathUtils.join(dir, bin);
+
+ if (await this.isExecutableFile(path)) {
+ return path;
+ }
+
+ for (let ext of exts) {
+ let file = path + ext;
+
+ if (await this.isExecutableFile(file)) {
+ return file;
+ }
+ }
+ }
+ let error = new Error(`Executable not found: ${bin}`);
+ error.errorCode = SubprocessConstants.ERROR_BAD_EXECUTABLE;
+ throw error;
+ },
+};
+
+export var SubprocessImpl = SubprocessWin;
diff --git a/toolkit/modules/subprocess/subprocess_win.worker.js b/toolkit/modules/subprocess/subprocess_win.worker.js
new file mode 100644
index 0000000000..22d3857f8c
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_win.worker.js
@@ -0,0 +1,785 @@
+/* -*- 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 */
+
+/* import-globals-from subprocess_shared.js */
+/* import-globals-from subprocess_shared_win.js */
+/* import-globals-from subprocess_worker_common.js */
+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_TIMEOUT = 5000;
+
+// 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.id);
+
+ 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.winLastError)) {
+ 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.winLastError) {
+ 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 Signal {
+ constructor(event) {
+ this.event = event;
+ }
+
+ cleanup() {
+ libc.CloseHandle(this.event);
+ this.event = null;
+ }
+
+ onError() {
+ io.shutdown();
+ }
+
+ onReady() {
+ io.messageCount += 1;
+ }
+}
+
+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.TerminateJobObject(this.jobHandle, 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];
+ }
+ 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);
+ }
+
+ // If we don't have a valid stderr handle, just pass it along without duplicating.
+ if (
+ String(srcHandle) == win32.INVALID_HANDLE_VALUE ||
+ String(srcHandle) == win32.NULL_HANDLE_VALUE
+ ) {
+ their_pipes[2] = srcHandle;
+ } else {
+ 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.
+ *
+ * @param {Array<string>} strings
+ * @returns {win32.WCHAR.array}
+ */
+ 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
+ *
+ * @param {string} str
+ * The argument string to quote.
+ * @returns {string}
+ */
+ 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;
+
+ if (
+ /\\cmd\.exe$/i.test(command) &&
+ args.length == 3 &&
+ /^(\/S)?\/C$/i.test(args[1])
+ ) {
+ // cmd.exe is insane and requires special treatment.
+ args = [this.quoteString(args[0]), "/S/C", `"${args[2]}"`];
+ } else {
+ args = args.map(arg => this.quoteString(arg));
+ }
+
+ if (/\.(bat|cmd)$/i.test(command)) {
+ command = io.comspec;
+ args = ["cmd.exe", "/s/c", `"${args.join(" ")}"`];
+ }
+
+ let envp = this.stringList(options.environment);
+
+ let handles = this.initPipes(options);
+
+ let processFlags =
+ win32.CREATE_NO_WINDOW |
+ win32.CREATE_SUSPENDED |
+ win32.CREATE_UNICODE_ENVIRONMENT;
+
+ let startupInfoEx = new win32.STARTUPINFOEXW();
+ let startupInfo = startupInfoEx.StartupInfo;
+
+ startupInfo.cb = win32.STARTUPINFOW.size;
+ startupInfo.dwFlags = win32.STARTF_USESTDHANDLES;
+
+ startupInfo.hStdInput = handles[0];
+ startupInfo.hStdOutput = handles[1];
+ startupInfo.hStdError = handles[2];
+
+ // Note: This needs to be kept alive until we destroy the attribute list.
+ let handleArray = win32.HANDLE.array()(handles);
+
+ let threadAttrs = win32.createThreadAttributeList(handleArray);
+ if (threadAttrs) {
+ // If have thread attributes to pass, pass the size of the full extended
+ // startup info struct.
+ processFlags |= win32.EXTENDED_STARTUPINFO_PRESENT;
+ startupInfo.cb = win32.STARTUPINFOEXW.size;
+
+ startupInfoEx.lpAttributeList = threadAttrs;
+ }
+
+ let procInfo = new win32.PROCESS_INFORMATION();
+
+ let errorMessage = "Failed to create process";
+ 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)) {
+ // If any of our handles are invalid, they don't have finalizers.
+ if (handle && handle.dispose) {
+ handle.dispose();
+ }
+ }
+
+ if (threadAttrs) {
+ libc.DeleteProcThreadAttributeList(threadAttrs);
+ }
+
+ if (ok) {
+ this.jobHandle = win32.Handle(libc.CreateJobObjectW(null, null));
+
+ let info = win32.JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
+ info.BasicLimitInformation.LimitFlags =
+ win32.JOB_OBJECT_LIMIT_BREAKAWAY_OK;
+
+ ok = libc.SetInformationJobObject(
+ this.jobHandle,
+ win32.JobObjectExtendedLimitInformation,
+ ctypes.cast(info.address(), ctypes.voidptr_t),
+ info.constructor.size
+ );
+ errorMessage = `Failed to set job limits: 0x${(
+ ctypes.winLastError || 0
+ ).toString(16)}`;
+ }
+
+ if (ok) {
+ ok = libc.AssignProcessToJobObject(this.jobHandle, procInfo.hProcess);
+ if (!ok) {
+ errorMessage = `Failed to attach process to job object: 0x${(
+ ctypes.winLastError || 0
+ ).toString(16)}`;
+ libc.TerminateProcess(procInfo.hProcess, TERMINATE_EXIT_CODE);
+ }
+ }
+
+ if (!ok) {
+ for (let pipe of this.pipes) {
+ pipe.close();
+ }
+ throw new Error(errorMessage);
+ }
+
+ this.handle = win32.Handle(procInfo.hProcess);
+ this.pid = procInfo.dwProcessId;
+
+ libc.ResumeThread(procInfo.hThread);
+ libc.CloseHandle(procInfo.hThread);
+ }
+
+ /**
+ * 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;
+
+ libc.TerminateJobObject(this.jobHandle, TERMINATE_EXIT_CODE);
+ this.jobHandle.dispose();
+ this.jobHandle = null;
+
+ for (let pipe of this.pipes) {
+ pipe.maybeClose();
+ }
+
+ io.updatePollEvents();
+
+ return exitCode;
+ }
+ }
+}
+
+io = {
+ events: null,
+ eventHandlers: null,
+
+ pipes: new Map(),
+
+ processes: new Map(),
+
+ messageCount: 0,
+
+ running: true,
+
+ polling: false,
+
+ init(details) {
+ this.comspec = details.comspec;
+
+ let signalEvent = ctypes.cast(
+ ctypes.uintptr_t(details.signalEvent),
+ win32.HANDLE
+ );
+ this.signal = new Signal(signalEvent);
+ this.updatePollEvents();
+
+ setTimeout(this.loop.bind(this), 0);
+ },
+
+ shutdown() {
+ if (this.running) {
+ this.running = false;
+
+ this.signal.cleanup();
+ this.signal = null;
+
+ self.postMessage({ msg: "close" });
+ self.close();
+ }
+ },
+
+ 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.signal,
+ ...this.pipes.values(),
+ ...this.processes.values(),
+ ];
+
+ handlers = handlers.filter(handler => handler.event);
+
+ // Our poll loop is only useful if we've got at least 1 thing to poll other than our own
+ // signal.
+ if (handlers.length == 1) {
+ this.polling = false;
+ } else if (!this.polling && this.running) {
+ // Restart the poll loop if necessary:
+ setTimeout(this.loop.bind(this), 0);
+ this.polling = true;
+ }
+
+ this.eventHandlers = handlers;
+
+ let handles = handlers.map(handler => handler.event);
+ this.events = win32.HANDLE.array()(handles);
+ },
+
+ loop() {
+ this.poll();
+ if (this.running && this.polling) {
+ setTimeout(this.loop.bind(this), 0);
+ }
+ },
+
+ poll() {
+ let timeout = this.messageCount > 0 ? 0 : POLL_TIMEOUT;
+ for (; ; timeout = 0) {
+ let events = this.events;
+ let handlers = this.eventHandlers;
+
+ let result = libc.WaitForMultipleObjects(
+ events.length,
+ events,
+ false,
+ 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);
+ },
+};
diff --git a/toolkit/modules/subprocess/subprocess_worker_common.js b/toolkit/modules/subprocess/subprocess_worker_common.js
new file mode 100644
index 0000000000..b22480c0dd
--- /dev/null
+++ b/toolkit/modules/subprocess/subprocess_worker_common.js
@@ -0,0 +1,211 @@
+/* -*- 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";
+
+// This file is loaded into the same context as subprocess_unix.worker.js
+// and subprocess_win.worker.js
+/* import-globals-from subprocess_unix.worker.js */
+
+/* exported BasePipe, BaseProcess, debug */
+
+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) {
+ 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.spawn(options);
+ }
+
+ /**
+ * Waits for the process to exit and all of its pending IO operations to
+ * complete.
+ *
+ * @returns {Promise<void>}
+ */
+ awaitFinished() {
+ return Promise.all([
+ this.exitPromise,
+ ...this.pipes.map(pipe => pipe.closedPromise),
+ ]);
+ }
+}
+
+let requests = {
+ init(details) {
+ io.init(details);
+
+ return { data: {} };
+ },
+
+ shutdown() {
+ io.shutdown();
+
+ return { data: {} };
+ },
+
+ 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();
+
+ process.awaitFinished().then(() => {
+ io.cleanupProcess(process);
+ });
+
+ return process.exitPromise.then(exitCode => {
+ 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())
+ .filter(proc => proc.exitCode == null)
+ .map(proc => [proc.id, proc.pid])
+ );
+ return { data };
+ },
+
+ waitForNoProcesses() {
+ return Promise.all(
+ Array.from(io.processes.values(), proc => proc.awaitFinished())
+ );
+ },
+};
+
+onmessage = event => {
+ io.messageCount--;
+
+ 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: {},
+ });
+ });
+};
diff --git a/toolkit/modules/subprocess/test/xpcshell/data_test_script.py b/toolkit/modules/subprocess/test/xpcshell/data_test_script.py
new file mode 100644
index 0000000000..e1f5f5de93
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/data_test_script.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+import os
+import signal
+import struct
+import sys
+
+
+def output(line, stream=sys.stdout, print_only=False):
+ if isinstance(line, str):
+ line = line.encode("utf-8", "surrogateescape")
+ if not print_only:
+ stream.buffer.write(struct.pack("@I", len(line)))
+ stream.buffer.write(line)
+ stream.flush()
+
+
+def echo_loop():
+ while True:
+ line = sys.stdin.buffer.readline()
+ if not line:
+ break
+
+ output(line)
+
+
+if sys.platform == "win32":
+ import msvcrt
+
+ msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
+
+
+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:
+ try:
+ signal.pause()
+ except AttributeError:
+ import time
+
+ time.sleep(3600)
+elif cmd == "print":
+ output(sys.argv[2], stream=sys.stdout, print_only=True)
+ output(sys.argv[3], stream=sys.stderr, print_only=True)
diff --git a/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt b/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt
diff --git a/toolkit/modules/subprocess/test/xpcshell/head.js b/toolkit/modules/subprocess/test/xpcshell/head.js
new file mode 100644
index 0000000000..a2b85047d3
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/head.js
@@ -0,0 +1,13 @@
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// eslint-disable-next-line no-unused-vars
+ChromeUtils.defineESModuleGetters(this, {
+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
+});
diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js
new file mode 100644
index 0000000000..51c9956d0d
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js
@@ -0,0 +1,870 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 18 : 9;
+const MAX_RETRIES = 5;
+
+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 = async function (pipe) {
+ let result = [];
+ let string;
+ while ((string = await pipe.readString())) {
+ result.push(string);
+ }
+
+ return result.join("");
+};
+
+add_task(async function setup() {
+ PYTHON = await Subprocess.pathSearch(Services.env.get("PYTHON"));
+
+ PYTHON_BIN = PathUtils.filename(PYTHON);
+ PYTHON_DIR = PathUtils.parent(PYTHON);
+});
+
+add_task(async function test_subprocess_io() {
+ let proc = await 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);
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ let [output] = await 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);
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ [output] = await 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 = await proc.stdout.readUint32().then(count => {
+ return proc.stdout.readJSON(count);
+ });
+
+ Assert.deepEqual(output, JSON_BLOB, "Got expected JSON output");
+
+ await proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_large_io() {
+ let proc = await 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 * 0.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 = await read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ output = await read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ output = await read(proc.stdout);
+ equal(output, LINE, "Got the expected output");
+
+ proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_huge() {
+ let proc = await 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 = await read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(
+ { skip_if: () => mozinfo.ccov },
+ async function test_subprocess_round_trip_perf() {
+ let roundTripTime = Infinity;
+ for (
+ let i = 0;
+ i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS;
+ i++
+ ) {
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ const LINE = "I'm a leaf on the wind.\n";
+
+ let now = Date.now();
+ const COUNT = 1000;
+ for (let j = 0; j < COUNT; j++) {
+ let [output] = await Promise.all([
+ read(proc.stdout),
+ proc.stdin.write(LINE),
+ ]);
+
+ // We don't want to log this for every iteration, but we still need
+ // to fail if it goes wrong.
+ if (output !== LINE) {
+ equal(output, LINE, "Got expected output");
+ }
+ }
+
+ roundTripTime = (Date.now() - now) / COUNT;
+
+ await proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ }
+
+ Assert.lessOrEqual(
+ roundTripTime,
+ MAX_ROUND_TRIP_TIME_MS,
+ `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`
+ );
+ }
+);
+
+add_task(async function test_subprocess_stderr_default() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = await 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 = await readAll(proc.stdout);
+
+ equal(stdout, LINE1, "Got the expected stdout output");
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_stderr_pipe() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ stderr: "pipe",
+ });
+
+ let [stdout, stderr] = await 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 } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_stderr_merged() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = await 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 = await readAll(proc.stdout);
+
+ equal(stdout, LINE1 + LINE2, "Got the expected merged stdout output");
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async 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 = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ stderr: "pipe",
+ });
+
+ let { exitCode } = await proc.wait();
+ equal(exitCode, 0, "Process exited with expected code");
+
+ let [stdout, stderr] = await 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(async function test_subprocess_lazy_close_output() {
+ let proc = await 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 = await read(proc.stdout);
+ let output2 = await read(proc.stdout);
+
+ await Promise.all([...writePromises, closedPromise]);
+
+ equal(output1, LINE1, "Got expected output");
+ equal(output2, LINE2, "Got expected output");
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_lazy_close_input() {
+ let proc = await 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 = await readPromise;
+ equal(len, LINE.length);
+
+ await closedPromise;
+
+ // Don't test for a successful exit here. The process may exit with a
+ // write error if we close the pipe after it's written the message
+ // size but before it's written the message.
+ await proc.wait();
+});
+
+add_task(async function test_subprocess_force_close() {
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ let readPromise = proc.stdout.readUint32();
+ let closedPromise = proc.stdout.close(true);
+
+ await 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"
+ );
+
+ await closedPromise;
+ await proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_eof() {
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ let readPromise = proc.stdout.readUint32();
+
+ await proc.stdin.close();
+
+ await 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 } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_invalid_json() {
+ let proc = await 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 = await proc.stdout.readUint32();
+ let readPromise = proc.stdout.readJSON(count);
+
+ await 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 } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+if (AppConstants.platform == "win") {
+ add_task(async function test_subprocess_inherited_descriptors() {
+ let { libc, win32 } = ChromeUtils.importESModule(
+ "resource://gre/modules/subprocess/subprocess_win.sys.mjs"
+ );
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+
+ let secAttr = new win32.SECURITY_ATTRIBUTES();
+ secAttr.nLength = win32.SECURITY_ATTRIBUTES.size;
+ secAttr.bInheritHandle = true;
+
+ let handles = win32.createPipe(secAttr, 0);
+
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ // Close the output end of the pipe.
+ // Ours should be the only copy, so reads should fail after this.
+ handles[1].dispose();
+
+ let buffer = new ArrayBuffer(1);
+ let succeeded = libc.ReadFile(
+ handles[0],
+ buffer,
+ buffer.byteLength,
+ null,
+ null
+ );
+
+ ok(!succeeded, "ReadFile should fail on broken pipe");
+ equal(
+ ctypes.winLastError,
+ win32.ERROR_BROKEN_PIPE,
+ "Read should fail with ERROR_BROKEN_PIPE"
+ );
+
+ proc.stdin.close();
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ });
+}
+
+add_task(async function test_subprocess_wait() {
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "exit", "42"],
+ });
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 42, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_pathSearch() {
+ let promise = Subprocess.call({
+ command: PYTHON_BIN,
+ arguments: ["-u", TEST_SCRIPT, "exit", "13"],
+ environment: {
+ PATH: PYTHON_DIR,
+ },
+ });
+
+ await Assert.rejects(
+ promise,
+ function (error) {
+ return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
+ },
+ "Subprocess.call should fail for a bad executable"
+ );
+});
+
+add_task(async function test_subprocess_workdir() {
+ let procDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
+ let tmpDir = PathUtils.normalize(PathUtils.tempDir);
+
+ notEqual(
+ procDir,
+ tmpDir,
+ "Current process directory must not be the current temp directory"
+ );
+
+ async function pwd(options) {
+ let proc = await Subprocess.call(
+ Object.assign(
+ {
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "pwd"],
+ },
+ options
+ )
+ );
+
+ let pwdOutput = read(proc.stdout);
+
+ let { exitCode } = await proc.wait();
+ equal(exitCode, 0, "Got expected exit code");
+
+ return pwdOutput;
+ }
+
+ let dir = await pwd({});
+ equal(
+ dir,
+ procDir,
+ "Process should normally launch in current process directory"
+ );
+
+ dir = await pwd({ workdir: tmpDir });
+ equal(
+ dir,
+ tmpDir,
+ "Process should launch in the directory specified in `workdir`"
+ );
+});
+
+add_task(async function test_subprocess_term() {
+ let proc = await 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 } = await proc.kill();
+
+ equal(exitCode, retVal, "Got expected exit code");
+
+ ({ exitCode } = await proc.wait());
+
+ equal(exitCode, retVal, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_kill() {
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ // Force kill with no gracefull termination timeout.
+ let { exitCode } = await proc.kill(0);
+
+ equal(exitCode, -9, "Got expected exit code");
+
+ ({ exitCode } = await proc.wait());
+
+ equal(exitCode, -9, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_kill_timeout() {
+ let proc = await 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 = await 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 } = await 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;
+ Assert.greaterOrEqual(
+ diff,
+ TIMEOUT,
+ `Process was killed after ${diff}ms (expected ~${TIMEOUT}ms)`
+ );
+ }
+
+ equal(exitCode, -9, "Got expected exit code");
+
+ ({ exitCode } = await proc.wait());
+
+ equal(exitCode, -9, "Got expected exit code");
+});
+
+add_task(async 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 = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print_args", ...args],
+ });
+
+ for (let [i, arg] of args.entries()) {
+ let val = await read(proc.stdout);
+ equal(val, arg, `Got correct value for args[${i}]`);
+ }
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_environment() {
+ let environment = {
+ FOO: "BAR",
+ EMPTY: "",
+ IGNORED: null,
+ };
+
+ // Our Windows environment can't handle launching python without
+ // PATH variables.
+ if (AppConstants.platform == "win") {
+ Object.assign(environment, {
+ PATH: Services.env.get("PATH"),
+ PATHEXT: Services.env.get("PATHEXT"),
+ SYSTEMROOT: Services.env.get("SYSTEMROOT"),
+ });
+ }
+
+ Services.env.set("BAR", "BAZ");
+
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "env", "FOO", "BAR", "EMPTY", "IGNORED"],
+ environment,
+ });
+
+ let foo = await read(proc.stdout);
+ let bar = await read(proc.stdout);
+ let empty = await read(proc.stdout);
+ let ignored = await read(proc.stdout);
+
+ equal(foo, "BAR", "Got expected $FOO value");
+ equal(bar, "!", "Got expected $BAR value");
+ equal(empty, "", "Got expected $EMPTY value");
+ equal(ignored, "!", "Got expected $IGNORED value");
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+add_task(async function test_subprocess_environmentAppend() {
+ Services.env.set("VALUE_FROM_BASE_ENV", "untouched");
+ Services.env.set("VALUE_FROM_BASE_ENV_EMPTY", "untouched");
+ Services.env.set("VALUE_FROM_BASE_ENV_REMOVED", "untouched");
+
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: [
+ "-u",
+ TEST_SCRIPT,
+ "env",
+ "VALUE_FROM_BASE_ENV",
+ "VALUE_FROM_BASE_ENV_EMPTY",
+ "VALUE_FROM_BASE_ENV_REMOVED",
+ "VALUE_APPENDED_ONCE",
+ ],
+ environmentAppend: true,
+ environment: {
+ VALUE_FROM_BASE_ENV_EMPTY: "",
+ VALUE_FROM_BASE_ENV_REMOVED: null,
+ VALUE_APPENDED_ONCE: "soon empty",
+ },
+ });
+
+ let valueFromBaseEnv = await read(proc.stdout);
+ let valueFromBaseEnvEmpty = await read(proc.stdout);
+ let valueFromBaseEnvRemoved = await read(proc.stdout);
+ let valueAppendedOnce = await read(proc.stdout);
+
+ equal(
+ valueFromBaseEnv,
+ "untouched",
+ "Got expected $VALUE_FROM_BASE_ENV value"
+ );
+ equal(
+ valueFromBaseEnvEmpty,
+ "",
+ "Got expected $VALUE_FROM_BASE_ENV_EMPTY value"
+ );
+ equal(
+ valueFromBaseEnvRemoved,
+ "!",
+ "Got expected $VALUE_FROM_BASE_ENV_REMOVED value"
+ );
+ equal(
+ valueAppendedOnce,
+ "soon empty",
+ "Got expected $VALUE_APPENDED_ONCE value"
+ );
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+
+ proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: [
+ "-u",
+ TEST_SCRIPT,
+ "env",
+ "VALUE_FROM_BASE_ENV",
+ "VALUE_APPENDED_ONCE",
+ ],
+ environmentAppend: true,
+ });
+
+ valueFromBaseEnv = await read(proc.stdout);
+ valueAppendedOnce = await read(proc.stdout);
+
+ equal(
+ valueFromBaseEnv,
+ "untouched",
+ "Got expected $VALUE_FROM_BASE_ENV value"
+ );
+ equal(valueAppendedOnce, "!", "Got expected $VALUE_APPENDED_ONCE value");
+
+ ({ exitCode } = await proc.wait());
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+if (AppConstants.platform !== "win") {
+ add_task(async function test_subprocess_nonASCII() {
+ const { libc } = ChromeUtils.importESModule(
+ "resource://gre/modules/subprocess/subprocess_unix.sys.mjs"
+ );
+
+ // Use TextDecoder rather than a string with a \xff escape, since
+ // the latter will automatically be normalized to valid UTF-8.
+ let val = new TextDecoder().decode(Uint8Array.of(1, 255));
+
+ libc.setenv(
+ "FOO",
+ Uint8Array.from(val + "\0", c => c.charCodeAt(0)),
+ 1
+ );
+
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "env", "FOO"],
+ });
+
+ let foo = await read(proc.stdout);
+
+ equal(foo, val, "Got expected $FOO value");
+
+ Services.env.set("FOO", "");
+
+ let { exitCode } = await proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ });
+}
+
+add_task(async 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: [],
+ });
+
+ await Assert.rejects(
+ promise,
+ function (error) {
+ if (AppConstants.platform == "win") {
+ return /Failed to create process/.test(error.message);
+ }
+ 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: [],
+ });
+
+ await Assert.rejects(
+ promise,
+ function (error) {
+ return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
+ },
+ "Subprocess.call should fail for a bad executable"
+ );
+});
+
+add_task(async function test_cleanup() {
+ let { getSubprocessImplForTest } = ChromeUtils.importESModule(
+ "resource://gre/modules/Subprocess.sys.mjs"
+ );
+
+ let worker = getSubprocessImplForTest().Process.getWorker();
+
+ let openFiles = await worker.call("getOpenFiles", []);
+ let processes = await worker.call("getProcesses", []);
+
+ equal(openFiles.size, 0, "No remaining open files");
+ equal(processes.size, 0, "No remaining processes");
+});
diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js
new file mode 100644
index 0000000000..cb4ea7247d
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js
@@ -0,0 +1,15 @@
+"use strict";
+
+add_task(async function test_getEnvironment() {
+ Services.env.set("FOO", "BAR");
+
+ let environment = Subprocess.getEnvironment();
+
+ equal(environment.FOO, "BAR");
+ equal(environment.PATH, Services.env.get("PATH"));
+
+ Services.env.set("FOO", null);
+
+ environment = Subprocess.getEnvironment();
+ equal(environment.FOO || "", "");
+});
diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js
new file mode 100644
index 0000000000..9d9374f536
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js
@@ -0,0 +1,87 @@
+"use strict";
+
+const PYTHON = Services.env.get("PYTHON");
+
+const PYTHON_BIN = PathUtils.filename(PYTHON);
+const PYTHON_DIR = PathUtils.parent(PYTHON);
+
+const DOES_NOT_EXIST = PathUtils.join(
+ PathUtils.tempDir,
+ "ThisPathDoesNotExist"
+);
+
+const PATH_SEP = AppConstants.platform == "win" ? ";" : ":";
+
+add_task(async function test_pathSearchAbsolute() {
+ let env = {};
+
+ let path = await Subprocess.pathSearch(PYTHON, env);
+ equal(path, PYTHON, "Full path resolves even with no PATH.");
+
+ env.PATH = "";
+ path = await Subprocess.pathSearch(PYTHON, env);
+ equal(path, PYTHON, "Full path resolves even with empty PATH.");
+
+ await 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(async function test_pathSearchRelative() {
+ let env = {};
+
+ await 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 = await Subprocess.pathSearch(PYTHON_BIN, env);
+ equal(path, PYTHON, "Correct executable should be found in the path");
+});
+
+add_task(
+ {
+ skip_if: () => AppConstants.platform != "win",
+ },
+ async 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 = await 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.
diff --git a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml b/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..776900b676
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+subprocess = true
+support-files = [
+ "data_text_file.txt",
+ "data_test_script.py",
+]
+
+["test_subprocess.js"]
+skip-if = [
+ "verify",
+ "apple_silicon", # bug 1729546
+]
+run-sequentially = "very high failure rate in parallel"
+
+["test_subprocess_getEnvironment.js"]
+
+["test_subprocess_pathSearch.js"]