/* 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/. */
exports.WatcherActor = class WatcherActor extends Actor { /** * Initialize a new WatcherActor which is the main entry point to debug * something. The main features of this actor are to: * - observe targets related to the context we are debugging. * This is done via watchTargets/unwatchTargets methods, and * target-available-form/target-destroyed-form events. * - observe resources related to the observed targets. * This is done via watchResources/unwatchResources methods, and * resources-available-array/resources-updated-array/resources-destroyed-array events. * Note that these events are also emited on both the watcher actor, * for resources observed from the parent process, as well as on the * target actors, when the resources are observed from the target's process or thread. * * @param {DevToolsServerConnection} conn * The connection to use in order to communicate back to the client. * @param {object} sessionContext * The Session Context to help know what is debugged. * See devtools/server/actors/watcher/session-context.js * @param {Number} sessionContext.browserId: If this is a "browser-element" context type, * the "browserId" of the <browser> element we would like to debug. * @param {Boolean} sessionContext.isServerTargetSwitchingEnabled: Flag to to know if we should * spawn new top level targets for the debugged context.
*/
constructor(conn, sessionContext) { super(conn, watcherSpec); this._sessionContext = sessionContext; if (sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { // Retrieve the <browser> element for the given browser ID const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
sessionContext.browserId
); if (!browsingContext) { thrownew Error( "Unable to retrieve the element for browserId=" +
sessionContext.browserId
);
} this._browserElement = browsingContext.embedderElement;
}
// Sometimes we get iframe targets before the top-level targets // mostly when doing bfcache navigations, lets cache the early iframes targets and // flush them after the top-level target is available. See Bug 1726568 for details. this._earlyIframeTargets = {};
// All currently available WindowGlobal target's form, keyed by `innerWindowId`. // // This helps to: // - determine if the iframe targets are early or not. // i.e. if it is notified before its parent target is available. // - notify the destruction of all children targets when a parent is destroyed. // i.e. have a reliable order of destruction between parent and children. // // Note that there should be just one top-level window target at a time, // but there are certain cases when a new target is available before the // old target is destroyed. this._currentWindowGlobalTargets = new Map();
// The Browser Toolbox requires to load modules in a distinct compartment in order // to be able to debug system compartments modules (most of Firefox internal codebase). // This is a requirement coming from SpiderMonkey Debugger API and relates to the thread actor. this._jsActorName =
sessionContext.type == SESSION_TYPES.ALL
? "BrowserToolboxDevToolsProcess"
: "DevToolsProcess";
}
get sessionContext() { returnthis._sessionContext;
}
/** * If we are debugging only one Tab or Document, returns its BrowserElement. * For Tabs, it will be the <browser> element used to load the web page. * * This is typicaly used to fetch: * - its `browserId` attribute, which uniquely defines it, * - its `browsingContextID` or `browsingContext`, which helps inspecting its content.
*/
get browserElement() { returnthis._browserElement;
}
/** * Helper to know if the context we are debugging has been already destroyed
*/
isContextDestroyed() { if (this.sessionContext.type == "browser-element") { return !this.browserElement.browsingContext;
} elseif (this.sessionContext.type == "webextension") { // This is no obvious browsing context to target for extensions, so always consider it running returnfalse;
} elseif (this.sessionContext.type == "all") { returnfalse;
} thrownew Error( "Unsupported session context type: " + this.sessionContext.type
);
}
destroy() { // Only try to notify content processes if the watcher was in the registry. // Otherwise it means that it wasn't connected to any process and the JS Process Actor // wouldn't be registered. if (ParentProcessWatcherRegistry.getWatcher(this.actorID)) { // Emit one IPC message on destroy to all the processes const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
domProcess.getActor(this._jsActorName).destroyWatcher({
watcherActorID: this.actorID,
});
}
}
// Ensure destroying all Resource Watcher instantiated in the parent process
Resources.unwatchResources( this,
Resources.getParentProcessResourceTypes(Object.values(Resources.TYPES))
);
// In case the watcher actor is leaked, prevent leaking the browser window this._browserElement = null;
// Destroy the actor in order to ensure destroying all its children actors. // As this actor is a pool with children actors, when the transport/connection closes // we expect all actors and its children to be destroyed. super.destroy();
}
/* * Get the list of the currently watched resources for this watcher. * * @return Array<String> * Returns the list of currently watched resource types.
*/
get sessionData() { return ParentProcessWatcherRegistry.getSessionData(this);
}
form() { return {
actor: this.actorID, // The resources and target traits should be removed all at the same time since the // client has generic ways to deal with all of them (See Bug 1680280).
traits: {
...this.sessionContext.supportedTargets,
resources: this.sessionContext.supportedResources,
},
};
}
/** * Start watching for a new target type. * * This will instantiate Target Actors for existing debugging context of this type, * but will also create actors as context of this type get created. * The actors are notified to the client via "target-available-form" RDP events. * We also notify about target actors destruction via "target-destroyed-form". * Note that we are guaranteed to receive all existing target actor by the time this method * resolves. * * @param {string} targetType * Type of context to observe. See Targets.TYPES object.
*/
async watchTargets(targetType) {
ParentProcessWatcherRegistry.watchTargets(this, targetType);
// When debugging a tab, ensure processing the top level target first // (for now, other session context types are instantiating the top level target // from the descriptor's getTarget method instead of the Watcher)
let topLevelTargetProcess; if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) {
topLevelTargetProcess = this.browserElement.browsingContext.currentWindowGlobal?.domProcess; if (topLevelTargetProcess) {
await topLevelTargetProcess.getActor(this._jsActorName).watchTargets({
watcherActorID: this.actorID,
targetType,
}); // Stop execution if we were destroyed in the meantime if (this.isDestroyed()) { return;
}
}
}
// We have to reach out all the content processes as the page may navigate // to any other content process when navigating to another origin. // It may even run in the parent process when loading about:robots. const domProcesses = ChromeUtils.getAllDOMProcesses(); const promises = []; for (const domProcess of domProcesses) { if (domProcess == topLevelTargetProcess) { continue;
}
promises.push(
domProcess
.getActor(this._jsActorName)
.watchTargets({
watcherActorID: this.actorID,
targetType,
})
.catch(e => { // Ignore any process that got destroyed while trying to send the request if (!domProcess.canSend) {
console.warn( "Content process closed while requesting targets",
domProcess.name,
domProcess.remoteType
); return;
} throw e;
})
);
}
await Promise.all(promises);
}
/** * Stop watching for a given target type. * * @param {string} targetType * Type of context to observe. See Targets.TYPES object. * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
unwatchTargets(targetType, options = {}) { const isWatchingTargets = ParentProcessWatcherRegistry.unwatchTargets( this,
targetType
); if (!isWatchingTargets) { return;
}
const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
domProcess.getActor(this._jsActorName).unwatchTargets({
watcherActorID: this.actorID,
targetType,
options,
});
}
}
/** * Flush any early iframe targets relating to this top level * window target. * @param {number} topInnerWindowID
*/
_flushIframeTargets(topInnerWindowID) { while (this._earlyIframeTargets[topInnerWindowID]?.length > 0) { const actor = this._earlyIframeTargets[topInnerWindowID].shift(); this.emit("target-available-form", actor);
}
}
/** * Called by a Watcher module, whenever a new target is available
*/
notifyTargetAvailable(actor) { // Emit immediately for worker, process & extension targets // as they don't have a parent browsing context. if (!actor.traits?.isBrowsingContext) { this.emit("target-available-form", actor); return;
}
// If isBrowsingContext trait is true, we are processing a WindowGlobalTarget. // (this trait should be renamed) this._currentWindowGlobalTargets.set(actor.innerWindowId, actor);
// The top-level is always the same for the browser-toolbox if (this.sessionContext.type == "all") { this.emit("target-available-form", actor); return;
}
if (actor.isTopLevelTarget) { this.emit("target-available-form", actor); // Flush any existing early iframe targets this._flushIframeTargets(actor.innerWindowId);
if (this.sessionContext.type == SESSION_TYPES.BROWSER_ELEMENT) { // Ignore any pending exception as this request may be pending // while the toolbox closes. And we don't want to delay target emission // on this as this is a implementation detail. this.updateDomainSessionDataForServiceWorkers(actor.url).catch(
() => {}
);
}
} elseif (this._currentWindowGlobalTargets.has(actor.topInnerWindowId)) { // Emit the event immediately if the top-level target is already available this.emit("target-available-form", actor);
} elseif (this._earlyIframeTargets[actor.topInnerWindowId]) { // Add the early iframe target to the list of other early targets. this._earlyIframeTargets[actor.topInnerWindowId].push(actor);
} else { // Set the first early iframe target this._earlyIframeTargets[actor.topInnerWindowId] = [actor];
}
}
/** * Called by a Watcher module, whenever a target has been destroyed * * @param {object} actor * the actor form of the target being destroyed * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
async notifyTargetDestroyed(actor, options = {}) { // Emit immediately for worker, process & extension targets // as they don't have a parent browsing context. if (!actor.innerWindowId) { this.emit("target-destroyed-form", actor, options); return;
} // Flush all iframe targets if we are destroying a top level target. if (actor.isTopLevelTarget) { // First compute the list of children actors, as notifyTargetDestroy will mutate _currentWindowGlobalTargets const childrenActors = [
...this._currentWindowGlobalTargets.values(),
].filter(
form =>
form.topInnerWindowId == actor.innerWindowId && // Ignore the top level target itself, because its topInnerWindowId will be its innerWindowId
form.innerWindowId != actor.innerWindowId
);
childrenActors.map(form => this.notifyTargetDestroyed(form, options));
} if (this._earlyIframeTargets[actor.innerWindowId]) { deletethis._earlyIframeTargets[actor.innerWindowId];
} this._currentWindowGlobalTargets.delete(actor.innerWindowId); const documentEventWatcher = Resources.getResourceWatcher( this,
Resources.TYPES.DOCUMENT_EVENT
); // If we have a Watcher class instantiated, ensure that target-destroyed is sent // *after* DOCUMENT_EVENT's will-navigate. Otherwise this resource will have an undefined // `targetFront` attribute, as it is associated with the target from which we navigate // and not the one we navigate to. // // About documentEventWatcher check: We won't have any watcher class if we aren't // using server side Watcher classes. // i.e. when we are using the legacy listener for DOCUMENT_EVENT. // This is still the case for all toolboxes but the one for local and remote tabs. // // About isServerTargetSwitchingEnabled check: if we are using the watcher class // we may still use client side target, which will still use legacy listeners for // will-navigate and so will-navigate will be emitted by the target actor itself. // // About isTopLevelTarget check: only top level targets emit will-navigate, // so there is no reason to delay target-destroy for remote iframes. if (
documentEventWatcher && this.sessionContext.isServerTargetSwitchingEnabled &&
actor.isTopLevelTarget
) {
await documentEventWatcher.onceWillNavigateIsEmitted(actor.innerWindowId);
} this.emit("target-destroyed-form", actor, options);
}
/** * Given a browsingContextID, returns its parent browsingContextID. Returns null if a * parent browsing context couldn't be found. Throws if the browsing context * corresponding to the passed browsingContextID couldn't be found. * * @param {Integer} browsingContextID * @returns {Integer|null}
*/
getParentBrowsingContextID(browsingContextID) { const browsingContext = BrowsingContext.get(browsingContextID); if (!browsingContext) { thrownew Error(
`BrowsingContext with ID=${browsingContextID} doesn't exist.`
);
} // Top-level documents of tabs, loaded in a <browser> element expose a null `parent`. // i.e. Their BrowsingContext has no parent and is considered top level. // But... in the context of the Browser Toolbox, we still consider them as child of the browser window. // So, for them, fallback on `embedderWindowGlobal`, which will typically be the WindowGlobal for browser.xhtml. if (browsingContext.parent) { return browsingContext.parent.id;
} if (browsingContext.embedderWindowGlobal) { return browsingContext.embedderWindowGlobal.browsingContext.id;
} returnnull;
}
/** * Called by Resource Watchers, when new resources are available, updated or destroyed. * * @param String updateType * Can be "available", "updated" or "destroyed" * @param String resourceType * The type of resources to be notified about. * @param Array<json> resources * List of all resource's form. A resource is a JSON object piped over to the client. * It can contain actor IDs, actor forms, to be manually marshalled by the client.
*/
notifyResources(updateType, resourceType, resources) { if (resources.length === 0) { // Don't try to emit if the resources array is empty. return;
}
/** * Try to retrieve Target Actors instantiated in the parent process which aren't * instantiated via the Watcher actor (and its dependencies): * - top level target for the browser toolboxes * - xpcshell targets for xpcshell debugging * * See comment in `watchResources`. * * @return {Set<TargetActor>} Matching target actors.
*/
getTargetActorsInParentProcess() { if (TargetActorRegistry.xpcShellTargetActors.size) { return TargetActorRegistry.xpcShellTargetActors;
}
// Note: For browser-element debugging, the WindowGlobalTargetActor returned here is created // for a parent process page and lives in the parent process. const actors = TargetActorRegistry.getTargetActors( this.sessionContext, // Note that we aren't using watcherConnectionPrefix as the ParentProcessTargetActor // are registered in `this.conn` (i.e The connection which is bound to the client) // directly and not in the DevToolsServerConnection running in the content process with `watcherConnectionPrefix` this.conn.prefix
);
switch (this.sessionContext.type) { case"all": const parentProcessTargetActor = actors.find(
actor => actor.typeName === "parentProcessTarget"
); if (parentProcessTargetActor) { returnnew Set([parentProcessTargetActor]);
} returnnew Set(); case"browser-element": case"webextension": // All target actors for browser-element and webextension sessions // should be created using the JS Window actors. returnnew Set(); default: thrownew Error( "Unsupported session context type: " + this.sessionContext.type
);
}
}
/** * Start watching for a list of resource types. * This should only resolve once all "already existing" resources of these types * are notified to the client via resources-available-array event on related target actors. * * @param {Array<string>} resourceTypes * List of all types to listen to.
*/
async watchResources(resourceTypes) { // First process resources which have to be listened from the parent process // (the watcher actor always runs in the parent process)
await Resources.watchResources( this,
Resources.getParentProcessResourceTypes(resourceTypes)
);
// Bail out early if all resources were watched from parent process. // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return;
}
const promises = []; const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
promises.push(
domProcess
.getActor(this._jsActorName)
.addOrSetSessionDataEntry({
watcherActorID: this.actorID,
sessionContext: this.sessionContext,
type: "resources",
entries: resourceTypes,
updateType: "add",
})
.catch(e => { // Ignore any process that got destroyed while trying to send the request if (!domProcess.canSend) {
console.warn( "Content process closed while requesting resources",
domProcess.name,
domProcess.remoteType
); return;
} throw e;
})
);
}
await Promise.all(promises);
// Stop execution if we were destroyed in the meantime if (this.isDestroyed()) { return;
}
/* * The Watcher actor doesn't support watching the top level target * (bug 1644397 and possibly some other followup). * * Because of that, we miss reaching these targets in the previous lines of this function. * Since all BrowsingContext target actors register themselves to the TargetActorRegistry, * we use it here in order to reach those missing targets, which are running in the * parent process (where this WatcherActor lives as well): * - the parent process target (which inherits from WindowGlobalTargetActor) * - top level tab target for documents loaded in the parent process (e.g. about:robots). * When the tab loads document in the content process, the FrameTargetHelper will * reach it via the JSWindowActor API. Even if it uses MessageManager for anything * else (RDP packet forwarding, creation and destruction). * * We will eventually get rid of this code once all targets are properly supported by * the Watcher Actor and we have target helpers for all of them.
*/ const targetActors = this.getTargetActorsInParentProcess(); for (const targetActor of targetActors) { const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
targetActor.targetType
);
await targetActor.addOrSetSessionDataEntry( "resources",
targetActorResourceTypes, false, "add"
);
}
}
/** * Stop watching for a list of resource types. * * @param {Array<string>} resourceTypes * List of all types to listen to.
*/
unwatchResources(resourceTypes) { // First process resources which are listened from the parent process // (the watcher actor always runs in the parent process)
Resources.unwatchResources( this,
Resources.getParentProcessResourceTypes(resourceTypes)
);
// Bail out early if all resources were all watched from parent process. // In this scenario, we do not need to update these resource types in the ParentProcessWatcherRegistry // as targets do not care about them. if (!Resources.hasResourceTypesForTargets(resourceTypes)) { return;
}
// Prevent trying to unwatch when the related BrowsingContext has already // been destroyed if (!this.isContextDestroyed()) { const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
domProcess.getActor(this._jsActorName).removeSessionDataEntry({
watcherActorID: this.actorID,
sessionContext: this.sessionContext,
type: "resources",
entries: resourceTypes,
});
}
}
// See comment in watchResources. const targetActors = this.getTargetActorsInParentProcess(); for (const targetActor of targetActors) { const targetActorResourceTypes = Resources.getResourceTypesForTargetType(
resourceTypes,
targetActor.targetType
);
targetActor.removeSessionDataEntry("resources", targetActorResourceTypes);
}
}
clearResources(resourceTypes) { // First process resources which have to be listened from the parent process // (the watcher actor always runs in the parent process) // TODO: content process / worker thread resources are not cleared. See Bug 1774573
Resources.clearResources( this,
Resources.getParentProcessResourceTypes(resourceTypes)
);
}
/** * Returns the network actor. * * @return {Object} actor * The network actor.
*/
getNetworkParentActor() { if (!this._networkParentActor) { this._networkParentActor = new NetworkParentActor(this);
}
returnthis._networkParentActor;
}
/** * Returns the blackboxing actor. * * @return {Object} actor * The blackboxing actor.
*/
getBlackboxingActor() { if (!this._blackboxingActor) { this._blackboxingActor = new BlackboxingActor(this);
}
returnthis._blackboxingActor;
}
/** * Returns the breakpoint list actor. * * @return {Object} actor * The breakpoint list actor.
*/
getBreakpointListActor() { if (!this._breakpointListActor) { this._breakpointListActor = new BreakpointListActor(this);
}
returnthis._breakpointListActor;
}
/** * Returns the target configuration actor. * * @return {Object} actor * The configuration actor.
*/
getTargetConfigurationActor() { if (!this._targetConfigurationListActor) { this._targetConfigurationListActor = new TargetConfigurationActor(this);
} returnthis._targetConfigurationListActor;
}
/** * Returns the thread configuration actor. * * @return {Object} actor * The configuration actor.
*/
getThreadConfigurationActor() { if (!this._threadConfigurationListActor) { this._threadConfigurationListActor = new ThreadConfigurationActor(this);
} returnthis._threadConfigurationListActor;
}
/** * Server internal API, called by other actors, but not by the client. * Used to agrement some new entries for a given data type (watchers target, resources, * breakpoints,...) * * @param {String} type * Data type to contribute to. * @param {Array<*>} entries * List of values to add or set for this data type. * @param {String} updateType * "add" will only add the new entries in the existing data set. * "set" will update the data set with the new entries.
*/
async addOrSetDataEntry(type, entries, updateType) {
ParentProcessWatcherRegistry.addOrSetSessionDataEntry( this,
type,
entries,
updateType
);
const promises = []; const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
promises.push(
domProcess
.getActor(this._jsActorName)
.addOrSetSessionDataEntry({
watcherActorID: this.actorID,
sessionContext: this.sessionContext,
type,
entries,
updateType,
})
.catch(e => { // Ignore any process that got destroyed while trying to send the request if (!domProcess.canSend) {
console.warn( "Content process closed while sending session data",
domProcess.name,
domProcess.remoteType
); return;
} throw e;
})
);
}
await Promise.all(promises);
// Stop execution if we were destroyed in the meantime if (this.isDestroyed()) { return;
}
// See comment in watchResources const targetActors = this.getTargetActorsInParentProcess(); for (const targetActor of targetActors) {
await targetActor.addOrSetSessionDataEntry(
type,
entries, false,
updateType
);
}
}
/** * Server internal API, called by other actors, but not by the client. * Used to remve some existing entries for a given data type (watchers target, resources, * breakpoints,...) * * @param {String} type * Data type to modify. * @param {Array<*>} entries * List of values to remove from this data type.
*/
removeDataEntry(type, entries) {
ParentProcessWatcherRegistry.removeSessionDataEntry(this, type, entries);
const domProcesses = ChromeUtils.getAllDOMProcesses(); for (const domProcess of domProcesses) {
domProcess.getActor(this._jsActorName).removeSessionDataEntry({
watcherActorID: this.actorID,
sessionContext: this.sessionContext,
type,
entries,
});
}
// See comment in addOrSetDataEntry const targetActors = this.getTargetActorsInParentProcess(); for (const targetActor of targetActors) {
targetActor.removeSessionDataEntry(type, entries);
}
}
/** * Retrieve the current watched data for the provided type. * * @param {String} type * Data type to retrieve.
*/
getSessionDataForType(type) { returnthis.sessionData?.[type];
}
/** * Special code dedicated to Service Worker debugging. * This will notify the Service Worker JS Process Actors about the new top level page domain. * So that we start tracking that domain's workers. * * @param {String} newTargetUrl
*/
async updateDomainSessionDataForServiceWorkers(newTargetUrl) {
let host = ""; // Accessing `host` can throw on some URLs with no valid host like about:home. // In such scenario, reset the host to an empty string. try {
host = new URL(newTargetUrl).host;
} catch (e) {}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.