Bug 1633712 - [devtools] Create WorkerTargets as soon as possible. r=ochameau,devtools-backward-compat-reviewers.

This patch adds support for dedicated worker targets in the Watcher actor.
Shared and Service workers are not handled yet.

In a similar manner to what we already have for frame targets, we add a worker-helper
file that will communicate with a JsWindowActor pair spawned on each document,
that will  manage workers (DevToolsWorkerParent/DevToolsWorkerChild).

For a given document, the DevToolsWorkerChild will enumerate the existing workers
related to it, as well as add an event listener to be notified when workers are
being registered and unregistered, and communicate that back to the DevToolsWorkerParent
on the main thread, so worker targets creation and destruction are notified by
the Watcher actor (via target-available-form and target-destroyed-form events).

When a worker is created, the DevToolsWorkerChild for the document the worker
was spawned from will create a WorkerTargetActor, that will live in the worker
thread (using worker-connector.js), passing it resources the Watcher is currently
listening for. It will also handle communication between the main thread and the
worker thread, when the watcher listen to new resources (or stop watching resources).

A WorkerTargetFront is created so the client can be notified about available
resources (via the resource-available-form event, emitted from the worker target).

Tests are added in the next patches of this queue.

Differential Revision: https://phabricator.services.mozilla.com/D85399
This commit is contained in:
Nicolas Chevobbe 2020-10-15 05:17:52 +00:00
Родитель 54da2bd313
Коммит 1e17daf388
23 изменённых файлов: 1222 добавлений и 40 удалений

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

@ -416,6 +416,7 @@ class SourceMapURLService {
if (!this._prefValue) {
return null;
}
if (this._target.isWorkerTarget) {
return;
}

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

@ -8,4 +8,5 @@ DevToolsModules(
'browsing-context.js',
'content-process.js',
'target-mixin.js',
'worker.js',
)

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

@ -411,7 +411,10 @@ function TargetMixin(parentClass) {
}
get isWorkerTarget() {
return this.typeName === "workerDescriptor";
// XXX Remove the check on `workerDescriptor` as part of Bug 1667404.
return (
this.typeName === "workerTarget" || this.typeName === "workerDescriptor"
);
}
get isLegacyAddon() {
@ -535,7 +538,13 @@ function TargetMixin(parentClass) {
if (this.isDestroyedOrBeingDestroyed()) {
return;
}
await this.attach();
// WorkerTargetFront don't have an attach function as the related console and thread
// actors are created right away (from devtools/server/startup/worker.js)
if (this.attach) {
await this.attach();
}
const isBrowserToolbox = targetList.targetFront.isParentProcess;
const isNonTopLevelFrameTarget =
!this.isTopLevel && this.targetType === targetList.TYPES.FRAME;

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

@ -0,0 +1,29 @@
/* 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 { workerTargetSpec } = require("devtools/shared/specs/targets/worker");
const {
FrontClassWithSpec,
registerFront,
} = require("devtools/shared/protocol");
const { TargetMixin } = require("devtools/client/fronts/targets/target-mixin");
class WorkerTargetFront extends TargetMixin(
FrontClassWithSpec(workerTargetSpec)
) {
form(json) {
this.actorID = json.actor;
// Save the full form for Target class usage.
// Do not use `form` name to avoid colliding with protocol.js's `form` method
this.targetForm = json;
this._title = json.title;
this._url = json.url;
}
}
exports.WorkerTargetFront = WorkerTargetFront;
registerFront(exports.WorkerTargetFront);

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

@ -15,6 +15,12 @@ loader.lazyRequireGetter(
"devtools/client/fronts/targets/browsing-context",
true
);
loader.lazyRequireGetter(
this,
"WorkerTargetFront",
"devtools/client/fronts/targets/worker",
true
);
class WatcherFront extends FrontClassWithSpec(watcherSpec) {
constructor(client, targetFront, parentFront) {
@ -34,10 +40,14 @@ class WatcherFront extends FrontClassWithSpec(watcherSpec) {
}
_onTargetAvailable(form) {
const front = new BrowsingContextTargetFront(this.conn, null, this);
const FrontCls = form.actor.includes("workerTarget")
? WorkerTargetFront
: BrowsingContextTargetFront;
const front = new FrontCls(this.conn, null, this);
front.actorID = form.actor;
front.form(form);
this.manage(front);
this.emit("target-available", front);
}

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

@ -412,7 +412,7 @@ function makeDebuggeeValue(targetActor, value) {
}
}
const dbgGlobal = targetActor.dbg.makeGlobalObjectReference(
targetActor.window
targetActor.window || targetActor.workerGlobal
);
return dbgGlobal.makeDebuggeeValue(value);
}

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

@ -50,6 +50,17 @@ const FrameTargetResources = augmentResourceDictionary({
path: "devtools/server/actors/resources/stylesheets",
},
});
// We'll only support a few resource types in Workers (console-message, source,
// breakpoints, …) as error and platform messages are not supported since we need access
// to Ci, which isn't available in worker context.
// Errors are emitted from the content process main thread so the user would still get them.
const WorkerTargetResources = augmentResourceDictionary({
[TYPES.CONSOLE_MESSAGE]: {
path: "devtools/server/actors/resources/console-messages",
},
});
const ParentProcessResources = augmentResourceDictionary({
[TYPES.NETWORK_EVENT]: {
path: "devtools/server/actors/resources/network-events",
@ -91,6 +102,8 @@ function getResourceTypeDictionaryForTargetType(targetType) {
switch (targetType) {
case Targets.TYPES.FRAME:
return FrameTargetResources;
case Targets.TYPES.WORKER:
return WorkerTargetResources;
default:
throw new Error(`Unsupported target actor typeName '${targetType}'`);
}

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

@ -14,6 +14,8 @@ const Targets = require("devtools/server/actors/targets/index");
const makeDebuggerUtil = require("devtools/server/actors/utils/make-debugger");
const { TabSources } = require("devtools/server/actors/utils/TabSources");
const Resources = require("devtools/server/actors/resources/index");
exports.WorkerTargetActor = ActorClassWithSpec(workerTargetSpec, {
targetType: Targets.TYPES.WORKER,
@ -22,12 +24,18 @@ exports.WorkerTargetActor = ActorClassWithSpec(workerTargetSpec, {
*
* @param {DevToolsServerConnection} connection: The connection to the client.
* @param {WorkerGlobalScope} workerGlobal: The worker global.
* @param {Object} workerDebuggerData: The worker debugger information
* @param {String} workerDebuggerData.id: The worker debugger id
* @param {String} workerDebuggerData.url: The worker debugger url
* @param {String} workerDebuggerData.type: The worker debugger type
*/
initialize: function(connection, workerGlobal) {
initialize: function(connection, workerGlobal, workerDebuggerData) {
Actor.prototype.initialize.call(this, connection);
// workerGlobal is needed by the console actor for evaluations.
this.workerGlobal = workerGlobal;
this._workerDebuggerData = workerDebuggerData;
this._sources = null;
this.makeDebugger = makeDebuggerUtil.bind(null, {
@ -36,6 +44,8 @@ exports.WorkerTargetActor = ActorClassWithSpec(workerTargetSpec, {
},
shouldAddNewGlobalAsDebuggee: () => true,
});
this.notifyResourceAvailable = this.notifyResourceAvailable.bind(this);
},
form() {
@ -43,10 +53,18 @@ exports.WorkerTargetActor = ActorClassWithSpec(workerTargetSpec, {
actor: this.actorID,
threadActor: this.threadActor?.actorID,
consoleActor: this._consoleActor?.actorID,
id: this._workerDebuggerData.id,
type: this._workerDebuggerData.type,
url: this._workerDebuggerData.url,
traits: {},
};
},
attach() {
if (this.threadActor) {
return;
}
// needed by the console actor
this.threadActor = new ThreadActor(this, this.workerGlobal);
@ -77,4 +95,58 @@ exports.WorkerTargetActor = ActorClassWithSpec(workerTargetSpec, {
// This isn't an RDP event and is only listened to from startup/worker.js.
this.emit("worker-thread-attached");
},
addWatcherDataEntry(type, entries) {
if (type == "resources") {
return this._watchTargetResources(entries);
}
return Promise.resolve();
},
removeWatcherDataEntry(type, entries) {
if (type == "resources") {
return this._unwatchTargetResources(entries);
}
return Promise.resolve();
},
/**
* These two methods will create and destroy resource watchers
* for each resource type. This will end up calling `notifyResourceAvailable`
* whenever new resources are observed.
*/
_watchTargetResources(resourceTypes) {
return Resources.watchResources(this, resourceTypes);
},
_unwatchTargetResources(resourceTypes) {
return Resources.unwatchResources(this, resourceTypes);
},
/**
* Called by Watchers, when new resources are available.
*
* @param Array<json> resources
* List of all available resources. A resource is a JSON object piped over to the client.
* It may contain actor IDs, actor forms, to be manually marshalled by the client.
*/
notifyResourceAvailable(resources) {
this.emit("resource-available-form", resources);
},
destroy() {
Actor.prototype.destroy.call(this);
if (this._sources) {
this._sources.destroy();
this._sources = null;
}
this.workerGlobal = null;
this._dbg = null;
this.threadActor = null;
this._consoleActor = null;
},
});

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

@ -22,6 +22,11 @@ loader.lazyRequireGetter(
Targets.TYPES.FRAME,
"devtools/server/actors/watcher/target-helpers/frame-helper"
);
loader.lazyRequireGetter(
TARGET_HELPERS,
Targets.TYPES.WORKER,
"devtools/server/actors/watcher/target-helpers/worker-helper"
);
exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
/**
@ -93,7 +98,9 @@ exports.WatcherActor = protocol.ActorClassWithSpec(watcherSpec, {
actor: this.actorID,
traits: {
// FF77+ supports frames in Watcher actor
frame: true,
[Targets.TYPES.FRAME]: true,
// FF83+ supports workers in Watcher actor for content toolbox.
[Targets.TYPES.WORKER]: hasBrowserElement,
resources: {
// FF81+ (bug 1642295) added support for:
// - CONSOLE_MESSAGE

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

@ -294,31 +294,49 @@ let isJSWindowActorRegistered = false;
* Register the JSWindowActor pair "DevToolsFrame".
*
* We should call this method before we try to use this JS Window Actor from the parent process
* via WindowGlobal.getActor("DevToolsFrame").
* (via `WindowGlobal.getActor("DevToolsFrame")` or `WindowGlobal.getActor("DevToolsWorker")`).
* Also, registering it will automatically force spawing the content process JSWindow Actor
* anytime a new document is opened (via DOMWindowCreated event).
*/
const JSWindowActorsConfig = {
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,
},
DevToolsWorker: {
parent: {
moduleURI:
"resource://devtools/server/connectors/js-window-actor/DevToolsWorkerParent.jsm",
},
child: {
moduleURI:
"resource://devtools/server/connectors/js-window-actor/DevToolsWorkerChild.jsm",
events: {
DOMWindowCreated: {},
},
},
allFrames: true,
},
};
function registerJSWindowActor() {
if (isJSWindowActorRegistered) {
return;
}
isJSWindowActorRegistered = true;
ActorManagerParent.addJSWindowActors({
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,
},
});
ActorManagerParent.addJSWindowActors(JSWindowActorsConfig);
// Force the immediate activation of this JSWindow Actor, so that we can immediately
// use the JSWindowActor, from the same event loop.
ActorManagerParent.flush();
@ -329,6 +347,9 @@ function unregisterJSWindowActor() {
return;
}
isJSWindowActorRegistered = false;
// ActorManagerParent doesn't expose a "removeActors" method, but it would be equivalent to that:
ChromeUtils.unregisterWindowActor("DevToolsFrame");
for (const JSWindowActorName of Object.keys(JSWindowActorsConfig)) {
// ActorManagerParent doesn't expose a "removeActors" method, but it would be equivalent to that:
ChromeUtils.unregisterWindowActor(JSWindowActorName);
}
}

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

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

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

@ -7,12 +7,21 @@
const Services = require("Services");
/**
* Helper function to know if a given WindowGlobal should be exposed via watchTargets("frame") API
* 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) {
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
@ -42,7 +51,11 @@ function shouldNotifyWindowGlobal(browsingContext, watchedBrowserId) {
return false;
}
// For now, we only mention the "remote frames".
if (options.acceptNonRemoteFrame) {
return true;
}
// If `acceptNonRemoteFrame` options isn't true, only mention the "remote frames".
// i.e. the frames which are in a distinct process compared to their parent document
return (
!browsingContext.parent ||

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

@ -0,0 +1,137 @@
/* 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 {
getAllRemoteBrowsingContexts,
shouldNotifyWindowGlobal,
} = require("devtools/server/actors/watcher/target-helpers/utils.js");
const DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME = "DevToolsWorker";
/**
* Force creating targets for all existing workers 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 DevToolsWorkerChild
// - Have the DevToolsWorkerChild to spawn the WorkerTargetActors
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const promises = [];
for (const browsingContext of browsingContexts) {
const promise = browsingContext.currentWindowGlobal
.getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
.instantiateWorkerTargets({
watcherActorID: watcher.actorID,
connectionPrefix: watcher.conn.prefix,
browserId: watcher.browserId,
watchedData: watcher.watchedData,
});
promises.push(promise);
}
// Await for the different queries in order to try to resolve only *after* we received
// the already available worker targets.
return Promise.all(promises);
}
/**
* Force destroying all worker targets which were related to a given watcher.
*
* @param WatcherActor watcher
* The Watcher Actor requesting to stop watching for new targets.
*/
async function destroyTargets(watcher) {
// Go over all existing BrowsingContext in order to destroy all targets
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
for (const browsingContext of browsingContexts) {
browsingContext.currentWindowGlobal
.getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
.destroyWorkerTargets({
watcherActorID: watcher.actorID,
browserId: watcher.browserId,
});
}
}
/**
* 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
*/
async function addWatcherDataEntry({ watcher, type, entries }) {
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
const promises = [];
for (const browsingContext of browsingContexts) {
const promise = browsingContext.currentWindowGlobal
.getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
.addWatcherDataEntry({
watcherActorID: watcher.actorID,
browserId: watcher.browserId,
type,
entries,
});
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 addWatcherDataEntry for argument documentation.
*/
function removeWatcherDataEntry({ watcher, type, entries }) {
const browsingContexts = getFilteredBrowsingContext(watcher.browserElement);
for (const browsingContext of browsingContexts) {
browsingContext.currentWindowGlobal
.getActor(DEVTOOLS_WORKER_JS_WINDOW_ACTOR_NAME)
.removeWatcherDataEntry({
watcherActorID: watcher.actorID,
browserId: watcher.browserId,
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,
addWatcherDataEntry,
removeWatcherDataEntry,
};

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

@ -0,0 +1,556 @@
/* 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 = ["DevToolsWorkerChild"];
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"wdm",
"@mozilla.org/dom/workers/workerdebuggermanager;1",
"nsIWorkerDebuggerManager"
);
XPCOMUtils.defineLazyGetter(this, "Loader", () =>
ChromeUtils.import("resource://devtools/shared/Loader.jsm")
);
// Name of the attribute into which we save data in `sharedData` object.
const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
class DevToolsWorkerChild extends JSWindowActorChild {
constructor() {
super();
// The map is indexed by the Watcher Actor ID.
// The values are objects containing the following properties:
// - connection: the DevToolsServerConnection itself
// - workers: An array of object containing the following properties:
// - dbg: A WorkerDebuggerInstance
// - workerTargetForm: The associated worker target instance form
// - workerThreadServerForwardingPrefix: The prefix used to forward events to the
// worker target on the worker thread ().
// - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
// between content and parent processes.
// - watchedData: Data (targets, resources, …) the watcher wants to be notified about.
// See WatcherRegistry.getWatchedData to see the full list of properties.
this._connections = new Map();
this._onConnectionChange = this._onConnectionChange.bind(this);
EventEmitter.decorate(this);
}
_onWorkerRegistered(dbg) {
if (!this._shouldHandleWorker(dbg)) {
return;
}
for (const [watcherActorID, { connection, forwardingPrefix }] of this
._connections) {
this._createWorkerTargetActor({
dbg,
connection,
forwardingPrefix,
watcherActorID,
});
}
}
_onWorkerUnregistered(dbg) {
for (const [watcherActorID, { workers, forwardingPrefix }] of this
._connections) {
// Check if the worker registration was handled for this watcherActorID.
const unregisteredActorIndex = workers.findIndex(
worker => worker.dbg.id === dbg.id
);
if (unregisteredActorIndex === -1) {
continue;
}
const [unregisteredActor] = workers.splice(unregisteredActorIndex, 1);
this.sendAsyncMessage("DevToolsWorkerChild:workerTargetDestroyed", {
watcherActorID,
forwardingPrefix,
workerTargetForm: unregisteredActor.workerTargetForm,
});
}
}
onDOMWindowCreated() {
const { sharedData } = Services.cpmm;
const watchedDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
if (!watchedDataByWatcherActor) {
throw new Error(
"Request to instantiate the target(s) for the Worker, but `sharedData` is empty about watched targets"
);
}
// Create one Target actor for each prefix/client which listen to workers
for (const [watcherActorID, watchedData] of watchedDataByWatcherActor) {
const { targets, connectionPrefix, browserId } = watchedData;
if (
targets.includes("worker") &&
shouldNotifyWindowGlobal(this.manager, browserId)
) {
this._watchWorkerTargets({
watcherActorID,
parentConnectionPrefix: connectionPrefix,
watchedData,
// When navigating, this code is triggered _before_ the workers living on the page
// we navigate from are terminated, which would create worker targets for them again.
// Since at this point the new document can't have any workers yet, we are going to
// ignore existing targets (i.e. the workers that belong to the previous document).
ignoreExistingTargets: true,
});
}
}
}
/**
* Function handling messages sent by DevToolsWorkerParent (part of JSWindowActor API).
*
* @param {Object} message
* @param {String} message.name
* @param {*} message.data
*/
receiveMessage(message) {
// All messages pass `browserId` (except packet) and are expected
// to match shouldNotifyWindowGlobal result.
if (message.name != "DevToolsWorkerParent:packet") {
const { browserId } = message.data;
// 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, browserId)
) {
throw new Error(
"Mismatch between DevToolsWorkerParent and DevToolsWorkerChild " +
(this.manager.browsingContext.browserId == browserId
? "window global shouldn't be notified (shouldNotifyWindowGlobal mismatch)"
: `expected browsing context with ID ${browserId}, but got ${this.manager.browsingContext.browserId}`)
);
}
}
switch (message.name) {
case "DevToolsWorkerParent:instantiate-already-available": {
const { watcherActorID, connectionPrefix, watchedData } = message.data;
return this._watchWorkerTargets({
watcherActorID,
parentConnectionPrefix: connectionPrefix,
watchedData,
});
}
case "DevToolsWorkerParent:destroy": {
const { watcherActorID } = message.data;
return this._destroyTargetActors(watcherActorID);
}
case "DevToolsWorkerParent:addWatcherDataEntry": {
const { watcherActorID, type, entries } = message.data;
return this._addWatcherDataEntry(watcherActorID, type, entries);
}
case "DevToolsWorkerParent:removeWatcherDataEntry": {
const { watcherActorID, type, entries } = message.data;
return this._removeWatcherDataEntry(watcherActorID, type, entries);
}
case "DevToolsWorkerParent:packet":
return this.emit("packet-received", message);
default:
throw new Error(
"Unsupported message in DevToolsWorkerParent: " + message.name
);
}
}
/**
* Instantiate targets for existing workers, watch for worker registration and listen
* for resources on those workers, for given connection and browserId. Targets are sent
* to the DevToolsWorkerParent via the DevToolsWorkerChild:workerTargetAvailable message.
*
* @param {Object} options
* @param {Integer} options.watcherActorID: The ID of the WatcherActor who requested to
* observe and create these target actors.
* @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
* of the Watcher Actor. This is used to compute a unique ID for the target actor.
* @param {Object} options.watchedData: Data (targets, resources, ) the watcher wants
* to be notified about. See WatcherRegistry.getWatchedData to see the full list
* of properties.
* @param {Boolean} options.ignoreExistingTargets: Set to true to not loop on existing
* workers. This is useful when this function is called at the very early stage
* of the life of a document, since workers of the previous document are still
* alive, and there's no way to filter them out.
*/
async _watchWorkerTargets({
watcherActorID,
parentConnectionPrefix,
ignoreExistingTargets,
watchedData,
}) {
if (this._connections.has(watcherActorID)) {
throw new Error(
"DevToolsWorkerChild _watchWorkerTargets was called more than once" +
` for the same Watcher (Actor ID: "${watcherActorID}")`
);
}
// Listen for new workers that will be spawned.
if (!this._workerDebuggerListener) {
this._workerDebuggerListener = {
onRegister: this._onWorkerRegistered.bind(this),
onUnregister: this._onWorkerUnregistered.bind(this),
};
wdm.addListener(this._workerDebuggerListener);
}
// 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
// WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
// (this.manager == WindowGlobalChild interface)
const forwardingPrefix =
parentConnectionPrefix + "workerGlobal" + this.manager.innerWindowId;
const connection = this._createConnection(forwardingPrefix);
this._connections.set(watcherActorID, {
connection,
workers: [],
forwardingPrefix,
watchedData,
});
if (ignoreExistingTargets !== true) {
await Promise.all(
Array.from(wdm.getWorkerDebuggerEnumerator())
.filter(dbg => this._shouldHandleWorker(dbg))
.map(dbg =>
this._createWorkerTargetActor({
dbg,
connection,
forwardingPrefix,
watcherActorID,
})
)
);
}
}
_createConnection(forwardingPrefix) {
const { DevToolsServer } = Loader.require(
"devtools/server/devtools-server"
);
DevToolsServer.init();
// We want a special server without any root actor and only target-scoped actors.
// We are going to spawn a WorkerTargetActor instance in the next few lines,
// it is going to act like a root actor without being one.
DevToolsServer.registerActors({ target: true });
DevToolsServer.on("connectionchange", this._onConnectionChange);
const connection = DevToolsServer.connectToParentWindowActor(
this,
forwardingPrefix
);
return connection;
}
/**
* Indicates whether or not we should handle the worker debugger
*
* @param {WorkerDebugger} dbg: The worker debugger we want to check.
* @returns {Boolean}
*/
_shouldHandleWorker(dbg) {
// We only want to create targets for non-closed dedicated worker, in the same document
return (
!dbg.isClosed &&
dbg.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED &&
dbg.window === this.manager?.browsingContext?.window
);
}
async _createWorkerTargetActor({
dbg,
connection,
forwardingPrefix,
watcherActorID,
}) {
// Prevent the debuggee from executing in this worker until the client has
// finished attaching to it. This call will throw if the debugger is already "registered"
// (i.e. if this is called outside of the register listener)
// See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
try {
dbg.setDebuggerReady(false);
} catch (e) {}
const watcherConnectionData = this._connections.get(watcherActorID);
const { watchedData } = watcherConnectionData;
const workerThreadServerForwardingPrefix = connection.allocID(
"workerTarget"
);
// Create the actual worker target actor, in the worker thread.
const { connectToWorker } = Loader.require(
"devtools/server/connectors/worker-connector"
);
const { workerTargetForm } = await connectToWorker(
connection,
dbg,
workerThreadServerForwardingPrefix,
{
watchedData,
}
);
watcherConnectionData.workers.push({
dbg,
workerTargetForm,
workerThreadServerForwardingPrefix,
});
this.sendAsyncMessage("DevToolsWorkerChild:workerTargetAvailable", {
watcherActorID,
forwardingPrefix,
workerTargetForm,
});
return workerTargetForm;
}
_destroyTargetActors(watcherActorID) {
const watcherConnectionData = this._connections.get(watcherActorID);
// This connection has already been cleaned?
if (!watcherConnectionData) {
console.error(
`Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
);
return;
}
for (const {
dbg,
workerThreadServerForwardingPrefix,
} of watcherConnectionData.workers) {
try {
dbg.postMessage(
JSON.stringify({
type: "disconnect",
forwardingPrefix: workerThreadServerForwardingPrefix,
})
);
} catch (e) {}
}
watcherConnectionData.connection.close();
this._connections.delete(watcherActorID);
}
/**
* Destroy the server once its last connection closes. Note that multiple
* worker scripts may be running in parallel and reuse the same server.
*/
_onConnectionChange() {
const { DevToolsServer } = Loader.require(
"devtools/server/devtools-server"
);
// Only destroy the server if there is no more connections to it. It may be
// used to debug another tab running in the same process.
if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
return;
}
if (this._destroyed) {
return;
}
this._destroyed = true;
DevToolsServer.off("connectionchange", this._onConnectionChange);
DevToolsServer.destroy();
}
async sendPacket(packet, prefix) {
return this.sendAsyncMessage("DevToolsWorkerChild:packet", {
packet,
prefix,
});
}
async _addWatcherDataEntry(watcherActorID, type, entries) {
const watcherConnectionData = this._connections.get(watcherActorID);
if (!watcherConnectionData) {
return;
}
if (!watcherConnectionData.watchedData[type]) {
watcherConnectionData.watchedData[type] = [];
}
watcherConnectionData.watchedData[type].push(...entries);
const promises = [];
for (const {
dbg,
workerThreadServerForwardingPrefix,
} of watcherConnectionData.workers) {
promises.push(
addWatcherDataEntryInWorkerTarget({
dbg,
workerThreadServerForwardingPrefix,
type,
entries,
})
);
}
await Promise.all(promises);
}
_removeWatcherDataEntry(watcherActorID, type, entries) {
const watcherConnectionData = this._connections.get(watcherActorID);
if (!watcherConnectionData) {
return;
}
if (watcherConnectionData.watchedData[type]) {
watcherConnectionData.watchedData[
type
] = watcherConnectionData.watchedData[type].filter(
entry => !entries.includes(entry)
);
}
for (const {
dbg,
workerThreadServerForwardingPrefix,
} of watcherConnectionData.workers) {
dbg.postMessage(
JSON.stringify({
type: "remove-watcher-data-entry",
forwardingPrefix: workerThreadServerForwardingPrefix,
dataEntryType: type,
entries,
})
);
}
}
handleEvent({ type }) {
// DOMWindowCreated is registered from the WatcherRegistry via `ActorManagerParent.addJSWindowActors`
// as a DOM event to be listened to and so is fired by JSWindowActor platform code.
if (type == "DOMWindowCreated") {
this.onDOMWindowCreated();
}
}
_removeExistingWorkerDebuggerListener() {
if (this._workerDebuggerListener) {
wdm.removeListener(this._workerDebuggerListener);
this._workerDebuggerListener = null;
}
}
/**
* Part of JSActor API
* https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
*
* > The didDestroy method, if present, will be called after the actor is no
* > longer able to receive any more messages.
*/
didDestroy() {
this._removeExistingWorkerDebuggerListener();
for (const [watcherActorID, watcherConnectionData] of this._connections) {
const { connection } = watcherConnectionData;
this._destroyTargetActors(watcherActorID);
connection.close();
}
this._connections.clear();
}
}
/**
* Helper function to know if we should watch for workers on a given windowGlobal
*/
function shouldNotifyWindowGlobal(windowGlobal, watchedBrowserId) {
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 (watchedBrowserId && browsingContext.browserId != watchedBrowserId) {
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.
*
* @returns {Promise} Returns a Promise that resolves once the data entry were handled
* by the worker target.
*/
function addWatcherDataEntryInWorkerTarget({
dbg,
workerThreadServerForwardingPrefix,
type,
entries,
}) {
return new Promise(resolve => {
// Wait until we're notified by the worker that the resources are watched.
// This is important so we know existing resources were handled.
const listener = {
onMessage: message => {
message = JSON.parse(message);
if (message.type === "watcher-data-entry-added") {
resolve();
dbg.removeListener(listener);
}
},
// Resolve if the worker is being destroyed so we don't have a dangling promise.
onClose: () => resolve(),
};
dbg.addListener(listener);
dbg.postMessage(
JSON.stringify({
type: "add-watcher-data-entry",
forwardingPrefix: workerThreadServerForwardingPrefix,
dataEntryType: type,
entries,
})
);
});
}

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

@ -0,0 +1,260 @@
/* 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 = ["DevToolsWorkerParent"];
const { loader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const { WatcherRegistry } = ChromeUtils.import(
"resource://devtools/server/actors/watcher/WatcherRegistry.jsm"
);
loader.lazyRequireGetter(
this,
"JsWindowActorTransport",
"devtools/shared/transport/js-window-actor-transport",
true
);
class DevToolsWorkerParent extends JSWindowActorParent {
constructor() {
super();
this._destroyed = false;
// Map of DevToolsServerConnection's used to forward the messages from/to
// the client. The connections run in the parent process, as this code. We
// may have more than one when there is more than one client debugging the
// same worker. For example, a content toolbox and the browser toolbox.
//
// The map is indexed by the connection prefix, and the values are object with the
// following properties:
// - watcher: The WatcherActor
// - actors: A Map of the worker target actors form, indexed by WorkerTarget actorID
// - transport: the JsWindowActorTransport
//
// Reminder about prefixes: all DevToolsServerConnections have a `prefix`
// which can be considered as a kind of id. On top of this, parent process
// DevToolsServerConnections also have forwarding prefixes because they are
// responsible for forwarding messages to content process connections.
this._connections = new Map();
this._onConnectionClosed = this._onConnectionClosed.bind(this);
EventEmitter.decorate(this);
}
/**
* Request the content process to create Worker Targets if workers matching the browserId
* are already available.
*/
instantiateWorkerTargets({
watcherActorID,
connectionPrefix,
browserId,
watchedData,
}) {
return this.sendQuery(
"DevToolsWorkerParent:instantiate-already-available",
{
watcherActorID,
connectionPrefix,
browserId,
watchedData,
}
);
}
destroyWorkerTargets({ watcherActorID, browserId }) {
return this.sendAsyncMessage("DevToolsWorkerParent:destroy", {
watcherActorID,
browserId,
});
}
/**
* Communicate to the content process that some data have been added.
*/
addWatcherDataEntry({ watcherActorID, type, entries }) {
return this.sendQuery("DevToolsWorkerParent:addWatcherDataEntry", {
watcherActorID,
type,
entries,
});
}
/**
* Communicate to the content process that some data have been removed.
*/
removeWatcherDataEntry({ watcherActorID, type, entries }) {
this.sendAsyncMessage("DevToolsWorkerParent:removeWatcherDataEntry", {
watcherActorID,
type,
entries,
});
}
workerTargetAvailable({
watcherActorID,
forwardingPrefix,
workerTargetForm,
}) {
const watcher = WatcherRegistry.getWatcher(watcherActorID);
if (!watcher) {
throw new Error(
`Watcher Actor with ID '${watcherActorID}' can't be found.`
);
}
const connection = watcher.conn;
const { prefix } = connection;
if (!this._connections.has(prefix)) {
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(prefix, {
watcher,
transport,
actors: new Map(),
});
}
const workerTargetActorId = workerTargetForm.actor;
this._connections
.get(prefix)
.actors.set(workerTargetActorId, workerTargetForm);
watcher.notifyTargetAvailable(workerTargetForm);
}
workerTargetDestroyed({
watcherActorID,
forwardingPrefix,
workerTargetForm,
}) {
const watcher = WatcherRegistry.getWatcher(watcherActorID);
if (!watcher) {
throw new Error(
`Watcher Actor with ID '${watcherActorID}' can't be found.`
);
}
const connection = watcher.conn;
const { prefix } = connection;
if (!this._connections.has(prefix)) {
return;
}
const workerTargetActorId = workerTargetForm.actor;
const { actors } = this._connections.get(prefix);
if (!actors.has(workerTargetActorId)) {
return;
}
actors.delete(workerTargetActorId);
watcher.notifyTargetDestroyed(workerTargetForm);
}
_onConnectionClosed(status, prefix) {
if (this._connections.has(prefix)) {
const { watcher } = this._connections.get(prefix);
this._cleanupConnection(watcher.conn);
}
}
async _cleanupConnection(connection) {
const { transport } = this._connections.get(connection.prefix);
connection.off("closed", this._onConnectionClosed);
if (transport) {
// If we have a child transport, the actor has already
// been created. We need to stop using this transport.
transport.close();
}
connection.cancelForwarding(transport._prefix);
this._connections.delete(connection.prefix);
if (!this._connections.size) {
this._destroy();
}
}
_destroy() {
if (this._destroyed) {
return;
}
this._destroyed = true;
for (const { actors, watcher } of this._connections.values()) {
for (const actor of actors.values()) {
watcher.notifyTargetDestroyed(actor);
}
this._cleanupConnection(watcher.conn);
}
}
/**
* Part of JSActor API
* https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
*
* > The didDestroy method, if present, will be called after the (JSWindow)actor is no
* > longer able to receive any more messages.
*/
didDestroy() {
this._destroy();
}
/**
* Supported Queries
*/
async sendPacket(packet, prefix) {
return this.sendAsyncMessage("DevToolsWorkerParent:packet", {
packet,
prefix,
});
}
/**
* JsWindowActor API
*/
async sendQuery(msg, args) {
try {
const res = await super.sendQuery(msg, args);
return res;
} catch (e) {
console.error("Failed to sendQuery in DevToolsWorkerParent", msg, e);
throw e;
}
}
receiveMessage(message) {
switch (message.name) {
case "DevToolsWorkerChild:workerTargetAvailable":
return this.workerTargetAvailable(message.data);
case "DevToolsWorkerChild:workerTargetDestroyed":
return this.workerTargetDestroyed(message.data);
case "DevToolsWorkerChild:packet":
return this.emit("packet-received", message);
default:
throw new Error(
"Unsupported message in DevToolsWorkerParent: " + message.name
);
}
}
}

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

@ -7,5 +7,7 @@
DevToolsModules(
'DevToolsFrameChild.jsm',
'DevToolsFrameParent.jsm',
'DevToolsWorkerChild.jsm',
'DevToolsWorkerParent.jsm',
'WindowGlobalLogger.jsm',
)

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

@ -21,6 +21,8 @@ loader.lazyRequireGetter(
* @params {String} forwardingPrefix: The prefix that will be used to forward messages
* to the DevToolsServer on the worker thread.
* @params {Object} options: An option object that will be passed with the "connect" packet.
* @params {Object} options.watchedData: The watchedData object that will be passed to the
* worker target actor.
*/
function connectToWorker(connection, dbg, forwardingPrefix, options) {
return new Promise((resolve, reject) => {
@ -100,6 +102,11 @@ function connectToWorker(connection, dbg, forwardingPrefix, options) {
type: "connect",
forwardingPrefix,
options,
workerDebuggerData: {
id: dbg.id,
type: dbg.type,
url: dbg.url,
},
})
);

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

@ -345,10 +345,10 @@ var DevToolsServer = {
return this._onConnection(transport, prefix, true);
},
connectToParentWindowActor(devtoolsFrameActor, forwardingPrefix) {
connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
this._checkInit();
const transport = new JsWindowActorTransport(
devtoolsFrameActor,
jsWindowChildActor,
forwardingPrefix
);

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

@ -54,7 +54,7 @@ DevToolsServer.createRootActor = function() {
// that, we handle a Map of the different connections, keyed by forwarding prefix.
const connections = new Map();
this.addEventListener("message", function(event) {
this.addEventListener("message", async function(event) {
const packet = JSON.parse(event.data);
switch (packet.type) {
case "connect":
@ -64,7 +64,11 @@ this.addEventListener("message", function(event) {
const connection = DevToolsServer.connectToParent(forwardingPrefix, this);
// Step 4: Create a WorkerTarget actor.
const workerTargetActor = new WorkerTargetActor(connection, global);
const workerTargetActor = new WorkerTargetActor(
connection,
global,
packet.workerDebuggerData
);
// Make the worker manage itself so it is put in a Pool and assigned an actorID.
workerTargetActor.manage(workerTargetActor);
@ -80,6 +84,7 @@ this.addEventListener("message", function(event) {
// it that a connection has been established.
connections.set(forwardingPrefix, {
connection,
workerTargetActor,
});
postMessage(
@ -90,6 +95,36 @@ this.addEventListener("message", function(event) {
})
);
// We might receive data to watch.
if (packet.options?.watchedData) {
const promises = [];
for (const [type, entries] of Object.entries(
packet.options.watchedData
)) {
promises.push(workerTargetActor.addWatcherDataEntry(type, entries));
}
await Promise.all(promises);
}
break;
case "add-watcher-data-entry":
await connections
.get(packet.forwardingPrefix)
.workerTargetActor.addWatcherDataEntry(
packet.dataEntryType,
packet.entries
);
postMessage(JSON.stringify({ type: "watcher-data-entry-added" }));
break;
case "remove-watcher-data-entry":
await connections
.get(packet.forwardingPrefix)
.workerTargetActor.removeWatcherDataEntry(
packet.dataEntryType,
packet.entries
);
break;
case "disconnect":

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

@ -363,7 +363,7 @@ class TargetList extends EventEmitter {
return TargetList.TYPES.PROCESS;
}
if (typeName == "workerDescriptor") {
if (typeName == "workerDescriptor" || typeName == "workerTarget") {
if (target.isSharedWorker) {
return TargetList.TYPES.SHARED_WORKER;
}

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

@ -28,7 +28,10 @@ async function createLocalClient() {
return client;
}
async function initResourceWatcherAndTarget(tab) {
async function initResourceWatcherAndTarget(
tab,
{ listenForWorkers = false } = {}
) {
const { TargetList } = require("devtools/shared/resources/target-list");
const {
ResourceWatcher,
@ -46,6 +49,9 @@ async function initResourceWatcherAndTarget(tab) {
const target = await descriptor.getTarget();
const targetList = new TargetList(client.mainRoot, target);
if (listenForWorkers) {
targetList.listenForWorkers = true;
}
await targetList.startListening();
// Now create a ResourceWatcher

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

@ -285,7 +285,7 @@ const Types = (exports.__TypesForTests = [
{
types: ["workerTarget"],
spec: "devtools/shared/specs/targets/worker",
front: null,
front: "devtools/client/fronts/targets/worker",
},
{
types: ["thread"],

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

@ -3,15 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { generateActorSpec } = require("devtools/shared/protocol");
// XXX: This actor doesn't expose any methods and events yet, but will in the future
// (e.g. resource-available-form and resource-destroyed-form).
const { Arg, generateActorSpec } = require("devtools/shared/protocol");
const workerTargetSpec = generateActorSpec({
typeName: "workerTarget",
methods: {},
events: {},
events: {
"resource-available-form": {
type: "resource-available-form",
resources: Arg(0, "array:json"),
},
},
});
exports.workerTargetSpec = workerTargetSpec;