Avoid unregistering the JS Process actor ASAP on DevTools closing as it may throw many exception if any RDP Request or JSProcess Actor query is still pending after the devtools closing. It required some tweaks in the parent process as WatcherActor.conn may now be null because of shifted executions and we weren't correctly unregistering it because of lack of `connectionPrefix`. As well as some tweaks around `sharedData` to force persisting it across processes, otherwise the content process may still hold now destroyed watcher actor sessionData and keep instantiating actor. Last, but not least, because there is some shift in precise execution timings, I had to tweak docshell DOM event listeners as we may leave listeners on docshell's window object when the cleanup method is call after the docshell starts being destroyed and docshell.domWindow is already null. Differential Revision: https://phabricator.services.mozilla.com/D306953
569 lines
19 KiB
JavaScript
569 lines
19 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
|
|
import { ContentProcessWatcherRegistry } from "resource://devtools/server/connectors/js-process-actor/ContentProcessWatcherRegistry.sys.mjs";
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(
|
|
lazy,
|
|
{
|
|
ContentScriptTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/content_script.sys.mjs",
|
|
ProcessTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/process.sys.mjs",
|
|
SessionDataHelpers:
|
|
"resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs",
|
|
ServiceWorkerTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/service_worker.sys.mjs",
|
|
SharedWorkerTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/shared_worker.sys.mjs",
|
|
WorkerTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/worker.sys.mjs",
|
|
WindowGlobalTargetWatcher:
|
|
"resource://devtools/server/connectors/js-process-actor/target-watchers/window-global.sys.mjs",
|
|
},
|
|
{ global: "contextual" }
|
|
);
|
|
|
|
// TargetActorRegistery has to be shared between all devtools instances
|
|
// and so is loaded into the shared global.
|
|
ChromeUtils.defineESModuleGetters(
|
|
lazy,
|
|
{
|
|
TargetActorRegistry:
|
|
"resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
|
|
},
|
|
{ global: "shared" }
|
|
);
|
|
|
|
export class DevToolsProcessChild extends JSProcessActorChild {
|
|
constructor() {
|
|
super();
|
|
|
|
// The EventEmitter interface is used for DevToolsTransport's packet-received event.
|
|
EventEmitter.decorate(this);
|
|
}
|
|
|
|
#watchers = {
|
|
// Keys are target types, which are defined in this CommonJS Module:
|
|
// https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/devtools/server/actors/targets/index.js#7-14
|
|
// We avoid loading it as this ESM should be lightweight and avoid spawning DevTools CommonJS Loader until
|
|
// whe know we have to instantiate a Target Actor.
|
|
frame: {
|
|
// Number of active watcher actors currently watching for the given target type
|
|
activeListener: 0,
|
|
|
|
// Instance of a target watcher class whose task is to observe new target instances
|
|
get watcher() {
|
|
return lazy.WindowGlobalTargetWatcher;
|
|
},
|
|
},
|
|
|
|
process: {
|
|
activeListener: 0,
|
|
get watcher() {
|
|
return lazy.ProcessTargetWatcher;
|
|
},
|
|
},
|
|
|
|
worker: {
|
|
activeListener: 0,
|
|
get watcher() {
|
|
return lazy.WorkerTargetWatcher;
|
|
},
|
|
},
|
|
|
|
service_worker: {
|
|
activeListener: 0,
|
|
get watcher() {
|
|
return lazy.ServiceWorkerTargetWatcher;
|
|
},
|
|
},
|
|
|
|
shared_worker: {
|
|
activeListener: 0,
|
|
get watcher() {
|
|
return lazy.SharedWorkerTargetWatcher;
|
|
},
|
|
},
|
|
|
|
content_script: {
|
|
activeListener: 0,
|
|
get watcher() {
|
|
return lazy.ContentScriptTargetWatcher;
|
|
},
|
|
},
|
|
};
|
|
|
|
#initialized = false;
|
|
|
|
/**
|
|
* Called when this JSProcess Actor instantiate either when we start observing for first target types,
|
|
* or when the process just started.
|
|
*/
|
|
instantiate() {
|
|
if (this.#initialized) {
|
|
return;
|
|
}
|
|
this.#initialized = true;
|
|
// Create and watch for future target actors for each watcher currently watching some target types
|
|
for (const watcherDataObject of ContentProcessWatcherRegistry.getAllWatchersDataObjects()) {
|
|
this.#watchInitialTargetsForWatcher(watcherDataObject);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiate and watch future target actors based on the already watched targets.
|
|
*
|
|
* @param Object watcherDataObject
|
|
* See ContentProcessWatcherRegistry.
|
|
*/
|
|
#watchInitialTargetsForWatcher(watcherDataObject) {
|
|
const { sessionData, sessionContext } = watcherDataObject;
|
|
|
|
// About WebExtension, see note in addOrSetSessionDataEntry.
|
|
// Their target actor aren't created by this class, but session data is still managed by it
|
|
// and we need to pass the initial session data coming to already instantiated target actor.
|
|
if (sessionContext.type == "webextension") {
|
|
const { watcherActorID } = watcherDataObject;
|
|
const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
|
|
const targetActors = lazy.TargetActorRegistry.getTargetActors(
|
|
sessionContext,
|
|
connectionPrefix
|
|
);
|
|
if (targetActors.length) {
|
|
// Pass initialization data to the target actor
|
|
for (const type in sessionData) {
|
|
// `sessionData` will also contain `browserId` as well as entries with empty arrays,
|
|
// which shouldn't be processed.
|
|
const entries = sessionData[type];
|
|
if (!Array.isArray(entries) || !entries.length) {
|
|
continue;
|
|
}
|
|
targetActors[0].addOrSetSessionDataEntry(type, entries, false, "set");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ignore the call if the watched targets property isn't populated yet.
|
|
// This typically happens when instantiating the JS Process Actor on toolbox opening,
|
|
// where the actor is spawn early and a watchTarget message comes later with the `targets` array set.
|
|
if (!sessionData.targets) {
|
|
return;
|
|
}
|
|
|
|
for (const targetType of sessionData.targets) {
|
|
this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiate and watch future target actors based on the already watched targets.
|
|
*
|
|
* @param Object watcherDataObject
|
|
* See ContentProcessWatcherRegistry.
|
|
* @param String targetType
|
|
* New typeof target to start watching.
|
|
* @param Boolean isProcessActorStartup
|
|
* True when we are watching for targets during this JS Process actor instantiation.
|
|
* It shouldn't be the case on toolbox opening, but only when a new process starts.
|
|
* On toolbox opening, the Actor will receive an explicit watchTargets query.
|
|
*/
|
|
#watchNewTargetTypeForWatcher(
|
|
watcherDataObject,
|
|
targetType,
|
|
isProcessActorStartup
|
|
) {
|
|
const { watchingTargetTypes } = watcherDataObject;
|
|
// Ensure creating and watching only once per target type and watcher actor.
|
|
if (watchingTargetTypes.includes(targetType)) {
|
|
return;
|
|
}
|
|
watchingTargetTypes.push(targetType);
|
|
|
|
// Update sessionData as watched target types are a Session Data
|
|
// used later for example by worker target watcher
|
|
lazy.SessionDataHelpers.addOrSetSessionDataEntry(
|
|
watcherDataObject.sessionData,
|
|
"targets",
|
|
[targetType],
|
|
"add"
|
|
);
|
|
|
|
this.#watchers[targetType].activeListener++;
|
|
|
|
// Start listening for platform events when we are observing this type for the first time
|
|
if (this.#watchers[targetType].activeListener === 1) {
|
|
this.#watchers[targetType].watcher.watch();
|
|
}
|
|
|
|
// And instantiate targets for the already existing instances
|
|
this.#watchers[targetType].watcher.createTargetsForWatcher(
|
|
watcherDataObject,
|
|
isProcessActorStartup
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stop watching for all target types and destroy all existing targets actor
|
|
* related to a given watcher actor.
|
|
*
|
|
* @param {object} watcherDataObject
|
|
* @param {string} targetType
|
|
* @param {object} options
|
|
*/
|
|
#unwatchTargetsForWatcher(watcherDataObject, targetType, options) {
|
|
const { watchingTargetTypes } = watcherDataObject;
|
|
const targetTypeIndex = watchingTargetTypes.indexOf(targetType);
|
|
// Ignore targetTypes which were not observed
|
|
if (targetTypeIndex === -1) {
|
|
return;
|
|
}
|
|
// Update to the new list of currently watched target types
|
|
watchingTargetTypes.splice(targetTypeIndex, 1);
|
|
|
|
// Update sessionData as watched target types are a Session Data
|
|
// used later for example by worker target watcher
|
|
lazy.SessionDataHelpers.removeSessionDataEntry(
|
|
watcherDataObject.sessionData,
|
|
"targets",
|
|
[targetType]
|
|
);
|
|
|
|
this.#watchers[targetType].activeListener--;
|
|
|
|
// Stop observing for platform events
|
|
if (this.#watchers[targetType].activeListener === 0) {
|
|
this.#watchers[targetType].watcher.unwatch();
|
|
}
|
|
|
|
// Destroy all targets which are still instantiated for this type
|
|
this.#watchers[targetType].watcher.destroyTargetsForWatcher(
|
|
watcherDataObject,
|
|
options
|
|
);
|
|
|
|
// Unregister the watcher if we stopped watching for all target types
|
|
if (!watchingTargetTypes.length) {
|
|
ContentProcessWatcherRegistry.remove(watcherDataObject);
|
|
}
|
|
|
|
// If we removed the last watcher, clean the internal state of this class.
|
|
if (ContentProcessWatcherRegistry.isEmpty()) {
|
|
this.didDestroy(options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup everything around a given watcher actor
|
|
*
|
|
* @param {object} watcherDataObject
|
|
*/
|
|
#destroyWatcher(watcherDataObject) {
|
|
const { watchingTargetTypes } = watcherDataObject;
|
|
// Clone the array as it will be modified during the loop execution
|
|
for (const targetType of [...watchingTargetTypes]) {
|
|
this.#unwatchTargetsForWatcher(watcherDataObject, targetType);
|
|
}
|
|
ContentProcessWatcherRegistry.remove(watcherDataObject);
|
|
}
|
|
|
|
/**
|
|
* Used by DevTools Transport to send packets to the content process.
|
|
*
|
|
* @param {JSON} packet
|
|
* @param {string} prefix
|
|
*/
|
|
sendPacket(packet, prefix) {
|
|
this.sendAsyncMessage("DevToolsProcessChild: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 DevToolsProcessChild", msg);
|
|
console.error(e.toString());
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the JSProcessActor API when the process process sent us a message.
|
|
*/
|
|
async receiveMessage(message) {
|
|
try {
|
|
switch (message.name) {
|
|
case "DevToolsProcessParent:watchTargets": {
|
|
const { watcherActorID, targetType } = message.data;
|
|
const watcherDataObject =
|
|
ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
|
|
this.#watchNewTargetTypeForWatcher(watcherDataObject, targetType);
|
|
break;
|
|
}
|
|
case "DevToolsProcessParent:unwatchTargets": {
|
|
const { watcherActorID, targetType, options } = message.data;
|
|
const watcherDataObject =
|
|
ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
|
|
this.#unwatchTargetsForWatcher(
|
|
watcherDataObject,
|
|
targetType,
|
|
options
|
|
);
|
|
break;
|
|
}
|
|
case "DevToolsProcessParent:addOrSetSessionDataEntry": {
|
|
const { watcherActorID, type, entries, updateType } = message.data;
|
|
await this.#addOrSetSessionDataEntry(
|
|
watcherActorID,
|
|
type,
|
|
entries,
|
|
updateType
|
|
);
|
|
break;
|
|
}
|
|
case "DevToolsProcessParent:removeSessionDataEntry": {
|
|
const { watcherActorID, type, entries } = message.data;
|
|
this.#removeSessionDataEntry(watcherActorID, type, entries);
|
|
break;
|
|
}
|
|
case "DevToolsProcessParent:destroyWatcher": {
|
|
const { watcherActorID } = message.data;
|
|
const watcherDataObject =
|
|
ContentProcessWatcherRegistry.getWatcherDataObject(
|
|
watcherActorID,
|
|
true
|
|
);
|
|
// The watcher may already be destroyed if the client unwatched for all target types.
|
|
if (watcherDataObject) {
|
|
this.#destroyWatcher(watcherDataObject);
|
|
}
|
|
break;
|
|
}
|
|
case "DevToolsProcessParent:packet":
|
|
this.emit("packet-received", message);
|
|
break;
|
|
default:
|
|
throw new Error(
|
|
"Unsupported message in DevToolsProcessParent: " + message.name
|
|
);
|
|
}
|
|
} catch (e) {
|
|
const stack =
|
|
e == "out of memory" ? ChromeUtils.getLastOOMStackTrace() : e.stack;
|
|
return {
|
|
// Follows the same logic as protocol.js's `Actor.writeError`
|
|
error: e.error || e.name || String(e),
|
|
message: e.message,
|
|
stack,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The parent process requested that some session data have been added or set.
|
|
*
|
|
* @param {string} watcherActorID
|
|
* The Watcher Actor ID requesting to add new session data
|
|
* @param {string} type
|
|
* The type of data to be added
|
|
* @param {Array<object>} entries
|
|
* The values to be added to this type of data
|
|
* @param {string} updateType
|
|
* "add" will only add the new entries in the existing data set.
|
|
* "set" will update the data set with the new entries.
|
|
*/
|
|
async #addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
|
|
const watcherDataObject =
|
|
ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID);
|
|
|
|
// Maintain the copy of `sessionData` so that it is up-to-date when
|
|
// a new worker target needs to be instantiated
|
|
const { sessionData } = watcherDataObject;
|
|
lazy.SessionDataHelpers.addOrSetSessionDataEntry(
|
|
sessionData,
|
|
type,
|
|
entries,
|
|
updateType
|
|
);
|
|
|
|
// This type is really specific to Service Workers and doesn't need to be transferred to any target.
|
|
// We only need to instantiate and destroy the target actors based on this new host.
|
|
const { watchingTargetTypes } = watcherDataObject;
|
|
if (type == "browser-element-host") {
|
|
if (watchingTargetTypes.includes("service_worker")) {
|
|
this.#watchers.service_worker.watcher.updateBrowserElementHost(
|
|
watcherDataObject
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const promises = [];
|
|
for (const targetActor of watcherDataObject.actors) {
|
|
promises.push(
|
|
targetActor.addOrSetSessionDataEntry(type, entries, false, updateType)
|
|
);
|
|
}
|
|
|
|
// Very special codepath for Web Extensions.
|
|
// Their WebExtension Target Actor is still created manually by WebExtensionDescritpor.getTarget,
|
|
// via a message manager. That, instead of being instantiated via the WatcherActor.watchTargets and this JSProcess actor.
|
|
// The Watcher Actor will still instantiate a JS Actor for the WebExt DOM Content Process
|
|
// and send the addOrSetSessionDataEntry query. But as the target actor isn't managed by the JS Actor,
|
|
// we have to manually retrieve it via the TargetActorRegistry.
|
|
if (sessionData.sessionContext.type == "webextension") {
|
|
const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
|
|
const targetActors = lazy.TargetActorRegistry.getTargetActors(
|
|
sessionData.sessionContext,
|
|
connectionPrefix
|
|
);
|
|
// We will have a single match only in the DOM Process where the add-on runs
|
|
if (targetActors.length) {
|
|
promises.push(
|
|
targetActors[0].addOrSetSessionDataEntry(
|
|
type,
|
|
entries,
|
|
false,
|
|
updateType
|
|
)
|
|
);
|
|
}
|
|
}
|
|
await Promise.all(promises);
|
|
|
|
if (watchingTargetTypes.includes("worker")) {
|
|
await this.#watchers.worker.watcher.addOrSetSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries,
|
|
updateType
|
|
);
|
|
}
|
|
if (watchingTargetTypes.includes("service_worker")) {
|
|
await this.#watchers.service_worker.watcher.addOrSetSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries,
|
|
updateType
|
|
);
|
|
}
|
|
if (watchingTargetTypes.includes("shared_worker")) {
|
|
await this.#watchers.shared_worker.watcher.addOrSetSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries,
|
|
updateType
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The parent process requested that some session data have been removed.
|
|
*
|
|
* @param {string} watcherActorID
|
|
* The Watcher Actor ID requesting to remove session data
|
|
* @param {string}} type
|
|
* The type of data to be removed
|
|
* @param {Array<object>} entries
|
|
* The values to be removed to this type of data
|
|
*/
|
|
#removeSessionDataEntry(watcherActorID, type, entries) {
|
|
const watcherDataObject =
|
|
ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID, true);
|
|
|
|
// When we unwatch resources after targets during the devtools shutdown,
|
|
// the watcher will be removed on last target type unwatch.
|
|
if (!watcherDataObject) {
|
|
return;
|
|
}
|
|
const { actors, sessionData, watchingTargetTypes } = watcherDataObject;
|
|
|
|
// Maintain the copy of `sessionData` so that it is up-to-date when
|
|
// a new worker target needs to be instantiated
|
|
lazy.SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries);
|
|
|
|
for (const targetActor of actors) {
|
|
targetActor.removeSessionDataEntry(type, entries);
|
|
}
|
|
|
|
// Special code paths for webextension toolboxes and worker targets
|
|
// See addOrSetSessionDataEntry for more details.
|
|
|
|
if (sessionData.sessionContext.type == "webextension") {
|
|
const connectionPrefix = watcherActorID.replace(/watcher\d+$/, "");
|
|
const targetActors = lazy.TargetActorRegistry.getTargetActors(
|
|
sessionData.sessionContext,
|
|
connectionPrefix
|
|
);
|
|
if (targetActors.length) {
|
|
targetActors[0].removeSessionDataEntry(type, entries);
|
|
}
|
|
}
|
|
|
|
if (watchingTargetTypes.includes("worker")) {
|
|
this.#watchers.worker.watcher.removeSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries
|
|
);
|
|
}
|
|
if (watchingTargetTypes.includes("service_worker")) {
|
|
this.#watchers.service_worker.watcher.removeSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries
|
|
);
|
|
}
|
|
if (watchingTargetTypes.includes("shared_worker")) {
|
|
this.#watchers.shared_worker.watcher.removeSessionDataEntry(
|
|
watcherDataObject,
|
|
type,
|
|
entries
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observer service notification handler.
|
|
*
|
|
* @param {DOMWindow|Document} subject
|
|
* A window for *-document-global-created
|
|
* A document for *-page-{shown|hide}
|
|
* @param {string} topic
|
|
*/
|
|
observe = (subject, topic) => {
|
|
if (topic === "init-devtools-content-process-actor") {
|
|
// This is triggered by the process actor registration and some code in process-helper.js
|
|
// which defines a unique topic to be observed
|
|
this.instantiate();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called by JS Process Actor API when the current process is destroyed,
|
|
* but also within this class when the last watcher stopped watching for targets.
|
|
*/
|
|
didDestroy() {
|
|
// Unregister all the active watchers.
|
|
// This will destroy all the active target actors and unregister the target observers.
|
|
for (const watcherDataObject of ContentProcessWatcherRegistry.getAllExistingWatchersDataObjects()) {
|
|
this.#destroyWatcher(watcherDataObject);
|
|
}
|
|
|
|
// The previous for loop should have removed all the elements,
|
|
// but just to be safe, wipe all stored data to avoid any possible leak.
|
|
ContentProcessWatcherRegistry.clear();
|
|
}
|
|
}
|
|
|
|
export class BrowserToolboxDevToolsProcessChild extends DevToolsProcessChild {}
|