summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/watcher/target-helpers/frame-helper.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/watcher/target-helpers/frame-helper.js')
-rw-r--r--devtools/server/actors/watcher/target-helpers/frame-helper.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/devtools/server/actors/watcher/target-helpers/frame-helper.js b/devtools/server/actors/watcher/target-helpers/frame-helper.js
new file mode 100644
index 0000000000..0e6f4f80d3
--- /dev/null
+++ b/devtools/server/actors/watcher/target-helpers/frame-helper.js
@@ -0,0 +1,331 @@
+/* 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/. */
+
+"use strict";
+
+const { WatcherRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
+ {
+ // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
+ // which also has to be a true singleton.
+ loadInDevToolsLoader: false,
+ }
+);
+const { WindowGlobalLogger } = ChromeUtils.importESModule(
+ "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs"
+);
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const browsingContextAttachedObserverByWatcher = new Map();
+
+/**
+ * Force creating targets for all existing BrowsingContext, that, for a given Watcher Actor.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ */
+async function createTargets(watcher) {
+ // Go over all existing BrowsingContext in order to:
+ // - Force the instantiation of a DevToolsFrameChild
+ // - Have the DevToolsFrameChild to spawn the WindowGlobalTargetActor
+
+ // If we have a browserElement, set the watchedByDevTools flag on its related browsing context
+ // TODO: We should also set the flag for the "parent process" browsing context when we're
+ // in the browser toolbox. This is blocked by Bug 1675763, and should be handled as part
+ // of Bug 1709529.
+ if (watcher.sessionContext.type == "browser-element") {
+ // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
+ // - reporting the contents of HTML loaded in the docshells
+ // - capturing stacks for the network monitor.
+ watcher.browserElement.browsingContext.watchedByDevTools = true;
+ }
+
+ if (!browsingContextAttachedObserverByWatcher.has(watcher)) {
+ // We store the browserId here as watcher.browserElement.browserId can momentary be
+ // set to 0 when there's a navigation to a new browsing context.
+ const browserId = watcher.sessionContext.browserId;
+ const onBrowsingContextAttached = browsingContext => {
+ // We want to set watchedByDevTools on new top-level browsing contexts:
+ // - in the case of the BrowserToolbox/BrowserConsole, that would be the browsing
+ // contexts of all the tabs we want to handle.
+ // - for the regular toolbox, browsing context that are being created when navigating
+ // to a page that forces a new browsing context.
+ // Then BrowsingContext will propagate to all the tree of children BrowsingContext's.
+ if (
+ !browsingContext.parent &&
+ (watcher.sessionContext.type != "browser-element" ||
+ browserId === browsingContext.browserId)
+ ) {
+ browsingContext.watchedByDevTools = true;
+ }
+ };
+ Services.obs.addObserver(
+ onBrowsingContextAttached,
+ "browsing-context-attached"
+ );
+ // We store the observer so we can retrieve it elsewhere (e.g. for removal in destroyTargets).
+ browsingContextAttachedObserverByWatcher.set(
+ watcher,
+ onBrowsingContextAttached
+ );
+ }
+
+ if (
+ watcher.sessionContext.isServerTargetSwitchingEnabled &&
+ watcher.sessionContext.type == "browser-element"
+ ) {
+ // If server side target switching is enabled, process the top level browsing context first,
+ // so that we guarantee it is notified to the client first.
+ // If it is disabled, the top level target will be created from the client instead.
+ await createTargetForBrowsingContext({
+ watcher,
+ browsingContext: watcher.browserElement.browsingContext,
+ retryOnAbortError: true,
+ });
+ }
+
+ const browsingContexts = watcher.getAllBrowsingContexts().filter(
+ // Filter out the top browsing context we just processed.
+ browsingContext =>
+ browsingContext != watcher.browserElement?.browsingContext
+ );
+ // Await for the all the queries in order to resolve only *after* we received all
+ // already available targets.
+ // i.e. each call to `createTargetForBrowsingContext` should end up emitting
+ // a target-available-form event via the WatcherActor.
+ await Promise.allSettled(
+ browsingContexts.map(browsingContext =>
+ createTargetForBrowsingContext({ watcher, browsingContext })
+ )
+ );
+}
+
+/**
+ * (internal helper method) Force creating the target actor for a given BrowsingContext.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to watch for new targets.
+ * @param BrowsingContext browsingContext
+ * The context for which a target should be created.
+ * @param Boolean retryOnAbortError
+ * Set to true to retry creating existing targets when receiving an AbortError.
+ * An AbortError is sent when the JSWindowActor pair was destroyed before the query
+ * was complete, which can happen if the document navigates while the query is pending.
+ */
+async function createTargetForBrowsingContext({
+ watcher,
+ browsingContext,
+ retryOnAbortError = false,
+}) {
+ logWindowGlobal(browsingContext.currentWindowGlobal, "Existing WindowGlobal");
+
+ // We need to set the watchedByDevTools flag on all top-level browsing context. In the
+ // case of a content toolbox, this is done in the tab descriptor, but when we're in the
+ // browser toolbox, such descriptor is not created.
+ // Then BrowsingContext will propagate to all the tree of children BbrowsingContext's.
+ if (!browsingContext.parent) {
+ browsingContext.watchedByDevTools = true;
+ }
+
+ try {
+ await browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .instantiateTarget({
+ watcherActorID: watcher.actorID,
+ connectionPrefix: watcher.conn.prefix,
+ sessionContext: watcher.sessionContext,
+ sessionData: watcher.sessionData,
+ });
+ } catch (e) {
+ console.warn(
+ "Failed to create DevTools Frame target for browsingContext",
+ browsingContext.id,
+ ": ",
+ e,
+ retryOnAbortError ? "retrying" : ""
+ );
+ if (retryOnAbortError && e.name === "AbortError") {
+ await createTargetForBrowsingContext({
+ watcher,
+ browsingContext,
+ retryOnAbortError,
+ });
+ } else {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Force destroying all BrowsingContext targets which were related to a given watcher.
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param {object} options
+ * @param {boolean} options.isModeSwitching
+ * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
+ */
+function destroyTargets(watcher, options) {
+ // Go over all existing BrowsingContext in order to destroy all targets
+ const browsingContexts = watcher.getAllBrowsingContexts();
+
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ if (!browsingContext.parent) {
+ browsingContext.watchedByDevTools = false;
+ }
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .destroyTarget({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ options,
+ });
+ }
+
+ if (watcher.sessionContext.type == "browser-element") {
+ watcher.browserElement.browsingContext.watchedByDevTools = false;
+ }
+
+ if (browsingContextAttachedObserverByWatcher.has(watcher)) {
+ Services.obs.removeObserver(
+ browsingContextAttachedObserverByWatcher.get(watcher),
+ "browsing-context-attached"
+ );
+ browsingContextAttachedObserverByWatcher.delete(watcher);
+ }
+}
+
+/**
+ * Go over all existing BrowsingContext in order to communicate about new data entries
+ *
+ * @param WatcherActor watcher
+ * The Watcher Actor requesting to stop watching for new targets.
+ * @param string type
+ * The type of data to be added
+ * @param Array<Object> entries
+ * The values to be added to this type of data
+ * @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 function addOrSetSessionDataEntry({
+ watcher,
+ type,
+ entries,
+ updateType,
+}) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ const promises = [];
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ const promise = browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .addOrSetSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ updateType,
+ });
+ promises.push(promise);
+ }
+ // Await for the queries in order to try to resolve only *after* the remote code processed the new data
+ return Promise.all(promises);
+}
+
+/**
+ * Notify all existing frame targets that some data entries have been removed
+ *
+ * See addOrSetSessionDataEntry for argument documentation.
+ */
+function removeSessionDataEntry({ watcher, type, entries }) {
+ const browsingContexts = getWatchingBrowsingContexts(watcher);
+ for (const browsingContext of browsingContexts) {
+ logWindowGlobal(
+ browsingContext.currentWindowGlobal,
+ "Existing WindowGlobal"
+ );
+
+ browsingContext.currentWindowGlobal
+ .getActor("DevToolsFrame")
+ .removeSessionDataEntry({
+ watcherActorID: watcher.actorID,
+ sessionContext: watcher.sessionContext,
+ type,
+ entries,
+ });
+ }
+}
+
+module.exports = {
+ createTargets,
+ destroyTargets,
+ addOrSetSessionDataEntry,
+ removeSessionDataEntry,
+};
+
+/**
+ * Return the list of BrowsingContexts which should be targeted in order to communicate
+ * updated session data.
+ *
+ * @param WatcherActor watcher
+ * The watcher actor will be used to know which target we debug
+ * and what BrowsingContext should be considered.
+ */
+function getWatchingBrowsingContexts(watcher) {
+ // If we are watching for additional frame targets, it means that the multiprocess or fission mode is enabled,
+ // either for a content toolbox or a BrowserToolbox via scope set to everything.
+ const watchingAdditionalTargets = WatcherRegistry.isWatchingTargets(
+ watcher,
+ Targets.TYPES.FRAME
+ );
+ if (watchingAdditionalTargets) {
+ return watcher.getAllBrowsingContexts();
+ }
+ // By default, when we are no longer watching for frame targets, we should no longer try to
+ // communicate with any browsing-context. But.
+ //
+ // For "browser-element" debugging, all targets are provided by watching by watching for frame targets.
+ // So, when we are no longer watching for frame, we don't expect to have any frame target to talk to.
+ // => we should no longer reach any browsing context.
+ //
+ // For "all" (=browser toolbox), there is only the special ParentProcessTargetActor we might want to return here.
+ // But this is actually handled by the WatcherActor which uses `WatcherActor.getTargetActorInParentProcess` to convey session data.
+ // => we should no longer reach any browsing context.
+ //
+ // For "webextension" debugging, there is the special WebExtensionTargetActor, which doesn't run in the parent process,
+ // so that we can't rely on the same code as the browser toolbox.
+ // => we should always reach out this particular browsing context.
+ if (watcher.sessionContext.type == "webextension") {
+ const browsingContext = BrowsingContext.get(
+ watcher.sessionContext.addonBrowsingContextID
+ );
+ // The add-on browsing context may be destroying, in which case we shouldn't try to communicate with it
+ if (browsingContext.currentWindowGlobal) {
+ return [browsingContext];
+ }
+ }
+ return [];
+}
+
+// Set to true to log info about about WindowGlobal's being watched.
+const DEBUG = false;
+
+function logWindowGlobal(windowGlobal, message) {
+ if (!DEBUG) {
+ return;
+ }
+
+ WindowGlobalLogger.logWindowGlobal(windowGlobal, message);
+}