468 lines
18 KiB
JavaScript
468 lines
18 KiB
JavaScript
/* 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 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".
|
|
// (See https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/js/xpconnect/loader/mozJSModuleLoader.cpp#699)
|
|
// 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(),
|
|
},
|
|
};
|
|
}
|