Bug 1886451: Add missing ifdef Nightly guards. r=dminor
[gecko.git] / remote / components / Marionette.sys.mjs
blob87a53679e407ba3c242a435938862441ad7a0b6e
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/. */
5 const lazy = {};
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 });
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";
43 const isRemote =
44   Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
46 class MarionetteParentProcess {
47   #browserStartupFinished;
49   constructor() {
50     this.server = null;
51     this._activePortPath;
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();
62   }
64   /**
65    * A promise that resolves when the initial application window has been opened.
66    *
67    * @returns {Promise}
68    *     Promise that resolves when the initial application window is open.
69    */
70   get browserStartupFinished() {
71     return this.#browserStartupFinished.promise;
72   }
74   get enabled() {
75     return this._enabled;
76   }
78   set enabled(value) {
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) {
82       return;
83     }
85     this._enabled = value;
86     lazy.logger.info(`Marionette enabled`);
87   }
89   get running() {
90     return !!this.server && this.server.alive;
91   }
93   receiveMessage({ name }) {
94     switch (name) {
95       case "Marionette:IsRunning":
96         return this.running;
98       default:
99         lazy.logger.warn("Unknown IPC message to parent process: " + name);
100         return null;
101     }
102   }
104   handle(cmdLine) {
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);
112   }
114   async observe(subject, topic) {
115     if (this.enabled) {
116       lazy.logger.trace(`Received observer notification ${topic}`);
117     }
119     switch (topic) {
120       case "profile-after-change":
121         Services.obs.addObserver(this, "command-line-startup");
122         break;
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);
133         if (this.enabled) {
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");
141           }
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(
148             ENV_PRESERVE_PREFS
149           )) {
150             switch (typeof value) {
151               case "string":
152                 Services.prefs.setStringPref(pref, value);
153                 break;
154               case "boolean":
155                 Services.prefs.setBoolPref(pref, value);
156                 break;
157               case "number":
158                 Services.prefs.setIntPref(pref, value);
159                 break;
160               default:
161                 throw new TypeError(`Invalid preference type: ${typeof value}`);
162             }
163           }
164         }
165         break;
167       case "domwindowopened":
168         Services.obs.removeObserver(this, topic);
169         this.suppressSafeModeDialog(subject);
170         break;
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");
179         await this.init();
180         break;
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(
186           this,
187           "browser-idle-startup-tasks-finished"
188         );
189         Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished");
190         this.#browserStartupFinished.resolve();
191         break;
193       case "quit-application":
194         Services.obs.removeObserver(this, topic);
195         await this.uninit();
196         break;
197     }
198   }
200   suppressSafeModeDialog(win) {
201     win.addEventListener(
202       "load",
203       () => {
204         let dialog = win.document.getElementById("safeModeDialog");
205         if (dialog) {
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();
210           });
211         }
212       },
213       { once: true }
214     );
215   }
217   async init() {
218     if (!this.enabled || this.running) {
219       lazy.logger.debug(
220         `Init aborted (enabled=${this.enabled}, running=${this.running})`
221       );
222       return;
223     }
225     try {
226       this.server = new lazy.TCPListener(lazy.MarionettePrefs.port);
227       await this.server.start();
228     } catch (e) {
229       lazy.logger.fatal("Marionette server failed to start", e);
230       Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
231       return;
232     }
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"
242     );
244     const data = `${this.server.port}`;
245     try {
246       await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data));
247     } catch (e) {
248       lazy.logger.warn(
249         `Failed to create ${this._activePortPath} (${e.message})`
250       );
251     }
252   }
254   async uninit() {
255     if (this.running) {
256       await this.server.stop();
257       Services.obs.notifyObservers(this, NOTIFY_LISTENING);
258       lazy.logger.debug("Marionette stopped listening");
260       try {
261         await IOUtils.remove(this._activePortPath);
262       } catch (e) {
263         lazy.logger.warn(
264           `Failed to remove ${this._activePortPath} (${e.message})`
265         );
266       }
267     }
268   }
270   get QueryInterface() {
271     return ChromeUtils.generateQI([
272       "nsICommandLineHandler",
273       "nsIMarionette",
274       "nsIObserver",
275     ]);
276   }
279 class MarionetteContentProcess {
280   constructor() {
281     this.classID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}");
282   }
284   get running() {
285     let reply = Services.cpmm.sendSyncMessage("Marionette:IsRunning");
286     if (!reply.length) {
287       lazy.logger.warn("No reply from parent process");
288       return false;
289     }
290     return reply[0];
291   }
293   get QueryInterface() {
294     return ChromeUtils.generateQI(["nsIMarionette"]);
295   }
298 export var Marionette;
299 if (isRemote) {
300   Marionette = new MarionetteContentProcess();
301 } else {
302   Marionette = new MarionetteParentProcess();
305 // This is used by the XPCOM codepath which expects a constructor
306 export const MarionetteFactory = function () {
307   return Marionette;