Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
loader: "resource://devtools/shared/loader/Loader.sys.mjs",
},
{ global: "contextual" }
);
ChromeUtils.defineESModuleGetters(
lazy,
{
releaseDistinctSystemPrincipalLoader:
"resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
useDistinctSystemPrincipalLoader:
"resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
},
{ global: "shared" }
);
// Name of the attribute into which we save data in `sharedData` object.
const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
// Map(String => Object)
// Map storing the data objects for all currently active watcher actors.
// The data objects are defined by `createWatcherDataObject()`.
// The main attribute of interest is the `sessionData` one which is set alongside
// various other attributes necessary to maintain state per watcher in the content process.
//
// The Session Data object is maintained by ParentProcessWatcherRegistry, in the parent process
// and is fetched from the content process via `sharedData` API.
// It is then manually maintained via DevToolsProcess JS Actor queries.
let gAllWatcherData = null;
export const ContentProcessWatcherRegistry = {
_getAllWatchersDataMap() {
if (gAllWatcherData) {
return gAllWatcherData;
}
const { sharedData } = Services.cpmm;
const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME);
if (!sessionDataByWatcherActorID) {
throw new Error("Missing session data in `sharedData`");
}
// Initialize a distinct Map to replicate the one read from `sharedData`.
// This distinct Map will be updated via DevToolsProcess JS Actor queries.
// This helps better control the execution flow.
gAllWatcherData = new Map();
// The Browser Toolbox will load its server modules in a distinct global/compartment whose name is "DevTools global".
// It means that this class will be instantiated twice, one in each global (the shared one and the browser toolbox one).
// We then have to distinguish the two subset of watcher actors accordingly within `sharedMap`,
// as `sharedMap` will be shared between the two module instances.
// Session type "all" relates to the Browser Toolbox.
const isInBrowserToolboxLoader =
// eslint-disable-next-line mozilla/reject-globalThis-modification
Cu.getRealmLocation(globalThis) == "DevTools global";
for (const [watcherActorID, sessionData] of sessionDataByWatcherActorID) {
// Filter in/out the watchers based on the current module loader and the watcher session type.
const isBrowserToolboxWatcher = sessionData.sessionContext.type == "all";
if (
(isInBrowserToolboxLoader && !isBrowserToolboxWatcher) ||
(!isInBrowserToolboxLoader && isBrowserToolboxWatcher)
) {
continue;
}
gAllWatcherData.set(
watcherActorID,
createWatcherDataObject(watcherActorID, sessionData)
);
}
return gAllWatcherData;
},
/**
* Get all data objects for all currently active watcher actors.
* If a specific target type is passed, this will only return objects of watcher actively watching for a given target type.
*
* @param {String} targetType
* Optional target type to filter only a subset of watchers.
* @return {Array|Iterator}
* List of data objects. (see createWatcherDataObject)
*/
getAllWatchersDataObjects(targetType) {
if (targetType) {
const list = [];
for (const watcherDataObject of this._getAllWatchersDataMap().values()) {
if (watcherDataObject.sessionData.targets?.includes(targetType)) {
list.push(watcherDataObject);
}
}
return list;
}
return this._getAllWatchersDataMap().values();
},
/**
* Similar to `getAllWatcherDataObjects`, but will only return the already existing registered watchers in this process.
*/
getAllExistingWatchersDataObjects() {
if (!gAllWatcherData) {
return [];
}
return gAllWatcherData.values();
},
/**
* Get the watcher data object for a given watcher actor.
*
* @param {String} watcherActorID
* @param {Boolean} onlyFromCache
* If set explicitly to true, will avoid falling back to shared data.
* This is typically useful on destructor/removing/cleanup to avoid creating unexpected data.
* It is also used to avoid the exception thrown when sharedData is cleared on toolbox destruction.
*/
getWatcherDataObject(watcherActorID, onlyFromCache = false) {
let data =
ContentProcessWatcherRegistry._getAllWatchersDataMap().get(
watcherActorID
);
if (!data && !onlyFromCache) {
// When there is more than one DevTools opened, the DevToolsProcess JS Actor spawned by the first DevTools
// created a cached Map in `_getAllWatchersDataMap`.
// When opening a second DevTools, this cached Map may miss some new SessionData related to this new DevTools instance,
// and new Watcher Actor.
// When such scenario happens, fallback to `sharedData` which should hopefully be containing the latest DevTools instance SessionData.
//
// May be the watcher should trigger a very first JS Actor query before any others in order to transfer the base Session Data object?
const { sharedData } = Services.cpmm;
const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME);
const sessionData = sessionDataByWatcherActorID.get(watcherActorID);
if (!sessionData) {
throw new Error("Unable to find data for watcher " + watcherActorID);
}
data = createWatcherDataObject(watcherActorID, sessionData);
gAllWatcherData.set(watcherActorID, data);
}
return data;
},
/**
* Instantiate a DevToolsServerConnection for a given Watcher.
*
* This function will be the one forcing to load the first DevTools CommonJS modules
* and spawning the DevTools Loader as well as the DevToolsServer. So better call it
* only once when it is strictly necessary.
*
* This connection will be the communication channel for RDP between this content process
* and the parent process, which will route RDP packets from/to the client by using
* a unique "forwarding prefix".
*
* @param {String} watcherActorID
* @param {Boolean} useDistinctLoader
* To be set to true when debugging a privileged context running the shared system principal global.
* This is a requirement for spidermonkey Debugger API used by the thread actor.
* @return {Object}
* Object with connection (DevToolsServerConnection) and loader (DevToolsLoader) attributes.
*/
getOrCreateConnectionForWatcher(watcherActorID, useDistinctLoader) {
const watcherDataObject =
ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
let { connection, loader } = watcherDataObject;
if (connection) {
return { connection, loader };
}
// When debugging a privileged page, like about:addons, this module will run in the same compartment
// as the debugged page. Both will run in the shared system compartment.
// The thread actor ultimately need to be in a distinct compartments from its debuggees.
// So we are using a special loader, which will use a distinct privileged global and compartment
// to load itself as well as all its modules.
//
// Note that when we are running the Browser Toolbox, this module will already be loaded in a special, distinct global and compartment
// Thanks to `loadInDevToolsLoader` flag of BrowserToolboxDevToolsProcess's JS Process Actor configuration.
// So that the Loader will also be loaded in the right, distinct compartment.
loader =
useDistinctLoader || watcherDataObject.sessionContext.type == "all"
? lazy.useDistinctSystemPrincipalLoader(watcherDataObject)
: lazy.loader;
watcherDataObject.loader = loader;
// Note that this a key step in loading DevTools backend / modules.
const { DevToolsServer } = loader.require(
"resource://devtools/server/devtools-server.js"
);
DevToolsServer.init();
// Within the content process, we only need the target scoped actors.
// (inspector, console, storage,...)
DevToolsServer.registerActors({ target: true });
// Instantiate a DevToolsServerConnection which will pipe all its outgoing RDP packets
// up to the parent process manager via DevToolsProcess JS Actor messages.
const { forwardingPrefix } = watcherDataObject;
connection = DevToolsServer.connectToParentWindowActor(
watcherDataObject.jsProcessActor,
forwardingPrefix,
"DevToolsProcessChild:packet"
);
watcherDataObject.connection = connection;
return { connection, loader };
},
/**
* Method to be called each time a new target actor is instantiated.
*
* @param {Object} watcherDataObject
* @param {Actor} targetActor
* @param {Boolean} isDocumentCreation
*/
onNewTargetActor(watcherDataObject, targetActor, isDocumentCreation = false) {
// There is no root actor in content processes and so
// the target actor can't be managed by it, but we do have to manage
// the actor to have it working and be registered in the DevToolsServerConnection.
// We make it manage itself and become a top level actor.
targetActor.manage(targetActor);
const { watcherActorID } = watcherDataObject;
targetActor.once("destroyed", options => {
// Maintain the registry and notify the parent process
ContentProcessWatcherRegistry.destroyTargetActor(
watcherDataObject,
targetActor,
options
);
});
watcherDataObject.actors.push(targetActor);
// Immediately queue a message for the parent process,
// in order to ensure that the JSWindowActorTransport is instantiated
// before any packet is sent from the content process.
// As messages are guaranteed to be delivered in the order they
// were queued, we don't have to wait for anything around this sendAsyncMessage call.
// In theory, the Target Actor may emit events in its constructor.
// If it does, such RDP packets may be lost. But in practice, no events
// are emitted during its construction. Instead the frontend will start
// the communication first.
const { forwardingPrefix } = watcherDataObject;
watcherDataObject.jsProcessActor.sendAsyncMessage(
"DevToolsProcessChild:targetAvailable",
{
watcherActorID,
forwardingPrefix,
targetActorForm: targetActor.form(),
}
);
// Pass initialization data to the target actor
const { sessionData } = watcherDataObject;
for (const type in sessionData) {
// `sessionData` will also contain `browserId` as well as entries with empty arrays,
// which shouldn't be processed.
const entries = sessionData[type];
if (!Array.isArray(entries) || !entries.length) {
continue;
}
targetActor.addOrSetSessionDataEntry(
type,
sessionData[type],
isDocumentCreation,
"set"
);
}
},
/**
* Method to be called each time a target actor is meant to be destroyed.
*
* @param {Object} watcherDataObject
* @param {Actor} targetActor
* @param {object} options
* @param {boolean} options.isModeSwitching
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
destroyTargetActor(watcherDataObject, targetActor, options) {
const idx = watcherDataObject.actors.indexOf(targetActor);
if (idx != -1) {
watcherDataObject.actors.splice(idx, 1);
}
const form = targetActor.form();
targetActor.destroy(options);
// And this will destroy the parent process one
try {
watcherDataObject.jsProcessActor.sendAsyncMessage(
"DevToolsProcessChild:targetDestroyed",
{
actors: [
{
watcherActorID: watcherDataObject.watcherActorID,
targetActorForm: form,
},
],
options,
}
);
} catch (e) {
// Ignore exception when the JSProcessActorChild has already been destroyed.
// We often try to emit this message while the process is being destroyed,
// but sendAsyncMessage doesn't have time to complete and throws.
if (
!e.message.includes("JSProcessActorChild cannot send at the moment")
) {
throw e;
}
}
},
/**
* Method to know if a given Watcher Actor is still registered.
*
* @param {String} watcherActorID
* @return {Boolean}
*/
has(watcherActorID) {
return gAllWatcherData.has(watcherActorID);
},
/**
* Method to unregister a given Watcher Actor.
*
* @param {Object} watcherDataObject
*/
remove(watcherDataObject) {
// We do not need to destroy each actor individually as they
// are all registered in this DevToolsServerConnection, which will
// destroy all the registered actors.
if (watcherDataObject.connection) {
watcherDataObject.connection.close();
}
// If we were using a distinct and dedicated loader,
// we have to manually release it.
if (watcherDataObject.loader && watcherDataObject.loader !== lazy.loader) {
lazy.releaseDistinctSystemPrincipalLoader(watcherDataObject);
}
gAllWatcherData.delete(watcherDataObject.watcherActorID);
if (gAllWatcherData.size == 0) {
gAllWatcherData = null;
}
},
/**
* Method to know if there is no more Watcher registered.
*
* @return {Boolean}
*/
isEmpty() {
return !gAllWatcherData || gAllWatcherData.size == 0;
},
/**
* Method to unregister all the Watcher Actors
*/
clear() {
if (!gAllWatcherData) {
return;
}
// Query gAllWatcherData internal map directly as we don't want to re-create the map from sharedData
for (const watcherDataObject of gAllWatcherData.values()) {
ContentProcessWatcherRegistry.remove(watcherDataObject);
}
gAllWatcherData = null;
},
};
function createWatcherDataObject(watcherActorID, sessionData) {
// The prefix of the DevToolsServerConnection of the Watcher Actor in the parent process.
// This is used to compute a unique ID for this process.
const parentConnectionPrefix = sessionData.connectionPrefix;
// Compute a unique prefix, just for this DOM Process.
// (nsIDOMProcessChild's childID should be unique across processes)
//
// This prefix will be used to create a JSWindowActorTransport pair between content and parent processes.
// This is slightly hacky as we typically compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
// but here, we can't have access to any DevTools connection as we could run really early in the content process startup.
//
// Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10...
const forwardingPrefix =
parentConnectionPrefix +
"process" +
ChromeUtils.domProcessChild.childID +
"/";
// The browser toolbox uses a distinct JS Actor, loaded in the "devtools" ESM loader.
const jsActorName =
sessionData.sessionContext.type == "all"
? "BrowserToolboxDevToolsProcess"
: "DevToolsProcess";
const jsProcessActor = ChromeUtils.domProcessChild.getActor(jsActorName);
return {
// {String}
// Actor ID for this watcher
watcherActorID,
// {Array<String>}
// List of currently watched target types for this watcher
watchingTargetTypes: [],
// {DevtoolsServerConnection}
// Connection bridge made from this content process to the parent process.
connection: null,
// {JSActor}
// Reference to the related DevToolsProcessChild instance.
jsProcessActor,
// {Object}
// Watcher's sessionContext object, which help identify the browser toolbox usecase.
sessionContext: sessionData.sessionContext,
// {Object}
// Watcher's sessionData object, which is initiated with `sharedData` version,
// but is later updated on each Session Data update (addOrSetSessionDataEntry/removeSessionDataEntry).
// `sharedData` isn't timely updated and can be out of date.
sessionData,
// {String}
// Prefix used against all RDP packets to route them correctly from/to this content process
forwardingPrefix,
// {Array<Object>}
// List of active WindowGlobal and ContentProcess target actor instances.
actors: [],
// {Object<Array<Object>>}
// We can't use `actors` list for workers as this code runs in the main thread and the WorkerTargetActors
// run in the worker thread.
// We store in each array, specific to each worker type (having a dedicated target watcher class),
// an object with the following attributes:
// - {WorkerDebugger} dbg
// - {String} workerThreadServerForwardingPrefix
// - {Object} workerTargetForm
// - {DevToolsTransport} transport
workers: {
service_worker: [],
shared_worker: [],
worker: [],
},
// {Object<Set<Array<Object>>>}
// A Set of arrays which will be populated with concurrent Session Data updates
// being done while a worker target is being instantiated.
// Each pending worker being initialized register a new dedicated array which will be removed
// from the Set once its initialization is over.
// We maintain one Set per target type which is managed by a dedicated target watcher class.
pendingWorkers: {
service_worker: new Set(),
shared_worker: new Set(),
worker: new Set(),
},
};
}