Bug 1742607 - Add initial documentation for MessageHandler architecture r=webdriver-reviewers,whimboo,Sasha

Differential Revision: https://phabricator.services.mozilla.com/D164393
This commit is contained in:
Julian Descottes 2022-12-14 10:17:47 +00:00
Родитель 9c0220e715
Коммит 38a84730f7
4 изменённых файлов: 250 добавлений и 3 удалений

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

@ -27,11 +27,13 @@ The following documentation pages apply to all remote protocols
CodeStyle.md
Security.md
Protocols
=========
.. _marionette-header:
Marionette
==========
----------
Marionette is used both by internal tools and testing solutions, but also by
geckodriver to implement the `WebDriver (HTTP) specification`_. The documentation
@ -44,7 +46,7 @@ for Marionette can be found under `testing/marionette`_.
.. _remote-protocol-cdp-header:
Remote Protocol (CDP)
=====================
---------------------
Firefox implements a subset of the `Chrome DevTools Protocol`_ (CDP) in order to
support third party automation tools such as `puppeteer`. The documentation for
@ -57,12 +59,23 @@ the remote protocol (CDP) implement can be found at `remote/cdp`_.
.. _webdriver-bidi-header:
WebDriver BiDi
==============
--------------
`The WebDriver BiDi specification <https://w3c.github.io/webdriver-bidi>`_
extends WebDriver HTTP to add bidirectional communication. Dedicated
documentation will be added as the Firefox implementation makes progress.
Architecture
============
Message Handler
---------------
The documentation for the framework used to build WebDriver BiDi modules can be
found at `remote/messagehandler`_.
.. _remote/messagehandler: messagehandler/
Bugs
====

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

@ -0,0 +1,81 @@
# 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:
```
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:
```
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.

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

@ -0,0 +1,142 @@
# 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.

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

@ -0,0 +1,11 @@
==============
MessageHandler
==============
MessageHandler is the framework used to implement WebDriver BiDi modules in Firefox.
.. toctree::
:maxdepth: 1
Intro.md
SimpleExample.md