/* -*- 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 { // Ignore the "duplicate" definitions here as this are also defined // in the "win" block above. // eslint-disable-next-line mozilla/valid-lazy 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} * * @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} */ 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; }