tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Marionette.sys.mjs (9552B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
      9  EnvironmentPrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
     10  Log: "chrome://remote/content/shared/Log.sys.mjs",
     11  MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
     12  RecommendedPreferences:
     13    "chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
     14  TCPListener: "chrome://remote/content/marionette/server.sys.mjs",
     15 });
     16 
     17 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     18  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     19 );
     20 
     21 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
     22 
     23 const NOTIFY_LISTENING = "marionette-listening";
     24 const SHARED_DATA_ACTIVE_KEY = "Marionette:Active";
     25 
     26 // Complements -marionette flag for starting the Marionette server.
     27 // We also set this if Marionette is running in order to start the server
     28 // again after a Firefox restart.
     29 const ENV_ENABLED = "MOZ_MARIONETTE";
     30 
     31 // Besides starting based on existing prefs in a profile and a command
     32 // line flag, we also support inheriting prefs out of an env var, and to
     33 // start Marionette that way.
     34 //
     35 // This allows marionette prefs to persist when we do a restart into
     36 // a different profile in order to test things like Firefox refresh.
     37 // The environment variable itself, if present, is interpreted as a
     38 // JSON structure, with the keys mapping to preference names in the
     39 // "marionette." branch, and the values to the values of those prefs. So
     40 // something like {"port": 4444} would result in the marionette.port
     41 // pref being set to 4444.
     42 const ENV_PRESERVE_PREFS = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS";
     43 
     44 const isRemote =
     45  Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
     46 
     47 class MarionetteParentProcess {
     48  #browserStartupFinished;
     49 
     50  constructor() {
     51    this.server = null;
     52    this._activePortPath;
     53 
     54    // Initially set the enabled state based on the environment variable.
     55    this.enabled = Services.env.exists(ENV_ENABLED);
     56 
     57    this.#browserStartupFinished = lazy.Deferred();
     58  }
     59 
     60  /**
     61   * A promise that resolves when the initial application window has been opened.
     62   *
     63   * @returns {Promise}
     64   *     Promise that resolves when the initial application window is open.
     65   */
     66  get browserStartupFinished() {
     67    return this.#browserStartupFinished.promise;
     68  }
     69 
     70  get enabled() {
     71    return this._enabled;
     72  }
     73 
     74  set enabled(value) {
     75    // Return early if Marionette is already marked as being enabled.
     76    // There is also no possibility to disable Marionette once it got enabled.
     77    if (this._enabled || !value) {
     78      return;
     79    }
     80 
     81    this._enabled = value;
     82    lazy.logger.info(`Marionette enabled`);
     83  }
     84 
     85  get running() {
     86    return !!this.server && this.server.alive;
     87  }
     88 
     89  /**
     90   * Syncs the Marionette active flag with the web content processes.
     91   *
     92   * @param {boolean} value - Flag indicating if Marionette is active or not.
     93   */
     94  updateWebdriverActiveFlag(value) {
     95    Services.ppmm.sharedData.set(SHARED_DATA_ACTIVE_KEY, value);
     96    Services.ppmm.sharedData.flush();
     97  }
     98 
     99  handle(cmdLine) {
    100    // `handle` is called too late in certain cases (eg safe mode, see comment
    101    // above "command-line-startup"). So the marionette command line argument
    102    // will always be processed in `observe`.
    103    // However it still needs to be consumed by the command-line-handler API,
    104    // to avoid issues on macos.
    105    // TODO: remove after Bug 1724251 is fixed.
    106    cmdLine.handleFlag("marionette", false);
    107  }
    108 
    109  async observe(subject, topic) {
    110    if (this.enabled) {
    111      lazy.logger.trace(`Received observer notification ${topic}`);
    112    }
    113 
    114    switch (topic) {
    115      case "profile-after-change":
    116        Services.obs.addObserver(this, "command-line-startup");
    117        break;
    118 
    119      // In safe mode the command line handlers are getting parsed after the
    120      // safe mode dialog has been closed. To allow Marionette to start
    121      // earlier, use the CLI startup observer notification for
    122      // special-cased handlers, which gets fired before the dialog appears.
    123      case "command-line-startup":
    124        Services.obs.removeObserver(this, topic);
    125 
    126        this.enabled = subject.handleFlag("marionette", false);
    127 
    128        if (this.enabled) {
    129          // Add annotation to crash report to indicate whether
    130          // Marionette was active.
    131          Services.appinfo.annotateCrashReport("Marionette", true);
    132 
    133          // Marionette needs to be initialized before any window is shown.
    134          Services.obs.addObserver(this, "final-ui-startup");
    135 
    136          // We want to suppress the modal dialog that's shown
    137          // when starting up in safe-mode to enable testing.
    138          if (Services.appinfo.inSafeMode) {
    139            Services.obs.addObserver(this, "domwindowopened");
    140          }
    141 
    142          lazy.RecommendedPreferences.applyPreferences();
    143 
    144          // Only set preferences to preserve in a new profile
    145          // when Marionette is enabled.
    146          for (let [pref, value] of lazy.EnvironmentPrefs.from(
    147            ENV_PRESERVE_PREFS
    148          )) {
    149            switch (typeof value) {
    150              case "string":
    151                Services.prefs.setStringPref(pref, value);
    152                break;
    153              case "boolean":
    154                Services.prefs.setBoolPref(pref, value);
    155                break;
    156              case "number":
    157                Services.prefs.setIntPref(pref, value);
    158                break;
    159              default:
    160                throw new TypeError(`Invalid preference type: ${typeof value}`);
    161            }
    162          }
    163        }
    164        break;
    165 
    166      case "domwindowopened":
    167        Services.obs.removeObserver(this, topic);
    168        this.suppressSafeModeDialog(subject);
    169        break;
    170 
    171      case "final-ui-startup":
    172        Services.obs.removeObserver(this, topic);
    173 
    174        Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
    175        Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
    176        Services.obs.addObserver(this, "quit-application");
    177 
    178        await this.init();
    179        break;
    180 
    181      // Used to wait until the initial application window has been opened.
    182      case "browser-idle-startup-tasks-finished":
    183      case "mail-idle-startup-tasks-finished":
    184        Services.obs.removeObserver(
    185          this,
    186          "browser-idle-startup-tasks-finished"
    187        );
    188        Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
    189        this.#browserStartupFinished.resolve();
    190        break;
    191 
    192      case "quit-application":
    193        Services.obs.removeObserver(this, topic);
    194        await this.uninit();
    195        break;
    196    }
    197  }
    198 
    199  suppressSafeModeDialog(win) {
    200    win.addEventListener(
    201      "load",
    202      () => {
    203        let dialog = win.document.getElementById("safeModeDialog");
    204        if (dialog) {
    205          // accept the dialog to start in safe-mode
    206          lazy.logger.trace("Safe mode detected, suppressing dialog");
    207          win.setTimeout(() => {
    208            dialog.getButton("accept").click();
    209          });
    210        }
    211      },
    212      { once: true }
    213    );
    214  }
    215 
    216  async init() {
    217    if (!this.enabled || this.running) {
    218      lazy.logger.debug(
    219        `Init aborted (enabled=${this.enabled}, running=${this.running})`
    220      );
    221      return;
    222    }
    223 
    224    try {
    225      this.server = new lazy.TCPListener(lazy.MarionettePrefs.port);
    226      await this.server.start();
    227    } catch (e) {
    228      lazy.logger.fatal("Marionette server failed to start", e);
    229      Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
    230      return;
    231    }
    232 
    233    this.updateWebdriverActiveFlag(true);
    234 
    235    Services.env.set(ENV_ENABLED, "1");
    236    Services.obs.notifyObservers(this, NOTIFY_LISTENING, true);
    237    lazy.logger.debug("Marionette is listening");
    238 
    239    // Write Marionette port to MarionetteActivePort file within the profile.
    240    this._activePortPath = PathUtils.join(
    241      PathUtils.profileDir,
    242      "MarionetteActivePort"
    243    );
    244 
    245    const data = `${this.server.port}`;
    246    try {
    247      await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data));
    248    } catch (e) {
    249      lazy.logger.warn(
    250        `Failed to create ${this._activePortPath} (${e.message})`
    251      );
    252    }
    253  }
    254 
    255  async uninit() {
    256    if (this.running) {
    257      await this.server.stop();
    258      this.updateWebdriverActiveFlag(false);
    259 
    260      Services.obs.notifyObservers(this, NOTIFY_LISTENING);
    261 
    262      try {
    263        await IOUtils.remove(this._activePortPath);
    264      } catch (e) {
    265        lazy.logger.warn(
    266          `Failed to remove ${this._activePortPath} (${e.message})`
    267        );
    268      }
    269 
    270      lazy.logger.debug("Marionette stopped listening");
    271    }
    272  }
    273 
    274  // XPCOM
    275 
    276  helpInfo = "  --marionette       Enable remote control server.\n";
    277 
    278  QueryInterface = ChromeUtils.generateQI([
    279    "nsICommandLineHandler",
    280    "nsIMarionette",
    281    "nsIObserver",
    282  ]);
    283 }
    284 
    285 class MarionetteContentProcess {
    286  get running() {
    287    return Services.cpmm.sharedData.get(SHARED_DATA_ACTIVE_KEY) ?? false;
    288  }
    289 
    290  // XPCOM
    291 
    292  QueryInterface = ChromeUtils.generateQI(["nsIMarionette"]);
    293 }
    294 
    295 export var Marionette;
    296 if (isRemote) {
    297  Marionette = new MarionetteContentProcess();
    298 } else {
    299  Marionette = new MarionetteParentProcess();
    300 }
    301 
    302 // This is used by the XPCOM codepath which expects a constructor
    303 export const MarionetteFactory = function () {
    304  return Marionette;
    305 };