toolkit/modules/subprocess/Subprocess.jsm
author Andrew Swan <aswan@mozilla.com>
Fri, 20 May 2016 15:27:43 -0700
changeset 369247 2f55b496853b3d6c20e4d93ddf146c78895b0569
parent 368448 947c6b8e19ef5290658d86edd545f37f3713749b
permissions -rw-r--r--
Bug 1270357 Add some test hooks to Subprocess.jsm r?kmag MozReview-Commit-ID: Or2EOAL1eC

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * These modules are loosely based on the subprocess.jsm module created
 * by Jan Gerber and Patrick Brunschwig, though the implementation
 * differs drastically.
 */

"use strict";

let EXPORTED_SYMBOLS = ["Subprocess"];

/* exported Subprocess */

var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/subprocess/subprocess_common.jsm");

if (AppConstants.platform == "win") {
  XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
                                    "resource://gre/modules/subprocess/subprocess_win.jsm");
} else {
  XPCOMUtils.defineLazyModuleGetter(this, "SubprocessImpl",
                                    "resource://gre/modules/subprocess/subprocess_unix.jsm");
}

/**
 * Allows for creation of and communication with OS-level sub-processes.
 * @namespace
 */
var Subprocess = {
  /**
   * Launches a process, and returns a handle to it.
   *
   * @param {object} options
   *        An object describing the process to launch.
   * @param {string} options.command
   *        The name of the execuable to launch. If not a full path, it is
   *        resolved to an executable by searching the directories in $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.
   * @param {boolean} [options.environmentAppend]
   *        If true, append the environment variables passed in `environment` to
   *        the existing set of environment variables. Otherwise, the values in
   *        'environment' constitute the entire set of environment variables
   *        passed to the new process.
   * @param {string} [options.stderr]
   *        Defines how the process's stderr output is handled. One of:
   *
   *         - "ignore": (default) The process's standard error is not
   *           redirected.
   *         - "stdout": The process's stderr is merged with its stdout.
   *         - "pipe": The process's stderr is redirected to a pipe, which can
   *           be read from via its `stderr` property.
   *
   * @param {string} [options.workdir]
   *        The working directory in which to launch the new process.
   *
   * @returns {Promise<Process>}
   *
   * @rejects {Error}
   *          May be rejected with an Error object if the process can not be
   *          launched. The object will include an `errorCode` property with
   *          one of the following values if it was rejected for the
   *          corresponding reason:
   *
   *            - Subprocess.ERROR_BAD_EXECUTABLE: The given command could not
   *              be found, or the file that it references is not executable.
   */
  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 this.pathSearch(options.command, environment).then(command => {
      options.arguments.unshift(options.command);
      options.command = command;

      let result = SubprocessImpl.call(options);
      result.then(proc => {
        this._startProc();
        proc.exitPromise.then(() => {
          this._stopProc();
        });
      });
      return result;
    });
  },

  /**
   * Returns an object with a key-value pair for every variable in the process's
   * current environment.
   *
   * @returns {object}
   */
  getEnvironment() {
    let environment = Object.create(null);
    for (let [k, v] of SubprocessImpl.getEnvironment()) {
      environment[k] = v;
    }
    return environment;
  },

  /**
   * Searches for the given executable file in the system executable
   * file paths as specified by the PATH environment variable.
   *
   * On Windows, if the unadorned filename cannot be found, the
   * extensions in the semicolon-separated list in the PATHSEP
   * environment variable are successively appended to the original
   * name and searched for in turn.
   *
   * @param {string} bin
   *        The name of the executable to find.
   * @param {object} environment
   *        An object containing a key for each environment variable to be used
   *        in the search.
   * @returns {Promise<string>}
   */
  pathSearch(command, environment) {
    // 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);
  },

  /**
   * Things below this point are meant only for use in tests
   */
  _liveProcCount: 0,
  _onLiveCountChange: null,

  _startProc() {
    this._liveProcCount++;
    if (this._onLiveCountChange) {
      this._onLiveCountChange(this._liveProcCount);
    }
  },

  _stopProc() {
    this._liveProcCount--;
    if (this._onLiveCountChange) {
      this._onLiveCountChange(this._liveProcCount);
    }
  },
};

Object.assign(Subprocess, SubprocessConstants);