mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
19cc83e98a
Differential Revision: https://phabricator.services.mozilla.com/D198681
309 lines
9.3 KiB
JavaScript
309 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
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",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
|
|
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
|
|
);
|
|
|
|
ChromeUtils.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";
|
|
|
|
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();
|
|
|
|
// Only set preferences to preserve in a new profile
|
|
// when Marionette is enabled.
|
|
for (let [pref, value] of lazy.EnvironmentPrefs.from(
|
|
ENV_PRESERVE_PREFS
|
|
)) {
|
|
switch (typeof value) {
|
|
case "string":
|
|
Services.prefs.setStringPref(pref, value);
|
|
break;
|
|
case "boolean":
|
|
Services.prefs.setBoolPref(pref, value);
|
|
break;
|
|
case "number":
|
|
Services.prefs.setIntPref(pref, value);
|
|
break;
|
|
default:
|
|
throw new TypeError(`Invalid preference type: ${typeof 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);
|
|
await this.server.start();
|
|
} catch (e) {
|
|
lazy.logger.fatal("Marionette server failed to start", e);
|
|
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) {
|
|
await 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;
|
|
};
|