Bug 1593937 - Implement watchTargets for remote frames. r=jdescottes

* Introduce a new actor "Watcher", which might in the future allow listening to anything.
Here, it only implements listening for additional remote frame's WindowGlobal
and notify about the WindowGlobalTargetActor's.
* Also, very important part here is that it instantiates the BrowsingContextTargetActor much earlier. Before anything from the page is executed.
It requires to instantiate the actor directly from the content processes, whereas before, we were doing it from a frontend request, after a notification sent from the parent process to the frontend.
* Last but not least, make the TargetList use this new watcher actor in order to
notify the frontend about the dynamically added remote frames.

Differential Revision: https://phabricator.services.mozilla.com/D63317
This commit is contained in:
Alexandre Poirot 2020-04-30 07:59:30 +00:00
Родитель 734e59b63d
Коммит 879475cfd3
24 изменённых файлов: 1105 добавлений и 114 удалений

Просмотреть файл

@ -18,16 +18,6 @@ add_task(async function() {
// Forces the Browser Toolbox to open on the inspector by default
await pushPref("devtools.browsertoolbox.panel", "inspector");
// Open the tab *before* opening the Browser Toolbox in order to already have the document
// loaded before it starts iterating over additional frame targets.
// Bug 1593937 should make it optional and be able to care about dynamically added targets.
const tab = await addTab(
`data:text/html,<div id="my-div" style="color: red">Foo</div><div id="second-div" style="color: blue">Foo</div>`
);
// Set a custom attribute on the tab's browser, in order to easily select it in the markup view
tab.linkedBrowser.setAttribute("test-tab", "true");
const ToolboxTask = await initBrowserToolboxTask({
enableBrowserToolboxFission: true,
});
@ -35,6 +25,14 @@ add_task(async function() {
selectNodeFront,
});
// Open the tab *after* opening the Browser Toolbox in order to force creating the remote frames
// late and exercise frame target watching code.
const tab = await addTab(
`data:text/html,<div id="my-div" style="color: red">Foo</div><div id="second-div" style="color: blue">Foo</div>`
);
// Set a custom attribute on the tab's browser, in order to easily select it in the markup view
tab.linkedBrowser.setAttribute("test-tab", "true");
const color = await ToolboxTask.spawn(null, async () => {
/* global gToolbox */
const inspector = gToolbox.getPanel("inspector");

Просмотреть файл

@ -93,6 +93,15 @@ class FrameDescriptorFront extends FrontClassWithSpec(frameDescriptorSpec) {
return parentDescriptor.getTarget();
}
getCachedWatcher() {
for (const child of this.poolChildren()) {
if (child.typeName == "watcher") {
return child;
}
}
return null;
}
destroy() {
this._frameTargetFront = null;
this._targetFrontPromise = null;

Просмотреть файл

@ -29,6 +29,7 @@ class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
form(json) {
this.id = json.id;
this.isParent = json.isParent;
this.traits = json.traits || {};
}
async _createProcessTargetFront(form) {
@ -92,6 +93,15 @@ class ProcessDescriptorFront extends FrontClassWithSpec(processDescriptorSpec) {
return this._targetFrontPromise;
}
getCachedWatcher() {
for (const child of this.poolChildren()) {
if (child.typeName == "watcher") {
return child;
}
}
return null;
}
destroy() {
if (this._processTargetFront) {
this._processTargetFront.destroy();

Просмотреть файл

@ -143,6 +143,15 @@ class TabDescriptorFront extends FrontClassWithSpec(tabDescriptorSpec) {
})();
return this._targetFrontPromise;
}
getCachedWatcher() {
for (const child of this.poolChildren()) {
if (child.typeName == "watcher") {
return child;
}
}
return null;
}
}
exports.TabDescriptorFront = TabDescriptorFront;

Просмотреть файл

@ -47,6 +47,7 @@ DevToolsModules(
'symbol-iterator.js',
'thread.js',
'walker.js',
'watcher.js',
'webconsole.js',
'websocket.js'
)

Просмотреть файл

@ -0,0 +1,49 @@
/* 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 { watcherSpec } = require("devtools/shared/specs/watcher");
const {
FrontClassWithSpec,
registerFront,
} = require("devtools/shared/protocol");
loader.lazyRequireGetter(
this,
"BrowsingContextTargetFront",
"devtools/client/fronts/targets/browsing-context",
true
);
class WatcherFront extends FrontClassWithSpec(watcherSpec) {
constructor(client, targetFront, parentFront) {
super(client, targetFront, parentFront);
this._onTargetAvailable = this._onTargetAvailable.bind(this);
this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
// Convert form, which is just JSON object to Fronts for these two events
this.on("target-available-form", this._onTargetAvailable);
this.on("target-destroyed-form", this._onTargetDestroyed);
}
form(json) {
this.actorID = json.actor;
this.traits = json.traits;
}
_onTargetAvailable(form) {
const front = new BrowsingContextTargetFront(this.conn, null, this);
front.actorID = form.actor;
front.form(form);
this.manage(front);
this.emit("target-available", front);
}
_onTargetDestroyed(form) {
const front = this.actor(form.actor);
this.emit("target-destroyed", front);
}
}
registerFront(WatcherFront);

Просмотреть файл

@ -25,7 +25,7 @@ loader.lazyRequireGetter(
const FrameDescriptorActor = ActorClassWithSpec(frameDescriptorSpec, {
initialize(connection, browsingContext) {
if (typeof browsingContext.id != "number") {
throw Error("Frame Descriptor Connect requires a valid browsingContext.");
throw Error("Frame Descriptor requires a valid BrowsingContext.");
}
Actor.prototype.initialize.call(this, connection);
this.destroy = this.destroy.bind(this);

Просмотреть файл

@ -4,6 +4,10 @@
# 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/.
DIRS += [
'watcher',
]
DevToolsModules(
'frame.js',
'process.js',

Просмотреть файл

@ -31,6 +31,12 @@ loader.lazyRequireGetter(
"devtools/server/connectors/content-process-connector",
true
);
loader.lazyRequireGetter(
this,
"WatcherActor",
"devtools/server/actors/descriptors/watcher/watcher",
true
);
const ProcessDescriptorActor = ActorClassWithSpec(processDescriptorSpec, {
initialize(connection, options = {}) {
@ -129,11 +135,28 @@ const ProcessDescriptorActor = ActorClassWithSpec(processDescriptorSpec, {
return this._childProcessConnect();
},
/**
* Return a Watcher actor, allowing to keep track of targets which
* already exists or will be created. It also helps knowing when they
* are destroyed.
*/
getWatcher() {
if (!this.watcher) {
this.watcher = new WatcherActor(this.conn);
this.manage(this.watcher);
}
return this.watcher;
},
form() {
return {
actor: this.actorID,
id: this.id,
isParent: this.isParent,
traits: {
// FF77+ supports the Watcher actor
watcher: true,
},
};
},

Просмотреть файл

@ -25,6 +25,13 @@ const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
const { tabDescriptorSpec } = require("devtools/shared/specs/descriptors/tab");
const { AppConstants } = require("resource://gre/modules/AppConstants.jsm");
loader.lazyRequireGetter(
this,
"WatcherActor",
"devtools/server/actors/descriptors/watcher/watcher",
true
);
/**
* Creates a target actor proxy for handling requests to a single browser frame.
* Both <xul:browser> and <iframe mozbrowser> are supported.
@ -61,6 +68,8 @@ const TabDescriptorActor = ActorClassWithSpec(tabDescriptorSpec, {
// This trait indicates that meta data such as title, url and
// outerWindowID are directly available on the TabDescriptor.
hasTabInfo: true,
// FF77+ supports the Watcher actor
watcher: true,
},
url: this._getUrl(),
};
@ -144,6 +153,19 @@ const TabDescriptorActor = ActorClassWithSpec(tabDescriptorSpec, {
});
},
/**
* Return a Watcher actor, allowing to keep track of targets which
* already exists or will be created. It also helps knowing when they
* are destroyed.
*/
getWatcher() {
if (!this.watcher) {
this.watcher = new WatcherActor(this.conn, { browser: this._browser });
this.manage(this.watcher);
}
return this.watcher;
},
get _tabbrowser() {
if (this._browser && typeof this._browser.getTabBrowser == "function") {
return this._browser.getTabBrowser();

Просмотреть файл

@ -0,0 +1,246 @@
/* 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";
var EXPORTED_SYMBOLS = ["registerWatcher", "unregisterWatcher", "getWatcher"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(
this,
"ActorManagerParent",
"resource://gre/modules/ActorManagerParent.jsm"
);
// We record a Map of all available FrameWatchers, indexed by their Connection's prefix string.
// This helps notifying about the target actor, created from the content processes
// via the other JS Window actor pair, i.e. DevToolsFrameChild.
// DevToolsFrameParent will receive the target actor, and be able to retrieve
// the right watcher from the connection's prefix.
// Map of [DevToolsServerConnection's prefix => FrameWatcher]
const watchers = new Map();
/**
* Register a new WatcherActor.
*
* This will save a reference to it in the global `watchers` Map and allow
* DevToolsFrameParent to use getWatcher method in order to retrieve watcher
* for the same connection prefix.
*
* @param WatcherActor watcher
* A watcher actor to register
* @param Number watchedBrowsingContextID (optional)
* If the watched is specific to one precise Browsing Context, pass its ID.
* If not pass, this will go through all Browsing Contexts:
* All top level windows and alls its inner frames,
* including <browser> elements and any inner frames of them.
*/
async function registerWatcher(watcher, watchedBrowsingContextID) {
const prefix = watcher.conn.prefix;
if (watchers.has(prefix)) {
throw new Error(
`A watcher has already been registered via prefix ${prefix}.`
);
}
watchers.set(prefix, watcher);
if (watchers.size == 1) {
// Register the JSWindowActor pair "DevToolsFrame" only once we register our first WindowGlobal Watcher
ActorManagerParent.addActors({
DevToolsFrame: {
parent: {
moduleURI:
"resource://devtools/server/connectors/js-window-actor/DevToolsFrameParent.jsm",
},
child: {
moduleURI:
"resource://devtools/server/connectors/js-window-actor/DevToolsFrameChild.jsm",
events: {
DOMWindowCreated: {},
},
},
allFrames: true,
},
});
// Force the immediate activation of this JSWindow Actor
// so that just a few lines after, `currentWindowGlobal.getActor("DevToolsFrame")` do work.
// (Note that I had to spin the event loop between flush and getActor in order to make it work while prototyping...)
ActorManagerParent.flush();
}
// Go over all existing BrowsingContext in order to:
// - Force the instantiation of a DevToolsFrameChild
// - Have the DevToolsFrameChild to spawn the BrowsingContextTargetActor
const browsingContexts = getAllRemoteBrowsingContexts(
watchedBrowsingContextID
);
for (const browsingContext of browsingContexts) {
if (
!shouldNotifyWindowGlobal(
browsingContext.currentWindowGlobal,
watchedBrowsingContextID
)
) {
continue;
}
logWindowGlobal(
browsingContext.currentWindowGlobal,
"Existing WindowGlobal"
);
// Await for the query in order to try to resolve only *after* we received these
// already available targets.
await browsingContext.currentWindowGlobal
.getActor("DevToolsFrame")
.instantiateTarget({
prefix,
browsingContextID: watchedBrowsingContextID,
});
}
}
function unregisterWatcher(watcher) {
watchers.delete(watcher.conn.prefix);
if (watchers.size == 0) {
// ActorManagerParent doesn't expose a "removeActors" method, but it would be equivalent to that:
ChromeUtils.unregisterWindowActor("DevToolsFrame");
}
}
function getWatcher(parentConnectionPrefix) {
return watchers.get(parentConnectionPrefix);
}
/**
* 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 Number browsingContextID (optional)
* If defined, this will restrict to only the Browsing Context matching this ID
* and any of its (nested) children.
*/
function getAllRemoteBrowsingContexts(browsingContextID) {
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 browsingContextID is passed, only walk through the given BrowsingContext
if (browsingContextID) {
walk(BrowsingContext.get(browsingContextID));
// Remove the top level browsing context we just added by calling walk.
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;
}
/**
* Helper function to know if a given WindowGlobal should be exposed via watchTargets(window-global) 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!)
*/
function shouldNotifyWindowGlobal(windowGlobal, watchedBrowsingContextID) {
const browsingContext = windowGlobal.browsingContext;
// Ignore extension for now as attaching to them is special.
if (browsingContext.currentRemoteType == "extension") {
return false;
}
// Ignore globals running in the parent process for now as they won't be in a distinct process anyway.
// And JSWindowActor will most likely only be created if we toggle includeChrome
// on the JSWindowActor registration.
if (windowGlobal.osPid == -1 && windowGlobal.isInProcess) {
return false;
}
// Ignore about:blank which are quickly replaced and destroyed by the final URI
// bug 1625026 aims at removing this workaround and allow debugging any about:blank load
if (
windowGlobal.documentURI &&
windowGlobal.documentURI.spec == "about:blank"
) {
return false;
}
if (
watchedBrowsingContextID &&
browsingContext.top.id != watchedBrowsingContextID
) {
return false;
}
// For now, we only mention the "remote frames".
// i.e. the frames which are in a distinct process compared to their parent document
return (
!browsingContext.parent ||
// In content process, `currentWindowGlobal` doesn't exists
windowGlobal.osPid !=
(
browsingContext.parent.currentWindowGlobal ||
browsingContext.parent.window.windowGlobalChild
).osPid
);
}
// If true, log info about WindowGlobal's being watched.
const DEBUG = false;
function logWindowGlobal(windowGlobal, message) {
if (!DEBUG) {
return;
}
const browsingContext = windowGlobal.browsingContext;
dump(
message +
" | BrowsingContext.id: " +
browsingContext.id +
" Inner Window ID: " +
windowGlobal.innerWindowId +
" pid:" +
windowGlobal.osPid +
" isClosed:" +
windowGlobal.isClosed +
" isInProcess:" +
windowGlobal.isInProcess +
" isCurrentGlobal:" +
windowGlobal.isCurrentGlobal +
" currentRemoteType:" +
browsingContext.currentRemoteType +
" => " +
(windowGlobal.documentURI ? windowGlobal.documentURI.spec : "no-uri") +
"\n"
);
}

Просмотреть файл

@ -0,0 +1,11 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'FrameWatchers.jsm',
'watcher.js',
)

Просмотреть файл

@ -0,0 +1,122 @@
/* 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");
const protocol = require("devtools/shared/protocol");
const { watcherSpec } = require("devtools/shared/specs/watcher");
const ChromeUtils = require("ChromeUtils");
const { registerWatcher, unregisterWatcher } = ChromeUtils.import(
"resource://devtools/server/actors/descriptors/watcher/FrameWatchers.jsm"
);
exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
/**
* Optionally pass a `browser` in the second argument
* in order to focus only on targets related to a given <browser> element.
*/
initialize: function(conn, options) {
protocol.Actor.prototype.initialize.call(this, conn);
this._browser = options && options.browser;
},
destroy: function() {
protocol.Actor.prototype.destroy.call(this);
// Force unwatching for all types, even if we weren't watching.
// This is fine as unwatchTarget is NOOP if we weren't already watching for this target type.
this.unwatchTargets("frame");
},
form() {
return {
actor: this.actorID,
traits: {
// FF77+ supports frames in Watcher actor
frame: true,
},
};
},
async watchTargets(targetType) {
// Use DevToolsServerConnection's prefix as a key as we may
// have multiple clients willing to watch for targets.
// For example, a Browser Toolbox debugging everything and a Content Toolbox debugging
// just one tab.
const { prefix } = this.conn;
const perPrefixMap =
Services.ppmm.sharedData.get("DevTools:watchedPerPrefix") || new Map();
let perPrefixData = perPrefixMap.get(prefix);
if (!perPrefixData) {
perPrefixData = {
targets: new Set(),
browsingContextID: null,
};
perPrefixMap.set(prefix, perPrefixData);
}
if (perPrefixData.targets.has(targetType)) {
throw new Error(`Already watching for '${targetType}' target`);
}
perPrefixData.targets.add(targetType);
if (this._browser) {
// TODO bug 1625027: update this if we navigate to parent process
// or <browser> navigates to another BrowsingContext.
perPrefixData.browsingContextID = this._browser.browsingContext.id;
}
Services.ppmm.sharedData.set("DevTools:watchedPerPrefix", perPrefixMap);
// Flush the data as registerWatcher will indirectly force reading the data
Services.ppmm.sharedData.flush();
if (targetType == "frame") {
// Await the registration in order to ensure receiving the already existing targets
await registerWatcher(
this,
this._browser ? this._browser.browsingContext.id : null
);
}
},
unwatchTargets(targetType) {
const perPrefixMap = Services.ppmm.sharedData.get(
"DevTools:watchedPerPrefix"
);
if (!perPrefixMap) {
return;
}
const { prefix } = this.conn;
const perPrefixData = perPrefixMap.get(prefix);
if (!perPrefixData) {
return;
}
perPrefixData.targets.delete(targetType);
if (perPrefixData.targets.size === 0) {
perPrefixMap.delete(prefix);
}
Services.ppmm.sharedData.set("DevTools:watchedPerPrefix", perPrefixMap);
// Flush the data in order to ensure unregister the target actor from DevToolsFrameChild sooner
Services.ppmm.sharedData.flush();
if (targetType == "frame") {
unregisterWatcher(this);
}
},
/**
* Called by a Watcher module, whenever a new target is available
*/
notifyTargetAvailable(actor) {
this.emit("target-available-form", actor);
},
/**
* Called by a Watcher module, whenever a target has been destroyed
*/
notifyTargetDestroyed(actor) {
this.emit("target-destroyed-form", actor);
},
});

Просмотреть файл

@ -9,8 +9,104 @@ var EXPORTED_SYMBOLS = ["DevToolsFrameChild"];
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const Loader = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
// If true, log info about WindowGlobal's being created.
const DEBUG = false;
/**
* Helper function to know if a given WindowGlobal should be exposed via watchTargets(window-global) API
*/
function shouldNotifyWindowGlobal(windowGlobal, watchedBrowsingContextID) {
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
);
if (!window.docShell.hasLoadedNonBlankURI) {
return false;
}
// If we are focusing only on a sub-tree of BrowsingContext,
// Ignore the out of the sub tree elements.
if (
watchedBrowsingContextID &&
browsingContext.top.id != watchedBrowsingContextID
) {
return false;
}
// For now, we 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 it only if this is the top level target we are watching.
// For now we don't expect a target to be created, but we will as TabDescriptors arise.
if (
!browsingContext.parent &&
browsingContext.id == watchedBrowsingContextID
) {
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 DevToolsFrameChild/JSWindowActorChild
// to be instantiated for WindowGlobals that aren't from this process... Is that expected?
if (Cu.isRemoteProxy(windowGlobal.window)) {
return false;
}
// When Fission is turned off, we still process here the iframes that are running in the
// same process.
// As we can't use isInProcess, nor osPid (see previous block), we have
// to fallback to other checks. Here we check if we are able to access the parent document's window.
// If we can, it means that it runs in the same process as the current iframe we are processing.
if (
browsingContext.parent &&
browsingContext.parent.window &&
!Cu.isRemoteProxy(browsingContext.parent.window)
) {
return false;
}
return true;
}
function logWindowGlobal(windowGlobal, message) {
if (!DEBUG) {
return;
}
const browsingContext = windowGlobal.browsingContext;
dump(
message +
" | BrowsingContext.id: " +
browsingContext.id +
" Inner Window ID: " +
windowGlobal.innerWindowId +
" pid:" +
windowGlobal.osPid +
" isClosed:" +
windowGlobal.isClosed +
" isInProcess:" +
windowGlobal.isInProcess +
" isCurrentGlobal:" +
windowGlobal.isCurrentGlobal +
" currentRemoteType:" +
browsingContext.currentRemoteType +
" hasParent:" +
(browsingContext.parent ? browsingContext.parent.id : "no") +
" => " +
(windowGlobal.documentURI ? windowGlobal.documentURI.spec : "no-uri") +
"\n"
);
}
class DevToolsFrameChild extends JSWindowActorChild {
constructor() {
super();
@ -22,35 +118,92 @@ class DevToolsFrameChild extends JSWindowActorChild {
this._connections = new Map();
this._onConnectionChange = this._onConnectionChange.bind(this);
this._onSharedDataChanged = this._onSharedDataChanged.bind(this);
EventEmitter.decorate(this);
}
connect(msg) {
this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
// When debugging chrome pages, use a new dedicated loader.
this.loader = this.useCustomLoader
? new Loader.DevToolsLoader({
invisibleToDebugger: true,
})
: Loader;
const { prefix } = msg.data;
if (this._connections.get(prefix)) {
instantiate() {
const { sharedData } = Services.cpmm;
const perPrefixMap = sharedData.get("DevTools:watchedPerPrefix");
if (!perPrefixMap) {
throw new Error(
"DevToolsFrameChild connect was called more than once" +
` for the same connection (prefix: "${prefix}")`
"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 [prefix, { targets, browsingContextID }] of perPrefixMap) {
if (
targets.has("frame") &&
shouldNotifyWindowGlobal(this.manager, browsingContextID)
) {
this._createTargetActor(prefix);
}
}
}
// Instantiate a new WindowGlobalTarget for the given connection
_createTargetActor(parentConnectionPrefix) {
if (this._connections.get(parentConnectionPrefix)) {
throw new Error(
"DevToolsFrameChild _createTargetActor was called more than once" +
` for the same connection (prefix: "${parentConnectionPrefix}")`
);
}
// 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 prefix =
parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId;
logWindowGlobal(
this.manager,
"Instantiate WindowGlobalTarget with prefix: " + prefix
);
const { connection, targetActor } = this._createConnectionAndActor(prefix);
this._connections.set(prefix, { connection, actor: targetActor });
const { actor } = this._connections.get(prefix);
return { actor: actor.form() };
if (!this._isListeningForChange) {
// Watch for disabling in order to destroy this DevToolsClientChild and its WindowGlobalTargets
Services.cpmm.sharedData.addEventListener(
"change",
this._onSharedDataChanged
);
this._isListeningForChange = true;
}
// 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 quaranteed to be delivered in the order they
// were queued, we don't have to wait for anything around this sendAsyncMessage call.
// In theory, the FrameTargetActor may emit events in its constructor.
// If it does, such RDP packets may be lost. But in practice, no events
// are emitted during its construction. Instead the frontend will start
// the communication first.
this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", {
parentConnectionPrefix,
prefix,
actor: targetActor.form(),
});
}
_createConnectionAndActor(prefix) {
this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal;
// When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment.
if (!this.loader) {
this.loader = this.useCustomLoader
? new Loader.DevToolsLoader({
invisibleToDebugger: true,
})
: Loader;
}
const { DevToolsServer } = this.loader.require(
"devtools/server/devtools-server"
);
@ -107,23 +260,6 @@ class DevToolsFrameChild extends JSWindowActorChild {
DevToolsServer.destroy();
}
disconnect(msg) {
const { prefix } = msg.data;
const connectionInfo = this._connections.get(prefix);
if (!connectionInfo) {
console.error(
"No connection available in DevToolsFrameChild::disconnect"
);
return;
}
// Call DevToolsServerConnection.close to destroy all child actors. It
// should end up calling DevToolsServerConnection.onClosed that would
// actually cleanup all actor pools.
connectionInfo.connection.close();
this._connections.delete(prefix);
}
/**
* Supported Queries
*/
@ -149,10 +285,16 @@ class DevToolsFrameChild extends JSWindowActorChild {
receiveMessage(data) {
switch (data.name) {
case "DevToolsFrameParent:connect":
return this.connect(data);
case "DevToolsFrameParent:disconnect":
return this.disconnect(data);
case "DevToolsFrameParent:instantiate-already-available":
const { prefix, browsingContextID } = data.data;
// Re-check here, just to ensure that both parent and content processes agree
// on what should or should not be watched.
if (!shouldNotifyWindowGlobal(this.manager, browsingContextID)) {
throw new Error(
"Mismatch between DevToolsFrameParent and DevToolsFrameChild shouldNotifyWindowGlobal"
);
}
return this._createTargetActor(prefix);
case "DevToolsFrameParent:packet":
return this.emit("packet-received", data);
default:
@ -162,12 +304,63 @@ class DevToolsFrameChild extends JSWindowActorChild {
}
}
handleEvent({ type }) {
// DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addActors`
// as a DOM event to be listened to and so is fired by JS Window Actor code platform code.
if (type == "DOMWindowCreated") {
this.instantiate();
}
}
_onSharedDataChanged({ type, changedKeys }) {
if (type == "change") {
if (!changedKeys.includes("DevTools:watchedPerPrefix")) {
return;
}
const { sharedData } = Services.cpmm;
const perPrefixMap = sharedData.get("DevTools:watchedPerPrefix");
if (!perPrefixMap) {
this.didDestroy();
return;
}
let isStillWatching = false;
// Destroy the JSWindow Actor if we stopped watching frames from all the clients.
for (const [prefix, { targets }] of perPrefixMap) {
// This one prefix/connection still watches for frame
if (targets.has("frame")) {
isStillWatching = true;
continue;
}
const connectionInfo = this._connections.get(prefix);
// This connection wasn't watching, or at least did not instantiate a target actor
if (!connectionInfo) {
continue;
}
connectionInfo.connection.close();
this._connections.delete(prefix);
}
// If all the connections stopped watching, destroy everything
if (!isStillWatching) {
this.didDestroy();
}
} else {
throw new Error("Unsupported event:" + type + "\n");
}
}
didDestroy() {
for (const [, connectionInfo] of this._connections) {
connectionInfo.connection.close();
}
this._connections.clear();
if (this.useCustomLoader) {
this.loader.destroy();
}
if (this._isListeningForChange) {
Services.cpmm.sharedData.removeEventListener(
"change",
this._onSharedDataChanged
);
}
}
}

Просмотреть файл

@ -6,6 +6,12 @@
var EXPORTED_SYMBOLS = ["DevToolsFrameParent"];
const { loader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const { getWatcher } = ChromeUtils.import(
"resource://devtools/server/actors/descriptors/watcher/FrameWatchers.jsm"
);
loader.lazyRequireGetter(
this,
@ -14,10 +20,6 @@ loader.lazyRequireGetter(
true
);
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
class DevToolsFrameParent extends JSWindowActorParent {
constructor() {
super();
@ -34,7 +36,7 @@ class DevToolsFrameParent extends JSWindowActorParent {
// - actor: the frame target actor(as a form)
// - connection: the DevToolsServerConnection used to communicate with the
// frame target actor
// - forwardingPrefix: the forwarding prefix used by the connection to know
// - prefix: the forwarding prefix used by the connection to know
// how to forward packets to the frame target
// - transport: the JsWindowActorTransport
//
@ -48,42 +50,51 @@ class DevToolsFrameParent extends JSWindowActorParent {
EventEmitter.decorate(this);
}
async connectToFrame(connection) {
// Compute the same prefix that's used by DevToolsServerConnection when
// forwarding packets to the target frame.
const forwardingPrefix = connection.allocID("child");
try {
const { actor } = await this.connect({ prefix: forwardingPrefix });
connection.on("closed", this._onConnectionClosed);
// Create a js-window-actor based transport.
const transport = new JsWindowActorTransport(this, forwardingPrefix);
transport.hooks = {
onPacket: connection.send.bind(connection),
onClosed() {},
};
transport.ready();
connection.setForwarding(forwardingPrefix, transport);
this._connections.set(connection.prefix, {
actor,
connection,
forwardingPrefix,
transport,
});
return actor;
} catch (e) {
// Might fail if we have an actor destruction.
console.error("Failed to connect to DevToolsFrameChild actor");
console.error(e.toString());
return null;
}
/**
* Request the content process to create the Frame Target if there is one
* already available that matches the Browsing Context ID
*/
instantiateTarget({ prefix, browsingContextID }) {
return this.sendQuery("DevToolsFrameParent:instantiate-already-available", {
prefix,
browsingContextID,
});
}
_onConnectionClosed(prefix) {
connectFromContent({ parentConnectionPrefix, prefix, actor }) {
const watcher = getWatcher(parentConnectionPrefix);
if (!watcher) {
throw new Error(
`Parent Connection with prefix '${parentConnectionPrefix}' can't be found.`
);
}
const connection = watcher.conn;
connection.on("closed", this._onConnectionClosed);
// Create a js-window-actor based transport.
const transport = new JsWindowActorTransport(this, prefix);
transport.hooks = {
onPacket: connection.send.bind(connection),
onClosed() {},
};
transport.ready();
connection.setForwarding(prefix, transport);
this._connections.set(parentConnectionPrefix, {
watcher,
connection,
prefix,
transport,
actor,
});
watcher.notifyTargetAvailable(actor);
}
_onConnectionClosed(status, prefix) {
if (this._connections.has(prefix)) {
const { connection } = this._connections.get(prefix);
this._cleanupConnection(connection);
@ -91,9 +102,7 @@ class DevToolsFrameParent extends JSWindowActorParent {
}
async _cleanupConnection(connection) {
const { forwardingPrefix, transport } = this._connections.get(
connection.prefix
);
const { prefix, transport } = this._connections.get(connection.prefix);
connection.off("closed", this._onConnectionClosed);
if (transport) {
@ -102,16 +111,7 @@ class DevToolsFrameParent extends JSWindowActorParent {
transport.close();
}
// Notify the child process to clean the target-scoped actors.
try {
// Bug 1169643: Ignore any exception as the child process
// may already be destroyed by now.
await this.disconnect({ prefix: forwardingPrefix });
} catch (e) {
// Nothing to do
}
connection.cancelForwarding(forwardingPrefix);
connection.cancelForwarding(prefix);
this._connections.delete(connection.prefix);
if (!this._connections.size) {
this._destroy();
@ -124,8 +124,11 @@ class DevToolsFrameParent extends JSWindowActorParent {
}
this._destroyed = true;
for (const { actor, connection } of this._connections.values()) {
if (actor) {
for (const { actor, connection, watcher } of this._connections.values()) {
watcher.notifyTargetDestroyed(actor);
// XXX: we should probably get rid of this
if (actor && connection.transport) {
// The FrameTargetActor within the child process doesn't necessary
// have time to uninitialize itself when the frame is closed/killed.
// So ensure telling the client that the related actor is detached.
@ -142,14 +145,6 @@ class DevToolsFrameParent extends JSWindowActorParent {
* Supported Queries
*/
async connect(args) {
return this.sendQuery("DevToolsFrameParent:connect", args);
}
async disconnect(args) {
return this.sendQuery("DevToolsFrameParent:disconnect", args);
}
async sendPacket(packet, prefix) {
return this.sendQuery("DevToolsFrameParent:packet", { packet, prefix });
}
@ -171,6 +166,8 @@ class DevToolsFrameParent extends JSWindowActorParent {
receiveMessage(data) {
switch (data.name) {
case "DevToolsFrameChild:connectFromContent":
return this.connectFromContent(data.data);
case "DevToolsFrameChild:packet":
return this.emit("packet-received", data);
default:

Просмотреть файл

@ -22,6 +22,7 @@ support-files =
doc_innerHTML.html
doc_iframe.html
doc_iframe_content.html
doc_iframe2.html
doc_perf.html
error-actor.js
grid.html
@ -167,4 +168,5 @@ fail-if = fission
[browser_stylesheets_getTextEmpty.js]
[browser_stylesheets_nested-iframes.js]
[browser_resource_list-remote-frames.js]
[browser_watcher-watchTargets-frames.js]
[browser_webextension_inspected_window.js]

Просмотреть файл

@ -0,0 +1,199 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test Watcher actor against frames.
*/
"use strict";
const TEST_DOC_URL = MAIN_DOMAIN + "doc_iframe.html";
// URL of the <iframe> already loaded in doc_iframe.html (the top level test document)
const TEST_DOC_URL1 =
MAIN_DOMAIN.replace("test1.example.org", "example.com") +
"doc_iframe_content.html";
// URL of an <iframe> dynamically inserted during the test
const TEST_DOC_URL2 =
MAIN_DOMAIN.replace("test1.example.org", "example.net") + "doc_iframe2.html";
add_task(async function() {
const tabTarget = await addTabTarget(TEST_DOC_URL);
const { mainRoot } = tabTarget.client;
// First test watching all frames. Really all of them.
// From all top level windows, tabs, and any inner remoted iframe.
await testWatchAllFrames(mainRoot);
// Then use the Watcher to watch only frames of a given tab.
await testWatchOneTabFrames(tabTarget);
});
async function testWatchAllFrames(mainRoot) {
info("Assert watchTargets against the whole browser");
const targets = [];
const processDescriptor = await mainRoot.getMainProcess();
const watcher = await processDescriptor.getWatcher();
const onNewTarget = target => {
dump(
" (+) target: " + target.url + " -- " + target.browsingContextID + "\n"
);
targets.push(target);
};
watcher.on("target-available", onNewTarget);
await watcher.watchTargets("frame");
// As it retrieve the whole browser browsing contexts, it is hard to know how many it will fetch
ok(targets.length > 0, "Got multiple frame targets");
// Assert that we get a target for the top level tab document
const tabTarget = targets.find(f => f.url == TEST_DOC_URL);
ok(tabTarget, "We get the target for the tab document");
const tabId = gBrowser.selectedTab.linkedBrowser.browsingContext.id;
is(
tabTarget.browsingContextID,
tabId,
"The tab frame target BrowsingContextID is correct"
);
// Assert that we also fetch the iframe targets
await assertTabIFrames(watcher, targets);
await watcher.unwatchTargets("frame");
watcher.off("target-available", onNewTarget);
}
async function testWatchOneTabFrames(tabTarget) {
info("Assert watchTargets against a given Tab");
const targets = [];
const tabDescriptor = tabTarget.descriptorFront;
const watcher = await tabDescriptor.getWatcher();
const onNewTarget = target => {
dump(
" (+) target: " + target.url + " -- " + target.browsingContextID + "\n"
);
targets.push(target);
};
watcher.on("target-available", onNewTarget);
await watcher.watchTargets("frame");
if (SpecialPowers.useRemoteSubframes) {
is(targets.length, 1, "With fission, one additional target is reported");
} else {
is(targets.length, 0, "Without fission no additional target is reported");
}
await assertTabIFrames(watcher, targets);
await watcher.unwatchTargets("frame");
watcher.off("target-available", onNewTarget);
}
async function assertTabIFrames(watcher, targets) {
// - The existing <iframe>
const existingIframeTarget = targets.find(f => f.url === TEST_DOC_URL1);
const existingIframeId = await ContentTask.spawn(
gBrowser.selectedBrowser,
null,
url => {
const iframe = content.document.querySelector("#remote-frame");
return iframe.frameLoader.browsingContext.id;
}
);
if (SpecialPowers.useRemoteSubframes) {
ok(existingIframeTarget, "And its remote child iframe");
is(
existingIframeTarget.browsingContextID,
existingIframeId,
"The iframe target BrowsingContextID is correct"
);
} else {
ok(
!existingIframeTarget,
"Without fission, the iframes are not spawning additional targets"
);
}
const originalFrameCount = targets.length;
if (SpecialPowers.useRemoteSubframes) {
// Assert that the frame target get destroyed and re-created on reload
// This only happens with Fission, as otherwise, the iframe doesn't get a Descriptor/Target anyway.
const onReloadedCreated = watcher.once("target-available");
const onReloadedDestroyed = watcher.once("target-destroyed");
SpecialPowers.spawn(BrowsingContext.get(existingIframeId), [], () => {
content.location.reload();
});
info("Waiting for previous target destruction");
const destroyedIframeTarget = await onReloadedDestroyed;
info("Waiting for new target creation");
const createdIframeTarget = await onReloadedCreated;
is(
destroyedIframeTarget,
existingIframeTarget,
"the destroyed target is the expected one, i.e. the previous one"
);
isnot(
createdIframeTarget,
existingIframeTarget,
"the created target is the expected one, i.e. a new one"
);
is(
createdIframeTarget.browsingContextID,
existingIframeId,
"The new iframe target BrowsingContextID is the same"
);
}
// Assert that we also get an event for iframes added after the call to watchTargets
const onCreated = watcher.once("target-available");
const newIframeId = await ContentTask.spawn(
gBrowser.selectedBrowser,
TEST_DOC_URL2,
url => {
const iframe = content.document.createElement("iframe");
iframe.src = url;
content.document.body.appendChild(iframe);
return iframe.frameLoader.browsingContext.id;
}
);
if (SpecialPowers.useRemoteSubframes) {
await onCreated;
is(targets.length, originalFrameCount + 2, "Got the additional frame");
// Match against the BrowsingContext ID as the target is created very early
// and the url isn't set yet when the target is created.
const newIframeTarget = targets.find(
f => f.browsingContextID === newIframeId
);
ok(newIframeTarget, "We got the target for the new iframe");
// Assert that we also get notified about destroyed frames
const onDestroyed = watcher.once("target-destroyed");
await ContentTask.spawn(gBrowser.selectedBrowser, newIframeId, id => {
const browsingContext = BrowsingContext.get(id);
const iframe = browsingContext.embedderElement;
iframe.remove();
});
const destroyedTarget = await onDestroyed;
is(
destroyedTarget,
newIframeTarget,
"We got notified about the iframe destruction"
);
} else {
is(
targets.length,
originalFrameCount,
"Without fission, the iframes are not spawning additional targets"
);
}
}

Просмотреть файл

@ -0,0 +1,15 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Sub document page</title>
</head>
<body>
Iframe document
</body>
</html>

Просмотреть файл

@ -77,6 +77,8 @@ class LegacyImplementationProcesses {
}
}
// Bug 1593937 made this code only used to support FF76- and can be removed
// once FF77 reach release channel.
class LegacyImplementationFrames {
constructor(targetList, onTargetAvailable) {
this.targetList = targetList;
@ -281,6 +283,13 @@ class TargetList {
*/
constructor(rootFront, targetFront) {
this.rootFront = rootFront;
// Once we have descriptor for all targets we create a toolbox for,
// we should try to only pass the descriptor to the Toolbox constructor,
// and, only receive the root and descriptor front as an argument to TargetList.
// Bug 1573779, we only miss descriptors for workers.
this.descriptorFront = targetFront.descriptorFront;
// Note that this is a public attribute, used outside of this class
// and helps knowing what is the current top level target we debug.
this.targetFront = targetFront;
@ -415,11 +424,23 @@ class TargetList {
}
this._setListening(type, true);
// Starting with FF77, we support frames watching via watchTargets for Tab and Process descriptors
const supportsWatcher = this.descriptorFront?.traits?.watcher;
if (supportsWatcher) {
const watcher = await this.descriptorFront.getWatcher();
if (watcher.traits[type]) {
if (!this._startedListeningToWatcher) {
this._startedListeningToWatcher = true;
watcher.on("target-available", this._onTargetAvailable);
watcher.on("target-destroyed", this._onTargetDestroyed);
}
await watcher.watchTargets(type);
continue;
}
}
if (this.legacyImplementation[type]) {
await this.legacyImplementation[type].listen();
} else {
// TO BE IMPLEMENTED via this.targetFront.watchFronts(type)
// For now we always go throught "legacy" codepath.
throw new Error(`Unsupported target type '${type}'`);
}
}
@ -432,11 +453,18 @@ class TargetList {
}
this._setListening(type, false);
// Starting with FF77, we support frames watching via watchTargets for Tab and Process descriptors
const supportsWatcher = this.descriptorFront?.traits?.watcher;
if (supportsWatcher) {
const watcher = this.descriptorFront.getCachedWatcher();
if (watcher && watcher.traits[type]) {
watcher.unwatchTargets(type);
continue;
}
}
if (this.legacyImplementation[type]) {
this.legacyImplementation[type].unlisten();
} else {
// TO BE IMPLEMENTED via this.targetFront.unwatchFronts(type)
// For now we always go throught "legacy" codepath.
throw new Error(`Unsupported target type '${type}'`);
}
}

Просмотреть файл

@ -15,6 +15,10 @@ const processDescriptorSpec = generateActorSpec({
process: RetVal("json"),
},
},
getWatcher: {
request: {},
response: RetVal("watcher"),
},
},
});

Просмотреть файл

@ -21,6 +21,10 @@ const tabDescriptorSpec = generateActorSpec({
favicon: RetVal("string"),
},
},
getWatcher: {
request: {},
response: RetVal("watcher"),
},
},
});

Просмотреть файл

@ -287,6 +287,11 @@ const Types = (exports.__TypesForTests = [
spec: "devtools/shared/specs/walker",
front: "devtools/client/fronts/walker",
},
{
types: ["watcher"],
spec: "devtools/shared/specs/watcher",
front: "devtools/client/fronts/watcher",
},
{
types: ["console"],
spec: "devtools/shared/specs/webconsole",

Просмотреть файл

@ -52,6 +52,7 @@ DevToolsModules(
'thread.js',
'timeline.js',
'walker.js',
'watcher.js',
'webconsole.js',
'websocket.js',
)

Просмотреть файл

@ -0,0 +1,39 @@
/* 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 { generateActorSpec, Arg } = require("devtools/shared/protocol");
const watcherSpecPrototype = {
typeName: "watcher",
methods: {
watchTargets: {
request: {
targetType: Arg(0, "string"),
},
response: {},
},
unwatchTargets: {
request: {
targetType: Arg(0, "string"),
},
oneway: true,
},
},
events: {
"target-available-form": {
type: "target-available-form",
target: Arg(0, "json"),
},
"target-destroyed-form": {
type: "target-destroyed-form",
target: Arg(0, "json"),
},
},
};
exports.watcherSpec = generateActorSpec(watcherSpecPrototype);