First commit

1.1.101
Chris Fuertes 4 years ago
commit 6d96b18476
  1. 7
      Dockerfile
  2. 7
      README.md
  3. 16
      chrome/bootstrap.js
  4. 4
      chrome/chrome.manifest
  5. 24
      chrome/content/modules/MimeParser.js
  6. 744
      chrome/content/modules/ipc/enigmailprocess_common.jsm
  7. 170
      chrome/content/modules/ipc/enigmailprocess_main.jsm
  8. 103
      chrome/content/modules/ipc/enigmailprocess_shared.js
  9. 161
      chrome/content/modules/ipc/enigmailprocess_shared_unix.js
  10. 574
      chrome/content/modules/ipc/enigmailprocess_shared_win.js
  11. 178
      chrome/content/modules/ipc/enigmailprocess_unix.jsm
  12. 178
      chrome/content/modules/ipc/enigmailprocess_win.jsm
  13. 265
      chrome/content/modules/ipc/enigmailprocess_worker_common.js
  14. 648
      chrome/content/modules/ipc/enigmailprocess_worker_unix.js
  15. 756
      chrome/content/modules/ipc/enigmailprocess_worker_win.js
  16. 400
      chrome/content/modules/ipc/subprocess.jsm
  17. 163
      chrome/content/modules/pEp.js
  18. 37
      chrome/content/modules/pEpAdapter.js
  19. 20
      chrome/content/modules/pEpCore.js
  20. 103
      chrome/content/modules/pEpFiles.js
  21. 34
      chrome/content/modules/pEpOs.js
  22. 398
      chrome/content/modules/pEpServer.js
  23. 18
      chrome/content/modules/pepdecrypter.js
  24. 83
      chrome/content/modules/pepencrypter.js
  25. 7
      chrome/content/modules/pepidentity.js
  26. 41
      chrome/content/modules/pepmessage.js
  27. 146
      chrome/content/modules/utils.js
  28. 74
      chrome/content/modules/xhrQueue.js
  29. 16
      chrome/content/pepmessenger.js
  30. 23
      chrome/content/pepmessenger.xul
  31. 3
      chrome/content/pepmessengercompose.js
  32. 16
      chrome/content/pepmessengercompose.xul
  33. 11
      chrome/defaults/preferences/pref.js
  34. 29
      chrome/install.rdf
  35. 1637
      package-lock.json
  36. 12
      package.json
  37. 7
      tests/config/testSetup.js
  38. 101
      tests/features/encrypt_feature_spec.js
  39. 54
      tests/features/get_version_feature_spec.js
  40. 4
      tests/mocha.opts
  41. 14
      tests/mocks/mockpepcore.js
  42. 36
      tests/mocks/mockpepfiles.js
  43. 23
      tests/mocks/mockpepos.js
  44. 27
      tests/mocks/mockxhrqueue.js
  45. 256
      tests/unit/pepserver_spec.js

@ -0,0 +1,7 @@
FROM alpine:latest
RUN apk add zip
WORKDIR /usr/src/app
COPY chrome .
CMD ["zip", "-r", "build/p4t.xpi", "."]

@ -0,0 +1,7 @@
# p≡p for Thunderbird
## HOW TO BUILD
Using docker
```docker build -t <image_tag> . && docker run -v build:/usr/src/app/build <image_tag> ```

@ -0,0 +1,16 @@
//TODO: Implement for restartless app https://developer.mozilla.org/en-US/docs/Archive/Add-ons/How_to_convert_an_overlay_extension_to_restartless#Step_9_bootstrap.js
function install() {
console.log("bootstrap.js: Install");
}
function startup() {
console.log("bootstrap.js: Startup");
}
function shutdown() {
console.log("bootstrap.js: Shutdown");
}
function uninstall() {
console.log("bootstrap.js: Uninstall");
}

@ -0,0 +1,4 @@
content p4t content/
overlay chrome://messenger/content/messenger.xul chrome://p4t/content/pepmessenger.xul
overlay chrome://messenger/content/messengercompose/messengercompose.xul chrome://p4t/content/pepmessengercompose.xul

@ -0,0 +1,24 @@
const EXPORTED_SYMBOLS = ["MimeParser"];
class MimeParser {
constructor() {
}
parse(text) {
let mimeObject;
//TODO To be implemented
return mimeObject;
}
toMime(obj) {
let mimeText;
//TODO To be implementfaed
return mimeText;
}
}
// export common.js module to allow one js file for browser and node.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = MimeParser;
}

@ -0,0 +1,744 @@
/* -*- 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 https://mozilla.org/MPL/2.0/. */
"use strict";
/* eslint-disable mozilla/balanced-listeners */
/* exported BaseProcess, PromiseWorker */
/* global Components: false, ChromeWorker: false, */
var {
classes: Cc,
interfaces: Ci,
utils: Cu,
results: Cr
} = Components;
// const {
// Services
// } = Cu.import("resource://gre/modules/Services.jsm", {}); /* global Services: false */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /* global XPCOMUtils: false */
Cu.importGlobalProperties(["TextDecoder", "TextEncoder"]);
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
"resource://gre/modules/AsyncShutdown.jsm"); /* global AsyncShutdown: false */
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm"); /* global Timer: false */
var SubScriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
SubScriptLoader.loadSubScript("chrome://p4t/content/modules/ipc/enigmailprocess_shared.js", this);
var EXPORTED_SYMBOLS = ["BaseProcess", "PromiseWorker", "SubprocessConstants"];
const BUFFER_SIZE = 4096;
let nextResponseId = 0;
/* global SubprocessConstants: true */
/**
* Wraps a ChromeWorker so that messages sent to it return a promise which
* resolves when the message has been received and the operation it triggers is
* complete.
*/
class PromiseWorker extends ChromeWorker {
constructor(url) {
super(url);
this.listeners = new Map();
this.pendingResponses = new Map();
this.addListener("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);
AsyncShutdown.webWorkersShutdown.addBlocker(
"Subprocess.jsm: Shut down IO worker",
this.shutdown);
}
onClose() {
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) {
Cu.reportError(e);
}
}
}
/**
* Called when a message sent to the worker has failed, and rejects its
* corresponding promise.
*
* @private
*/
onFailure({
msgId, error
}) {
this.pendingResponses.get(msgId).reject(error);
this.pendingResponses.delete(msgId);
}
/**
* Called when a message sent to the worker has succeeded, and resolves its
* corresponding promise.
*
* @private
*/
onSuccess({
msgId, data
}) {
this.pendingResponses.get(msgId).resolve(data);
this.pendingResponses.delete(msgId);
}
onDebug({
message
}) {
//dump(`Worker debug: ${message}\n`);
}
/**
* Calls the given method in the worker, and returns a promise which resolves
* or rejects when the method has completed.
*
* @param {string} method
* The name of the method to call.
* @param {Array} args
* The arguments to pass to the method.
* @param {Array} [transferList]
* A list of objects to transfer to the worker, rather than cloning.
* @returns {Promise}
*/
call(method, args, transferList = []) {
let msgId = nextResponseId++;
return new Promise((resolve, reject) => {
this.pendingResponses.set(msgId, {
resolve, reject
});
let message = {
msg: method,
msgId,
args
};
this.postMessage(message, transferList);
});
}
}
/**
* Represents an input or output pipe connected to a subprocess.
*
* @property {integer} fd
* The file descriptor number of the pipe on the child process's side.
* @readonly
*/
class Pipe {
/**
* @param {Process} process
* The child process that this pipe is connected to.
* @param {integer} fd
* The file descriptor number of the pipe on the child process's side.
* @param {integer} id
* The internal ID of the pipe, which ties it to the corresponding Pipe
* object on the Worker side.
*/
constructor(process, fd, id) {
this.id = id;
this.fd = fd;
this.processId = process.id;
this.worker = process.worker;
/**
* @property {boolean} closed
* True if the file descriptor has been closed, and can no longer
* be read from or written to. Pending IO operations may still
* complete, but new operations may not be initiated.
* @readonly
*/
this.closed = false;
}
/**
* Closes the end of the pipe which belongs to this process.
*
* @param {boolean} force
* If true, the pipe is closed immediately, regardless of any pending
* IO operations. If false, the pipe is closed after any existing
* pending IO operations have completed.
* @returns {Promise<object>}
* Resolves to an object with no properties once the pipe has been
* closed.
*/
close(force = false) {
this.closed = true;
return this.worker.call("close", [this.id, force]);
}
}
/**
* Represents an output-only pipe, to which data may be written.
*/
class OutputPipe extends Pipe {
constructor(...args) {
super(...args);
this.encoder = new TextEncoder();
}
/**
* Writes the given data to the stream.
*
* When given an array buffer or typed array, ownership of the buffer is
* transferred to the IO worker, and it may no longer be used from this
* thread.
*
* @param {ArrayBuffer|TypedArray|string} buffer
* Data to write to the stream.
* @returns {Promise<object>}
* Resolves to an object with a `bytesWritten` property, containing
* the number of bytes successfully written, once the operation has
* completed.
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* all of the data in `buffer` could be written to it.
*/
write(buffer) {
if (typeof buffer === "string") {
buffer = this.encoder.encode(buffer);
}
if (Cu.getClassName(buffer, true) !== "ArrayBuffer") {
if (buffer.byteLength === buffer.buffer.byteLength) {
buffer = buffer.buffer;
}
else {
buffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
}
let args = [this.id, buffer];
return this.worker.call("write", args, [buffer]);
}
}
/**
* Represents an input-only pipe, from which data may be read.
*/
class InputPipe extends Pipe {
constructor(...args) {
super(...args);
this.buffers = [];
/**
* @property {integer} dataAvailable
* The number of readable bytes currently stored in the input
* buffer.
* @readonly
*/
this.dataAvailable = 0;
this.decoder = new TextDecoder();
this.pendingReads = [];
this._pendingBufferRead = null;
this.fillBuffer();
}
/**
* @property {integer} bufferSize
* The current size of the input buffer. This varies depending on
* the size of pending read operations.
* @readonly
*/
get bufferSize() {
if (this.pendingReads.length) {
return Math.max(this.pendingReads[0].length, BUFFER_SIZE);
}
return BUFFER_SIZE;
}
/**
* Attempts to fill the input buffer.
*
* @private
*/
fillBuffer() {
let dataWanted = this.bufferSize - this.dataAvailable;
if (!this._pendingBufferRead && dataWanted > 0) {
this._pendingBufferRead = this._read(dataWanted);
this._pendingBufferRead.then((result) => {
this._pendingBufferRead = null;
if (result) {
this.onInput(result.buffer);
this.fillBuffer();
}
});
}
}
_read(size) {
let args = [this.id, size];
return this.worker.call("read", args).catch(e => {
this.closed = true;
for (let {
length, resolve, reject
}
of this.pendingReads.splice(0)) {
if (length === null && e.errorCode === SubprocessConstants.ERROR_END_OF_FILE) {
resolve(new ArrayBuffer(0));
}
else {
reject(e);
}
}
});
}
/**
* Adds the given data to the end of the input buffer.
*
* @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>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
read(length = null) {
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
throw new RangeError("Length must be a non-negative integer");
}
if (length == 0) {
return Promise.resolve(new ArrayBuffer(0));
}
return new Promise((resolve, reject) => {
this.pendingReads.push({
length, resolve, reject
});
this.checkPendingReads();
});
}
/**
* Reads exactly `length` bytes from the input stream, and parses them as
* UTF-8 JSON data.
*
* @param {integer} length
* The number of bytes to read.
* @returns {Promise<object>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
* - Subprocess.ERROR_INVALID_JSON: The data read from the pipe
* could not be parsed as a valid JSON string.
*/
readJSON(length) {
if (!Number.isInteger(length) || length <= 0) {
throw new RangeError("Length must be a positive integer");
}
return this.readString(length).then(string => {
try {
return JSON.parse(string);
}
catch (e) {
e.errorCode = SubprocessConstants.ERROR_INVALID_JSON;
throw e;
}
});
}
/**
* Reads a chunk of UTF-8 data from the input stream, and converts it to a
* JavaScript string.
*
* If `length` is provided, reads exactly `length` bytes. Otherwise, reads the
* first chunk of data to become available, and returns an empty string on end
* of file. In the latter case, the chunk is decoded in streaming mode, and
* any incomplete UTF-8 sequences at the end of a chunk are returned at the
* start of a subsequent read operation.
*
* @param {integer} [length]
* The number of bytes to read.
* @param {object} [options]
* An options object as expected by TextDecoder.decode.
* @returns {Promise<string>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
readString(length = null, options = {
stream: length === null
}) {
if (length !== null && !(Number.isInteger(length) && length >= 0)) {
throw new RangeError("Length must be a non-negative integer");
}
return this.read(length).then(buffer => {
return this.decoder.decode(buffer, options);
});
}
/**
* Reads 4 bytes from the input stream, and parses them as an unsigned
* integer, in native byte order.
*
* @returns {Promise<integer>}
*
* @rejects {object}
* May be rejected with an Error object, or an object with similar
* properties. The object will include an `errorCode` property with
* one of the following values if it was rejected for the
* corresponding reason:
*
* - Subprocess.ERROR_END_OF_FILE: The pipe was closed before
* enough input could be read to satisfy the request.
*/
readUint32() {
return this.read(4).then(buffer => {
return new Uint32Array(buffer)[0];
});
}
}
/**
* @class Process
* @extends BaseProcess
*/
/**
* Represents a currently-running process, and allows interaction with it.
*/
class BaseProcess {
/**
* @param {PromiseWorker} worker
* The worker instance which owns the process.
* @param {integer} processId
* The internal ID of the Process object, which ties it to the
* corresponding process on the Worker side.
* @param {integer[]} fds
* An array of internal Pipe IDs, one for each standard file descriptor
* in the child process.
* @param {integer} pid
* The operating system process ID of the process.
*/
constructor(worker, processId, fds, pid) {
this.id = processId;
this.worker = worker;
/**
* @property {integer} pid
* The process ID of the process, assigned by the operating system.
* @readonly
*/
this.pid = pid;
this.exitCode = null;
this.exitPromise = new Promise(resolve => {
this.worker.call("wait", [this.id]).then(({
exitCode
}) => {
resolve(Object.freeze({
exitCode
}));
this.exitCode = exitCode;
});
});
if (fds[0] !== undefined) {
/**
* @property {OutputPipe} stdin
* A Pipe object which allows writing to the process's standard
* input.
* @readonly
*/
this.stdin = new OutputPipe(this, 0, fds[0]);
}
if (fds[1] !== undefined) {
/**
* @property {InputPipe} stdout
* A Pipe object which allows reading from the process's standard
* output.
* @readonly
*/
this.stdout = new InputPipe(this, 1, fds[1]);
}
if (fds[2] !== undefined) {
/**
* @property {InputPipe} [stderr]
* An optional Pipe object which allows reading from the
* process's standard error output.
* @readonly
*/
this.stderr = new InputPipe(this, 2, fds[2]);
}
}
/**
* Spawns a process, and resolves to a BaseProcess instance on success.
*
* @param {object} options
* An options object as passed to `Subprocess.call`.
*
* @returns {Promise<BaseProcess>}
*/
static create(options) {
let worker = this.getWorker();
return worker.call("spawn", [options]).then(({
processId, fds, pid
}) => {
return new this(worker, processId, fds, pid);
});
}
static get WORKER_URL() {
throw new Error("Not implemented");
}
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) {
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;
}
}

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

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

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

@ -0,0 +1,574 @@
/* -*- 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 https://mozilla.org/MPL/2.0/. */
"use strict";
/* exported LIBC, Win, createPipe, libc */
/* global ctypes: false, OS: false, Library: false */
/* eslint no-void: 0 */
const LIBC = OS.Constants.libc;
const Win = OS.Constants.Win;
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_FLAG_OVERLAPPED: 0x40000000,
PIPE_TYPE_BYTE: 0x00,
PIPE_ACCESS_INBOUND: 0x01,
PIPE_ACCESS_OUTBOUND: 0x02,
PIPE_ACCESS_DUPLEX: 0x03,
PIPE_WAIT: 0x00,
PIPE_NOWAIT: 0x01,
STILL_ACTIVE: 259,
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
})