mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			308 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  Preferences: "resource://gre/modules/Preferences.sys.mjs",
 | 
						|
 | 
						|
  Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
 | 
						|
  EnvironmentPrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
 | 
						|
  Log: "chrome://remote/content/shared/Log.sys.mjs",
 | 
						|
  MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
 | 
						|
  RecommendedPreferences:
 | 
						|
    "chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
 | 
						|
  TCPListener: "chrome://remote/content/marionette/server.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
 | 
						|
  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
 | 
						|
 | 
						|
const NOTIFY_LISTENING = "marionette-listening";
 | 
						|
 | 
						|
// Complements -marionette flag for starting the Marionette server.
 | 
						|
// We also set this if Marionette is running in order to start the server
 | 
						|
// again after a Firefox restart.
 | 
						|
const ENV_ENABLED = "MOZ_MARIONETTE";
 | 
						|
 | 
						|
// Besides starting based on existing prefs in a profile and a command
 | 
						|
// line flag, we also support inheriting prefs out of an env var, and to
 | 
						|
// start Marionette that way.
 | 
						|
//
 | 
						|
// This allows marionette prefs to persist when we do a restart into
 | 
						|
// a different profile in order to test things like Firefox refresh.
 | 
						|
// The environment variable itself, if present, is interpreted as a
 | 
						|
// JSON structure, with the keys mapping to preference names in the
 | 
						|
// "marionette." branch, and the values to the values of those prefs. So
 | 
						|
// something like {"port": 4444} would result in the marionette.port
 | 
						|
// pref being set to 4444.
 | 
						|
const ENV_PRESERVE_PREFS = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS";
 | 
						|
 | 
						|
// Map of Marionette-specific preferences that should be set via
 | 
						|
// RecommendedPreferences.
 | 
						|
const RECOMMENDED_PREFS = new Map([
 | 
						|
  // Automatically unload beforeunload alerts
 | 
						|
  ["dom.disable_beforeunload", true],
 | 
						|
]);
 | 
						|
 | 
						|
const isRemote =
 | 
						|
  Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
 | 
						|
 | 
						|
class MarionetteParentProcess {
 | 
						|
  #browserStartupFinished;
 | 
						|
 | 
						|
  constructor() {
 | 
						|
    this.server = null;
 | 
						|
    this._activePortPath;
 | 
						|
 | 
						|
    this.classID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
 | 
						|
    this.helpInfo = "  --marionette       Enable remote control server.\n";
 | 
						|
 | 
						|
    // Initially set the enabled state based on the environment variable.
 | 
						|
    this.enabled = Services.env.exists(ENV_ENABLED);
 | 
						|
 | 
						|
    Services.ppmm.addMessageListener("Marionette:IsRunning", this);
 | 
						|
 | 
						|
    this.#browserStartupFinished = lazy.Deferred();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * A promise that resolves when the initial application window has been opened.
 | 
						|
   *
 | 
						|
   * @returns {Promise}
 | 
						|
   *     Promise that resolves when the initial application window is open.
 | 
						|
   */
 | 
						|
  get browserStartupFinished() {
 | 
						|
    return this.#browserStartupFinished.promise;
 | 
						|
  }
 | 
						|
 | 
						|
  get enabled() {
 | 
						|
    return this._enabled;
 | 
						|
  }
 | 
						|
 | 
						|
  set enabled(value) {
 | 
						|
    // Return early if Marionette is already marked as being enabled.
 | 
						|
    // There is also no possibility to disable Marionette once it got enabled.
 | 
						|
    if (this._enabled || !value) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this._enabled = value;
 | 
						|
    lazy.logger.info(`Marionette enabled`);
 | 
						|
  }
 | 
						|
 | 
						|
  get running() {
 | 
						|
    return !!this.server && this.server.alive;
 | 
						|
  }
 | 
						|
 | 
						|
  receiveMessage({ name }) {
 | 
						|
    switch (name) {
 | 
						|
      case "Marionette:IsRunning":
 | 
						|
        return this.running;
 | 
						|
 | 
						|
      default:
 | 
						|
        lazy.logger.warn("Unknown IPC message to parent process: " + name);
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  handle(cmdLine) {
 | 
						|
    // `handle` is called too late in certain cases (eg safe mode, see comment
 | 
						|
    // above "command-line-startup"). So the marionette command line argument
 | 
						|
    // will always be processed in `observe`.
 | 
						|
    // However it still needs to be consumed by the command-line-handler API,
 | 
						|
    // to avoid issues on macos.
 | 
						|
    // TODO: remove after Bug 1724251 is fixed.
 | 
						|
    cmdLine.handleFlag("marionette", false);
 | 
						|
  }
 | 
						|
 | 
						|
  async observe(subject, topic) {
 | 
						|
    if (this.enabled) {
 | 
						|
      lazy.logger.trace(`Received observer notification ${topic}`);
 | 
						|
    }
 | 
						|
 | 
						|
    switch (topic) {
 | 
						|
      case "profile-after-change":
 | 
						|
        Services.obs.addObserver(this, "command-line-startup");
 | 
						|
        break;
 | 
						|
 | 
						|
      // In safe mode the command line handlers are getting parsed after the
 | 
						|
      // safe mode dialog has been closed. To allow Marionette to start
 | 
						|
      // earlier, use the CLI startup observer notification for
 | 
						|
      // special-cased handlers, which gets fired before the dialog appears.
 | 
						|
      case "command-line-startup":
 | 
						|
        Services.obs.removeObserver(this, topic);
 | 
						|
 | 
						|
        this.enabled = subject.handleFlag("marionette", false);
 | 
						|
 | 
						|
        if (this.enabled) {
 | 
						|
          // Marionette needs to be initialized before any window is shown.
 | 
						|
          Services.obs.addObserver(this, "final-ui-startup");
 | 
						|
 | 
						|
          // We want to suppress the modal dialog that's shown
 | 
						|
          // when starting up in safe-mode to enable testing.
 | 
						|
          if (Services.appinfo.inSafeMode) {
 | 
						|
            Services.obs.addObserver(this, "domwindowopened");
 | 
						|
          }
 | 
						|
 | 
						|
          lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS);
 | 
						|
 | 
						|
          // Only set preferences to preserve in a new profile
 | 
						|
          // when Marionette is enabled.
 | 
						|
          for (let [pref, value] of lazy.EnvironmentPrefs.from(
 | 
						|
            ENV_PRESERVE_PREFS
 | 
						|
          )) {
 | 
						|
            lazy.Preferences.set(pref, value);
 | 
						|
          }
 | 
						|
        }
 | 
						|
        break;
 | 
						|
 | 
						|
      case "domwindowopened":
 | 
						|
        Services.obs.removeObserver(this, topic);
 | 
						|
        this.suppressSafeModeDialog(subject);
 | 
						|
        break;
 | 
						|
 | 
						|
      case "final-ui-startup":
 | 
						|
        Services.obs.removeObserver(this, topic);
 | 
						|
 | 
						|
        Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
 | 
						|
        Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
 | 
						|
        Services.obs.addObserver(this, "quit-application");
 | 
						|
 | 
						|
        await this.init();
 | 
						|
        break;
 | 
						|
 | 
						|
      // Used to wait until the initial application window has been opened.
 | 
						|
      case "browser-idle-startup-tasks-finished":
 | 
						|
      case "mail-idle-startup-tasks-finished":
 | 
						|
        Services.obs.removeObserver(
 | 
						|
          this,
 | 
						|
          "browser-idle-startup-tasks-finished"
 | 
						|
        );
 | 
						|
        Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
 | 
						|
        this.#browserStartupFinished.resolve();
 | 
						|
        break;
 | 
						|
 | 
						|
      case "quit-application":
 | 
						|
        Services.obs.removeObserver(this, topic);
 | 
						|
        await this.uninit();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  suppressSafeModeDialog(win) {
 | 
						|
    win.addEventListener(
 | 
						|
      "load",
 | 
						|
      () => {
 | 
						|
        let dialog = win.document.getElementById("safeModeDialog");
 | 
						|
        if (dialog) {
 | 
						|
          // accept the dialog to start in safe-mode
 | 
						|
          lazy.logger.trace("Safe mode detected, supressing dialog");
 | 
						|
          win.setTimeout(() => {
 | 
						|
            dialog.getButton("accept").click();
 | 
						|
          });
 | 
						|
        }
 | 
						|
      },
 | 
						|
      { once: true }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async init() {
 | 
						|
    if (!this.enabled || this.running) {
 | 
						|
      lazy.logger.debug(
 | 
						|
        `Init aborted (enabled=${this.enabled}, running=${this.running})`
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      this.server = new lazy.TCPListener(lazy.MarionettePrefs.port);
 | 
						|
      this.server.start();
 | 
						|
    } catch (e) {
 | 
						|
      lazy.logger.fatal("Marionette server failed to start", e);
 | 
						|
      await this.uninit();
 | 
						|
      Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    Services.env.set(ENV_ENABLED, "1");
 | 
						|
    Services.obs.notifyObservers(this, NOTIFY_LISTENING, true);
 | 
						|
    lazy.logger.debug("Marionette is listening");
 | 
						|
 | 
						|
    // Write Marionette port to MarionetteActivePort file within the profile.
 | 
						|
    this._activePortPath = PathUtils.join(
 | 
						|
      PathUtils.profileDir,
 | 
						|
      "MarionetteActivePort"
 | 
						|
    );
 | 
						|
 | 
						|
    const data = `${this.server.port}`;
 | 
						|
    try {
 | 
						|
      await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data));
 | 
						|
    } catch (e) {
 | 
						|
      lazy.logger.warn(
 | 
						|
        `Failed to create ${this._activePortPath} (${e.message})`
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async uninit() {
 | 
						|
    if (this.running) {
 | 
						|
      this.server.stop();
 | 
						|
      Services.obs.notifyObservers(this, NOTIFY_LISTENING);
 | 
						|
      lazy.logger.debug("Marionette stopped listening");
 | 
						|
 | 
						|
      try {
 | 
						|
        await IOUtils.remove(this._activePortPath);
 | 
						|
      } catch (e) {
 | 
						|
        lazy.logger.warn(
 | 
						|
          `Failed to remove ${this._activePortPath} (${e.message})`
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  get QueryInterface() {
 | 
						|
    return ChromeUtils.generateQI([
 | 
						|
      "nsICommandLineHandler",
 | 
						|
      "nsIMarionette",
 | 
						|
      "nsIObserver",
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class MarionetteContentProcess {
 | 
						|
  constructor() {
 | 
						|
    this.classID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
 | 
						|
  }
 | 
						|
 | 
						|
  get running() {
 | 
						|
    let reply = Services.cpmm.sendSyncMessage("Marionette:IsRunning");
 | 
						|
    if (!reply.length) {
 | 
						|
      lazy.logger.warn("No reply from parent process");
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return reply[0];
 | 
						|
  }
 | 
						|
 | 
						|
  get QueryInterface() {
 | 
						|
    return ChromeUtils.generateQI(["nsIMarionette"]);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export var Marionette;
 | 
						|
if (isRemote) {
 | 
						|
  Marionette = new MarionetteContentProcess();
 | 
						|
} else {
 | 
						|
  Marionette = new MarionetteParentProcess();
 | 
						|
}
 | 
						|
 | 
						|
// This is used by the XPCOM codepath which expects a constructor
 | 
						|
export const MarionetteFactory = function() {
 | 
						|
  return Marionette;
 | 
						|
};
 |