diff options
Diffstat (limited to '')
-rw-r--r-- | remote/doc/messagehandler/Intro.md | 83 | ||||
-rw-r--r-- | remote/doc/messagehandler/SimpleExample.md | 147 | ||||
-rw-r--r-- | remote/doc/messagehandler/index.rst | 11 |
3 files changed, 241 insertions, 0 deletions
diff --git a/remote/doc/messagehandler/Intro.md b/remote/doc/messagehandler/Intro.md new file mode 100644 index 0000000000..e1db1a5945 --- /dev/null +++ b/remote/doc/messagehandler/Intro.md @@ -0,0 +1,83 @@ +# Introduction + +## Overview + +When developing browser tools in Firefox, you need to reach objects or APIs only available in certain layers (eg. processes or threads). There are powerful APIs available to communicate across layers (JSWindowActors, JSProcessActors) but they don't usually match all the needs from browser tool developers. For instance support for sessions, for events, ... + +### Modules + +The MessageHandler framework proposes to organize your code in modules, with the restriction that a given module can only run in a specific layer. Thanks to this, the framework will instantiate the modules where needed, and will provide easy ways to communicate between modules across layers. The goal is to take away all the complexity of routing information so that developers can simply focus on implementing the logic for their modules. + +### Commands and Events + +The framework is also designed around commands and events. Each module developed for the MessageHandler framework should expose commands and/or events. Commands follow a request/response model, and are conceptually similar to function calls where the caller could live in a different process than the callee. Events are emitted at the initiative of the module, and can reach listeners located in other layers. The role of modules is to implement the logic to handle commands (eg "click on an element") or generate events. The role of the framework is to send commands to modules, or to bubble events from modules. Commands and events are both used to communicate internally between modules, as well as externally with the consumer of your tooling modules. + +The "MessageHandler" name comes from this role of "handling" commands and events, aka "messages". + +### Summary + +As a summary, the MessageHandler framework proposes to write tooling code as modules, which will run in various processes or threads, and communicate across layers using commands and events. + +## Basic Architecture + +### MessageHandler Network + +Modules created for the MessageHandler framework need to run in several processes, threads, ... + +To support this the framework will dynamically create a network of [MessageHandler](https://searchfox.org/mozilla-central/source/remote/shared/messagehandler/MessageHandler.sys.mjs) instances in the various layers that need to be accessed by your modules. The MessageHandler class is obviously named after the framework, but the name is appropriate because its role is mainly to route commands and events. + +On top of routing duties, the MessageHandler class is also responsible for instantiating and managing modules. Typically, processing a command has two possible outcomes. Either it's not intended for this particular layer, in which case the MessageHandler will analyze the command and send it towards the appropriate recipient. But if it is intended for this layer, then the MessageHandler will try to delegate the command to the appropriate module. This means instantiating the module if it wasn't done before. So each node of a MessageHandler network also contains module instances. + +The root of this network is the [RootMessageHandler](https://searchfox.org/mozilla-central/source/remote/shared/messagehandler/RootMessageHandler.sys.mjs) and lives in the parent process. For consumers, this is also the single entry point exposing the commands and events of your modules. It can also own module instances, if you have modules which are supposed to live in the parent process (aka root layer). + +At the moment we only support another type of MessageHandler, the [WindowGlobalMessageHandler](https://searchfox.org/mozilla-central/source/remote/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs) which will be used for the windowglobal layer and lives in the content process. + +### Simplified architecture example + +Let's imagine a very simple example, with a couple of modules: + +* a root module called "version" with a command returning the current version of the browser + +* a windowglobal module called "location" with a command returning the location of the windowglobal + +Suppose the browser has 2 tabs, running in different processes. If the consumer used the "version" module, and the "location" module but only for one of the two tabs, the network will look like: + +```text + parent process content process 1 +┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + ╔═══════════════════════╗ ┌─────────────┐ │ +│ ╔═══════════════════════╗ │ │ ║ WindowGlobal ╠──────┤ location │ + ║ RootMessageHandler ║◀ ─ ─ ─ ─▶║ MessageHandler ║ │ module │ │ +│ ╚══════════╦════════════╝ │ │ ╚═══════════════════════╝ └─────────────┘ + │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +│ │ │ + ┌──────┴──────┐ content process 2 +│ │ version │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + │ module │ │ +│ └─────────────┘ │ │ + │ +│ │ │ + ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +``` + +But if the consumer sends another command, to retrieve the location of the other tab, the network will then evolve to: + +```text + parent process content process 1 +┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + ╔═══════════════════════╗ ┌─────────────┐ │ +│ ╔═══════════════════════╗ │ │ ║ WindowGlobal ╠──────┤ location │ + ║ RootMessageHandler ║◀ ─ ┬ ─ ─▶║ MessageHandler ║ │ module │ │ +│ ╚══════════╦════════════╝ │ │ ╚═══════════════════════╝ └─────────────┘ + │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +│ │ │ + ┌──────┴──────┐ │ content process 2 +│ │ version │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + │ module │ │ ╔═══════════════════════╗ ┌─────────────┐ │ +│ └─────────────┘ │ │ ║ WindowGlobal ╠──────┤ location │ + └ ─ ▶ ║ MessageHandler ║ │ module │ │ +│ │ │ ╚═══════════════════════╝ └─────────────┘ + ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ +``` + +We can already see here that while RootMessageHandler is connected to both WindowGlobalMessageHandler(s), they are not connected with each other. There are restriction on the way messages can travel on the network both for commands and events, which will be the topic for other documentation pages. diff --git a/remote/doc/messagehandler/SimpleExample.md b/remote/doc/messagehandler/SimpleExample.md new file mode 100644 index 0000000000..8adb9451c3 --- /dev/null +++ b/remote/doc/messagehandler/SimpleExample.md @@ -0,0 +1,147 @@ +# Simple Example + +As a tutorial, let's create a very simple example, with a couple of modules: + +* a root (parent process) module to retrieve the current version of the browser + +* a windowglobal (content process) module to retrieve the location of a given tab + +Some concepts used here will not be explained in details. More documentation should follow to clarify those. + +We will not use events in this example, only commands. + +## Create a root `version` module + +First let's create the root module. + +```javascript +import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +class VersionModule extends Module { + destroy() {} + + getVersion() { + return Services.appinfo.platformVersion; + } +} + +export const version = VersionModule; +``` + +All modules should extend Module.sys.mjs and must define a destroy method. +Each public method of a Module class will be exposed as a command for this module. +The name used to export the module class will be the public name of the module, used to call commands on it. + +## Create a windowglobal `location` module + +Let's create the second module. + +```javascript +import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs"; + +class LocationModule extends Module { + #window; + + constructor(messageHandler) { + super(messageHandler); + + // LocationModule will be a windowglobal module, so `messageHandler` will + // be a WindowGlobalMessageHandler which comes with a few helpful getters + // such as a `window` getter. + this.#window = messageHandler.window; + } + + destroy() { + this.#window = null; + } + + getLocation() { + return this.#window.location.href; + } +} + +export const location = LocationModule; +``` + +We could simplify the module and simply write `getLocation` to return `this.messageHandler.window.location.href`, but this gives us the occasion to get a glimpse at the module constructor. + +## Register the modules as Firefox modules + +Before we register those modules for the MessageHandler framework, we need to register them as Firefox modules first. For the sake of simplicity, we can assume they are added under a new folder `remote/example`: + +* `remote/example/modules/root/version.sys.mjs` + +* `remote/example/modules/windowglobal/location.sys.mjs` + +Register them in the jar.mn so that they can be loaded as any other Firefox module. + +The paths contain the corresponding layer (root, windowglobal) only for clarity. We don't rely on this as a naming convention to actually load the modules so you could decide to organize your folders differently. However the name used to export the module's class (eg `location`) will be the official name of the module, used in commands and events, so pay attention and use the correct export name. + +## Define a ModuleRegistry + +We do need to instruct the framework where each module should be loaded however. + +This is done via a ModuleRegistry. Without getting into too much details, each "set of modules" intended to work with the MessageHandler framework needs to provide a ModuleRegistry module which exports a single `getModuleClass` helper. This method will be called by the framework to know which modules are available. For now let's just define the simplest registry possible for us under `remote/example/modules/root/ModuleRegistry.sys.mjs` + +```javascript +export const getModuleClass = function(moduleName, moduleFolder) { + if (moduleName === "version" && moduleFolder === "root") { + return ChromeUtils.importESModule( + "chrome://remote/content/example/modules/root/version.sys.mjs" + ).version; + } + if (moduleName === "location" && moduleFolder === "windowglobal") { + return ChromeUtils.importESModule( + "chrome://remote/content/example/modules/windowglobal/location.sys.mjs" + ).location; + } + return null; +}; +``` + +Note that this can (and should) be improved by defining some naming conventions or patterns, but for now each set of modules is really free to implement this logic as needed. + +Add this module to jar.mn as well so that it becomes a valid Firefox module. + +### Temporary workaround to use the custom ModuleRegistry + +With this we have a set of modules which is almost ready to use. Except that for now MessageHandler is hardcoded to use WebDriver BiDi modules only. Once [Bug 1722464](https://bugzilla.mozilla.org/show_bug.cgi?id=1722464) is fixed we will be able to specify other protocols, but at the moment, the only way to instruct the MessageHandler framework to use non-bidi modules is to update the [following line](https://searchfox.org/mozilla-central/rev/08f7e9ef03dd2a83118fba6768d1143d809f5ebe/remote/shared/messagehandler/ModuleCache.sys.mjs#25) to point to `remote/example/modules/ModuleRegistry.sys.mjs`. + +Now with this, you should be able to create a MessageHandler network and use your modules. + +## Try it out + +For instance, you can open the Browser Console and run the following snippet: + +```javascript +(async function() { + const { RootMessageHandlerRegistry } = ChromeUtils.importESModule( + "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs" + ); + const messageHandler = RootMessageHandlerRegistry.getOrCreateMessageHandler("test-session"); + const version = await messageHandler.handleCommand({ + moduleName: "version", + commandName: "getVersion", + params: {}, + destination: { + type: "ROOT", + }, + }); + console.log({ version }); + + const location = await messageHandler.handleCommand({ + moduleName: "location", + commandName: "getLocation", + params: {}, + destination: { + type: "WINDOW_GLOBAL", + id: gBrowser.selectedBrowser.browsingContext.id, + }, + }); + console.log({ location }); +})(); +``` + +This should print a version number `{ version: "109.0a1" }` and a location `{ location: "https://www.mozilla.org/en-US/" }` (actual values should of course be different for you). + +We are voluntarily skipping detailed explanations about the various parameters passed to `handleCommand`, as well as about the `RootMessageHandlerRegistry`, but this should give you some idea already of how you can start creating modules and using them. diff --git a/remote/doc/messagehandler/index.rst b/remote/doc/messagehandler/index.rst new file mode 100644 index 0000000000..68cf4aef24 --- /dev/null +++ b/remote/doc/messagehandler/index.rst @@ -0,0 +1,11 @@ +============== +MessageHandler +============== + +MessageHandler is the framework used to implement WebDriver BiDi modules in Firefox. + +.. toctree:: + :maxdepth: 1 + + Intro.md + SimpleExample.md |