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/. */
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",
17 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
18 lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
21 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
23 const NOTIFY_LISTENING = "marionette-listening";
25 // Complements -marionette flag for starting the Marionette server.
26 // We also set this if Marionette is running in order to start the server
27 // again after a Firefox restart.
28 const ENV_ENABLED = "MOZ_MARIONETTE";
30 // Besides starting based on existing prefs in a profile and a command
31 // line flag, we also support inheriting prefs out of an env var, and to
32 // start Marionette that way.
34 // This allows marionette prefs to persist when we do a restart into
35 // a different profile in order to test things like Firefox refresh.
36 // The environment variable itself, if present, is interpreted as a
37 // JSON structure, with the keys mapping to preference names in the
38 // "marionette." branch, and the values to the values of those prefs. So
39 // something like {"port": 4444} would result in the marionette.port
40 // pref being set to 4444.
41 const ENV_PRESERVE_PREFS = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS";
44 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
46 class MarionetteParentProcess {
47 #browserStartupFinished;
53 this.classID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
54 this.helpInfo = " --marionette Enable remote control server.\n";
56 // Initially set the enabled state based on the environment variable.
57 this.enabled = Services.env.exists(ENV_ENABLED);
59 Services.ppmm.addMessageListener("Marionette:IsRunning", this);
61 this.#browserStartupFinished = lazy.Deferred();
65 * A promise that resolves when the initial application window has been opened.
68 * Promise that resolves when the initial application window is open.
70 get browserStartupFinished() {
71 return this.#browserStartupFinished.promise;
79 // Return early if Marionette is already marked as being enabled.
80 // There is also no possibility to disable Marionette once it got enabled.
81 if (this._enabled || !value) {
85 this._enabled = value;
86 lazy.logger.info(`Marionette enabled`);
90 return !!this.server && this.server.alive;
93 receiveMessage({ name }) {
95 case "Marionette:IsRunning":
99 lazy.logger.warn("Unknown IPC message to parent process: " + name);
105 // `handle` is called too late in certain cases (eg safe mode, see comment
106 // above "command-line-startup"). So the marionette command line argument
107 // will always be processed in `observe`.
108 // However it still needs to be consumed by the command-line-handler API,
109 // to avoid issues on macos.
110 // TODO: remove after Bug 1724251 is fixed.
111 cmdLine.handleFlag("marionette", false);
114 async observe(subject, topic) {
116 lazy.logger.trace(`Received observer notification ${topic}`);
120 case "profile-after-change":
121 Services.obs.addObserver(this, "command-line-startup");
124 // In safe mode the command line handlers are getting parsed after the
125 // safe mode dialog has been closed. To allow Marionette to start
126 // earlier, use the CLI startup observer notification for
127 // special-cased handlers, which gets fired before the dialog appears.
128 case "command-line-startup":
129 Services.obs.removeObserver(this, topic);
131 this.enabled = subject.handleFlag("marionette", false);
134 // Marionette needs to be initialized before any window is shown.
135 Services.obs.addObserver(this, "final-ui-startup");
137 // We want to suppress the modal dialog that's shown
138 // when starting up in safe-mode to enable testing.
139 if (Services.appinfo.inSafeMode) {
140 Services.obs.addObserver(this, "domwindowopened");
143 lazy.RecommendedPreferences.applyPreferences();
145 // Only set preferences to preserve in a new profile
146 // when Marionette is enabled.
147 for (let [pref, value] of lazy.EnvironmentPrefs.from(
150 switch (typeof value) {
152 Services.prefs.setStringPref(pref, value);
155 Services.prefs.setBoolPref(pref, value);
158 Services.prefs.setIntPref(pref, value);
161 throw new TypeError(`Invalid preference type: ${typeof value}`);
167 case "domwindowopened":
168 Services.obs.removeObserver(this, topic);
169 this.suppressSafeModeDialog(subject);
172 case "final-ui-startup":
173 Services.obs.removeObserver(this, topic);
175 Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
176 Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
177 Services.obs.addObserver(this, "quit-application");
182 // Used to wait until the initial application window has been opened.
183 case "browser-idle-startup-tasks-finished":
184 case "mail-idle-startup-tasks-finished":
185 Services.obs.removeObserver(
187 "browser-idle-startup-tasks-finished"
189 Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
190 this.#browserStartupFinished.resolve();
193 case "quit-application":
194 Services.obs.removeObserver(this, topic);
200 suppressSafeModeDialog(win) {
201 win.addEventListener(
204 let dialog = win.document.getElementById("safeModeDialog");
206 // accept the dialog to start in safe-mode
207 lazy.logger.trace("Safe mode detected, supressing dialog");
208 win.setTimeout(() => {
209 dialog.getButton("accept").click();
218 if (!this.enabled || this.running) {
220 `Init aborted (enabled=${this.enabled}, running=${this.running})`
226 this.server = new lazy.TCPListener(lazy.MarionettePrefs.port);
227 await this.server.start();
229 lazy.logger.fatal("Marionette server failed to start", e);
230 Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
234 Services.env.set(ENV_ENABLED, "1");
235 Services.obs.notifyObservers(this, NOTIFY_LISTENING, true);
236 lazy.logger.debug("Marionette is listening");
238 // Write Marionette port to MarionetteActivePort file within the profile.
239 this._activePortPath = PathUtils.join(
240 PathUtils.profileDir,
241 "MarionetteActivePort"
244 const data = `${this.server.port}`;
246 await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data));
249 `Failed to create ${this._activePortPath} (${e.message})`
256 await this.server.stop();
257 Services.obs.notifyObservers(this, NOTIFY_LISTENING);
258 lazy.logger.debug("Marionette stopped listening");
261 await IOUtils.remove(this._activePortPath);
264 `Failed to remove ${this._activePortPath} (${e.message})`
270 get QueryInterface() {
271 return ChromeUtils.generateQI([
272 "nsICommandLineHandler",
279 class MarionetteContentProcess {
281 this.classID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
285 let reply = Services.cpmm.sendSyncMessage("Marionette:IsRunning");
287 lazy.logger.warn("No reply from parent process");
293 get QueryInterface() {
294 return ChromeUtils.generateQI(["nsIMarionette"]);
298 export var Marionette;
300 Marionette = new MarionetteContentProcess();
302 Marionette = new MarionetteParentProcess();
305 // This is used by the XPCOM codepath which expects a constructor
306 export const MarionetteFactory = function () {