Marionette.sys.mjs (9552B)
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/. */ 4 5 const lazy = {}; 6 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 }); 16 17 ChromeUtils.defineLazyGetter(lazy, "logger", () => 18 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 19 ); 20 21 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder()); 22 23 const NOTIFY_LISTENING = "marionette-listening"; 24 const SHARED_DATA_ACTIVE_KEY = "Marionette:Active"; 25 26 // Complements -marionette flag for starting the Marionette server. 27 // We also set this if Marionette is running in order to start the server 28 // again after a Firefox restart. 29 const ENV_ENABLED = "MOZ_MARIONETTE"; 30 31 // Besides starting based on existing prefs in a profile and a command 32 // line flag, we also support inheriting prefs out of an env var, and to 33 // start Marionette that way. 34 // 35 // This allows marionette prefs to persist when we do a restart into 36 // a different profile in order to test things like Firefox refresh. 37 // The environment variable itself, if present, is interpreted as a 38 // JSON structure, with the keys mapping to preference names in the 39 // "marionette." branch, and the values to the values of those prefs. So 40 // something like {"port": 4444} would result in the marionette.port 41 // pref being set to 4444. 42 const ENV_PRESERVE_PREFS = "MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS"; 43 44 const isRemote = 45 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; 46 47 class MarionetteParentProcess { 48 #browserStartupFinished; 49 50 constructor() { 51 this.server = null; 52 this._activePortPath; 53 54 // Initially set the enabled state based on the environment variable. 55 this.enabled = Services.env.exists(ENV_ENABLED); 56 57 this.#browserStartupFinished = lazy.Deferred(); 58 } 59 60 /** 61 * A promise that resolves when the initial application window has been opened. 62 * 63 * @returns {Promise} 64 * Promise that resolves when the initial application window is open. 65 */ 66 get browserStartupFinished() { 67 return this.#browserStartupFinished.promise; 68 } 69 70 get enabled() { 71 return this._enabled; 72 } 73 74 set enabled(value) { 75 // Return early if Marionette is already marked as being enabled. 76 // There is also no possibility to disable Marionette once it got enabled. 77 if (this._enabled || !value) { 78 return; 79 } 80 81 this._enabled = value; 82 lazy.logger.info(`Marionette enabled`); 83 } 84 85 get running() { 86 return !!this.server && this.server.alive; 87 } 88 89 /** 90 * Syncs the Marionette active flag with the web content processes. 91 * 92 * @param {boolean} value - Flag indicating if Marionette is active or not. 93 */ 94 updateWebdriverActiveFlag(value) { 95 Services.ppmm.sharedData.set(SHARED_DATA_ACTIVE_KEY, value); 96 Services.ppmm.sharedData.flush(); 97 } 98 99 handle(cmdLine) { 100 // `handle` is called too late in certain cases (eg safe mode, see comment 101 // above "command-line-startup"). So the marionette command line argument 102 // will always be processed in `observe`. 103 // However it still needs to be consumed by the command-line-handler API, 104 // to avoid issues on macos. 105 // TODO: remove after Bug 1724251 is fixed. 106 cmdLine.handleFlag("marionette", false); 107 } 108 109 async observe(subject, topic) { 110 if (this.enabled) { 111 lazy.logger.trace(`Received observer notification ${topic}`); 112 } 113 114 switch (topic) { 115 case "profile-after-change": 116 Services.obs.addObserver(this, "command-line-startup"); 117 break; 118 119 // In safe mode the command line handlers are getting parsed after the 120 // safe mode dialog has been closed. To allow Marionette to start 121 // earlier, use the CLI startup observer notification for 122 // special-cased handlers, which gets fired before the dialog appears. 123 case "command-line-startup": 124 Services.obs.removeObserver(this, topic); 125 126 this.enabled = subject.handleFlag("marionette", false); 127 128 if (this.enabled) { 129 // Add annotation to crash report to indicate whether 130 // Marionette was active. 131 Services.appinfo.annotateCrashReport("Marionette", true); 132 133 // Marionette needs to be initialized before any window is shown. 134 Services.obs.addObserver(this, "final-ui-startup"); 135 136 // We want to suppress the modal dialog that's shown 137 // when starting up in safe-mode to enable testing. 138 if (Services.appinfo.inSafeMode) { 139 Services.obs.addObserver(this, "domwindowopened"); 140 } 141 142 lazy.RecommendedPreferences.applyPreferences(); 143 144 // Only set preferences to preserve in a new profile 145 // when Marionette is enabled. 146 for (let [pref, value] of lazy.EnvironmentPrefs.from( 147 ENV_PRESERVE_PREFS 148 )) { 149 switch (typeof value) { 150 case "string": 151 Services.prefs.setStringPref(pref, value); 152 break; 153 case "boolean": 154 Services.prefs.setBoolPref(pref, value); 155 break; 156 case "number": 157 Services.prefs.setIntPref(pref, value); 158 break; 159 default: 160 throw new TypeError(`Invalid preference type: ${typeof value}`); 161 } 162 } 163 } 164 break; 165 166 case "domwindowopened": 167 Services.obs.removeObserver(this, topic); 168 this.suppressSafeModeDialog(subject); 169 break; 170 171 case "final-ui-startup": 172 Services.obs.removeObserver(this, topic); 173 174 Services.obs.addObserver(this, "browser-idle-startup-tasks-finished"); 175 Services.obs.addObserver(this, "mail-idle-startup-tasks-finished"); 176 Services.obs.addObserver(this, "quit-application"); 177 178 await this.init(); 179 break; 180 181 // Used to wait until the initial application window has been opened. 182 case "browser-idle-startup-tasks-finished": 183 case "mail-idle-startup-tasks-finished": 184 Services.obs.removeObserver( 185 this, 186 "browser-idle-startup-tasks-finished" 187 ); 188 Services.obs.removeObserver(this, "mail-idle-startup-tasks-finished"); 189 this.#browserStartupFinished.resolve(); 190 break; 191 192 case "quit-application": 193 Services.obs.removeObserver(this, topic); 194 await this.uninit(); 195 break; 196 } 197 } 198 199 suppressSafeModeDialog(win) { 200 win.addEventListener( 201 "load", 202 () => { 203 let dialog = win.document.getElementById("safeModeDialog"); 204 if (dialog) { 205 // accept the dialog to start in safe-mode 206 lazy.logger.trace("Safe mode detected, suppressing dialog"); 207 win.setTimeout(() => { 208 dialog.getButton("accept").click(); 209 }); 210 } 211 }, 212 { once: true } 213 ); 214 } 215 216 async init() { 217 if (!this.enabled || this.running) { 218 lazy.logger.debug( 219 `Init aborted (enabled=${this.enabled}, running=${this.running})` 220 ); 221 return; 222 } 223 224 try { 225 this.server = new lazy.TCPListener(lazy.MarionettePrefs.port); 226 await this.server.start(); 227 } catch (e) { 228 lazy.logger.fatal("Marionette server failed to start", e); 229 Services.startup.quit(Ci.nsIAppStartup.eForceQuit); 230 return; 231 } 232 233 this.updateWebdriverActiveFlag(true); 234 235 Services.env.set(ENV_ENABLED, "1"); 236 Services.obs.notifyObservers(this, NOTIFY_LISTENING, true); 237 lazy.logger.debug("Marionette is listening"); 238 239 // Write Marionette port to MarionetteActivePort file within the profile. 240 this._activePortPath = PathUtils.join( 241 PathUtils.profileDir, 242 "MarionetteActivePort" 243 ); 244 245 const data = `${this.server.port}`; 246 try { 247 await IOUtils.write(this._activePortPath, lazy.textEncoder.encode(data)); 248 } catch (e) { 249 lazy.logger.warn( 250 `Failed to create ${this._activePortPath} (${e.message})` 251 ); 252 } 253 } 254 255 async uninit() { 256 if (this.running) { 257 await this.server.stop(); 258 this.updateWebdriverActiveFlag(false); 259 260 Services.obs.notifyObservers(this, NOTIFY_LISTENING); 261 262 try { 263 await IOUtils.remove(this._activePortPath); 264 } catch (e) { 265 lazy.logger.warn( 266 `Failed to remove ${this._activePortPath} (${e.message})` 267 ); 268 } 269 270 lazy.logger.debug("Marionette stopped listening"); 271 } 272 } 273 274 // XPCOM 275 276 helpInfo = " --marionette Enable remote control server.\n"; 277 278 QueryInterface = ChromeUtils.generateQI([ 279 "nsICommandLineHandler", 280 "nsIMarionette", 281 "nsIObserver", 282 ]); 283 } 284 285 class MarionetteContentProcess { 286 get running() { 287 return Services.cpmm.sharedData.get(SHARED_DATA_ACTIVE_KEY) ?? false; 288 } 289 290 // XPCOM 291 292 QueryInterface = ChromeUtils.generateQI(["nsIMarionette"]); 293 } 294 295 export var Marionette; 296 if (isRemote) { 297 Marionette = new MarionetteContentProcess(); 298 } else { 299 Marionette = new MarionetteParentProcess(); 300 } 301 302 // This is used by the XPCOM codepath which expects a constructor 303 export const MarionetteFactory = function () { 304 return Marionette; 305 };