Bug 1788659 - [bidi] Implement "script.realmDestroyed" event. r=webdriver-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D187550
This commit is contained in:
Alexandra Borovova 2023-09-12 09:39:56 +00:00
Родитель 497ea8ec6a
Коммит f76d7e94f9
9 изменённых файлов: 373 добавлений и 32 удалений

Просмотреть файл

@ -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<SessionDataItem>} sessionDataItems
* Initial session data items for this MessageHandler.
*/
async applyInitialSessionDataItems(sessionDataItems) {}
async initialize(sessionDataItems) {}
/**
* Returns the module path corresponding to this MessageHandler class.

Просмотреть файл

@ -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);

Просмотреть файл

@ -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);
};
}

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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]

Просмотреть файл

@ -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);
});

Просмотреть файл

@ -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();
});

Просмотреть файл

@ -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

Просмотреть файл

@ -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"];
}
}