diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs')
-rw-r--r-- | devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs new file mode 100644 index 0000000000..66c71cbc1e --- /dev/null +++ b/devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs @@ -0,0 +1,574 @@ +/* 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/. */ + +import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters( + lazy, + { + isWindowGlobalPartOfContext: + "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", + WindowGlobalLogger: + "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs", + }, + { global: "contextual" } +); + +// TargetActorRegistery has to be shared between all devtools instances +// and so is loaded into the shared global. +ChromeUtils.defineESModuleGetters( + lazy, + { + TargetActorRegistry: + "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", + }, + { global: "shared" } +); + +const isEveryFrameTargetEnabled = Services.prefs.getBoolPref( + "devtools.every-frame-target.enabled", + false +); + +// If true, log info about DOMProcess's being created. +const DEBUG = false; + +/** + * Print information about operation being done against each Window Global. + * + * @param {WindowGlobalChild} windowGlobal + * The window global for which we should log a message. + * @param {String} message + * Message to log. + */ +function logWindowGlobal(windowGlobal, message) { + if (!DEBUG) { + return; + } + lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message); +} + +function watch() { + // Set the following preference in this function, so that we can easily + // toggle these preferences on and off from tests and have the new value being picked up. + + // bfcache-in-parent changes significantly how navigation behaves. + // We may start reusing previously existing WindowGlobal and so reuse + // previous set of JSWindowActor pairs (i.e. DevToolsProcessParent/DevToolsProcessChild). + // When enabled, regular navigations may also change and spawn new BrowsingContexts. + // If the page we navigate from supports being stored in bfcache, + // the navigation will use a new BrowsingContext. And so force spawning + // a new top-level target. + ChromeUtils.defineLazyGetter( + lazy, + "isBfcacheInParentEnabled", + () => + Services.appinfo.sessionHistoryInParent && + Services.prefs.getBoolPref("fission.bfcacheInParent", false) + ); + + // Observe for all necessary event to track new and destroyed WindowGlobals. + Services.obs.addObserver(observe, "content-document-global-created"); + Services.obs.addObserver(observe, "chrome-document-global-created"); + Services.obs.addObserver(observe, "content-page-shown"); + Services.obs.addObserver(observe, "chrome-page-shown"); + Services.obs.addObserver(observe, "content-page-hidden"); + Services.obs.addObserver(observe, "chrome-page-hidden"); + Services.obs.addObserver(observe, "inner-window-destroyed"); + Services.obs.addObserver(observe, "initial-document-element-inserted"); +} + +function unwatch() { + // Observe for all necessary event to track new and destroyed WindowGlobals. + Services.obs.removeObserver(observe, "content-document-global-created"); + Services.obs.removeObserver(observe, "chrome-document-global-created"); + Services.obs.removeObserver(observe, "content-page-shown"); + Services.obs.removeObserver(observe, "chrome-page-shown"); + Services.obs.removeObserver(observe, "content-page-hidden"); + Services.obs.removeObserver(observe, "chrome-page-hidden"); + Services.obs.removeObserver(observe, "inner-window-destroyed"); + Services.obs.removeObserver(observe, "initial-document-element-inserted"); +} + +function createTargetsForWatcher(watcherDataObject, isProcessActorStartup) { + const { sessionContext } = watcherDataObject; + // Bug 1785266 - For now, in browser, when debugging the parent process (childID == 0), + // we spawn only the ParentProcessTargetActor, which will debug all the BrowsingContext running in the process. + // So that we have to avoid instantiating any here. + if ( + sessionContext.type == "all" && + ChromeUtils.domProcessChild.childID === 0 + ) { + return; + } + + function lookupForTargets(window) { + // Do not only track top level BrowsingContext in this content process, + // but also any nested iframe which may be running in the same process. + for (const browsingContext of window.docShell.browsingContext.getAllBrowsingContextsInSubtree()) { + const { currentWindowContext } = browsingContext; + // Only consider Window Global which are running in this process + if (!currentWindowContext || !currentWindowContext.isInProcess) { + continue; + } + + // WindowContext's windowGlobalChild should be defined for WindowGlobal running in this process + const { windowGlobalChild } = currentWindowContext; + + // getWindowEnumerator will expose somewhat unexpected WindowGlobal when a tab navigated. + // This will expose WindowGlobals of past navigations. Document which are in the bfcache + // and aren't the current WindowGlobal of their BrowsingContext. + if (!windowGlobalChild.isCurrentGlobal) { + continue; + } + + // Accept the initial about:blank document: + // - only from createTargetsForWatcher, when instantiating the target for the already existing WindowGlobals, + // - when we do that on toolbox opening, to prevent creating one when the process is starting. + // + // This is to allow debugging blank tabs, which are on an initial about:blank document. + // + // We want to avoid creating transient targets for initial about blank when a new WindowGlobal + // just get created as it will most likely navigate away just after and confuse the frontend with short lived target. + const acceptInitialDocument = !isProcessActorStartup; + + if ( + lazy.isWindowGlobalPartOfContext(windowGlobalChild, sessionContext, { + acceptInitialDocument, + }) + ) { + createWindowGlobalTargetActor(watcherDataObject, windowGlobalChild); + } else if ( + !browsingContext.parent && + sessionContext.browserId && + browsingContext.browserId == sessionContext.browserId && + browsingContext.window.document.isInitialDocument + ) { + // In order to succesfully get the devtools-html-content event in SourcesManager, + // we have to ensure flagging the initial about:blank document... + // While we don't create a target for it, we need to set this flag for this event to be emitted. + browsingContext.watchedByDevTools = true; + } + } + } + for (const window of Services.ww.getWindowEnumerator()) { + lookupForTargets(window); + + // `lookupForTargets` uses `getAllBrowsingContextsInSubTree`, but this will ignore browser elements + // using type="content". So manually retrieve the windows for these browser elements, + // in case we have tabs opened on document loaded in the same process. + // This codepath is meant when we are in the parent process, with browser.xhtml having these <browser type="content"> + // elements for tabs. + for (const browser of window.document.querySelectorAll( + `browser[type="content"]` + )) { + const childWindow = browser.browsingContext.window; + // If the tab isn't on a document loaded in the parent process, + // the window will be null. + if (childWindow) { + lookupForTargets(childWindow); + } + } + } +} + +function destroyTargetsForWatcher(watcherDataObject, options) { + // Unregister and destroy the existing target actors for this target type + const actorsToDestroy = watcherDataObject.actors.filter( + actor => actor.targetType == "frame" + ); + watcherDataObject.actors = watcherDataObject.actors.filter( + actor => actor.targetType != "frame" + ); + + for (const actor of actorsToDestroy) { + ContentProcessWatcherRegistry.destroyTargetActor( + watcherDataObject, + actor, + options + ); + } +} + +/** + * Called whenever a new WindowGlobal is instantiated either: + * - when navigating to a new page (DOMWindowCreated) + * - by a bfcache navigation (pageshow) + * + * @param {Window} window + * @param {Object} options + * @param {Boolean} options.isBFCache + * True, if the request to instantiate a new target comes from a bfcache navigation. + * i.e. when we receive a pageshow event with persisted=true. + * This will be true regardless of bfcacheInParent being enabled or disabled. + * @param {Boolean} options.ignoreIfExisting + * By default to false. If true is passed, we avoid instantiating a target actor + * if one already exists for this windowGlobal. + */ +function onWindowGlobalCreated( + window, + { isBFCache = false, ignoreIfExisting = false } = {} +) { + try { + const windowGlobal = window.windowGlobalChild; + + // For bfcache navigations, we only create new targets when bfcacheInParent is enabled, + // as this would be the only case where new DocShells will be created. This requires us to spawn a + // new WindowGlobalTargetActor as such actor is bound to a unique DocShell. + const forceAcceptTopLevelTarget = + isBFCache && lazy.isBfcacheInParentEnabled; + + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + "frame" + )) { + const { sessionContext } = watcherDataObject; + if ( + lazy.isWindowGlobalPartOfContext(windowGlobal, sessionContext, { + forceAcceptTopLevelTarget, + }) + ) { + // If this was triggered because of a navigation, we want to retrieve the existing + // target we were debugging so we can destroy it before creating the new target. + // This is important because we had cases where the destruction of an old target + // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398). + + // We're checking for an existing target given a watcherActorID + browserId + browsingContext. + // Note that a target switch might create a new browsing context, so we wouldn't + // retrieve the existing target here. We are okay with this as: + // - this shouldn't happen much + // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document) + const existingTarget = findTargetActor({ + watcherDataObject, + innerWindowId: windowGlobal.innerWindowId, + }); + + // See comment in `observe()` method and `DOMDocElementInserted` condition to know why we sometime + // ignore this method call if a target actor already exists. + // It means that we got a previous DOMWindowCreated event, related to a non-about:blank document, + // and we should ignore the DOMDocElementInserted. + // In any other scenario, destroy the already existing target and re-create a new one. + if (existingTarget && ignoreIfExisting) { + continue; + } + + // Bail if there is already an existing WindowGlobalTargetActor which wasn't + // created from a JSWIndowActor. + // This means we are reloading or navigating (same-process) a Target + // which has not been created using the Watcher, but from the client (most likely + // the initial target of a local-tab toolbox). + // However, we force overriding the first message manager based target in case of + // BFCache navigations. + if ( + existingTarget && + !existingTarget.createdFromJsWindowActor && + !isBFCache + ) { + continue; + } + + // If we decide to instantiate a new target and there was one before, + // first destroy the previous one. + // Otherwise its destroy sequence will be executed *after* the new one + // is being initialized and may easily revert changes made against platform API. + // (typically toggle platform boolean attributes back to default…) + if (existingTarget) { + existingTarget.destroy({ isTargetSwitching: true }); + } + + // When navigating to another process, the Watcher Actor won't have sent any query + // to the new process JS Actor as the debugged tab was on another process before navigation. + // But `sharedData` will have data about all the current watchers. + // Here we have to ensure calling watchTargetsForWatcher in order to populate #connections + // for the currently processed watcher actor and start listening for future targets. + if ( + !ContentProcessWatcherRegistry.has(watcherDataObject.watcherActorID) + ) { + throw new Error("Watcher data seems out of sync"); + } + + createWindowGlobalTargetActor(watcherDataObject, windowGlobal, true); + } + } + } catch (e) { + // Ensure logging exception as they are silently ignore otherwise + dump( + " Exception while observing a new window: " + e + "\n" + e.stack + "\n" + ); + } +} + +/** + * Called whenever a WindowGlobal just got destroyed, when closing the tab, or navigating to another one. + * + * @param {innerWindowId} innerWindowId + * The WindowGlobal's unique identifier. + */ +function onWindowGlobalDestroyed(innerWindowId) { + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects( + "frame" + )) { + const existingTarget = findTargetActor({ + watcherDataObject, + innerWindowId, + }); + + if (!existingTarget) { + continue; + } + + // Do not do anything if both bfcache in parent and server targets are disabled + // As history navigations will be handled within the same DocShell and by the + // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself. + // We should not destroy any target. + if ( + !lazy.isBfcacheInParentEnabled && + !watcherDataObject.sessionContext.isServerTargetSwitchingEnabled + ) { + continue; + } + // If the target actor isn't in watcher data object, it is a top level actor + // instantiated via a Descriptor's getTarget method. It isn't registered into Watcher objects. + // But we still want to destroy such target actor, and need to manually emit the targetDestroyed to the parent process. + // Hopefully bug 1754452 should allow us to get rid of this workaround by making the top level actor + // be created and managed by the watcher universe, like all the others. + const isTopLevelActorRegisteredOutsideOfWatcherActor = + !watcherDataObject.actors.find( + actor => actor.innerWindowId == innerWindowId + ); + const targetActorForm = isTopLevelActorRegisteredOutsideOfWatcherActor + ? existingTarget.form() + : null; + + existingTarget.destroy(); + + if (isTopLevelActorRegisteredOutsideOfWatcherActor) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:targetDestroyed", + { + actors: [ + { + watcherActorID: watcherDataObject.watcherActorID, + targetActorForm, + }, + ], + options: {}, + } + ); + } + } +} + +/** + * Instantiate a WindowGlobal target actor for a given browsing context + * and for a given watcher actor. + * + * @param {Object} watcherDataObject + * @param {BrowsingContext} windowGlobalChild + * @param {Boolean} isDocumentCreation + */ +function createWindowGlobalTargetActor( + watcherDataObject, + windowGlobalChild, + isDocumentCreation = false +) { + logWindowGlobal(windowGlobalChild, "Instantiate WindowGlobalTarget"); + + // When debugging privileged pages running a the shared system compartment, and we aren't in the browser toolbox (which already uses a distinct loader), + // we have to use the distinct loader in order to ensure running DevTools in a distinct compartment than the page we are about to debug + // Such page could be about:addons, chrome://browser/content/browser.xhtml,... + const { browsingContext } = windowGlobalChild; + const useDistinctLoader = + browsingContext.associatedWindow.document.nodePrincipal.isSystemPrincipal; + const { connection, loader } = + ContentProcessWatcherRegistry.getOrCreateConnectionForWatcher( + watcherDataObject.watcherActorID, + useDistinctLoader + ); + + const { WindowGlobalTargetActor } = loader.require( + "devtools/server/actors/targets/window-global" + ); + + // In the case of the browser toolbox, tab's BrowsingContext don't have + // any parent BC and shouldn't be considered as top-level. + // This is why we check for browserId's. + const { sessionContext } = watcherDataObject; + const isTopLevelTarget = + !browsingContext.parent && + browsingContext.browserId == sessionContext.browserId; + + // Create the actual target actor. + const targetActor = new WindowGlobalTargetActor(connection, { + docShell: browsingContext.docShell, + // Targets created from the server side, via Watcher actor and DevToolsProcess JSWindow + // actor pairs are following WindowGlobal lifecycle. i.e. will be destroyed on any + // type of navigation/reload. + followWindowGlobalLifeCycle: true, + isTopLevelTarget, + ignoreSubFrames: isEveryFrameTargetEnabled, + sessionContext, + }); + targetActor.createdFromJsWindowActor = true; + + ContentProcessWatcherRegistry.onNewTargetActor( + watcherDataObject, + targetActor, + isDocumentCreation + ); +} + +/** + * Observer service notification handler. + * + * @param {DOMWindow|Document} subject + * A window for *-document-global-created + * A document for *-page-{shown|hide} + * @param {String} topic + */ +function observe(subject, topic) { + if ( + topic == "content-document-global-created" || + topic == "chrome-document-global-created" + ) { + onWindowGlobalCreated(subject); + } else if (topic == "inner-window-destroyed") { + const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + onWindowGlobalDestroyed(innerWindowId); + } else if (topic == "content-page-shown" || topic == "chrome-page-shown") { + // The observer service notification doesn't receive the "persisted" DOM Event attribute, + // but thanksfully is fired just before the dispatching of that DOM event. + subject.defaultView.addEventListener("pageshow", handleEvent, { + capture: true, + once: true, + }); + } else if (topic == "content-page-hidden" || topic == "chrome-page-hidden") { + // Same as previous elseif branch + subject.defaultView.addEventListener("pagehide", handleEvent, { + capture: true, + once: true, + }); + } else if (topic == "initial-document-element-inserted") { + // We may be notified about SVG documents which we don't care about here. + if (!subject.location || !subject.defaultView) { + return; + } + // We might have ignored the DOMWindowCreated event because it was the initial about:blank document. + // But when loading same-process iframes, we reuse the WindowGlobal of the previously ignored about:bank document + // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated + // for the actual document. But there is a DOMDocElementInserted fired just after, that we are processing here + // to create a target for same-process iframes. We only have to tell onWindowGlobalCreated to ignore + // the call if a target was created on the DOMWindowCreated event (if that was a non-about:blank document). + // + // All this means that we still do not create any target for the initial documents. + // It is complex to instantiate targets for initial documents because: + // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId + // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events + // both on client and server) + onWindowGlobalCreated(subject.defaultView, { ignoreIfExisting: true }); + } +} + +/** + * DOM Event handler. + * + * @param {String} type + * DOM event name + * @param {Boolean} persisted + * A flag set to true in cache of BFCache navigation + * @param {Document} target + * The navigating document + */ +function handleEvent({ type, persisted, target }) { + // If persisted=true, this is a BFCache navigation. + // + // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell + // in the same process: + // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called) + // and a 'pagehide' with persisted=true will be emitted on it. + // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true + // will be emitted. + if (type === "pageshow" && persisted) { + // Notify all bfcache navigations, even the one for which we don't create a new target + // as that's being useful for parent process storage resource watchers. + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:bf-cache-navigation-pageshow", + { + browsingContextId: target.defaultView.browsingContext.id, + } + ); + } + + // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event. + // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled. + // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation, + // we don't want to spawn new targets. + onWindowGlobalCreated(target.defaultView, { + isBFCache: true, + }); + } + + if (type === "pagehide" && persisted) { + // Notify all bfcache navigations, even the one for which we don't create a new target + // as that's being useful for parent process storage resource watchers. + for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) { + watcherDataObject.jsProcessActor.sendAsyncMessage( + "DevToolsProcessChild:bf-cache-navigation-pagehide", + { + browsingContextId: target.defaultView.browsingContext.id, + } + ); + } + + // We might navigate away for the first top level target, + // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget). + // We have to unregister it from the TargetActorRegistry, otherwise, + // if we navigate back to it, the next DOMWindowCreated won't create a new target for it. + onWindowGlobalDestroyed(target.defaultView.windowGlobalChild.innerWindowId); + } +} + +/** + * Return an existing Window Global target for given a WatcherActor + * and against a given WindowGlobal. + * + * @param {Object} options + * @param {String} options.watcherDataObject + * @param {Number} options.innerWindowId + * The WindowGlobal inner window ID. + * + * @returns {WindowGlobalTargetActor|null} + */ +function findTargetActor({ watcherDataObject, innerWindowId }) { + // First let's check if a target was created for this watcher actor in this specific + // DevToolsProcessChild instance. + const targetActor = watcherDataObject.actors.find( + actor => actor.innerWindowId == innerWindowId + ); + if (targetActor) { + return targetActor; + } + + // Ensure retrieving the one target actor related to this connection. + // This allows to distinguish actors created for various toolboxes. + // For ex, regular toolbox versus browser console versus browser toolbox + const connectionPrefix = watcherDataObject.watcherActorID.replace( + /watcher\d+$/, + "" + ); + const targetActors = lazy.TargetActorRegistry.getTargetActors( + watcherDataObject.sessionContext, + connectionPrefix + ); + + return targetActors.find(actor => actor.innerWindowId == innerWindowId); +} + +export const WindowGlobalTargetWatcher = { + watch, + unwatch, + createTargetsForWatcher, + destroyTargetsForWatcher, +}; |