Bug 1745240 - [devtools] Unify target helpers and JS Window actors to use a shared method to filter BrowsingContext/WindowGlobal's. r=nchevobbe,jdescottes

Popup debugging (bug 1569859) will force to revisit how we filter out the BrowsingContext
that are meant to be debugged. We won't only accept BrowsingContext based on their browserId.
This would force us to carefuly review all the codes where we filter BrowsingContexts.
And if we later have to tweak this, do this again.

It would be nice to have a unique method to filter things out.
It will also be beneficial once we add new debuggable contexts like workers
as we would only have to tweak this method.

For now, this patch focuses only on Target helpers and JSWindowActor's,
but I'll followup to other server modules.

Note that I'm changing the behavior of getAllRemoteBrowsingContexts
in order to also return the top browsing context by default.
We were having a few places where we were re-adding it after,
but that's not trivial. It is easier to remove it in the rare function that need that.

Differential Revision: https://phabricator.services.mozilla.com/D134422
This commit is contained in:
Alexandre Poirot 2022-01-10 17:42:09 +00:00
parent f64039933c
commit d2db0a255a
12 changed files with 399 additions and 383 deletions

View File

@ -15,8 +15,8 @@ const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
false
);
const {
getAllRemoteBrowsingContexts,
} = require("devtools/server/actors/watcher/target-helpers/utils.js");
getAllBrowsingContextsForContext,
} = require("devtools/server/actors/watcher/browsing-context-helpers.jsm");
const {
WILL_NAVIGATE_TIME_SHIFT,
} = require("devtools/server/actors/webconsole/listeners/document-events");
@ -54,9 +54,9 @@ class ParentProcessDocumentEventWatcher {
this._onceWillNavigate = new Map();
// Filter browsing contexts to only have the top BrowsingContext of each tree of BrowsingContexts…
const topLevelBrowsingContexts = this.getAllBrowsingContexts().filter(
browsingContext => browsingContext.top == browsingContext
);
const topLevelBrowsingContexts = getAllBrowsingContextsForContext(
this.watcherActor.sessionContext
).filter(browsingContext => browsingContext.top == browsingContext);
// Only register one WebProgressListener per BrowsingContext tree.
// We will be notified about children BrowsingContext navigations/state changes via the top level BrowsingContextWebProgressListener,
@ -79,29 +79,6 @@ class ParentProcessDocumentEventWatcher {
});
}
getAllBrowsingContexts() {
if (this.watcherActor.sessionContext.type == "browser-element") {
const browsingContext = this.watcherActor.browserElement.browsingContext;
return browsingContext.getAllBrowsingContextsInSubtree();
}
if (this.watcherActor.sessionContext.type == "all") {
return getAllRemoteBrowsingContexts();
}
if (this.watcherActor.sessionContext.type == "webextension") {
return getAllRemoteBrowsingContexts().filter(
bc =>
bc.currentWindowGlobal.documentPrincipal.addonId ==
this.watcherActor.sessionContext.addonId
);
}
throw new Error(
"Unsupported session context type=" +
this.watcherActor.sessionContext.type
);
}
/**
* Wait for the emission of will-navigate for a given WindowGlobal
*

View File

@ -14,6 +14,9 @@ const {
WatcherRegistry,
} = require("devtools/server/actors/watcher/WatcherRegistry.jsm");
const Targets = require("devtools/server/actors/targets/index");
const {
getAllBrowsingContextsForContext,
} = require("devtools/server/actors/watcher/browsing-context-helpers.jsm");
const TARGET_HELPERS = {};
loader.lazyRequireGetter(
@ -143,6 +146,10 @@ exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
return this._browserElement;
},
getAllBrowsingContexts(options) {
return getAllBrowsingContextsForContext(this.sessionContext, options);
},
/**
* Helper to know if the context we are debugging has been already destroyed
*/

View File

@ -0,0 +1,297 @@
/* 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 EXPORTED_SYMBOLS = [
"isBrowsingContextPartOfContext",
"isWindowGlobalPartOfContext",
"getAllBrowsingContextsForContext",
];
let Services;
if (typeof module == "object") {
// Allow this JSM to also be loaded as a CommonJS module
Services = require("Services");
} else {
({ Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"));
}
const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
"devtools.every-frame-target.enabled",
false
);
/**
* Helper function to know if a given BrowsingContext should be debugged by scope
* described by the given session context.
*
* @param {BrowsingContext} browsingContext
* The browsing context we want to check if it is part of debugged context
* @param {Object} sessionContext
* WatcherActor's session context. This helps know what is the overall debugged scope.
* See watcher actor constructor for more info.
* @param {Object} options
* Optional arguments passed via a dictionary.
* @param {Boolean} options.forceAcceptTopLevelTarget
* If true, we will accept top level browsing context even when server target switching
* is disabled. In case of client side target switching, the top browsing context
* is debugged via a target actor that is being instantiated manually by the frontend.
* And this target actor isn't created, nor managed by the watcher actor.
* @param {WindowGlobal} options.windowGlobal
* When we are in the content process, we can't easily retrieve the WindowGlobal
* for a given BrowsingContext. So allow to pass it via this argument.
* Also, there is some race conditions where browsingContext.currentWindowGlobal
* is null, while the callsite may have a reference to the WindowGlobal.
*/
function isBrowsingContextPartOfContext(
browsingContext,
sessionContext,
{ forceAcceptTopLevelTarget = false, windowGlobal } = {}
) {
// When we are in the parent process, we have some parent-process only checks,
// made possible thanks to the extra attributes available on CanonicalBrowsingContext interface.
if (browsingContext instanceof CanonicalBrowsingContext) {
windowGlobal = browsingContext.currentWindowGlobal;
// Loading or destroying BrowsingContext won't have any associated WindowGlobal.
// Ignore them. They should be either handled via DOMWindowCreated event or JSWindowActor destroy
if (!windowGlobal) {
return false;
}
// For now, reject debugging chrome BrowsingContext.
// This is for example top level chrome windows like browser.xhtml or webconsole/index.html (only the browser console)
//
// Tab and WebExtension debugging shouldn't target any such privileged document.
// All their document should be of type "content".
//
// This may only be an issue for the Browser Toolbox.
// For now, we expect the ParentProcessTargetActor to debug these.
// Note that we should probably revisit that, and have each WindowGlobal be debugged
// by one dedicated WindowGlobalTargetActor (bug 1685500). This requires some tweaks, at least in console-message
// resource watcher, which makes the ParentProcessTarget's console message resource watcher watch
// for all documents messages. It should probably only care about window-less messages and have one target per window global,
// each target fetching one window global messages.
//
// Such project would be about applying "EFT" to the browser toolbox and non-content documents
if (!browsingContext.isContent) {
return false;
}
} else if (!windowGlobal) {
throw new Error(
"isBrowsingContextPartOfContext expect a windowGlobal argument when called from the content process"
);
}
if (sessionContext.type == "all") {
// see browser-element jsdoc
if (!isEveryFrameTargetEnabled && !windowGlobal.isProcessRoot) {
return false;
}
return true;
}
if (sessionContext.type == "browser-element") {
if (browsingContext.browserId != sessionContext.browserId) {
return false;
}
// For client-side target switching, only mention the "remote frames".
// i.e. the frames which are in a distinct process compared to their parent document
// If there is no parent, this is most likely the top level document which we want to ignore.
//
// `forceAcceptTopLevelTarget` is set:
// * when navigating to and from pages in the bfcache, we ignore client side target
// and start emitting top level target from the server.
// * when the callsite care about all the debugged browsing contexts,
// no matter if their related targets are created by client or server.
const isClientSideTargetSwitching = !sessionContext.isServerTargetSwitchingEnabled;
const isTopLevelBrowsingContext = !browsingContext.parent;
if (
isClientSideTargetSwitching &&
!forceAcceptTopLevelTarget &&
isTopLevelBrowsingContext
) {
return false;
}
// We may process an iframe that runs in the same process as its parent and we don't want
// to create targets for them if same origin targets (=EFT) are not enabled.
// Instead the WindowGlobalTargetActor will inspect these children document via docShell tree
// (typically via `docShells` or `windows` getters).
// This is quite common when Fission is off as any iframe will run in same process
// as their parent document. But it can also happen with Fission enabled if iframes have
// children iframes using the same origin.
if (!isEveryFrameTargetEnabled && !windowGlobal.isProcessRoot) {
return false;
}
return true;
}
if (sessionContext.type == "webextension") {
// documentPrincipal is only exposed on WindowGlobalParent,
// use a fallback for WindowGlobalChild.
const principal =
windowGlobal.documentPrincipal ||
browsingContext.window.document.nodePrincipal;
return principal.addonId == sessionContext.addonId;
}
throw new Error("Unsupported session context type: " + sessionContext.type);
}
/**
* Helper function to know if a given WindowGlobal should be debugged by scope
* described by the given session context. This method could be called from any process
* as so accept either WindowGlobalParent or WindowGlobalChild instances.
*
* @param {WindowGlobalParent|WindowGlobalChild} windowGlobal
* The WindowGlobal we want to check if it is part of debugged context
* @param {Object} sessionContext
* WatcherActor's session context. This helps know what is the overall debugged scope.
* See watcher actor constructor for more info.
* @param {Object} options
* Optional arguments passed via a dictionary.
* See `isBrowsingContextPartOfContext` jsdoc.
*/
function isWindowGlobalPartOfContext(
windowGlobal,
sessionContext,
{ forceAcceptTopLevelTarget = false, acceptInitialDocument = false } = {}
) {
const window = Services.wm.getCurrentInnerWindowWithId(
windowGlobal.innerWindowId
);
// We sometimes process WindowGlobal's that run in another content process.
// (It is a bit surprising to have a JSWindowActorChild to be instantiated for
// WindowGlobals that aren't from this process??)
// And filtering them out is hard because:
// * `isInProcess` is always false, even if the window runs in the same process.
// It will only become true for pages in parent process. But the WindowGlobal
// may run in another content process.
// * `osPid` attribute is not set on WindowGlobalChild
//
// So it is hard to guess if the given WindowGlobal runs in this process or not,
// which is what we want to know here. Here is a workaround way to know it :/
if (Cu.isRemoteProxy(window)) {
return false;
}
// By default, before loading the actual document (even an about:blank document),
// we do load immediately "the initial about:blank document".
// This is expected by the spec. Typically when creating a new BrowsingContext/DocShell/iframe,
// we would have such transient initial document.
// `Document.isInitialDocument` helps identify this transient document, which
// we want to ignore as it would instantiate a very short lived target which
// confuses many tests and triggers race conditions by spamming many targets.
//
// We also ignore some other transient empty documents created while using `window.open()`
// When using this API with cross process loads, we may create up to three documents/WindowGlobals.
// We get a first initial about:blank document, and a second document created
// for moving the document in the right principal.
// The third document will be the actual document we expect to debug.
// The second document is an implementation artifact which ideally wouldn't exist
// and isn't expected by the spec.
// Note that `window.print` and print preview are using `window.open` and are going through this.
//
// WindowGlobalParent will have `isInitialDocument` attribute, while we have to go through the Document for WindowGlobalChild.
if (
(windowGlobal.isInitialDocument || window?.document.isInitialDocument) &&
!acceptInitialDocument
) {
return false;
}
return isBrowsingContextPartOfContext(
windowGlobal.browsingContext,
sessionContext,
{
forceAcceptTopLevelTarget,
windowGlobal,
}
);
}
/**
* Get all the BrowsingContexts that should be debugged by the given session context.
*
* Really all of them:
* - For all the privileged windows (browser.xhtml, browser console, ...)
* - For all chrome *and* content contexts (privileged windows, as well as <browser> elements and their inner content documents)
* - For all nested browsing context. We fetch the contexts recursively.
*
* @param {Object} sessionContext
* WatcherActor's session context. This helps know what is the overall debugged scope.
* See watcher actor constructor for more info.
*/
function getAllBrowsingContextsForContext(sessionContext) {
const browsingContexts = [];
// For a given BrowsingContext, add the `browsingContext`
// all of its children, that, recursively.
function walk(browsingContext) {
if (browsingContexts.includes(browsingContext)) {
return;
}
browsingContexts.push(browsingContext);
for (const child of browsingContext.children) {
walk(child);
}
if (
(sessionContext.type == "all" || sessionContext.type == "webextension") &&
browsingContext.window
) {
// If the document is in the parent process, also iterate over each <browser>'s browsing context.
// BrowsingContext.children doesn't cross chrome to content boundaries,
// so we have to cross these boundaries by ourself.
// (This is also the reason why we aren't using BrowsingContext.getAllBrowsingContextsInSubtree())
for (const browser of browsingContext.window.document.querySelectorAll(
`browser[remote="true"]`
)) {
walk(browser.browsingContext);
}
}
}
// If target a single browser element, only walk through its BrowsingContext
if (sessionContext.type == "browser-element") {
const topBrowsingContext = BrowsingContext.getCurrentTopByBrowserId(
sessionContext.browserId
);
// Unfortunately, getCurrentTopByBrowserId is subject to race conditions and may refer to a BrowsingContext
// that already navigated away.
// Query the current "live" BrowsingContext by going through the embedder element (i.e. the <browser>/<iframe> element)
// devtools/client/responsive/test/browser/browser_navigation.js covers this with fission enabled.
const realTopBrowsingContext =
topBrowsingContext.embedderElement.browsingContext;
walk(realTopBrowsingContext);
} else if (
sessionContext.type == "all" ||
sessionContext.type == "webextension"
) {
// For the browser toolbox and web extension, retrieve all possible BrowsingContext.
// For WebExtension, we will then filter out the BrowsingContexts via `isBrowsingContextPartOfContext`.
//
// Fetch all top level window's browsing contexts
for (const window of Services.ww.getWindowEnumerator()) {
if (window.docShell.browsingContext) {
walk(window.docShell.browsingContext);
}
}
} else {
throw new Error("Unsupported session context type: " + sessionContext.type);
}
return browsingContexts.filter(bc =>
// We force accepting the top level browsing context, otherwise
// it would only be returned if sessionContext.isServerSideTargetSwitching is enabled.
isBrowsingContextPartOfContext(bc, sessionContext, {
forceAcceptTopLevelTarget: true,
})
);
}
if (typeof module == "object") {
module.exports = {
isBrowsingContextPartOfContext,
isWindowGlobalPartOfContext,
getAllBrowsingContextsForContext,
};
}

View File

@ -9,6 +9,7 @@ DIRS += [
]
DevToolsModules(
"browsing-context-helpers.jsm",
"SessionDataHelpers.jsm",
"WatcherRegistry.jsm",
)

View File

@ -12,18 +12,9 @@ const {
WindowGlobalLogger,
} = require("devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm");
const Targets = require("devtools/server/actors/targets/index");
const {
getAllRemoteBrowsingContexts,
shouldNotifyWindowGlobal,
} = require("devtools/server/actors/watcher/target-helpers/utils.js");
const browsingContextAttachedObserverByWatcher = new Map();
const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
"devtools.every-frame-target.enabled",
false
);
/**
* Force creating targets for all existing BrowsingContext, that, for a given Watcher Actor.
*
@ -90,8 +81,10 @@ async function createTargets(watcher) {
});
}
const browsingContexts = getFilteredRemoteBrowsingContext(
watcher.browserElement
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.
@ -168,17 +161,7 @@ async function createTargetForBrowsingContext({
*/
function destroyTargets(watcher) {
// Go over all existing BrowsingContext in order to destroy all targets
const browsingContexts = getFilteredRemoteBrowsingContext(
watcher.browserElement
);
if (
watcher.sessionContext.isServerTargetSwitchingEnabled &&
watcher.sessionContext.type == "browser-element"
) {
// If server side target switching is enabled, we should also destroy the top level browsing context.
// If it is disabled, the top level target will be destroyed from the client instead.
browsingContexts.push(watcher.browserElement.browsingContext);
}
const browsingContexts = watcher.getAllBrowsingContexts();
for (const browsingContext of browsingContexts) {
logWindowGlobal(
@ -277,7 +260,7 @@ module.exports = {
/**
* Return the list of BrowsingContexts which should be targeted in order to communicate
* a new list of resource types to listen or stop listening to.
* updated session data.
*
* @param WatcherActor watcher
* The watcher actor will be used to know which target we debug
@ -290,57 +273,33 @@ function getWatchingBrowsingContexts(watcher) {
watcher,
Targets.TYPES.FRAME
);
const { browserElement } = watcher;
const browsingContexts = watchingAdditionalTargets
? getFilteredRemoteBrowsingContext(browserElement)
: [];
// Even if we aren't watching additional target, we want to process the top level target.
// The top level target isn't returned by getFilteredRemoteBrowsingContext, so add it in both cases.
if (
watcher.sessionContext.type == "browser-element" ||
watcher.sessionContext.type == "webextension"
) {
let topBrowsingContext;
if (watcher.sessionContext.type == "browser-element") {
topBrowsingContext = watcher.browserElement.browsingContext;
} else if (watcher.sessionContext.type == "webextension") {
topBrowsingContext = BrowsingContext.get(
watcher.sessionContext.addonBrowsingContextID
);
}
// Ignore if we are against a page running in the parent process,
// which would not support JSWindowActor API
// XXX May be we should toggle `includeChrome` and ensure watch/unwatch works
// with such page?
//
// Also ignore BrowsingContext which are being destroyed or navigating.
if (
topBrowsingContext.currentWindowGlobal &&
topBrowsingContext.currentWindowGlobal.osPid != -1
) {
browsingContexts.push(topBrowsingContext);
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 browsingContexts;
}
/**
* Get the list of all BrowsingContext we should interact with.
* The precise condition of which BrowsingContext we should interact with are defined
* in `shouldNotifyWindowGlobal`
*
* @param BrowserElement browserElement (optional)
* If defined, this will restrict to only the Browsing Context matching this
* Browser Element and any of its (nested) children iframes.
*/
function getFilteredRemoteBrowsingContext(browserElement) {
return getAllRemoteBrowsingContexts(browserElement?.browsingContext).filter(
browsingContext =>
shouldNotifyWindowGlobal(browsingContext, browserElement?.browserId, {
acceptNonRemoteFrame: isEveryFrameTargetEnabled,
})
);
return [];
}
// Set to true to log info about about WindowGlobal's being watched.

View File

@ -7,6 +7,5 @@
DevToolsModules(
"frame-helper.js",
"process-helper.js",
"utils.js",
"worker-helper.js",
)

View File

@ -1,126 +0,0 @@
/* 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 Services = require("Services");
/**
* Helper function to know if a given WindowGlobal should be exposed via watchTargets API
* XXX: We probably want to share this function with DevToolsFrameChild,
* but may be not, it looks like the checks are really differents because WindowGlobalParent and WindowGlobalChild
* expose very different attributes. (WindowGlobalChild exposes much less!)
*
* @param {BrowsingContext} browsingContext: The browsing context we want to check the window global for
* @param {String} watchedBrowserId
* @param {Object} options
* @param {Boolean} options.acceptNonRemoteFrame: Set to true to not restrict to remote frame only
*/
function shouldNotifyWindowGlobal(
browsingContext,
watchedBrowserId,
options = {}
) {
const windowGlobal = browsingContext.currentWindowGlobal;
// Loading or destroying BrowsingContext won't have any associated WindowGlobal.
// Ignore them. They should be either handled via DOMWindowCreated event or JSWindowActor destroy
if (!windowGlobal) {
return false;
}
// Only accept WindowGlobal running in parent, but only for tab debugging.
//
// When we are in the browser toolbox, we don't want to create targets
// for all parent process document just yet.
// The very final check done in this function, `windowGlobal.isProcessRoot` will be true
// and we would start creating a target for all top level browser windows.
// For now, we expect the ParentProcessTargetActor to debug these.
// Note that we should probably revisit that, and have each WindowGlobal be debugged
// by one dedicated WindowGlobalTargetActor (bug 1685500). This requires some tweaks, at least in console-message
// resource watcher, which makes the ParentProcessTarget's console message resource watcher watch
// for all documents messages. It should probably only care about window-less messages and have one target per window global,
// each target fetching one window global messages.
const isParentProcessWindowGlobal =
windowGlobal.osPid == -1 && windowGlobal.isInProcess;
const isTabDebugging = !!watchedBrowserId;
if (isParentProcessWindowGlobal && !isTabDebugging) {
return false;
}
if (watchedBrowserId && browsingContext.browserId != watchedBrowserId) {
return false;
}
if (options.acceptNonRemoteFrame) {
return true;
}
// If `acceptNonRemoteFrame` options isn't true, only mention the "remote frames".
// i.e. the documents which have no parent, or are in a distinct process compared to their parent
return windowGlobal.isProcessRoot;
}
/**
* Get all the BrowsingContexts.
*
* Really all of them:
* - For all the privileged windows (browser.xhtml, browser console, ...)
* - For all chrome *and* content contexts (privileged windows, as well as <browser> elements and their inner content documents)
* - For all nested browsing context. We fetch the contexts recursively.
*
* @param BrowsingContext topBrowsingContext (optional)
* If defined, this will restrict to this Browsing Context only
* and any of its (nested) children.
*/
function getAllRemoteBrowsingContexts(topBrowsingContext) {
const browsingContexts = [];
// For a given BrowsingContext, add the `browsingContext`
// all of its children, that, recursively.
function walk(browsingContext) {
if (browsingContexts.includes(browsingContext)) {
return;
}
browsingContexts.push(browsingContext);
for (const child of browsingContext.children) {
walk(child);
}
if (browsingContext.window) {
// If the document is in the parent process, also iterate over each <browser>'s browsing context.
// BrowsingContext.children doesn't cross chrome to content boundaries,
// so we have to cross these boundaries by ourself.
for (const browser of browsingContext.window.document.querySelectorAll(
`browser[remote="true"]`
)) {
walk(browser.browsingContext);
}
}
}
// If a Browsing Context is passed, only walk through the given BrowsingContext
if (topBrowsingContext) {
walk(topBrowsingContext);
// Remove the top level browsing context we just added by calling walk()
// We expect to return only the remote iframe BrowserContext from this method,
// not the top level one.
browsingContexts.shift();
} else {
// Fetch all top level window's browsing contexts
// Note that getWindowEnumerator works from all processes, including the content process.
for (const window of Services.ww.getWindowEnumerator()) {
if (window.docShell.browsingContext) {
walk(window.docShell.browsingContext);
}
}
}
return browsingContexts;
}
module.exports = {
getAllRemoteBrowsingContexts,
shouldNotifyWindowGlobal,
};

View File

@ -4,11 +4,6 @@
"use strict";
const {
getAllRemoteBrowsingContexts,
shouldNotifyWindowGlobal,
} = require("devtools/server/actors/watcher/target-helpers/utils.js");
const DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME = "DevToolsWorker";
/**
@ -21,7 +16,10 @@ async function createTargets(watcher) {
// Go over all existing BrowsingContext in order to:
// - Force the instantiation of a DevToolsWorkerChild
// - Have the DevToolsWorkerChild to spawn the WorkerTargetActors
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const browsingContexts = watcher.getAllBrowsingContexts({
acceptSameProcessIframes: true,
forceAcceptTopLevelTarget: true,
});
const promises = [];
for (const browsingContext of browsingContexts) {
const promise = browsingContext.currentWindowGlobal
@ -48,7 +46,10 @@ async function createTargets(watcher) {
*/
async function destroyTargets(watcher) {
// Go over all existing BrowsingContext in order to destroy all targets
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const browsingContexts = watcher.getAllBrowsingContexts({
acceptSameProcessIframes: true,
forceAcceptTopLevelTarget: true,
});
for (const browsingContext of browsingContexts) {
let windowActor;
try {
@ -77,7 +78,10 @@ async function destroyTargets(watcher) {
* The values to be added to this type of data
*/
async function addSessionDataEntry({ watcher, type, entries }) {
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const browsingContexts = watcher.getAllBrowsingContexts({
acceptSameProcessIframes: true,
forceAcceptTopLevelTarget: true,
});
const promises = [];
for (const browsingContext of browsingContexts) {
const promise = browsingContext.currentWindowGlobal
@ -100,7 +104,10 @@ async function addSessionDataEntry({ watcher, type, entries }) {
* See addSessionDataEntry for argument documentation.
*/
function removeSessionDataEntry({ watcher, type, entries }) {
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const browsingContexts = watcher.getAllBrowsingContexts({
acceptSameProcessIframes: true,
forceAcceptTopLevelTarget: true,
});
for (const browsingContext of browsingContexts) {
browsingContext.currentWindowGlobal
.getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
@ -113,29 +120,6 @@ function removeSessionDataEntry({ watcher, type, entries }) {
}
}
/**
* Get the list of all BrowsingContext we should interact with.
* The precise condition of which BrowsingContext we should interact with are defined
* in `shouldNotifyWindowGlobal`
*
* @param BrowserElement browserElement (optional)
* If defined, this will restrict to only the Browsing Context matching this
* Browser Element and any of its (nested) children iframes.
*/
function getFilteredBrowsingContext(browserElement) {
const browsingContexts = getAllRemoteBrowsingContexts(
browserElement?.browsingContext
);
if (browserElement?.browsingContext) {
browsingContexts.push(browserElement?.browsingContext);
}
return browsingContexts.filter(browsingContext =>
shouldNotifyWindowGlobal(browsingContext, browserElement?.browserId, {
acceptNonRemoteFrame: true,
})
);
}
module.exports = {
createTargets,
destroyTargets,

View File

@ -17,11 +17,10 @@ const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
isWindowGlobalPartOfContext:
"resource://devtools/server/actors/watcher/browsing-context-helpers.jsm",
TargetActorRegistry:
"resource://devtools/server/actors/targets/target-actor-registry.jsm",
});
XPCOMUtils.defineLazyModuleGetters(this, {
WindowGlobalLogger:
"resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.jsm",
});
@ -34,80 +33,6 @@ const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
// Name of the attribute into which we save data in `sharedData` object.
const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
/**
* Helper function to know if a given WindowGlobal should be exposed via watchTargets("frame") API
*/
function shouldNotifyWindowGlobal(
windowGlobal,
sessionContext,
{ acceptTopLevelTarget = false }
) {
const browsingContext = windowGlobal.browsingContext;
// Ignore about:blank loads, which spawn a document that never finishes loading
// and would require somewhat useless Target and all its related overload.
const window = Services.wm.getCurrentInnerWindowWithId(
windowGlobal.innerWindowId
);
// By default, before loading the actual document (even an about:blank document),
// we do load immediately "the initial about:blank document".
// This is expected by the spec. Typically when creating a new BrowsingContext/DocShell/iframe,
// we would have such transient initial document.
// `Document.isInitialDocument` helps identifying this transcient document, which
// we want to ignore as it would instantiate a very short lived target which
// confuses many tests and triggers race conditions by spamming many targets.
//
// We also ignore some other transient empty documents created while using `window.open()`
// When using this API with cross process loads, we may create up to three documents/WindowGlobals.
// We get a first initial about:blank document, and a second document created
// for moving the document in the right principal.
// The third document will be the actual document we expect to debug.
// The second document is an implementation artifact which ideally wouldn't exist
// and isn't expected by the spec.
// Note that `window.print` and print preview are using `window.open` and are going throught this.
if (window.document.isInitialDocument) {
return false;
}
// If we are focusing only on a sub-tree of Browsing Element,
// Ignore the out of the sub tree elements.
if (
sessionContext.type == "browser-element" &&
browsingContext.browserId != sessionContext.browserId
) {
return false;
}
// For client-side target switching, only mention the "remote frames".
// i.e. the frames which are in a distinct process compared to their parent document
// If there is no parent, this is most likely the top level document.
//
// Ignore this check for the browser toolbox, as tab's BrowsingContext have no
// parent and aren't the top level target.
//
// `acceptTopLevelTarget` is set both when server side target switching is enabled
// or when navigating to and from pages in the bfcache
if (
!acceptTopLevelTarget &&
sessionContext.type == "browser-element" &&
!browsingContext.parent
) {
return false;
}
// We may process an iframe that runs in the same process as its parent and we don't want
// to create targets for them if same origin targets are not enabled. Instead the WindowGlobalTargetActor
// will inspect these children document via docShell tree (typically via `docShells` or `windows` getters).
// This is quite common when Fission is off as any iframe will run in same process
// as their parent document. But it can also happen with Fission enabled if iframes have
// children iframes using the same origin.
if (!isEveryFrameTargetEnabled && !windowGlobal.isProcessRoot) {
return false;
}
return true;
}
// If true, log info about WindowGlobal's being created.
const DEBUG = false;
function logWindowGlobal(windowGlobal, message) {
@ -178,18 +103,15 @@ class DevToolsFrameChild extends JSWindowActorChild {
// Create one Target actor for each prefix/client which listen to frames
for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
const { connectionPrefix, sessionContext } = sessionData;
// Always create new targets when server targets are enabled as we create targets for all the WindowGlobal's,
// including all WindowGlobal's of the top target.
// 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 acceptTopLevelTarget =
sessionContext.isServerTargetSwitchingEnabled ||
(isBFCache && this.isBfcacheInParentEnabled);
const forceAcceptTopLevelTarget =
isBFCache && this.isBfcacheInParentEnabled;
if (
sessionData.targets.includes("frame") &&
shouldNotifyWindowGlobal(this.manager, sessionContext, {
acceptTopLevelTarget,
isWindowGlobalPartOfContext(this.manager, sessionContext, {
forceAcceptTopLevelTarget,
})
) {
// If this was triggered because of a navigation, we want to retrieve the existing
@ -483,8 +405,8 @@ class DevToolsFrameChild extends JSWindowActorChild {
}
receiveMessage(message) {
// When debugging only a given tab, all messages but "packet" one pass `browserId`
// and are expected to match shouldNotifyWindowGlobal result.
// 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"
@ -494,14 +416,18 @@ class DevToolsFrameChild extends JSWindowActorChild {
// on what should or should not be watched.
if (
this.manager.browsingContext.browserId != browserId &&
!shouldNotifyWindowGlobal(this.manager, browserId, {
acceptTopLevelTarget: true,
})
!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 (shouldNotifyWindowGlobal mismatch)"
? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
: `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`)
);
}

View File

@ -29,6 +29,8 @@ XPCOMUtils.defineLazyGetter(this, "DevToolsUtils", () =>
Loader.require("devtools/shared/DevToolsUtils")
);
XPCOMUtils.defineLazyModuleGetters(this, {
isWindowGlobalPartOfContext:
"resource://devtools/server/actors/watcher/browsing-context-helpers.jsm",
SessionDataHelpers:
"resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
});
@ -122,7 +124,11 @@ class DevToolsWorkerChild extends JSWindowActorChild {
const { targets, connectionPrefix, sessionContext } = sessionData;
if (
targets.includes("worker") &&
shouldNotifyWindowGlobal(this.manager, sessionContext)
isWindowGlobalPartOfContext(this.manager, sessionContext, {
acceptInitialDocument: true,
forceAcceptTopLevelTarget: true,
acceptSameProcessIframes: true,
})
) {
this._watchWorkerTargets({
watcherActorID,
@ -142,19 +148,25 @@ class DevToolsWorkerChild extends JSWindowActorChild {
*/
receiveMessage(message) {
// All messages pass `sessionContext` (except packet) and are expected
// to match shouldNotifyWindowGlobal result.
// to match isWindowGlobalPartOfContext result.
if (message.name != "DevToolsWorkerParent:packet") {
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 &&
!shouldNotifyWindowGlobal(this.manager, message.data.sessionContext)
!isWindowGlobalPartOfContext(
this.manager,
message.data.sessionContext,
{
acceptInitialDocument: true,
}
)
) {
throw new Error(
"Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
(this.manager.browsingContext.browserId == browserId
? "window global shouldn't be notified (shouldNotifyWindowGlobal mismatch)"
? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)"
: `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
);
}
@ -524,35 +536,6 @@ class DevToolsWorkerChild extends JSWindowActorChild {
}
}
/**
* Helper function to know if we should watch for workers on a given windowGlobal
*/
function shouldNotifyWindowGlobal(windowGlobal, sessionContext) {
const browsingContext = windowGlobal.browsingContext;
// If we are focusing only on a sub-tree of Browsing Element, ignore elements that are
// not part of it.
if (
sessionContext.type == "browser-element" &&
browsingContext.browserId != sessionContext.browserId
) {
return false;
}
// `isInProcess` is always false, even if the window runs in the same process.
// `osPid` attribute is not set on WindowGlobalChild
// so it is hard to guess if the given WindowGlobal runs in this process or not,
// which is what we want to know here. Here is a workaround way to know it :/
// ---
// Also. It might be a bit surprising to have a DevToolsWorkerChild/JSWindowActorChild
// to be instantiated for WindowGlobals that aren't from this process... Is that expected?
if (Cu.isRemoteProxy(windowGlobal.window)) {
return false;
}
return true;
}
/**
* Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
*

View File

@ -44,6 +44,10 @@ const WindowGlobalLogger = {
const { browsingContext } = windowGlobal;
const { parent } = browsingContext;
const windowGlobalUri = getWindowGlobalUri(windowGlobal);
const isInitialDocument =
"isInitialDocument" in windowGlobal
? windowGlobal.isInitialDocument
: windowGlobal.browsingContext.window?.document.isInitialDocument;
const details = [];
details.push(
@ -54,10 +58,13 @@ const WindowGlobalLogger = {
"isClosed: " + windowGlobal.isClosed,
"isInProcess: " + windowGlobal.isInProcess,
"isCurrentGlobal: " + windowGlobal.isCurrentGlobal,
"isProcessRoot: " + windowGlobal.isProcessRoot,
"currentRemoteType: " + browsingContext.currentRemoteType,
"hasParent: " + (parent ? parent.id : "no"),
"uri: " + (windowGlobalUri ? windowGlobalUri : "no uri"),
"isProcessRoot: " + windowGlobal.isProcessRoot
"isProcessRoot: " + windowGlobal.isProcessRoot,
"BrowsingContext.isContent: " + windowGlobal.browsingContext.isContent,
"isInitialDocument: " + isInitialDocument
);
const header = "[WindowGlobalLogger] " + message;

View File

@ -21,6 +21,7 @@ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
// Steal various globals only available in JSM scope (and not Sandbox one)
const {
CanonicalBrowsingContext,
BrowsingContext,
WindowGlobalParent,
WindowGlobalChild,
@ -254,6 +255,7 @@ exports.globals = {
atob,
Blob,
btoa,
CanonicalBrowsingContext,
BrowsingContext,
WindowGlobalParent,
WindowGlobalChild,