summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/transports
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/messagehandler/transports')
-rw-r--r--remote/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs57
-rw-r--r--remote/shared/messagehandler/transports/RootTransport.sys.mjs188
-rw-r--r--remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs51
-rw-r--r--remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs111
-rw-r--r--remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.sys.mjs127
5 files changed, 534 insertions, 0 deletions
diff --git a/remote/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs b/remote/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs
new file mode 100644
index 0000000000..482f90948a
--- /dev/null
+++ b/remote/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs
@@ -0,0 +1,57 @@
+/* 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/. */
+
+function isExtensionContext(browsingContext) {
+ let principal;
+ if (CanonicalBrowsingContext.isInstance(browsingContext)) {
+ principal = browsingContext.currentWindowGlobal.documentPrincipal;
+ } else {
+ principal = browsingContext.window.document.nodePrincipal;
+ }
+
+ // In practice, note that the principal will never be an expanded principal.
+ // The are only used for content scripts executed in a Sandbox, and do not
+ // have a browsing context on their own.
+ // But we still use this flag because there is no isAddonPrincipal flag.
+ return principal.isAddonOrExpandedAddonPrincipal;
+}
+
+function isParentProcess(browsingContext) {
+ if (CanonicalBrowsingContext.isInstance(browsingContext)) {
+ return browsingContext.currentWindowGlobal.osPid === -1;
+ }
+
+ // If `browsingContext` is not a `CanonicalBrowsingContext`, then we are
+ // necessarily in a content process page.
+ return false;
+}
+
+/**
+ * Check if the given browsing context is valid for the message handler
+ * to use.
+ *
+ * @param {BrowsingContext} browsingContext
+ * The browsing context to check.
+ * @param {object=} options
+ * @param {string=} options.browserId
+ * The id of the browser to filter the browsing contexts by (optional).
+ * @returns {boolean}
+ * True if the browsing context is valid, false otherwise.
+ */
+export function isBrowsingContextCompatible(browsingContext, options = {}) {
+ const { browserId } = options;
+
+ // If a browserId was provided, skip browsing contexts which are not
+ // associated with this browserId.
+ if (browserId !== undefined && browsingContext.browserId !== browserId) {
+ return false;
+ }
+
+ // Skip:
+ // - extension contexts until we support debugging webextensions, see Bug 1755014.
+ // - privileged contexts until we support debugging Chrome context, see Bug 1713440.
+ return (
+ !isExtensionContext(browsingContext) && !isParentProcess(browsingContext)
+ );
+}
diff --git a/remote/shared/messagehandler/transports/RootTransport.sys.mjs b/remote/shared/messagehandler/transports/RootTransport.sys.mjs
new file mode 100644
index 0000000000..b60d3726ef
--- /dev/null
+++ b/remote/shared/messagehandler/transports/RootTransport.sys.mjs
@@ -0,0 +1,188 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContextDescriptorType:
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
+ isBrowsingContextCompatible:
+ "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ MessageHandlerFrameActor:
+ "chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+const MAX_RETRY_ATTEMPTS = 10;
+
+/**
+ * RootTransport is intended to be used from a ROOT MessageHandler to communicate
+ * with WINDOW_GLOBAL MessageHandlers via the MessageHandlerFrame JSWindow
+ * actors.
+ */
+export class RootTransport {
+ /**
+ * @param {MessageHandler} messageHandler
+ * The MessageHandler instance which owns this RootTransport instance.
+ */
+ constructor(messageHandler) {
+ this._messageHandler = messageHandler;
+
+ // RootTransport will rely on the MessageHandlerFrame JSWindow actors.
+ // Make sure they are registered when instanciating a RootTransport.
+ lazy.MessageHandlerFrameActor.register();
+ }
+
+ /**
+ * Forward the provided command to WINDOW_GLOBAL MessageHandlers via the
+ * MessageHandlerFrame actors.
+ *
+ * @param {Command} command
+ * The command to forward. See type definition in MessageHandler.js
+ * @returns {Promise}
+ * Returns a promise that resolves with the result of the command after
+ * being processed by WINDOW_GLOBAL MessageHandlers.
+ */
+ forwardCommand(command) {
+ if (command.destination.id && command.destination.contextDescriptor) {
+ throw new Error(
+ "Invalid command destination with both 'id' and 'contextDescriptor' properties"
+ );
+ }
+
+ // With an id given forward the command to only this specific destination.
+ if (command.destination.id) {
+ const browsingContext = BrowsingContext.get(command.destination.id);
+ if (!browsingContext) {
+ throw new Error(
+ "Unable to find a BrowsingContext for id " + command.destination.id
+ );
+ }
+ return this._sendCommandToBrowsingContext(command, browsingContext);
+ }
+
+ // ... otherwise broadcast to destinations matching the contextDescriptor.
+ if (command.destination.contextDescriptor) {
+ return this._broadcastCommand(command);
+ }
+
+ throw new Error(
+ "Unrecognized command destination, missing 'id' or 'contextDescriptor' properties"
+ );
+ }
+
+ _broadcastCommand(command) {
+ const { contextDescriptor } = command.destination;
+ const browsingContexts =
+ this._getBrowsingContextsForDescriptor(contextDescriptor);
+
+ return Promise.all(
+ browsingContexts.map(async browsingContext => {
+ try {
+ return await this._sendCommandToBrowsingContext(
+ command,
+ browsingContext
+ );
+ } catch (e) {
+ console.error(
+ `Failed to broadcast a command to browsingContext ${browsingContext.id}`,
+ e
+ );
+ return null;
+ }
+ })
+ );
+ }
+
+ async _sendCommandToBrowsingContext(command, browsingContext) {
+ const name = `${command.moduleName}.${command.commandName}`;
+
+ // The browsing context might be destroyed by a navigation. Keep a reference
+ // to the webProgress, which will persist, and always use it to retrieve the
+ // currently valid browsing context.
+ const webProgress = browsingContext.webProgress;
+
+ const { retryOnAbort = false } = command;
+
+ let attempts = 0;
+ while (true) {
+ try {
+ return await webProgress.browsingContext.currentWindowGlobal
+ .getActor("MessageHandlerFrame")
+ .sendCommand(command, this._messageHandler.sessionId);
+ } catch (e) {
+ if (!retryOnAbort || e.name != "AbortError") {
+ // Only retry if the command supports retryOnAbort and when the
+ // JSWindowActor pair gets destroyed.
+ throw e;
+ }
+
+ if (++attempts > MAX_RETRY_ATTEMPTS) {
+ lazy.logger.trace(
+ `RootTransport reached the limit of retry attempts (${MAX_RETRY_ATTEMPTS})` +
+ ` for command ${name} and browsing context ${webProgress.browsingContext.id}.`
+ );
+ throw e;
+ }
+
+ lazy.logger.trace(
+ `RootTransport retrying command ${name} for ` +
+ `browsing context ${webProgress.browsingContext.id}, attempt: ${attempts}.`
+ );
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+ }
+ }
+ }
+
+ toString() {
+ return `[object ${this.constructor.name} ${this._messageHandler.name}]`;
+ }
+
+ _getBrowsingContextsForDescriptor(contextDescriptor) {
+ const { id, type } = contextDescriptor;
+
+ if (type === lazy.ContextDescriptorType.All) {
+ return this._getBrowsingContexts();
+ }
+
+ if (type === lazy.ContextDescriptorType.TopBrowsingContext) {
+ return this._getBrowsingContexts({ browserId: id });
+ }
+
+ // TODO: Handle other types of context descriptors.
+ throw new Error(
+ `Unsupported contextDescriptor type for broadcasting: ${type}`
+ );
+ }
+
+ /**
+ * Get all browsing contexts, optionally matching the provided options.
+ *
+ * @param {object} options
+ * @param {string=} options.browserId
+ * The id of the browser to filter the browsing contexts by (optional).
+ * @returns {Array<BrowsingContext>}
+ * The browsing contexts matching the provided options or all browsing contexts
+ * if no options are provided.
+ */
+ _getBrowsingContexts(options = {}) {
+ // extract browserId from options
+ const { browserId } = options;
+ let browsingContexts = [];
+
+ // Fetch all tab related browsing contexts for top-level windows.
+ for (const { browsingContext } of lazy.TabManager.browsers) {
+ if (lazy.isBrowsingContextCompatible(browsingContext, { browserId })) {
+ browsingContexts = browsingContexts.concat(
+ browsingContext.getAllBrowsingContextsInSubtree()
+ );
+ }
+ }
+
+ return browsingContexts;
+ }
+}
diff --git a/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs
new file mode 100644
index 0000000000..c236cebac7
--- /dev/null
+++ b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.sys.mjs
@@ -0,0 +1,51 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs",
+
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+const FRAME_ACTOR_CONFIG = {
+ parent: {
+ esModuleURI:
+ "chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.sys.mjs",
+ },
+ child: {
+ esModuleURI:
+ "chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs",
+ events: {
+ DOMWindowCreated: {},
+ pagehide: {},
+ pageshow: {},
+ },
+ },
+ allFrames: true,
+ messageManagerGroups: ["browsers"],
+};
+
+/**
+ * MessageHandlerFrameActor exposes a simple registration helper to lazily
+ * register MessageHandlerFrame JSWindow actors.
+ */
+export const MessageHandlerFrameActor = {
+ registered: false,
+
+ register() {
+ if (this.registered) {
+ return;
+ }
+
+ lazy.ActorManagerParent.addJSWindowActors({
+ MessageHandlerFrame: FRAME_ACTOR_CONFIG,
+ });
+ this.registered = true;
+ lazy.logger.trace("Registered MessageHandlerFrame actors");
+ },
+};
diff --git a/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs
new file mode 100644
index 0000000000..52a8fdc4c9
--- /dev/null
+++ b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs
@@ -0,0 +1,111 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ isBrowsingContextCompatible:
+ "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs",
+ MessageHandlerRegistry:
+ "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+});
+
+/**
+ * Map from MessageHandlerRegistry to MessageHandlerFrameChild actor. This will
+ * allow a WindowGlobalMessageHandler to find the JSWindowActorChild instance to
+ * use to send commands.
+ */
+const registryToActor = new WeakMap();
+
+/**
+ * Retrieve the MessageHandlerFrameChild which is linked to the provided
+ * WindowGlobalMessageHandler instance.
+ *
+ * @param {WindowGlobalMessageHandler} messageHandler
+ * The WindowGlobalMessageHandler for which to get the JSWindowActor.
+ * @returns {MessageHandlerFrameChild}
+ * The corresponding MessageHandlerFrameChild instance.
+ */
+export function getMessageHandlerFrameChildActor(messageHandler) {
+ return registryToActor.get(messageHandler.registry);
+}
+
+/**
+ * Child actor for the MessageHandlerFrame JSWindowActor. The
+ * MessageHandlerFrame actor is used by RootTransport to communicate between
+ * ROOT MessageHandlers and WINDOW_GLOBAL MessageHandlers.
+ */
+export class MessageHandlerFrameChild extends JSWindowActorChild {
+ actorCreated() {
+ this.type = lazy.WindowGlobalMessageHandler.type;
+ this.context = this.manager.browsingContext;
+
+ this._registry = new lazy.MessageHandlerRegistry(this.type, this.context);
+ registryToActor.set(this._registry, this);
+
+ this._onRegistryEvent = this._onRegistryEvent.bind(this);
+
+ // MessageHandlerFrameChild is responsible for forwarding events from
+ // WindowGlobalMessageHandler to the parent process.
+ // Such events are re-emitted on the MessageHandlerRegistry to avoid
+ // setting up listeners on individual MessageHandler instances.
+ this._registry.on("message-handler-registry-event", this._onRegistryEvent);
+ }
+
+ handleEvent({ persisted, type }) {
+ if (type == "DOMWindowCreated" || (type == "pageshow" && persisted)) {
+ // When the window is created or is retrieved from BFCache, instantiate
+ // a MessageHandler for all sessions which might need it.
+ if (lazy.isBrowsingContextCompatible(this.manager.browsingContext)) {
+ this._registry.createAllMessageHandlers();
+ }
+ } else if (type == "pagehide" && persisted) {
+ // When the page is moved to BFCache, all the currently created message
+ // handlers should be destroyed.
+ this._registry.destroy();
+ }
+ }
+
+ async receiveMessage(message) {
+ if (message.name === "MessageHandlerFrameParent:sendCommand") {
+ const { sessionId, command } = message.data;
+ const messageHandler =
+ this._registry.getOrCreateMessageHandler(sessionId);
+ try {
+ return await messageHandler.handleCommand(command);
+ } catch (e) {
+ if (e?.isRemoteError) {
+ return {
+ error: e.toJSON(),
+ isMessageHandlerError: e.isMessageHandlerError,
+ };
+ }
+ throw e;
+ }
+ }
+
+ return null;
+ }
+
+ sendCommand(command, sessionId) {
+ return this.sendQuery("MessageHandlerFrameChild:sendCommand", {
+ command,
+ sessionId,
+ });
+ }
+
+ _onRegistryEvent(eventName, wrappedEvent) {
+ this.sendAsyncMessage(
+ "MessageHandlerFrameChild:messageHandlerEvent",
+ wrappedEvent
+ );
+ }
+
+ didDestroy() {
+ this._registry.off("message-handler-registry-event", this._onRegistryEvent);
+ this._registry.destroy();
+ }
+}
diff --git a/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.sys.mjs b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.sys.mjs
new file mode 100644
index 0000000000..a4901571d9
--- /dev/null
+++ b/remote/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.sys.mjs
@@ -0,0 +1,127 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ RootMessageHandlerRegistry:
+ "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
+ WindowGlobalMessageHandler:
+ "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+ChromeUtils.defineLazyGetter(lazy, "WebDriverError", () => {
+ return ChromeUtils.importESModule(
+ "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
+ ).error.WebDriverError;
+});
+
+/**
+ * Parent actor for the MessageHandlerFrame JSWindowActor. The
+ * MessageHandlerFrame actor is used by RootTransport to communicate between
+ * ROOT MessageHandlers and WINDOW_GLOBAL MessageHandlers.
+ */
+export class MessageHandlerFrameParent extends JSWindowActorParent {
+ async receiveMessage(message) {
+ switch (message.name) {
+ case "MessageHandlerFrameChild:sendCommand": {
+ return this.#handleSendCommandMessage(message.data);
+ }
+ case "MessageHandlerFrameChild:messageHandlerEvent": {
+ return this.#handleMessageHandlerEventMessage(message.data);
+ }
+ default:
+ throw new Error("Unsupported message:" + message.name);
+ }
+ }
+
+ /**
+ * Send a command to the corresponding MessageHandlerFrameChild actor via a
+ * JSWindowActor query.
+ *
+ * @param {Command} command
+ * The command to forward. See type definition in MessageHandler.js
+ * @param {string} sessionId
+ * ID of the session that sent the command.
+ * @returns {Promise}
+ * Promise that will resolve with the result of query sent to the
+ * MessageHandlerFrameChild actor.
+ */
+ async sendCommand(command, sessionId) {
+ const result = await this.sendQuery(
+ "MessageHandlerFrameParent:sendCommand",
+ {
+ command,
+ sessionId,
+ }
+ );
+
+ if (result?.error) {
+ if (result.isMessageHandlerError) {
+ throw lazy.error.MessageHandlerError.fromJSON(result.error);
+ }
+
+ // TODO: Do not assume WebDriver is the session protocol, see Bug 1779026.
+ throw lazy.WebDriverError.fromJSON(result.error);
+ }
+
+ return result;
+ }
+
+ async #handleMessageHandlerEventMessage(messageData) {
+ const { name, contextInfo, data, sessionId } = messageData;
+ const [moduleName] = name.split(".");
+
+ // Re-emit the event on the RootMessageHandler.
+ const messageHandler =
+ lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId);
+ // TODO: getModuleInstance expects a CommandDestination in theory,
+ // but only uses the MessageHandler type in practice, see Bug 1776389.
+ const module = messageHandler.moduleCache.getModuleInstance(moduleName, {
+ type: lazy.WindowGlobalMessageHandler.type,
+ });
+ let eventPayload = data;
+
+ // Modify an event payload if there is a special method in the targeted module.
+ // If present it can be found in windowglobal-in-root module.
+ if (module?.interceptEvent) {
+ eventPayload = await module.interceptEvent(name, data);
+
+ if (eventPayload === null) {
+ lazy.logger.trace(
+ `${moduleName}.interceptEvent returned null, skipping event: ${name}, data: ${data}`
+ );
+ return;
+ }
+ // Make sure that an event payload is returned.
+ if (!eventPayload) {
+ throw new Error(
+ `${moduleName}.interceptEvent doesn't return the event payload`
+ );
+ }
+ }
+ messageHandler.emitEvent(name, eventPayload, contextInfo);
+ }
+
+ async #handleSendCommandMessage(messageData) {
+ const { sessionId, command } = messageData;
+ const messageHandler =
+ lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId);
+ try {
+ return await messageHandler.handleCommand(command);
+ } catch (e) {
+ if (e?.isRemoteError) {
+ return {
+ error: e.toJSON(),
+ isMessageHandlerError: e.isMessageHandlerError,
+ };
+ }
+ throw e;
+ }
+ }
+}