diff --git a/remote/shared/messagehandler/MessageHandler.sys.mjs b/remote/shared/messagehandler/MessageHandler.sys.mjs index 86eb2dc950aa..18ec6b820c36 100644 --- a/remote/shared/messagehandler/MessageHandler.sys.mjs +++ b/remote/shared/messagehandler/MessageHandler.sys.mjs @@ -263,15 +263,15 @@ export class MessageHandler extends EventEmitter { } /** - * Apply the initial session data items provided to this MessageHandler on - * startup. Implementation is specific to each MessageHandler class. + * Execute the required initialization steps, inlcluding apply the initial session data items + * provided to this MessageHandler on startup. Implementation is specific to each MessageHandler class. * * By default the implementation is a no-op. * * @param {Array} sessionDataItems * Initial session data items for this MessageHandler. */ - async applyInitialSessionDataItems(sessionDataItems) {} + async initialize(sessionDataItems) {} /** * Returns the module path corresponding to this MessageHandler class. diff --git a/remote/shared/messagehandler/MessageHandlerRegistry.sys.mjs b/remote/shared/messagehandler/MessageHandlerRegistry.sys.mjs index 538f4bfc29bd..6a09173f5023 100644 --- a/remote/shared/messagehandler/MessageHandlerRegistry.sys.mjs +++ b/remote/shared/messagehandler/MessageHandlerRegistry.sys.mjs @@ -202,7 +202,7 @@ export class MessageHandlerRegistry extends EventEmitter { ); messageHandler.on("message-handler-event", this._onMessageHandlerEvent); - messageHandler.applyInitialSessionDataItems(sessionDataItems); + messageHandler.initialize(sessionDataItems); this._messageHandlersMap.set(sessionId, messageHandler); diff --git a/remote/shared/messagehandler/RootMessageHandler.sys.mjs b/remote/shared/messagehandler/RootMessageHandler.sys.mjs index aef945d15f46..e3c2652927fe 100644 --- a/remote/shared/messagehandler/RootMessageHandler.sys.mjs +++ b/remote/shared/messagehandler/RootMessageHandler.sys.mjs @@ -25,6 +25,7 @@ ChromeUtils.defineESModuleGetters(lazy, { */ export class RootMessageHandler extends MessageHandler { #navigationManager; + #realms; #rootTransport; #sessionData; @@ -67,12 +68,25 @@ export class RootMessageHandler extends MessageHandler { this.#sessionData = new lazy.SessionData(this); this.#navigationManager = new lazy.NavigationManager(); this.#navigationManager.startMonitoring(); + + // Map with inner window ids as keys, and sets of realm ids, assosiated with + // this window as values. + this.#realms = new Map(); + // In the general case, we don't get notified that realms got destroyed, + // because there is no communication between content and parent process at this moment, + // so we have to listen to the this notification to clean up the internal + // map and trigger the events. + Services.obs.addObserver(this, "window-global-destroyed"); } get navigationManager() { return this.#navigationManager; } + get realms() { + return this.#realms; + } + get sessionData() { return this.#sessionData; } @@ -80,6 +94,10 @@ export class RootMessageHandler extends MessageHandler { destroy() { this.#sessionData.destroy(); this.#navigationManager.destroy(); + + Services.obs.removeObserver(this, "window-global-destroyed"); + this.#realms = null; + super.destroy(); } @@ -96,6 +114,22 @@ export class RootMessageHandler extends MessageHandler { return this.updateSessionData([sessionData]); } + emitEvent(name, eventPayload, contextInfo) { + // Intercept realm created and destroyed events to update internal map. + if (name === "realm-created") { + this.#onRealmCreated(eventPayload); + } + // We receive this events in the case of moving the page to BFCache. + if (name === "windowglobal-pagehide") { + this.#cleanUpRealmsForWindow( + eventPayload.innerWindowId, + eventPayload.context + ); + } + + super.emitEvent(name, eventPayload, contextInfo); + } + /** * Emit a public protocol event. This event will be sent over to the client. * @@ -137,6 +171,17 @@ export class RootMessageHandler extends MessageHandler { return true; } + observe(subject, topic) { + if (topic !== "window-global-destroyed") { + return; + } + + this.#cleanUpRealmsForWindow( + subject.innerWindowId, + subject.browsingContext + ); + } + /** * Remove session data items of a given module, category and * contextDescriptor. @@ -160,4 +205,33 @@ export class RootMessageHandler extends MessageHandler { async updateSessionData(sessionData = []) { await this.#sessionData.updateSessionData(sessionData); } + + #cleanUpRealmsForWindow(innerWindowId, context) { + const realms = this.#realms.get(innerWindowId); + + if (!realms) { + return; + } + + realms.forEach(realm => { + this.#realms.get(innerWindowId).delete(realm); + + this.emitEvent("realm-destroyed", { + context, + realm, + }); + }); + + this.#realms.delete(innerWindowId); + } + + #onRealmCreated = data => { + const { innerWindowId, realmId } = data; + + if (!this.#realms.has(innerWindowId)) { + this.#realms.set(innerWindowId, new Set()); + } + + this.#realms.get(innerWindowId).add(realmId); + }; } diff --git a/remote/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs b/remote/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs index 1e0164cf44fe..d2ec6024ae47 100644 --- a/remote/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs +++ b/remote/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs @@ -34,15 +34,33 @@ export class WindowGlobalMessageHandler extends MessageHandler { this.#innerWindowId = this.context.window.windowGlobalChild.innerWindowId; - // Maps sandbox names to instances of window realms, - // the default realm is mapped to an empty string sandbox name. - this.#realms = new Map([["", new lazy.WindowRealm(this.context.window)]]); + // Maps sandbox names to instances of window realms. + this.#realms = new Map(); + } + + initialize(sessionDataItems) { + // Create the default realm, it is mapped to an empty string sandbox name. + this.#realms.set("", this.#createRealm()); + + // This method, even though being async, is not awaited on purpose, + // since for now the sessionDataItems are passed in response to an event in a for loop. + this.#applyInitialSessionDataItems(sessionDataItems); + + // With the session data applied the handler is now ready to be used. + this.emitEvent("window-global-handler-created", { + contextId: this.contextId, + innerWindowId: this.#innerWindowId, + }); } destroy() { for (const realm of this.#realms.values()) { realm.destroy(); } + this.emitEvent("windowglobal-pagehide", { + context: this.context, + innerWindowId: this.innerWindowId, + }); this.#realms = null; super.destroy(); @@ -92,6 +110,19 @@ export class WindowGlobalMessageHandler extends MessageHandler { return this.context.window; } + #createRealm(sandboxName = null) { + const realm = new lazy.WindowRealm(this.context.window, { + sandboxName, + }); + + this.emitEvent("realm-created", { + realmId: realm.id, + innerWindowId: this.innerWindowId, + }); + + return realm; + } + #getRealmFromSandboxName(sandboxName = null) { if (sandboxName === null || sandboxName === "") { return this.#realms.get(""); @@ -101,16 +132,14 @@ export class WindowGlobalMessageHandler extends MessageHandler { return this.#realms.get(sandboxName); } - const realm = new lazy.WindowRealm(this.context.window, { - sandboxName, - }); + const realm = this.#createRealm(sandboxName); this.#realms.set(sandboxName, realm); return realm; } - async applyInitialSessionDataItems(sessionDataItems) { + async #applyInitialSessionDataItems(sessionDataItems) { if (!Array.isArray(sessionDataItems)) { return; } @@ -163,12 +192,6 @@ export class WindowGlobalMessageHandler extends MessageHandler { } await Promise.all(sessionDataPromises); - - // With the session data applied the handler is now ready to be used. - this.emitEvent("window-global-handler-created", { - contextId: this.contextId, - innerWindowId: this.#innerWindowId, - }); } forwardCommand(command) { diff --git a/remote/shared/messagehandler/test/browser/browser.ini b/remote/shared/messagehandler/test/browser/browser.ini index 8a3cc19d8b56..c603b63a3e3e 100644 --- a/remote/shared/messagehandler/test/browser/browser.ini +++ b/remote/shared/messagehandler/test/browser/browser.ini @@ -16,6 +16,7 @@ prefs = [browser_handle_command_retry.js] [browser_handle_simple_command.js] [browser_navigation_manager.js] +[browser_realms.js] [browser_registry.js] [browser_session_data.js] [browser_session_data_browser_element.js] diff --git a/remote/shared/messagehandler/test/browser/browser_events_module.js b/remote/shared/messagehandler/test/browser/browser_events_module.js index 536c1d9754a3..32b60d34b14f 100644 --- a/remote/shared/messagehandler/test/browser/browser_events_module.js +++ b/remote/shared/messagehandler/test/browser/browser_events_module.js @@ -24,19 +24,38 @@ add_task(async function test_event() { const browsingContext = tab.linkedBrowser.browsingContext; const rootMessageHandler = createRootMessageHandler("session-id-event"); + let messageHandlerEvent; + let registryEvent; // Events are emitted both as generic message-handler-event events as well // as under their own name. We expect to receive the event for both. - const onHandlerEvent = rootMessageHandler.once("message-handler-event"); + const _onMessageHandlerEvent = (eventName, eventData) => { + if (eventData.name === "event-from-window-global") { + messageHandlerEvent = eventData; + } + }; + rootMessageHandler.on("message-handler-event", _onMessageHandlerEvent); const onNamedEvent = rootMessageHandler.once("event-from-window-global"); // MessageHandlerRegistry should forward all the message-handler-events. - const onRegistryEvent = RootMessageHandlerRegistry.once( - "message-handler-registry-event" + const _onMessageHandlerRegistryEvent = (eventName, eventData) => { + if (eventData.name === "event-from-window-global") { + registryEvent = eventData; + } + }; + RootMessageHandlerRegistry.on( + "message-handler-registry-event", + _onMessageHandlerRegistryEvent ); callTestEmitEvent(rootMessageHandler, browsingContext.id); - const messageHandlerEvent = await onHandlerEvent; + const namedEvent = await onNamedEvent; + is( + namedEvent.text, + `event from ${browsingContext.id}`, + "Received the expected payload" + ); + is( messageHandlerEvent.name, "event-from-window-global", @@ -48,20 +67,16 @@ add_task(async function test_event() { "Received the expected payload" ); - const namedEvent = await onNamedEvent; - is( - namedEvent.text, - `event from ${browsingContext.id}`, - "Received the expected payload" - ); - - const registryEvent = await onRegistryEvent; is( registryEvent, messageHandlerEvent, "The event forwarded by the MessageHandlerRegistry is identical to the MessageHandler event" ); - + rootMessageHandler.off("message-handler-event", _onMessageHandlerEvent); + RootMessageHandlerRegistry.off( + "message-handler-registry-event", + _onMessageHandlerRegistryEvent + ); rootMessageHandler.destroy(); gBrowser.removeTab(tab); }); diff --git a/remote/shared/messagehandler/test/browser/browser_realms.js b/remote/shared/messagehandler/test/browser/browser_realms.js new file mode 100644 index 000000000000..2ec98ca08dc9 --- /dev/null +++ b/remote/shared/messagehandler/test/browser/browser_realms.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { RootMessageHandler } = ChromeUtils.importESModule( + "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs" +); + +add_task(async function test_tab_is_removed() { + const tab = await addTab("https://example.com/document-builder.sjs?html=tab"); + const sessionId = "realms"; + const browsingContext = tab.linkedBrowser.browsingContext; + const contextDescriptor = { + type: ContextDescriptorType.TopBrowsingContext, + id: browsingContext.browserId, + }; + + const rootMessageHandler = createRootMessageHandler(sessionId); + + const onRealmCreated = rootMessageHandler.once("realm-created"); + + // Add a new session data item to get window global handlers created + await rootMessageHandler.addSessionDataItem({ + moduleName: "command", + category: "browser_realms", + contextDescriptor, + values: [true], + }); + + const realmCreatedEvent = await onRealmCreated; + const createdRealmId = realmCreatedEvent.realmId; + + is(rootMessageHandler.realms.size, 1, "Realm is added in the internal map"); + + const onRealmDestroyed = rootMessageHandler.once("realm-destroyed"); + + gBrowser.removeTab(tab); + + const realmDestroyedEvent = await onRealmDestroyed; + + is( + realmDestroyedEvent.realm, + createdRealmId, + "Received a correct realm id in realm-destroyed event" + ); + is(rootMessageHandler.realms.size, 0, "The realm map is cleaned up"); + + rootMessageHandler.destroy(); +}); + +add_task(async function test_same_origin_navigation() { + const tab = await addTab("https://example.com/document-builder.sjs?html=tab"); + const sessionId = "realms"; + const browsingContext = tab.linkedBrowser.browsingContext; + const contextDescriptor = { + type: ContextDescriptorType.TopBrowsingContext, + id: browsingContext.browserId, + }; + + const rootMessageHandler = createRootMessageHandler(sessionId); + + const onRealmCreated = rootMessageHandler.once("realm-created"); + + // Add a new session data item to get window global handlers created + await rootMessageHandler.addSessionDataItem({ + moduleName: "command", + category: "browser_realms", + contextDescriptor, + values: [true], + }); + + const realmCreatedEvent = await onRealmCreated; + const createdRealmId = realmCreatedEvent.realmId; + + is(rootMessageHandler.realms.size, 1, "Realm is added in the internal map"); + + const onRealmDestroyed = rootMessageHandler.once("realm-destroyed"); + const onNewRealmCreated = rootMessageHandler.once("realm-created"); + + // Navigate to another page with the same origin + await loadURL( + tab.linkedBrowser, + "https://example.com/document-builder.sjs?html=othertab" + ); + + const realmDestroyedEvent = await onRealmDestroyed; + + is( + realmDestroyedEvent.realm, + createdRealmId, + "Received a correct realm id in realm-destroyed event" + ); + + await onNewRealmCreated; + + is(rootMessageHandler.realms.size, 1, "Realm is added in the internal map"); + + gBrowser.removeTab(tab); + rootMessageHandler.destroy(); +}); + +add_task(async function test_cross_origin_navigation() { + const tab = await addTab("https://example.com/document-builder.sjs?html=tab"); + const sessionId = "realms"; + const browsingContext = tab.linkedBrowser.browsingContext; + const contextDescriptor = { + type: ContextDescriptorType.TopBrowsingContext, + id: browsingContext.browserId, + }; + + const rootMessageHandler = createRootMessageHandler(sessionId); + + const onRealmCreated = rootMessageHandler.once("realm-created"); + + // Add a new session data item to get window global handlers created + await rootMessageHandler.addSessionDataItem({ + moduleName: "command", + category: "browser_realms", + contextDescriptor, + values: [true], + }); + + const realmCreatedEvent = await onRealmCreated; + const createdRealmId = realmCreatedEvent.realmId; + + is(rootMessageHandler.realms.size, 1, "Realm is added in the internal map"); + + const onRealmDestroyed = rootMessageHandler.once("realm-destroyed"); + const onNewRealmCreated = rootMessageHandler.once("realm-created"); + + // Navigate to another page with the different origin + await loadURL( + tab.linkedBrowser, + "https://example.com/document-builder.sjs?html=otherorigin" + ); + + const realmDestroyedEvent = await onRealmDestroyed; + + is( + realmDestroyedEvent.realm, + createdRealmId, + "Received a correct realm id in realm-destroyed event" + ); + + await onNewRealmCreated; + + is(rootMessageHandler.realms.size, 1, "Realm is added in the internal map"); + + gBrowser.removeTab(tab); + rootMessageHandler.destroy(); +}); diff --git a/remote/shared/messagehandler/test/browser/browser_session_data.js b/remote/shared/messagehandler/test/browser/browser_session_data.js index f0fef31883d5..17015e892d2c 100644 --- a/remote/shared/messagehandler/test/browser/browser_session_data.js +++ b/remote/shared/messagehandler/test/browser/browser_session_data.js @@ -186,7 +186,9 @@ add_task(async function test_sessionDataRootOnlyModule() { "https://example.com/document-builder.sjs?html=tab" ); - const windowGlobalCreated = rootMessageHandler.once("message-handler-event"); + const windowGlobalCreated = rootMessageHandler.once( + "window-global-handler-created" + ); info("Test that adding SessionData items works the root module"); // Updating the session data on the root message handler should not cause diff --git a/remote/webdriver-bidi/modules/root/script.sys.mjs b/remote/webdriver-bidi/modules/root/script.sys.mjs index 4c72621144cc..1adcc1d43270 100644 --- a/remote/webdriver-bidi/modules/root/script.sys.mjs +++ b/remote/webdriver-bidi/modules/root/script.sys.mjs @@ -40,6 +40,7 @@ const ScriptEvaluateResultType = { class ScriptModule extends Module { #preloadScriptMap; + #subscribedEvents; constructor(messageHandler) { super(messageHandler); @@ -48,10 +49,14 @@ class ScriptModule extends Module { // with an item named expression, which is a string, // and an item named sandbox which is a string or null. this.#preloadScriptMap = new Map(); + + // Set of event names which have active subscriptions. + this.#subscribedEvents = new Set(); } destroy() { this.#preloadScriptMap = null; + this.#subscribedEvents = null; } /** @@ -746,8 +751,77 @@ class ScriptModule extends Module { .filter(realm => realm.context !== null); } + #onRealmDestroyed = (eventName, { realm, context }) => { + // This event is emitted from the parent process but for a given browsing + // context. Set the event's contextInfo to the message handler corresponding + // to this browsing context. + const contextInfo = { + contextId: context.id, + type: lazy.WindowGlobalMessageHandler.type, + }; + + this.emitEvent("script.realmDestroyed", { realm }, contextInfo); + }; + + #startListingOnRealmDestroyed() { + if (!this.#subscribedEvents.has("script.realmDestroyed")) { + this.messageHandler.on("realm-destroyed", this.#onRealmDestroyed); + } + } + + #stopListingOnRealmDestroyed() { + if (this.#subscribedEvents.has("script.realmDestroyed")) { + this.messageHandler.off("realm-destroyed", this.#onRealmDestroyed); + } + } + + #subscribeEvent(event) { + switch (event) { + case "script.realmDestroyed": { + this.#startListingOnRealmDestroyed(); + this.#subscribedEvents.add(event); + break; + } + } + } + + #unsubscribeEvent(event) { + switch (event) { + case "script.realmDestroyed": { + this.#stopListingOnRealmDestroyed(); + this.#subscribedEvents.delete(event); + break; + } + } + } + + _applySessionData(params) { + // TODO: Bug 1775231. Move this logic to a shared module or an abstract + // class. + const { category } = params; + if (category === "event") { + const filteredSessionData = params.sessionData.filter(item => + this.messageHandler.matchesContext(item.contextDescriptor) + ); + for (const event of this.#subscribedEvents.values()) { + const hasSessionItem = filteredSessionData.some( + item => item.value === event + ); + // If there are no session items for this context, we should unsubscribe from the event. + if (!hasSessionItem) { + this.#unsubscribeEvent(event); + } + } + + // Subscribe to all events, which have an item in SessionData. + for (const { value } of filteredSessionData) { + this.#subscribeEvent(value); + } + } + } + static get supportedEvents() { - return ["script.message"]; + return ["script.message", "script.realmDestroyed"]; } }