Bug 1713443 - [remote] Support shared global session data that supports BiDi and CDP r=webdriver-reviewers,whimboo,ochameau

Differential Revision: https://phabricator.services.mozilla.com/D127698
This commit is contained in:
Julian Descottes 2021-11-11 22:01:46 +00:00
parent 7ae4b5ed14
commit e788f6a08c
10 changed files with 664 additions and 1 deletions

View File

@ -30,6 +30,8 @@ remote.jar:
content/shared/messagehandler/RootMessageHandler.jsm (shared/messagehandler/RootMessageHandler.jsm)
content/shared/messagehandler/RootMessageHandlerRegistry.jsm (shared/messagehandler/RootMessageHandlerRegistry.jsm)
content/shared/messagehandler/WindowGlobalMessageHandler.jsm (shared/messagehandler/WindowGlobalMessageHandler.jsm)
content/shared/messagehandler/sessiondata/SessionData.jsm (shared/messagehandler/sessiondata/SessionData.jsm)
content/shared/messagehandler/sessiondata/SessionDataReader.jsm (shared/messagehandler/sessiondata/SessionDataReader.jsm)
content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.jsm (shared/messagehandler/transports/js-window-actors/MessageHandlerFrameActor.jsm)
content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.jsm (shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.jsm)
content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.jsm (shared/messagehandler/transports/js-window-actors/MessageHandlerFrameParent.jsm)

View File

@ -4,7 +4,7 @@
"use strict";
const EXPORTED_SYMBOLS = ["MessageHandler"];
const EXPORTED_SYMBOLS = ["CONTEXT_DESCRIPTOR_TYPES", "MessageHandler"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
@ -20,6 +20,34 @@ XPCOMUtils.defineLazyModuleGetters(this, {
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
/**
* A ContextDescriptor object provides information to decide if a broadcast or
* a session data item should be applied to a specific MessageHandler context.
*
* TODO: At the moment we only support one value: { type: "all", id: "all" },
* but the format of the ContextDescriptor object is designed to fit more
* complex values.
* As soon as we start supporting broadcasts targeting only a part of the
* context tree, we will add additional context types. This work will begin with
* Bug 1725111, where we will support filtering on a single navigable. It will
* be later expanded to filter on a worker, a webextension, a process etc...
*
* @typedef {Object} ContextDescriptor
* @property {String} type
* The type of context, one of CONTEXT_DESCRIPTOR_TYPES
* @property {String=} id
* Unique id of a given context for the provided type. Optional for
* CONTEXT_DESCRIPTOR_TYPES.ALL, since there is only one context
*/
// Enum of ContextDescriptor types.
// TODO: At the moment we only support the type "all", but additional context
// types will be added. See comment for the Context type definition.
//
const CONTEXT_DESCRIPTOR_TYPES = {
ALL: "all",
};
/**
* MessageHandler instances are dedicated to handle both Commands and Events
* to enable automation and introspection for remote control protocols.

View File

@ -15,6 +15,8 @@ XPCOMUtils.defineLazyModuleGetters(this, {
"chrome://remote/content/shared/messagehandler/transports/FrameTransport.jsm",
MessageHandler:
"chrome://remote/content/shared/messagehandler/MessageHandler.jsm",
SessionData:
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.jsm",
WindowGlobalMessageHandler:
"chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.jsm",
});
@ -61,6 +63,16 @@ class RootMessageHandler extends MessageHandler {
super(sessionId, null);
this._frameTransport = new FrameTransport(this);
this._sessionData = new SessionData(this);
}
get sessionData() {
return this._sessionData;
}
destroy() {
this._sessionData.destroy();
super.destroy();
}
/**

View File

@ -0,0 +1,251 @@
/* 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/. */
"use strict";
const EXPORTED_SYMBOLS = ["SESSION_DATA_SHARED_DATA_KEY", "SessionData"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
CONTEXT_DESCRIPTOR_TYPES:
"chrome://remote/content/shared/messagehandler/MessageHandler.jsm",
Log: "chrome://remote/content/shared/Log.jsm",
RootMessageHandler:
"chrome://remote/content/shared/messagehandler/RootMessageHandler.jsm",
});
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
const SESSION_DATA_SHARED_DATA_KEY = "MessageHandlerSessionData";
// This is a map from session id to session data, which will be persisted and
// propagated to all processes using Services' sharedData.
// We have to store this as a unique object under a unique shared data key
// because new MessageHandlers in other processes will need to access this data
// without any notion of a specific session.
// This is a singleton.
const sessionDataMap = new Map();
/**
* @typedef {Object} SessionDataItem
* @property {String} moduleName
* The name of the module responsible for this data item.
* @property {String} category
* The category of data. The supported categories depend on the module.
* @property {(string|number|boolean)} value
* Value of the session data item.
* @property {ContextDescriptor} contextDescriptor
* ContextDescriptor to which this session data applies
*/
/**
* SessionData provides APIs to read and write the session data for a specific
* ROOT message handler. It holds the session data as a property and acts as the
* source of truth for this session data.
*
* The session data of a given message handler network should contain all the
* information that might be needed to setup new contexts, for instance a list
* of subscribed events, a list of breakpoints etc.
*
* The actual session data is an array of SessionDataItems. Example below:
* ```
* data: [
* {
* moduleName: "log",
* category: "event",
* value: "log.entryAdded",
* contextDescriptor: { type: "all" }
* },
* {
* moduleName: "browsingContext",
* category: "event",
* value: "browsingContext.contextCreated",
* contextDescriptor: { type: "browser-element", id: "7"}
* },
* {
* moduleName: "browsingContext",
* category: "event",
* value: "browsingContext.contextCreated",
* contextDescriptor: { type: "browser-element", id: "12"}
* },
* ]
* ```
*
* The session data will be persisted using Services.ppmm.sharedData, so that
* new contexts living in different processes can also access the information
* during their startup.
*
* This class should only be used from a ROOT MessageHandler, or from modules
* owned by a ROOT MessageHandler. Other MessageHandlers should rely on
* SessionDataReader's readSessionData to get read-only access to session data.
*
*/
class SessionData {
constructor(messageHandler) {
if (messageHandler.constructor.type != RootMessageHandler.type) {
throw new Error(
"SessionData should only be used from a ROOT MessageHandler"
);
}
this._messageHandler = messageHandler;
/*
* The actual data for this session. This is an array of SessionDataItems.
*/
this._data = [];
}
/**
* Add new session data items of a given module, category and
* contextDescriptor.
*
* A new SessionDataItem will be created for each value of the values array.
*
* If a SessionDataItem already exists for the provided value, moduleName,
* category and contextDescriptor, it will be skipped to avoid duplicated
* SessionDataItems.
*
* The data will be persisted across processes at the end of this method.
*
* @param {String} moduleName
* The name of the module responsible for this data item.
* @param {String} category
* The session data category.
* @param {ContextDescriptor} contextDescriptor
* The contextDescriptor object defining the scope of the session data
* values.
* @param {Array<(string|number|boolean)>} values
* Array of session data item values.
*/
addSessionData(moduleName, category, contextDescriptor, values) {
for (const value of values) {
const item = { moduleName, category, contextDescriptor, value };
const hasItem = this._data.some(_item => this._isSameItem(item, _item));
if (!hasItem) {
// This is a new data item, create it and add it to the data.
this._data.push(item);
} else {
logger.warn(
`Duplicated session data item was not added: ${JSON.stringify(item)}`
);
}
}
// Persist the sessionDataMap.
this._persist();
}
destroy() {
// Update the sessionDataMap singleton.
sessionDataMap.delete(this._messageHandler.sessionId);
// Update sharedData and flush to force consistency.
Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
Services.ppmm.sharedData.flush();
}
/**
* Retrieve the SessionDataItems for a given module and type.
*
* @param {String} moduleName
* The name of the module responsible for this data item.
* @param {String} category
* The session data category.
* @param {ContextDescriptor=} contextDescriptor
* Optional context descriptor, to retrieve only session data items added
* for a specific context descriptor.
* @return {Array<SessionDataItem>}
* Array of SessionDataItems for the provided module and type.
*/
getSessionData(moduleName, category, contextDescriptor) {
return this._data.filter(
item =>
item.moduleName === moduleName &&
item.category === category &&
(!contextDescriptor ||
this._isSameContextDescriptor(
item.contextDescriptor,
contextDescriptor
))
);
}
/**
* Remove values for the provided module, category and context.
* Values which don't match any existing SessionDataItem will be ignored.
*
* The updated sessionDataMap will be persisted across processes at the end.
*
* @param {String} moduleName
* The name of the module responsible for this data item.
* @param {String} category
* The session data category.
* @param {ContextDescriptor} contextDescriptor
* The contextDescriptor object defining the scope of the session data
* values.
* @param {Array<(string|number|boolean)>} values
* Array of session data item values.
*/
removeSessionData(moduleName, category, contextDescriptor, values) {
// Remove the provided context from the contexts Map of the provided items.
for (const value of values) {
const item = { moduleName, category, contextDescriptor, value };
const itemIndex = this._data.findIndex(_item =>
this._isSameItem(item, _item)
);
if (itemIndex != -1) {
// The item was found in the session data, remove it.
this._data.splice(itemIndex, 1);
} else {
logger.warn(
`Missing session data item was not removed: ${JSON.stringify(item)}`
);
}
}
// Persist the sessionDataMap.
this._persist();
}
_isSameItem(item1, item2) {
const descriptor1 = item1.contextDescriptor;
const descriptor2 = item2.contextDescriptor;
return (
item1.moduleName === item2.moduleName &&
item1.category === item2.category &&
this._isSameContextDescriptor(descriptor1, descriptor2) &&
item1.value === item2.value
);
}
_isSameContextDescriptor(contextDescriptor1, contextDescriptor2) {
if (contextDescriptor1.type === CONTEXT_DESCRIPTOR_TYPES.ALL) {
// Ignore the id for type ALL since we made the id optional for this type.
return contextDescriptor1.type === contextDescriptor2.type;
}
return (
contextDescriptor1.type === contextDescriptor2.type &&
contextDescriptor1.id === contextDescriptor2.id
);
}
_persist() {
// Update the sessionDataMap singleton.
sessionDataMap.set(this._messageHandler.sessionId, this._data);
// Update sharedData and flush to force consistency.
Services.ppmm.sharedData.set(SESSION_DATA_SHARED_DATA_KEY, sessionDataMap);
Services.ppmm.sharedData.flush();
}
}

View File

@ -0,0 +1,35 @@
/* 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/. */
"use strict";
const EXPORTED_SYMBOLS = ["readSessionData"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
Services: "resource://gre/modules/Services.jsm",
SESSION_DATA_SHARED_DATA_KEY:
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.jsm",
});
XPCOMUtils.defineLazyGetter(this, "sharedData", () => {
const isInParent =
Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
return isInParent ? Services.ppmm.sharedData : Services.cpmm.sharedData;
});
/**
* Returns a snapshot of the session data map, which is cloned from the
* sessionDataMap singleton of SessionData.jsm.
*
* @return {Map.<string, Array<SessionDataItem>>}
* Map of session id to arrays of SessionDataItems.
*/
const readSessionData = () =>
sharedData.get(SESSION_DATA_SHARED_DATA_KEY) || new Map();

View File

@ -11,3 +11,4 @@ prefs =
[browser_handle_command_errors.js]
[browser_handle_simple_command.js]
[browser_registry.js]
[browser_session_data.js]

View File

@ -0,0 +1,183 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { MessageHandlerRegistry } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm"
);
const { CONTEXT_DESCRIPTOR_TYPES } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/MessageHandler.jsm"
);
const { RootMessageHandler } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/RootMessageHandler.jsm"
);
const { SessionData } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.jsm"
);
const TEST_PAGE = "http://example.com/document-builder.sjs?html=tab";
add_task(async function test_sessionData() {
info("Navigate the initial tab to the test URL");
const tab1 = gBrowser.selectedTab;
await loadURL(tab1.linkedBrowser, TEST_PAGE);
const sessionId = "sessionData-test";
const contextDescriptorAll = { type: CONTEXT_DESCRIPTOR_TYPES.ALL };
const rootMessageHandlerRegistry = new MessageHandlerRegistry(
RootMessageHandler.type
);
const rootMessageHandler = rootMessageHandlerRegistry.getOrCreateMessageHandler(
sessionId
);
ok(rootMessageHandler, "Valid ROOT MessageHandler created");
const sessionData = rootMessageHandler.sessionData;
ok(
sessionData instanceof SessionData,
"ROOT MessageHandler has a valid sessionData"
);
let sessionDataSnapshot = await getSessionDataFromContent();
is(sessionDataSnapshot.size, 0, "session data is empty");
info("Store a string value in session data");
sessionData.addSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
["value-1"]
);
sessionDataSnapshot = await getSessionDataFromContent();
is(sessionDataSnapshot.size, 1, "session data contains 1 session");
ok(sessionDataSnapshot.has(sessionId));
let snapshot = sessionDataSnapshot.get(sessionId);
ok(Array.isArray(snapshot));
is(snapshot.length, 1);
const stringDataItem = snapshot[0];
checkSessionDataItem(
stringDataItem,
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
"value-1"
);
info("Store a number value in session data");
sessionData.addSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
[12]
);
snapshot = (await getSessionDataFromContent()).get(sessionId);
is(snapshot.length, 2);
const numberDataItem = snapshot[1];
checkSessionDataItem(
numberDataItem,
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
12
);
info("Store a boolean value in session data");
sessionData.addSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
[true]
);
snapshot = (await getSessionDataFromContent()).get(sessionId);
is(snapshot.length, 3);
const boolDataItem = snapshot[2];
checkSessionDataItem(
boolDataItem,
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
true
);
info("Remove one value");
sessionData.removeSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
[12]
);
snapshot = (await getSessionDataFromContent()).get(sessionId);
is(snapshot.length, 2);
checkSessionDataItem(
snapshot[0],
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
"value-1"
);
checkSessionDataItem(
snapshot[1],
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
true
);
info("Remove all values");
sessionData.removeSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
["value-1", true]
);
snapshot = (await getSessionDataFromContent()).get(sessionId);
is(snapshot.length, 0, "Session data is now empty");
info("Add another value before destroy");
sessionData.addSessionData(
"fakemodule",
"testCategory",
contextDescriptorAll,
["value-2"]
);
snapshot = (await getSessionDataFromContent()).get(sessionId);
is(snapshot.length, 1);
checkSessionDataItem(
snapshot[0],
"fakemodule",
"testCategory",
CONTEXT_DESCRIPTOR_TYPES.ALL,
"value-2"
);
sessionData.destroy();
sessionDataSnapshot = await getSessionDataFromContent();
is(sessionDataSnapshot.size, 0, "session data should be empty again");
});
function checkSessionDataItem(item, moduleName, category, contextType, value) {
is(item.moduleName, moduleName, "Data item has the expected module name");
is(item.category, category, "Data item has the expected category");
is(
item.contextDescriptor.type,
contextType,
"Data item has the expected context type"
);
is(item.value, value, "Data item has the expected value");
}
function getSessionDataFromContent() {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
const { readSessionData } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/sessiondata/SessionDataReader.jsm"
);
return readSessionData();
});
}

View File

@ -0,0 +1,146 @@
/* 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 { CONTEXT_DESCRIPTOR_TYPES } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/MessageHandler.jsm"
);
const { RootMessageHandler } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/RootMessageHandler.jsm"
);
const { SessionData } = ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/sessiondata/SessionData.jsm"
);
add_task(async function test_sessionData() {
const sessionData = new SessionData(new RootMessageHandler("session-id-1"));
equal(sessionData.getSessionData("mod", "event").length, 0);
const globalContext = {
type: CONTEXT_DESCRIPTOR_TYPES.ALL,
};
const otherContext = { type: "other-type", id: "some-id" };
info("Add a first event for the global context");
sessionData.addSessionData("mod", "event", globalContext, ["first.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
]);
info("Add the exact same data (same module, type, context, value)");
sessionData.addSessionData("mod", "event", globalContext, ["first.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
]);
info("Add another context for the same event");
sessionData.addSessionData("mod", "event", otherContext, ["first.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
{
value: "first.event",
contextDescriptor: otherContext,
},
]);
info("Add a second event for the global context");
sessionData.addSessionData("mod", "event", globalContext, ["second.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
{
value: "first.event",
contextDescriptor: otherContext,
},
{
value: "second.event",
contextDescriptor: globalContext,
},
]);
info("Add two events for the global context");
sessionData.addSessionData("mod", "event", globalContext, [
"third.event",
"fourth.event",
]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
{
value: "first.event",
contextDescriptor: otherContext,
},
{
value: "second.event",
contextDescriptor: globalContext,
},
{
value: "third.event",
contextDescriptor: globalContext,
},
{
value: "fourth.event",
contextDescriptor: globalContext,
},
]);
info("Remove the second, third and fourth events");
sessionData.removeSessionData("mod", "event", globalContext, [
"second.event",
"third.event",
"fourth.event",
]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: globalContext,
},
{
value: "first.event",
contextDescriptor: otherContext,
},
]);
info("Remove the global context from the first event");
sessionData.removeSessionData("mod", "event", globalContext, ["first.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), [
{
value: "first.event",
contextDescriptor: otherContext,
},
]);
info("Remove the other context from the first event");
sessionData.removeSessionData("mod", "event", otherContext, ["first.event"]);
checkEvents(sessionData.getSessionData("mod", "event"), []);
});
function checkEvents(events, expectedEvents) {
// Check the arrays have the same size.
equal(events.length, expectedEvents.length);
// Check all the expectedEvents can be found in the events array.
for (const expected of expectedEvents) {
ok(
events.some(
event =>
expected.contextDescriptor.type === event.contextDescriptor.type &&
expected.contextDescriptor.id === event.contextDescriptor.id &&
expected.value == event.value
)
);
}
}

View File

@ -3,3 +3,4 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[test_Errors.js]
[test_SessionData.js]

View File

@ -10,6 +10,10 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
ChromeUtils.import(
"chrome://remote/content/shared/messagehandler/sessiondata/SessionDataReader.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
error: "chrome://remote/content/shared/messagehandler/Errors.jsm",
MessageHandlerRegistry: