2012-03-22 19:19:57 +04:00
|
|
|
/* 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/. */
|
|
|
|
|
2015-03-20 00:12:58 +03:00
|
|
|
"use strict";
|
|
|
|
|
2018-01-30 02:20:18 +03:00
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
2017-03-09 14:12:53 +03:00
|
|
|
|
2017-04-15 03:50:29 +03:00
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
|
|
this, "env", "@mozilla.org/process/environment;1", "nsIEnvironment");
|
2018-01-30 02:20:18 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "Log",
|
2018-01-22 20:19:09 +03:00
|
|
|
"resource://gre/modules/Log.jsm");
|
2018-01-30 02:20:18 +03:00
|
|
|
ChromeUtils.defineModuleGetter(this, "Preferences",
|
2017-07-29 12:03:00 +03:00
|
|
|
"resource://gre/modules/Preferences.jsm");
|
2018-01-22 20:19:09 +03:00
|
|
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
|
|
|
let log = Log.repository.getLogger("Marionette");
|
|
|
|
log.addAppender(new Log.DumpAppender());
|
|
|
|
return log;
|
|
|
|
});
|
2017-07-30 21:05:22 +03:00
|
|
|
|
2018-01-27 22:03:18 +03:00
|
|
|
const PREF_ENABLED = "marionette.enabled";
|
2017-03-09 23:15:32 +03:00
|
|
|
const PREF_PORT = "marionette.port";
|
|
|
|
const PREF_PORT_FALLBACK = "marionette.defaultPrefs.port";
|
|
|
|
const PREF_LOG_LEVEL = "marionette.log.level";
|
|
|
|
const PREF_LOG_LEVEL_FALLBACK = "marionette.logging";
|
|
|
|
|
2017-03-09 14:12:53 +03:00
|
|
|
const DEFAULT_LOG_LEVEL = "info";
|
2018-01-27 21:49:03 +03:00
|
|
|
const NOTIFY_RUNNING = "remote-active";
|
2017-03-09 14:12:53 +03:00
|
|
|
|
2017-04-15 03:50:29 +03:00
|
|
|
// 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";
|
|
|
|
|
2017-03-09 14:12:53 +03:00
|
|
|
// 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
|
2017-04-13 17:08:14 +03:00
|
|
|
// something like {"port": 4444} would result in the marionette.port
|
|
|
|
// pref being set to 4444.
|
2017-04-15 03:50:29 +03:00
|
|
|
const ENV_PRESERVE_PREFS = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS";
|
2016-04-08 19:17:47 +03:00
|
|
|
|
2018-01-27 22:34:32 +03:00
|
|
|
const isRemote = Services.appinfo.processType ==
|
|
|
|
Services.appinfo.PROCESS_TYPE_CONTENT;
|
|
|
|
|
2018-01-27 21:00:40 +03:00
|
|
|
const LogLevel = {
|
|
|
|
get(level) {
|
|
|
|
let levels = new Map([
|
|
|
|
["fatal", Log.Level.Fatal],
|
|
|
|
["error", Log.Level.Error],
|
|
|
|
["warn", Log.Level.Warn],
|
|
|
|
["info", Log.Level.Info],
|
|
|
|
["config", Log.Level.Config],
|
|
|
|
["debug", Log.Level.Debug],
|
|
|
|
["trace", Log.Level.Trace],
|
|
|
|
]);
|
|
|
|
|
|
|
|
let s = String(level).toLowerCase();
|
|
|
|
if (!levels.has(s)) {
|
|
|
|
return DEFAULT_LOG_LEVEL;
|
|
|
|
}
|
|
|
|
return levels.get(s);
|
|
|
|
},
|
|
|
|
};
|
2017-07-30 21:05:22 +03:00
|
|
|
|
|
|
|
function getPrefVal(pref) {
|
2018-01-27 20:57:33 +03:00
|
|
|
const {PREF_STRING, PREF_BOOL, PREF_INT, PREF_INVALID} = Ci.nsIPrefBranch;
|
|
|
|
|
|
|
|
let type = Services.prefs.getPrefType(pref);
|
|
|
|
switch (type) {
|
2017-07-30 21:05:22 +03:00
|
|
|
case PREF_STRING:
|
2018-01-27 20:57:33 +03:00
|
|
|
return Services.prefs.getStringPref(pref);
|
2017-07-30 21:05:22 +03:00
|
|
|
|
|
|
|
case PREF_BOOL:
|
2018-01-27 20:57:33 +03:00
|
|
|
return Services.prefs.getBoolPref(pref);
|
2017-07-30 21:05:22 +03:00
|
|
|
|
|
|
|
case PREF_INT:
|
2018-01-27 20:57:33 +03:00
|
|
|
return Services.prefs.getIntPref(pref);
|
2017-07-30 21:05:22 +03:00
|
|
|
|
|
|
|
case PREF_INVALID:
|
2018-01-27 20:57:33 +03:00
|
|
|
return undefined;
|
2017-07-30 21:05:22 +03:00
|
|
|
|
|
|
|
default:
|
2018-01-27 20:57:33 +03:00
|
|
|
throw new TypeError(`Unexpected preference type (${type}) for ${pref}`);
|
2017-07-30 21:05:22 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-27 16:28:32 +03:00
|
|
|
// Get preference value of |preferred|, falling back to |fallback|
|
|
|
|
// if |preferred| is not user-modified and |fallback| exists.
|
2017-06-30 02:40:24 +03:00
|
|
|
function getPref(preferred, fallback) {
|
2017-07-30 21:05:22 +03:00
|
|
|
if (!Services.prefs.prefHasUserValue(preferred) &&
|
|
|
|
Services.prefs.getPrefType(fallback) != Ci.nsIPrefBranch.PREF_INVALID) {
|
|
|
|
return getPrefVal(fallback, getPrefVal(preferred));
|
2017-03-27 16:28:32 +03:00
|
|
|
}
|
2017-07-30 21:05:22 +03:00
|
|
|
return getPrefVal(preferred);
|
2017-03-27 16:28:32 +03:00
|
|
|
}
|
|
|
|
|
2017-03-09 14:12:53 +03:00
|
|
|
// Marionette preferences recently changed names. This is an abstraction
|
|
|
|
// that first looks for the new name, but falls back to using the old name
|
|
|
|
// if the new does not exist.
|
|
|
|
//
|
|
|
|
// This shim can be removed when Firefox 55 ships.
|
|
|
|
const prefs = {
|
2017-06-30 02:40:24 +03:00
|
|
|
get port() {
|
2017-03-27 16:28:32 +03:00
|
|
|
return getPref(PREF_PORT, PREF_PORT_FALLBACK);
|
2017-03-09 14:12:53 +03:00
|
|
|
},
|
2016-01-02 18:08:54 +03:00
|
|
|
|
2017-06-30 02:40:24 +03:00
|
|
|
get logLevel() {
|
2017-03-27 16:28:32 +03:00
|
|
|
let s = getPref(PREF_LOG_LEVEL, PREF_LOG_LEVEL_FALLBACK);
|
2018-01-27 21:00:40 +03:00
|
|
|
return LogLevel.get(s);
|
2017-03-09 14:12:53 +03:00
|
|
|
},
|
|
|
|
|
2017-06-30 02:40:24 +03:00
|
|
|
readFromEnvironment(key) {
|
2017-03-09 14:12:53 +03:00
|
|
|
const env = Cc["@mozilla.org/process/environment;1"]
|
|
|
|
.getService(Ci.nsIEnvironment);
|
|
|
|
|
|
|
|
if (env.exists(key)) {
|
|
|
|
let prefs;
|
|
|
|
try {
|
|
|
|
prefs = JSON.parse(env.get(key));
|
|
|
|
} catch (e) {
|
|
|
|
Cu.reportError(
|
|
|
|
"Invalid Marionette preferences in environment; " +
|
|
|
|
"preferences will not have been applied");
|
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (prefs) {
|
|
|
|
for (let prefName of Object.keys(prefs)) {
|
2017-05-31 14:48:06 +03:00
|
|
|
Preferences.set(prefName, prefs[prefName]);
|
2017-03-09 14:12:53 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2015-03-20 00:12:58 +03:00
|
|
|
};
|
|
|
|
|
2018-01-27 22:34:32 +03:00
|
|
|
class MarionetteMainProcess {
|
2018-01-27 21:39:47 +03:00
|
|
|
constructor() {
|
|
|
|
this.server = null;
|
2017-03-09 20:57:26 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
// holds reference to ChromeWindow
|
|
|
|
// used to run GFX sanity tests on Windows
|
|
|
|
this.gfxWindow = null;
|
2017-03-09 20:57:26 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
// indicates that all pending window checks have been completed
|
|
|
|
// and that we are ready to start the Marionette server
|
|
|
|
this.finalUIStartup = false;
|
2017-03-09 20:57:26 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
log.level = prefs.logLevel;
|
2017-07-05 11:33:23 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
this.enabled = env.exists(ENV_ENABLED);
|
2018-01-27 22:03:18 +03:00
|
|
|
|
|
|
|
Services.prefs.addObserver(PREF_ENABLED, this);
|
2018-01-27 22:34:32 +03:00
|
|
|
Services.ppmm.addMessageListener("Marionette:IsRunning", this);
|
2017-07-05 11:33:23 +03:00
|
|
|
}
|
2017-03-09 14:12:53 +03:00
|
|
|
|
2018-01-27 21:46:17 +03:00
|
|
|
get running() {
|
|
|
|
return this.server && this.server.alive;
|
|
|
|
}
|
|
|
|
|
2018-01-27 22:03:18 +03:00
|
|
|
set enabled(value) {
|
|
|
|
Services.prefs.setBoolPref(PREF_ENABLED, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
get enabled() {
|
|
|
|
return Services.prefs.getBoolPref(PREF_ENABLED);
|
|
|
|
}
|
|
|
|
|
2018-01-27 22:34:32 +03:00
|
|
|
receiveMessage({name}) {
|
|
|
|
switch (name) {
|
|
|
|
case "Marionette:IsRunning":
|
|
|
|
return this.running;
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.warn("Unknown IPC message to main process: " + name);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
observe(subject, topic) {
|
|
|
|
log.debug(`Received observer notification ${topic}`);
|
|
|
|
|
|
|
|
switch (topic) {
|
2018-01-27 22:03:18 +03:00
|
|
|
case "nsPref:changed":
|
|
|
|
if (Services.prefs.getBoolPref(PREF_ENABLED)) {
|
|
|
|
this.init();
|
|
|
|
} else {
|
|
|
|
this.uninit();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
case "profile-after-change":
|
|
|
|
Services.obs.addObserver(this, "command-line-startup");
|
|
|
|
Services.obs.addObserver(this, "sessionstore-windows-restored");
|
2017-08-10 19:04:47 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
prefs.readFromEnvironment(ENV_PRESERVE_PREFS);
|
|
|
|
break;
|
2013-10-15 22:20:35 +04:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
// 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":
|
2017-03-09 14:12:53 +03:00
|
|
|
Services.obs.removeObserver(this, topic);
|
2018-01-27 21:53:02 +03:00
|
|
|
|
|
|
|
if (!this.enabled && subject.handleFlag("marionette", false)) {
|
|
|
|
this.enabled = true;
|
|
|
|
}
|
2017-03-07 21:38:51 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
// We want to suppress the modal dialog that's shown
|
|
|
|
// when starting up in safe-mode to enable testing.
|
|
|
|
if (this.enabled && Services.appinfo.inSafeMode) {
|
|
|
|
Services.obs.addObserver(this, "domwindowopened");
|
2017-03-07 21:38:51 +03:00
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
break;
|
|
|
|
|
|
|
|
case "domwindowclosed":
|
|
|
|
if (this.gfxWindow === null || subject === this.gfxWindow) {
|
|
|
|
Services.obs.removeObserver(this, topic);
|
2017-03-07 21:38:51 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
Services.obs.addObserver(this, "xpcom-shutdown");
|
|
|
|
this.finalUIStartup = true;
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "domwindowopened":
|
|
|
|
Services.obs.removeObserver(this, topic);
|
|
|
|
this.suppressSafeModeDialog(subject);
|
|
|
|
break;
|
2017-03-07 21:38:51 +03:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
case "sessionstore-windows-restored":
|
|
|
|
Services.obs.removeObserver(this, topic);
|
|
|
|
|
|
|
|
// When Firefox starts on Windows, an additional GFX sanity test
|
|
|
|
// window may appear off-screen. Marionette should wait for it
|
|
|
|
// to close.
|
|
|
|
let winEn = Services.wm.getEnumerator(null);
|
|
|
|
while (winEn.hasMoreElements()) {
|
|
|
|
let win = winEn.getNext();
|
|
|
|
if (win.document.documentURI == "chrome://gfxsanity/content/sanityparent.html") {
|
|
|
|
this.gfxWindow = win;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.gfxWindow) {
|
|
|
|
Services.obs.addObserver(this, "domwindowclosed");
|
|
|
|
} else {
|
|
|
|
Services.obs.addObserver(this, "xpcom-shutdown");
|
|
|
|
this.finalUIStartup = true;
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "xpcom-shutdown":
|
|
|
|
Services.obs.removeObserver(this, "xpcom-shutdown");
|
|
|
|
this.uninit();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
suppressSafeModeDialog(win) {
|
|
|
|
win.addEventListener("load", () => {
|
|
|
|
if (win.document.getElementById("safeModeDialog")) {
|
|
|
|
// accept the dialog to start in safe-mode
|
|
|
|
log.debug("Safe mode detected, supressing dialog");
|
|
|
|
win.setTimeout(() => {
|
|
|
|
win.document.documentElement.getButton("accept").click();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, {once: true});
|
2015-03-20 00:12:58 +03:00
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
init() {
|
|
|
|
if (this.running || !this.enabled || !this.finalUIStartup) {
|
|
|
|
return;
|
2012-03-22 19:19:57 +04:00
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
// wait for delayed startup...
|
|
|
|
Services.tm.idleDispatchToMainThread(async () => {
|
|
|
|
// ... and for startup tests
|
|
|
|
let startupRecorder = Promise.resolve();
|
|
|
|
if ("@mozilla.org/test/startuprecorder;1" in Cc) {
|
|
|
|
startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"]
|
|
|
|
.getService().wrappedJSObject.done;
|
|
|
|
}
|
|
|
|
await startupRecorder;
|
|
|
|
|
|
|
|
try {
|
|
|
|
ChromeUtils.import("chrome://marionette/content/server.js");
|
|
|
|
let listener = new server.TCPListener(prefs.port);
|
|
|
|
listener.start();
|
|
|
|
this.server = listener;
|
|
|
|
} catch (e) {
|
|
|
|
log.fatal("Remote protocol server failed to start", e);
|
|
|
|
Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
|
|
|
|
}
|
2018-01-27 21:49:03 +03:00
|
|
|
|
|
|
|
Services.obs.notifyObservers(this, NOTIFY_RUNNING, true);
|
|
|
|
log.info(`Listening on port ${this.server.port}`);
|
2018-01-27 21:39:47 +03:00
|
|
|
});
|
2015-03-20 00:12:58 +03:00
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
uninit() {
|
2018-01-27 21:46:17 +03:00
|
|
|
if (this.running) {
|
|
|
|
this.server.stop();
|
2018-01-27 21:49:03 +03:00
|
|
|
Services.obs.notifyObservers(this, NOTIFY_RUNNING);
|
2018-01-22 20:21:52 +03:00
|
|
|
}
|
2018-01-27 21:39:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get QueryInterface() {
|
|
|
|
return XPCOMUtils.generateQI([
|
|
|
|
Ci.nsICommandLineHandler,
|
|
|
|
Ci.nsIMarionette,
|
2018-01-27 22:03:18 +03:00
|
|
|
Ci.nsIObserver,
|
2018-01-27 21:39:47 +03:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-27 22:34:32 +03:00
|
|
|
class MarionetteContentProcess {
|
|
|
|
get running() {
|
|
|
|
let reply = Services.cpmm.sendSyncMessage("Marionette:IsRunning");
|
|
|
|
if (reply.length == 0) {
|
|
|
|
log.warn("No reply from main process");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return reply[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
get QueryInterface() {
|
|
|
|
return XPCOMUtils.generateQI([Ci.nsIMarionette]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
const MarionetteFactory = {
|
2018-01-27 22:34:32 +03:00
|
|
|
instance_: null,
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
createInstance(outer, iid) {
|
|
|
|
if (outer) {
|
|
|
|
throw Cr.NS_ERROR_NO_AGGREGATION;
|
2018-01-22 20:21:52 +03:00
|
|
|
}
|
2018-01-27 21:39:47 +03:00
|
|
|
|
2018-01-27 22:34:32 +03:00
|
|
|
if (!this.instance_) {
|
|
|
|
if (isRemote) {
|
|
|
|
this.instance_ = new MarionetteContentProcess();
|
|
|
|
} else {
|
|
|
|
this.instance_ = new MarionetteMainProcess();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.instance_.QueryInterface(iid);
|
2018-01-27 21:39:47 +03:00
|
|
|
},
|
2015-03-20 00:12:58 +03:00
|
|
|
};
|
2012-03-22 19:19:57 +04:00
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
function Marionette() {}
|
|
|
|
|
|
|
|
Marionette.prototype = {
|
|
|
|
classDescription: "Marionette component",
|
|
|
|
classID: Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}"),
|
|
|
|
contractID: "@mozilla.org/remote/marionette;1",
|
|
|
|
|
|
|
|
/* eslint-disable camelcase */
|
|
|
|
_xpcom_factory: MarionetteFactory,
|
|
|
|
|
|
|
|
_xpcom_categories: [
|
|
|
|
{category: "command-line-handler", entry: "b-marionette"},
|
|
|
|
{category: "profile-after-change", service: true},
|
|
|
|
],
|
|
|
|
/* eslint-enable camelcase */
|
|
|
|
|
|
|
|
helpInfo: " --marionette Enable remote control server.\n",
|
2012-03-22 19:19:57 +04:00
|
|
|
};
|
|
|
|
|
2018-01-27 21:39:47 +03:00
|
|
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Marionette]);
|