summaryrefslogtreecommitdiffstats
path: root/remote/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /remote/components
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/components')
-rw-r--r--remote/components/Marionette.sys.mjs307
-rw-r--r--remote/components/RemoteAgent.sys.mjs522
-rw-r--r--remote/components/components.conf29
-rw-r--r--remote/components/moz.build17
-rw-r--r--remote/components/nsIMarionette.idl17
-rw-r--r--remote/components/nsIRemoteAgent.idl36
6 files changed, 928 insertions, 0 deletions
diff --git a/remote/components/Marionette.sys.mjs b/remote/components/Marionette.sys.mjs
new file mode 100644
index 0000000000..cbaaa45034
--- /dev/null
+++ b/remote/components/Marionette.sys.mjs
@@ -0,0 +1,307 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+
+ 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",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+XPCOMUtils.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";
+
+// Map of Marionette-specific preferences that should be set via
+// RecommendedPreferences.
+const RECOMMENDED_PREFS = new Map([
+ // Automatically unload beforeunload alerts
+ ["dom.disable_beforeunload", true],
+]);
+
+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(RECOMMENDED_PREFS);
+
+ // Only set preferences to preserve in a new profile
+ // when Marionette is enabled.
+ for (let [pref, value] of lazy.EnvironmentPrefs.from(
+ ENV_PRESERVE_PREFS
+ )) {
+ lazy.Preferences.set(pref, 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;
+};
diff --git a/remote/components/RemoteAgent.sys.mjs b/remote/components/RemoteAgent.sys.mjs
new file mode 100644
index 0000000000..31453d910a
--- /dev/null
+++ b/remote/components/RemoteAgent.sys.mjs
@@ -0,0 +1,522 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CDP: "chrome://remote/content/cdp/CDP.sys.mjs",
+ Deferred: "chrome://remote/content/shared/Sync.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ WebDriverBiDi: "chrome://remote/content/webdriver-bidi/WebDriverBiDi.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ HttpServer: "chrome://remote/content/server/HTTPD.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+XPCOMUtils.defineLazyGetter(lazy, "activeProtocols", () => {
+ const protocols = Services.prefs.getIntPref("remote.active-protocols");
+ if (protocols < 1 || protocols > 3) {
+ throw Error(`Invalid remote protocol identifier: ${protocols}`);
+ }
+
+ return protocols;
+});
+
+const WEBDRIVER_BIDI_ACTIVE = 0x1;
+const CDP_ACTIVE = 0x2;
+
+const DEFAULT_HOST = "localhost";
+const DEFAULT_PORT = 9222;
+
+const isRemote =
+ Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+
+class RemoteAgentParentProcess {
+ #allowHosts;
+ #allowOrigins;
+ #browserStartupFinished;
+ #classID;
+ #enabled;
+ #host;
+ #port;
+ #server;
+
+ #cdp;
+ #webDriverBiDi;
+
+ constructor() {
+ this.#allowHosts = null;
+ this.#allowOrigins = null;
+ this.#browserStartupFinished = lazy.Deferred();
+ this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
+ this.#enabled = false;
+
+ // Configuration for httpd.js
+ this.#host = DEFAULT_HOST;
+ this.#port = DEFAULT_PORT;
+ this.#server = null;
+
+ // Supported protocols
+ this.#cdp = null;
+ this.#webDriverBiDi = null;
+
+ Services.ppmm.addMessageListener("RemoteAgent:IsRunning", this);
+ }
+
+ get allowHosts() {
+ if (this.#allowHosts !== null) {
+ return this.#allowHosts;
+ }
+
+ if (this.#server) {
+ // If the server is bound to a hostname, not an IP address, return it as
+ // allowed host.
+ const hostUri = Services.io.newURI(`https://${this.#host}`);
+ if (!this.#isIPAddress(hostUri)) {
+ return [RemoteAgent.host];
+ }
+
+ // Following Bug 1220810 localhost is guaranteed to resolve to a loopback
+ // address (127.0.0.1 or ::1) unless network.proxy.allow_hijacking_localhost
+ // is set to true, which should not be the case.
+ const loopbackAddresses = ["127.0.0.1", "[::1]"];
+
+ // If the server is bound to an IP address and this IP address is a localhost
+ // loopback address, return localhost as allowed host.
+ if (loopbackAddresses.includes(this.#host)) {
+ return ["localhost"];
+ }
+ }
+
+ // Otherwise return an empty array.
+ return [];
+ }
+
+ get allowOrigins() {
+ return this.#allowOrigins;
+ }
+
+ /**
+ * 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 cdp() {
+ return this.#cdp;
+ }
+
+ get debuggerAddress() {
+ if (!this.#server) {
+ return "";
+ }
+
+ return `${this.#host}:${this.#port}`;
+ }
+
+ get enabled() {
+ return this.#enabled;
+ }
+
+ get host() {
+ return this.#host;
+ }
+
+ get port() {
+ return this.#port;
+ }
+
+ get running() {
+ return !!this.#server && !this.#server.isStopped();
+ }
+
+ get scheme() {
+ return this.#server?.identity.primaryScheme;
+ }
+
+ get server() {
+ return this.#server;
+ }
+
+ get webDriverBiDi() {
+ return this.#webDriverBiDi;
+ }
+
+ /**
+ * Check if the provided URI's host is an IP address.
+ *
+ * @param {nsIURI} uri
+ * The URI to check.
+ * @returns {boolean}
+ */
+ #isIPAddress(uri) {
+ try {
+ // getBaseDomain throws an explicit error if the uri host is an IP address.
+ Services.eTLD.getBaseDomain(uri);
+ } catch (e) {
+ return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
+ }
+ return false;
+ }
+
+ handle(cmdLine) {
+ // remote-debugging-port has to be consumed in nsICommandLineHandler:handle
+ // to avoid issues on macos. See Marionette.jsm::handle() for more details.
+ // TODO: remove after Bug 1724251 is fixed.
+ try {
+ cmdLine.handleFlagWithParam("remote-debugging-port", false);
+ } catch (e) {
+ cmdLine.handleFlag("remote-debugging-port", false);
+ }
+ }
+
+ async #listen(port) {
+ if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ throw Components.Exception(
+ "May only be instantiated in parent process",
+ Cr.NS_ERROR_LAUNCHED_CHILD_PROCESS
+ );
+ }
+
+ if (this.running) {
+ return;
+ }
+
+ // Try to resolve localhost to an IPv4 and / or IPv6 address so that the
+ // server can be started on a given IP. Only fallback to use localhost if
+ // the hostname cannot be resolved.
+ //
+ // Note: This doesn't force httpd.js to use the dual stack support.
+ let isIPv4Host = false;
+ try {
+ const addresses = await this.#resolveHostname(DEFAULT_HOST);
+ lazy.logger.trace(
+ `Available local IP addresses: ${addresses.join(", ")}`
+ );
+
+ // Prefer IPv4 over IPv6 addresses.
+ const addressesIPv4 = addresses.filter(value => !value.includes(":"));
+ isIPv4Host = !!addressesIPv4.length;
+ if (isIPv4Host) {
+ this.#host = addressesIPv4[0];
+ } else {
+ this.#host = addresses.length ? addresses[0] : DEFAULT_HOST;
+ }
+ } catch (e) {
+ this.#host = DEFAULT_HOST;
+
+ lazy.logger.debug(
+ `Failed to resolve hostname "localhost" to IP address: ${e.message}`
+ );
+ }
+
+ // nsIServerSocket uses -1 for atomic port allocation
+ if (port === 0) {
+ port = -1;
+ }
+
+ try {
+ // Bug 1783938: httpd.js refuses connections when started on a IPv4
+ // address. As workaround start on localhost and add another identity
+ // for that IP address.
+ this.#server = new lazy.HttpServer();
+ const host = isIPv4Host ? DEFAULT_HOST : this.#host;
+ this.server._start(port, host);
+ this.#port = this.server._port;
+
+ if (isIPv4Host) {
+ this.server.identity.add("http", this.#host, this.#port);
+ }
+
+ Services.obs.notifyObservers(null, "remote-listening", true);
+
+ await Promise.all([this.#webDriverBiDi?.start(), this.#cdp?.start()]);
+ } catch (e) {
+ await this.#stop();
+ lazy.logger.error(`Unable to start remote agent: ${e.message}`, e);
+ }
+ }
+
+ /**
+ * Resolves a hostname to one or more IP addresses.
+ *
+ * @param {string} hostname
+ *
+ * @returns {Array<string>}
+ */
+ #resolveHostname(hostname) {
+ return new Promise((resolve, reject) => {
+ let originalRequest;
+
+ const onLookupCompleteListener = {
+ onLookupComplete(request, record, status) {
+ if (request === originalRequest) {
+ if (!Components.isSuccessCode(status)) {
+ reject({ message: ChromeUtils.getXPCOMErrorName(status) });
+ return;
+ }
+
+ record.QueryInterface(Ci.nsIDNSAddrRecord);
+
+ const addresses = [];
+ while (record.hasMore()) {
+ let addr = record.getNextAddrAsString();
+ if (addr.includes(":") && !addr.startsWith("[")) {
+ // Make sure that the IPv6 address is wrapped with brackets.
+ addr = `[${addr}]`;
+ }
+ if (!addresses.includes(addr)) {
+ // Sometimes there are duplicate records with the same IP.
+ addresses.push(addr);
+ }
+ }
+
+ resolve(addresses);
+ }
+ },
+ };
+
+ try {
+ originalRequest = Services.dns.asyncResolve(
+ hostname,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
+ null,
+ onLookupCompleteListener,
+ null, //Services.tm.mainThread,
+ {} /* defaultOriginAttributes */
+ );
+ } catch (e) {
+ reject({ message: e.message });
+ }
+ });
+ }
+
+ async #stop() {
+ if (!this.running) {
+ return;
+ }
+
+ // Stop each protocol before stopping the HTTP server.
+ await this.#cdp?.stop();
+ await this.#webDriverBiDi?.stop();
+
+ try {
+ await this.#server.stop();
+ this.#server = null;
+ Services.obs.notifyObservers(null, "remote-listening");
+ } catch (e) {
+ // this function must never fail
+ lazy.logger.error("Unable to stop listener", e);
+ }
+ }
+
+ /**
+ * Handle the --remote-debugging-port command line argument.
+ *
+ * @param {nsICommandLine} cmdLine
+ * Instance of the command line interface.
+ *
+ * @returns {boolean}
+ * Return `true` if the command line argument has been found.
+ */
+ handleRemoteDebuggingPortFlag(cmdLine) {
+ let enabled = false;
+
+ try {
+ // Catch cases when the argument, and a port have been specified.
+ const port = cmdLine.handleFlagWithParam("remote-debugging-port", false);
+ if (port !== null) {
+ enabled = true;
+
+ // In case of an invalid port keep the default port
+ const parsed = Number(port);
+ if (!isNaN(parsed)) {
+ this.#port = parsed;
+ }
+ }
+ } catch (e) {
+ // If no port has been given check for the existence of the argument.
+ enabled = cmdLine.handleFlag("remote-debugging-port", false);
+ }
+
+ return enabled;
+ }
+
+ handleAllowHostsFlag(cmdLine) {
+ try {
+ const hosts = cmdLine.handleFlagWithParam("remote-allow-hosts", false);
+ return hosts.split(",");
+ } catch (e) {
+ return null;
+ }
+ }
+
+ handleAllowOriginsFlag(cmdLine) {
+ try {
+ const origins = cmdLine.handleFlagWithParam(
+ "remote-allow-origins",
+ false
+ );
+ return origins.split(",");
+ } catch (e) {
+ return null;
+ }
+ }
+
+ 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;
+
+ case "command-line-startup":
+ Services.obs.removeObserver(this, topic);
+
+ this.#enabled = this.handleRemoteDebuggingPortFlag(subject);
+
+ if (this.#enabled) {
+ Services.obs.addObserver(this, "final-ui-startup");
+
+ this.#allowHosts = this.handleAllowHostsFlag(subject);
+ this.#allowOrigins = this.handleAllowOriginsFlag(subject);
+
+ Services.obs.addObserver(this, "browser-idle-startup-tasks-finished");
+ Services.obs.addObserver(this, "mail-idle-startup-tasks-finished");
+ Services.obs.addObserver(this, "quit-application");
+
+ // With Bug 1717899 we will extend the lifetime of the Remote Agent to
+ // the whole Firefox session, which will be identical to Marionette. For
+ // now prevent logging if the component is not enabled during startup.
+ if (
+ (lazy.activeProtocols & WEBDRIVER_BIDI_ACTIVE) ===
+ WEBDRIVER_BIDI_ACTIVE
+ ) {
+ this.#webDriverBiDi = new lazy.WebDriverBiDi(this);
+ if (this.#enabled) {
+ lazy.logger.debug("WebDriver BiDi enabled");
+ }
+ }
+
+ if ((lazy.activeProtocols & CDP_ACTIVE) === CDP_ACTIVE) {
+ this.#cdp = new lazy.CDP(this);
+ if (this.#enabled) {
+ lazy.logger.debug("CDP enabled");
+ }
+ }
+ }
+ break;
+
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, topic);
+
+ try {
+ await this.#listen(this.#port);
+ } catch (e) {
+ throw Error(`Unable to start remote agent: ${e}`);
+ }
+
+ 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;
+
+ // Listen for application shutdown to also shutdown the Remote Agent
+ // and a possible running instance of httpd.js.
+ case "quit-application":
+ Services.obs.removeObserver(this, topic);
+ this.#stop();
+ break;
+ }
+ }
+
+ receiveMessage({ name }) {
+ switch (name) {
+ case "RemoteAgent:IsRunning":
+ return this.running;
+
+ default:
+ lazy.logger.warn("Unknown IPC message to parent process: " + name);
+ return null;
+ }
+ }
+
+ // XPCOM
+
+ get classID() {
+ return this.#classID;
+ }
+
+ get helpInfo() {
+ return ` --remote-debugging-port [<port>] Start the Firefox Remote Agent,
+ which is a low-level remote debugging interface used for WebDriver
+ BiDi and CDP. Defaults to port 9222.
+ --remote-allow-hosts <hosts> Values of the Host header to allow for incoming requests.
+ Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html
+ --remote-allow-origins <origins> Values of the Origin header to allow for incoming requests.
+ Please read security guidelines at https://firefox-source-docs.mozilla.org/remote/Security.html\n`;
+ }
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI([
+ "nsICommandLineHandler",
+ "nsIObserver",
+ "nsIRemoteAgent",
+ ]);
+ }
+}
+
+class RemoteAgentContentProcess {
+ #classID;
+
+ constructor() {
+ this.#classID = Components.ID("{8f685a9d-8181-46d6-a71d-869289099c6d}");
+ }
+
+ get running() {
+ let reply = Services.cpmm.sendSyncMessage("RemoteAgent:IsRunning");
+ if (!reply.length) {
+ lazy.logger.warn("No reply from parent process");
+ return false;
+ }
+ return reply[0];
+ }
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIRemoteAgent"]);
+ }
+}
+
+export var RemoteAgent;
+if (isRemote) {
+ RemoteAgent = new RemoteAgentContentProcess();
+} else {
+ RemoteAgent = new RemoteAgentParentProcess();
+}
+
+// This is used by the XPCOM codepath which expects a constructor
+export var RemoteAgentFactory = function () {
+ return RemoteAgent;
+};
diff --git a/remote/components/components.conf b/remote/components/components.conf
new file mode 100644
index 0000000000..e518af3dd5
--- /dev/null
+++ b/remote/components/components.conf
@@ -0,0 +1,29 @@
+# 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/.
+
+Classes = [
+ # Remote Agent
+ {
+ "cid": "{8f685a9d-8181-46d6-a71d-869289099c6d}",
+ "contract_ids": ["@mozilla.org/remote/agent;1"],
+ "categories": {
+ "command-line-handler": "m-remote",
+ "profile-after-change": "RemoteAgent",
+ },
+ 'esModule': "chrome://remote/content/components/RemoteAgent.sys.mjs",
+ "constructor": "RemoteAgentFactory",
+ },
+
+ # Marionette
+ {
+ "cid": "{786a1369-dca5-4adc-8486-33d23c88010a}",
+ "contract_ids": ["@mozilla.org/remote/marionette;1"],
+ "categories": {
+ "command-line-handler": "m-marionette",
+ "profile-after-change": "Marionette",
+ },
+ 'esModule': "chrome://remote/content/components/Marionette.sys.mjs",
+ "constructor": "MarionetteFactory",
+ },
+]
diff --git a/remote/components/moz.build b/remote/components/moz.build
new file mode 100644
index 0000000000..82b4891b6a
--- /dev/null
+++ b/remote/components/moz.build
@@ -0,0 +1,17 @@
+# 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/.
+
+XPIDL_MODULE = "remote"
+
+XPIDL_SOURCES += [
+ "nsIMarionette.idl",
+ "nsIRemoteAgent.idl",
+]
+
+XPCOM_MANIFESTS += ["components.conf"]
+
+with Files("Marionette.sys.mjs"):
+ BUG_COMPONENT = ("Remote Protocol", "Marionette")
+with Files("nsIMarionette.idl"):
+ BUG_COMPONENT = ("Remote Protocol", "Marionette")
diff --git a/remote/components/nsIMarionette.idl b/remote/components/nsIMarionette.idl
new file mode 100644
index 0000000000..c10bf4b17b
--- /dev/null
+++ b/remote/components/nsIMarionette.idl
@@ -0,0 +1,17 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#define NS_MARIONETTE_CONTRACTID "@mozilla.org/remote/marionette;1"
+%}
+
+/** Interface for accessing the Marionette server instance. */
+[scriptable, uuid(13fa7d76-f976-4711-a00c-29ac9c1881e1)]
+interface nsIMarionette : nsISupports
+{
+ /** Indicates whether Marionette is running. */
+ readonly attribute boolean running;
+};
diff --git a/remote/components/nsIRemoteAgent.idl b/remote/components/nsIRemoteAgent.idl
new file mode 100644
index 0000000000..89d637bed2
--- /dev/null
+++ b/remote/components/nsIRemoteAgent.idl
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * The Gecko remote agent is an RPC subsystem that exposes
+ * browser-internal interfaces and services to the surrounding
+ * system.
+ *
+ * Consumers, whether remote or browser-local, can interface with
+ * the browser through an assorted set of services ranging from
+ * document introspection and script evaluation, to instrumentation,
+ * user interaction simulation, and event subscription.
+ */
+[scriptable, uuid(8f685a9d-8181-46d6-a71d-869289099c6d)]
+interface nsIRemoteAgent : nsISupports
+{
+ /**
+ * Address of the HTTP server under which the remote agent is reachable.
+ */
+ readonly attribute AString debuggerAddress;
+
+ /**
+ * Indicates whether the Remote Agent is running.
+ */
+ readonly attribute boolean running;
+};
+
+%{C++
+#define NS_REMOTEAGENT_CONTRACTID "@mozilla.org/remote/agent;1"
+#define NS_REMOTEAGENT_CID \
+ { 0x8f685a9d, 0x8181, 0x46d6, \
+ { 0xa7, 0x1d, x86, x92, x89, x09, x9c, x6d } }
+%}