summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs')
-rw-r--r--devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs689
1 files changed, 689 insertions, 0 deletions
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
new file mode 100644
index 0000000000..c7d5bd0718
--- /dev/null
+++ b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs
@@ -0,0 +1,689 @@
+/* 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 { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
+import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ isWindowGlobalPartOfContext:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+ releaseDistinctSystemPrincipalLoader:
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
+ TargetActorRegistry:
+ "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
+ useDistinctSystemPrincipalLoader:
+ "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
+ WindowGlobalLogger:
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs",
+});
+
+const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
+ "devtools.every-frame-target.enabled",
+ false
+);
+
+// Name of the attribute into which we save data in `sharedData` object.
+const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
+
+// If true, log info about WindowGlobal's being created.
+const DEBUG = false;
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+ lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}
+
+export class DevToolsFrameChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ // The map is indexed by the Watcher Actor ID.
+ // The values are objects containing the following properties:
+ // - connection: the DevToolsServerConnection itself
+ // - actor: the WindowGlobalTargetActor instance
+ this._connections = new Map();
+
+ EventEmitter.decorate(this);
+
+ // Set the following preference on the constructor, 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. DevToolsFrameParent/DevToolsFrameChild).
+ // 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.
+ XPCOMUtils.defineLazyGetter(
+ this,
+ "isBfcacheInParentEnabled",
+ () =>
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent", false)
+ );
+ }
+
+ /**
+ * Try to instantiate new target actors for the current WindowGlobal, and that,
+ * for all the currently registered Watcher actors.
+ *
+ * Read the `sharedData` to get metadata about all registered watcher actors.
+ * If these watcher actors are interested in the current WindowGlobal,
+ * instantiate a new dedicated target actor for each of the watchers.
+ *
+ * @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.
+ */
+ instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) {
+ const { sharedData } = Services.cpmm;
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ // Create one Target actor for each prefix/client which listen to frames
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { connectionPrefix, sessionContext } = sessionData;
+ // 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 one such actor is bound to a unique DocShell.
+ const forceAcceptTopLevelTarget =
+ isBFCache && this.isBfcacheInParentEnabled;
+ if (
+ sessionData.targets?.includes("frame") &&
+ lazy.isWindowGlobalPartOfContext(this.manager, 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 = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ browsingContextId: this.manager.browsingContext.id,
+ });
+
+ // See comment in handleEvent(DOMDocElementInserted) to know why we try to
+ // create targets if none already exists
+ 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 });
+ }
+
+ this._createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ isDocumentCreation: true,
+ });
+ }
+ }
+ }
+
+ /**
+ * Instantiate a new WindowGlobalTarget for the given connection.
+ *
+ * @param Object options
+ * @param String options.watcherActorID
+ * The ID of the WatcherActor who requested to observe and create these target actors.
+ * @param String options.parentConnectionPrefix
+ * The prefix of the DevToolsServerConnection of the Watcher Actor.
+ * This is used to compute a unique ID for the target actor.
+ * @param Object options.sessionData
+ * All data managed by the Watcher Actor and WatcherRegistry.jsm, containing
+ * target types, resources types to be listened as well as breakpoints and any
+ * other data meant to be shared across processes and threads.
+ * @param Boolean options.isDocumentCreation
+ * Set to true if the function is called from `instantiate`, i.e. when we're
+ * handling a new document being created.
+ * @param Boolean options.fromInstantiateAlreadyAvailable
+ * Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available`
+ * query.
+ */
+ _createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix,
+ sessionData,
+ isDocumentCreation,
+ fromInstantiateAlreadyAvailable,
+ }) {
+ if (this._connections.get(watcherActorID)) {
+ // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available`
+ // message, we might have a legitimate race condition:
+ // In frame-helper, we want to create the initial targets for a given browser element.
+ // It might happen that the `DevToolsFrameParent:instantiate-already-available` is
+ // aborted if the page navigates (and the document is destroyed) while the query is still pending.
+ // In such case, frame-helper will try to send a new message. In the meantime,
+ // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and
+ // the new target already created.
+ // We don't want to throw in such case, as our end-goal, having a target for the document,
+ // is achieved.
+ if (fromInstantiateAlreadyAvailable) {
+ return;
+ }
+ throw new Error(
+ "DevToolsFrameChild _createTargetActor was called more than once" +
+ ` for the same Watcher (Actor ID: "${watcherActorID}")`
+ );
+ }
+
+ // Compute a unique prefix, just for this WindowGlobal,
+ // which will be used to create a JSWindowActorTransport pair between content and parent processes.
+ // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
+ // but here, we can't have access to any DevTools connection as we are really early in the content process startup
+ // XXX: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe?
+ // (this.manager == WindowGlobalChild interface)
+ const forwardingPrefix =
+ parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId;
+
+ logWindowGlobal(
+ this.manager,
+ "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix
+ );
+
+ const { connection, targetActor } = this._createConnectionAndActor(
+ forwardingPrefix,
+ sessionData
+ );
+ const form = targetActor.form();
+ // Ensure unregistering and destroying the related DevToolsServerConnection+Transport
+ // on both content and parent process JSWindowActors.
+ targetActor.once("destroyed", options => {
+ // This will destroy the content process one
+ this._destroyTargetActor(watcherActorID, options);
+ // And this will destroy the parent process one
+ try {
+ this.sendAsyncMessage("DevToolsFrameChild:destroy", {
+ actors: [
+ {
+ watcherActorID,
+ form,
+ },
+ ],
+ options,
+ });
+ } catch (e) {
+ // Ignore exception when the JSWindowActorChild has already been destroyed.
+ // We often try to emit this message while the WindowGlobal is in the process of being
+ // destroyed. We eagerly destroy the target actor during reloads,
+ // just before the WindowGlobal starts destroying, but sendAsyncMessage
+ // doesn't have time to complete and throws.
+ if (
+ !e.message.includes("JSWindowActorChild cannot send at the moment")
+ ) {
+ throw e;
+ }
+ }
+ });
+ this._connections.set(watcherActorID, {
+ connection,
+ actor: 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 the order of messages is 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 WindowGlobalTargetActor may emit events in its constructor.
+ // If it does, such RDP packets may be lost.
+ // The important point here is to send this message before processing the sessionData,
+ // which will start the Watcher and start emitting resources on the target actor.
+ this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", {
+ watcherActorID,
+ forwardingPrefix,
+ actor: targetActor.form(),
+ });
+
+ // Pass initialization data to the target actor
+ 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.addSessionDataEntry(type, entries, isDocumentCreation);
+ }
+ }
+
+ /**
+ * @param {string} watcherActorID
+ * @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(watcherActorID, options) {
+ const connectionInfo = this._connections.get(watcherActorID);
+ // This connection has already been cleaned?
+ if (!connectionInfo) {
+ throw new Error(
+ `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
+ );
+ }
+ connectionInfo.connection.close(options);
+ this._connections.delete(watcherActorID);
+ if (this._connections.size == 0) {
+ this.didDestroy(options);
+ }
+ }
+
+ _createConnectionAndActor(forwardingPrefix, sessionData) {
+ this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
+
+ if (!this.loader) {
+ // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
+ this.loader = this.useCustomLoader
+ ? lazy.useDistinctSystemPrincipalLoader(this)
+ : Loader;
+ }
+ const { DevToolsServer } = this.loader.require(
+ "resource://devtools/server/devtools-server.js"
+ );
+
+ const { WindowGlobalTargetActor } = this.loader.require(
+ "resource://devtools/server/actors/targets/window-global.js"
+ );
+
+ DevToolsServer.init();
+
+ // We want a special server without any root actor and only target-scoped actors.
+ // We are going to spawn a WindowGlobalTargetActor instance in the next few lines,
+ // it is going to act like a root actor without being one.
+ DevToolsServer.registerActors({ target: true });
+
+ const connection = DevToolsServer.connectToParentWindowActor(
+ this,
+ forwardingPrefix
+ );
+
+ // 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 browsingContext = this.manager.browsingContext;
+ const isTopLevelTarget =
+ !browsingContext.parent &&
+ browsingContext.browserId == sessionData.sessionContext.browserId;
+
+ // Create the actual target actor.
+ const targetActor = new WindowGlobalTargetActor(connection, {
+ docShell: this.docShell,
+ // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow
+ // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any
+ // type of navigation/reload.
+ followWindowGlobalLifeCycle: true,
+ isTopLevelTarget,
+ ignoreSubFrames: isEveryFrameTargetEnabled,
+ sessionContext: sessionData.sessionContext,
+ });
+ targetActor.manage(targetActor);
+ targetActor.createdFromJsWindowActor = true;
+
+ return { connection, targetActor };
+ }
+
+ /**
+ * Supported Queries
+ */
+
+ sendPacket(packet, prefix) {
+ this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix });
+ }
+
+ /**
+ * JsWindowActor API
+ */
+
+ async sendQuery(msg, args) {
+ try {
+ const res = await super.sendQuery(msg, args);
+ return res;
+ } catch (e) {
+ console.error("Failed to sendQuery in DevToolsFrameChild", msg);
+ console.error(e.toString());
+ throw e;
+ }
+ }
+
+ receiveMessage(message) {
+ // Assert that the message is intended for this window global,
+ // except for "packet" messages which use a dedicated routing
+ if (
+ message.name != "DevToolsFrameParent:packet" &&
+ message.data.sessionContext.type == "browser-element"
+ ) {
+ const { browserId } = message.data.sessionContext;
+ // Re-check here, just to ensure that both parent and content processes agree
+ // on what should or should not be watched.
+ if (
+ this.manager.browsingContext.browserId != browserId &&
+ !lazy.isWindowGlobalPartOfContext(
+ this.manager,
+ message.data.sessionContext,
+ {
+ forceAcceptTopLevelTarget: true,
+ }
+ )
+ ) {
+ throw new Error(
+ "Mismatch between DevToolsFrameParent and DevToolsFrameChild " +
+ (this.manager.browsingContext.browserId == browserId
+ ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
+ : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
+ );
+ }
+ }
+ switch (message.name) {
+ case "DevToolsFrameParent:instantiate-already-available": {
+ const { watcherActorID, connectionPrefix, sessionData } = message.data;
+
+ return this._createTargetActor({
+ watcherActorID,
+ parentConnectionPrefix: connectionPrefix,
+ sessionData,
+ fromInstantiateAlreadyAvailable: true,
+ });
+ }
+ case "DevToolsFrameParent:destroy": {
+ const { watcherActorID, options } = message.data;
+ return this._destroyTargetActor(watcherActorID, options);
+ }
+ case "DevToolsFrameParent:addSessionDataEntry": {
+ const { watcherActorID, sessionContext, type, entries } = message.data;
+ return this._addSessionDataEntry(
+ watcherActorID,
+ sessionContext,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:removeSessionDataEntry": {
+ const { watcherActorID, sessionContext, type, entries } = message.data;
+ return this._removeSessionDataEntry(
+ watcherActorID,
+ sessionContext,
+ type,
+ entries
+ );
+ }
+ case "DevToolsFrameParent:packet":
+ return this.emit("packet-received", message);
+ default:
+ throw new Error(
+ "Unsupported message in DevToolsFrameParent: " + message.name
+ );
+ }
+ }
+
+ /**
+ * Return an existing target given a WatcherActor id, a browserId and an optional
+ * browsing context id.
+ * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple,
+ * for example if we have 2 remote iframes sharing the same origin, which is why you
+ * might want to pass a specific browsing context id to filter the list down.
+ *
+ * @param {Object} options
+ * @param {Object} options.watcherActorID
+ * @param {Object} options.sessionContext
+ * @param {Object} options.browsingContextId: Optional browsing context id to narrow the
+ * search to a specific browsing context.
+ *
+ * @returns {WindowGlobalTargetActor|null}
+ */
+ _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) {
+ // First let's check if a target was created for this watcher actor in this specific
+ // DevToolsFrameChild instance.
+ const connectionInfo = this._connections.get(watcherActorID);
+ const targetActor = connectionInfo ? connectionInfo.actor : null;
+ if (targetActor) {
+ return targetActor;
+ }
+
+ // If we couldn't find such target, we want to see if a target was created for this
+ // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance.
+ // This might be the case if we're navigating to a new page with server side target
+ // enabled and we want to retrieve the target of the page we're navigating from.
+ if (
+ lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, {
+ forceAcceptTopLevelTarget: true,
+ })
+ ) {
+ // 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 = watcherActorID.replace(/watcher\d+$/, "");
+ const targetActors = lazy.TargetActorRegistry.getTargetActors(
+ sessionContext,
+ connectionPrefix
+ );
+
+ if (!browsingContextId) {
+ return targetActors[0] || null;
+ }
+ return targetActors.find(
+ actor => actor.browsingContextID === browsingContextId
+ );
+ }
+ return null;
+ }
+
+ _addSessionDataEntry(watcherActorID, sessionContext, type, entries) {
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const targetActor = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+
+ if (!targetActor) {
+ throw new Error(
+ `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}`
+ );
+ }
+ return targetActor.addSessionDataEntry(type, entries);
+ }
+
+ _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) {
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const targetActor = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+ // By the time we are calling this, the target may already have been destroyed.
+ if (targetActor) {
+ return targetActor.removeSessionDataEntry(type, entries);
+ }
+ return null;
+ }
+
+ handleEvent({ type, persisted, target }) {
+ // Ignore any event that may fire for children WindowGlobals/documents
+ if (target != this.document) {
+ return;
+ }
+
+ // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors`
+ // as a DOM event to be listened to and so is fired by JS Window Actor code platform code.
+ if (type == "DOMWindowCreated") {
+ this.instantiate();
+ 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 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. There is a DOMDocElementInserted fired just after, that we can catch
+ // to create a target for same-process iframes.
+ // 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)
+ if (type == "DOMDocElementInserted") {
+ this.instantiate({ ignoreIfExisting: true });
+ return;
+ }
+
+ // 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.
+ this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow");
+
+ // 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.
+ this.instantiate({
+ isBFCache: true,
+ });
+ return;
+ }
+
+ 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.
+ this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide");
+
+ // 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.
+ const { sharedData } = Services.cpmm;
+ const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
+ if (!sessionDataByWatcherActor) {
+ throw new Error(
+ "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets"
+ );
+ }
+
+ const actors = [];
+ // A flag to know if the following for loop ended up destroying all the actors.
+ // It may not be the case if one Watcher isn't having server target switching enabled.
+ let allActorsAreDestroyed = true;
+ for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
+ const { sessionContext } = sessionData;
+
+ // /!\ We may have an issue here as there could be multiple targets for a given
+ // (watcherActorID,browserId) pair.
+ // This should be clarified as part of Bug 1725623.
+ const existingTarget = this._findTargetActor({
+ watcherActorID,
+ sessionContext,
+ });
+
+ if (!existingTarget) {
+ continue;
+ }
+
+ // Use `originalWindow` as `window` can be set when a document was selected from
+ // the iframe picker in the toolbox toolbar.
+ if (existingTarget.originalWindow.document != target) {
+ throw new Error("Existing target actor is for a distinct document");
+ }
+ // 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 (
+ !this.isBfcacheInParentEnabled &&
+ !sessionContext.isServerTargetSwitchingEnabled
+ ) {
+ allActorsAreDestroyed = false;
+ continue;
+ }
+
+ actors.push({
+ watcherActorID,
+ form: existingTarget.form(),
+ });
+ existingTarget.destroy();
+ }
+
+ if (actors.length) {
+ // The most important is to unregister the actor from TargetActorRegistry,
+ // so that it is no longer present in the list when new DOMWindowCreated fires.
+ // This will also help notify the client that the target has been destroyed.
+ // And if we navigate back to this target, the client will receive the same target actor ID,
+ // so that it is really important to destroy it correctly on both server and client.
+ this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors });
+ }
+
+ if (allActorsAreDestroyed) {
+ // Completely clear this JSWindow Actor.
+ // Do this after having called _findTargetActor,
+ // as it would clear the registered target actors.
+ this.didDestroy();
+ }
+ }
+ }
+
+ didDestroy(options) {
+ logWindowGlobal(this.manager, "Destroy WindowGlobalTarget");
+ for (const [, connectionInfo] of this._connections) {
+ connectionInfo.connection.close(options);
+ }
+ this._connections.clear();
+
+ if (this.loader) {
+ if (this.useCustomLoader) {
+ lazy.releaseDistinctSystemPrincipalLoader(this);
+ }
+ this.loader = null;
+ }
+ }
+}