Source code

Revision control

Copy as Markdown

Other Tools

/* 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 { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule(
"resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs",
// ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent
// which also has to be a true singleton.
{ global: "shared" }
);
const lazy = {};
loader.lazyRequireGetter(
lazy,
"JsWindowActorTransport",
"devtools/shared/transport/js-window-actor-transport",
true
);
export class DevToolsProcessParent extends JSProcessActorParent {
constructor() {
super();
// 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 frame. For example, a content toolbox and the browser toolbox.
//
// The map is indexed by the connection prefix.
// The values are objects containing the following properties:
// - actor: the frame target actor(as a form)
// - connection: the DevToolsServerConnection used to communicate with the
// frame target actor
// - prefix: the forwarding prefix used by the connection to know
// how to forward packets to the frame target
// - 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.
EventEmitter.decorate(this);
}
// Boolean to indicate if the related content process is slow to respond
// and may be hanging or frozen. When true, we should avoid waiting for its replies.
#frozen = false;
#destroyed = false;
#connections = new Map();
/**
* Request the content process to create all the targets currently watched
* and start observing for new ones to be created later.
*/
watchTargets({ watcherActorID, targetType }) {
return this.sendQuery("DevToolsProcessParent:watchTargets", {
watcherActorID,
targetType,
});
}
/**
* Request the content process to stop observing for currently watched targets
* and destroy all the currently active ones.
*/
unwatchTargets({ watcherActorID, targetType, options }) {
this.sendAsyncMessage("DevToolsProcessParent:unwatchTargets", {
watcherActorID,
targetType,
options,
});
}
/**
* Communicate to the content process that some data have been added or set.
*/
addOrSetSessionDataEntry({ watcherActorID, type, entries, updateType }) {
return this.sendQuery("DevToolsProcessParent:addOrSetSessionDataEntry", {
watcherActorID,
type,
entries,
updateType,
});
}
/**
* Communicate to the content process that some data have been removed.
*/
removeSessionDataEntry({ watcherActorID, type, entries }) {
this.sendAsyncMessage("DevToolsProcessParent:removeSessionDataEntry", {
watcherActorID,
type,
entries,
});
}
destroyWatcher({ watcherActorID }) {
return this.sendAsyncMessage("DevToolsProcessParent:destroyWatcher", {
watcherActorID,
});
}
/**
* Called when the content process notified us about a new target actor
*/
#onTargetAvailable({ watcherActorID, forwardingPrefix, targetActorForm }) {
const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
if (!watcher) {
throw new Error(
`Watcher Actor with ID '${watcherActorID}' can't be found.`
);
}
const connection = watcher.conn;
// If this is the first target actor for this watcher,
// hook up the DevToolsServerConnection which will bridge
// communication between the parent process DevToolsServer
// and the content process.
if (!this.#connections.get(watcher.conn.prefix)) {
connection.on("closed", this.#onConnectionClosed);
// Create a js-window-actor based transport.
const transport = new lazy.JsWindowActorTransport(
this,
forwardingPrefix,
"DevToolsProcessParent:packet"
);
transport.hooks = {
onPacket: connection.send.bind(connection),
onClosed() {},
};
transport.ready();
connection.setForwarding(forwardingPrefix, transport);
this.#connections.set(watcher.conn.prefix, {
watcher,
connection,
// This prefix is the prefix of the DevToolsServerConnection, running
// in the content process, for which we should forward packets to, based on its prefix.
// While `watcher.connection` is also a DevToolsServerConnection, but from this process,
// the parent process. It is the one receiving Client packets and the one, from which
// we should forward packets from.
forwardingPrefix,
transport,
targetActorForms: [],
});
}
this.#connections
.get(watcher.conn.prefix)
.targetActorForms.push(targetActorForm);
watcher.notifyTargetAvailable(targetActorForm);
}
/**
* Called when the content process notified us about a target actor that has been destroyed.
*/
#onTargetDestroyed({ actors, options }) {
for (const { watcherActorID, targetActorForm } of actors) {
const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID);
// As we instruct to destroy all targets when the watcher is destroyed,
// we may easily receive the target destruction notification *after*
// the watcher has been removed from the registry.
if (!watcher || watcher.isDestroyed()) {
continue;
}
watcher.notifyTargetDestroyed(targetActorForm, options);
const connectionInfo = this.#connections.get(watcher.conn.prefix);
if (connectionInfo) {
const idx = connectionInfo.targetActorForms.findIndex(
form => form.actor == targetActorForm.actor
);
if (idx != -1) {
connectionInfo.targetActorForms.splice(idx, 1);
}
// Once the last active target is removed, disconnect the DevTools transport
// and cleanup everything bound to this DOM Process. We will re-instantiate
// a new connection/transport on the next reported target actor.
if (!connectionInfo.targetActorForms.length) {
this.#cleanupConnection(connectionInfo.connection);
}
}
}
}
#onConnectionClosed = (status, prefix) => {
if (this.#connections.has(prefix)) {
const { connection } = this.#connections.get(prefix);
this.#cleanupConnection(connection);
}
};
/**
* Close and unregister a given DevToolsServerConnection.
*
* @param {DevToolsServerConnection} connection
* @param {object} options
* @param {boolean} options.isModeSwitching
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
async #cleanupConnection(connection, options = {}) {
const watcherConnectionInfo = this.#connections.get(connection.prefix);
if (watcherConnectionInfo) {
const { forwardingPrefix, transport } = watcherConnectionInfo;
if (transport) {
// If we have a child transport, the actor has already
// been created. We need to stop using this transport.
transport.close(options);
}
// When cancelling the forwarding, one RDP event is sent to the client to purge all requests
// and actors related to a given prefix.
// Be careful that any late RDP event would be ignored by the client passed this call.
connection.cancelForwarding(forwardingPrefix);
}
connection.off("closed", this.#onConnectionClosed);
this.#connections.delete(connection.prefix);
if (!this.#connections.size) {
this.#destroy(options);
}
}
/**
* Destroy and cleanup everything for this DOM Process.
*
* @param {object} options
* @param {boolean} options.isModeSwitching
* true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
*/
#destroy(options) {
if (this.#destroyed) {
return;
}
this.#destroyed = true;
for (const {
targetActorForms,
connection,
watcher,
} of this.#connections.values()) {
for (const actor of targetActorForms) {
watcher.notifyTargetDestroyed(actor, options);
}
this.#cleanupConnection(connection, options);
}
}
/**
* Used by DevTools Transport to send packets to the content process.
*/
sendPacket(packet, prefix) {
this.sendAsyncMessage("DevToolsProcessParent:packet", { packet, prefix });
}
/**
* JsProcessActor API
*/
/**
* JS Actor override of `sendQuery` method, whose main goal is the ignore possibly freezing processes.
* This also prints a warning when the query failed to be sent, or when a process hangs.
*
* @param String msg
* @param Array<json> args
* @return Promise<undefined>
* We only use sendQuery for two queries ("watchTargets" and "addOrSetSessionDataEntry") and
* none of them use any returned value (except a promise to know when their processing is done).
*/
async sendQuery(msg, args) {
// If any preview query timed out and did not reply yet, the process is considered frozen
// and are no longer waiting for the process response.
if (this.#frozen) {
this.sendAsyncMessage(msg, args);
return Promise.resolve();
}
// Cache `osPid` and avoid querying `this.manager` attribute later as it may result into
// a `AssertReturnTypeMatchesJitinfo` assertion crash into GenericGetter .
const { osPid } = this.manager;
return new Promise((resolve, reject) => {
// The process may be slow to resolve the query, or even be completely frozen.
// Use a timeout to detect when it happens.
const timeout = setTimeout(() => {
this.#frozen = true;
console.error(
`Content process ${osPid} isn't responsive while sending "${msg}" request. DevTools will ignore this process for now.`
);
// Do not consider timeout as an error as it may easily break the frontend.
resolve();
}, 1000);
super.sendQuery(msg, args).then(
() => {
if (this.#frozen && !this.#destroyed) {
console.error(
`Content process ${osPid} is responsive again. DevTools resumes operations against it.`
);
}
clearTimeout(timeout);
// If any of the ongoing query resolved, consider the process as responsive again
this.#frozen = false;
resolve();
},
async e => {
// Ignore frozen processes when the JS Process Actor is destroyed.
// Either the process was shut down or DevTools unregistered the Actor.
if (this.#frozen && !this.#destroyed) {
console.error(
`Content process ${osPid} is responsive again. DevTools resumes operations against it.`
);
}
clearTimeout(timeout);
// If any of the ongoing query resolved, consider the process as responsive again
this.#frozen = false;
console.error("Failed to sendQuery in DevToolsProcessParent", msg);
console.error(e.toString());
reject(e);
}
);
});
}
/**
* Called by the JSProcessActor API when the content process sent us a message
*/
receiveMessage(message) {
switch (message.name) {
case "DevToolsProcessChild:targetAvailable":
return this.#onTargetAvailable(message.data);
case "DevToolsProcessChild:packet":
return this.emit("packet-received", message);
case "DevToolsProcessChild:targetDestroyed":
return this.#onTargetDestroyed(message.data);
case "DevToolsProcessChild:bf-cache-navigation-pageshow": {
const browsingContext = BrowsingContext.get(
message.data.browsingContextId
);
for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
browsingContext.browserId
)) {
watcherActor.emit("bf-cache-navigation-pageshow", {
windowGlobal: browsingContext.currentWindowGlobal,
});
}
return null;
}
case "DevToolsProcessChild:bf-cache-navigation-pagehide": {
const browsingContext = BrowsingContext.get(
message.data.browsingContextId
);
for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId(
browsingContext.browserId
)) {
watcherActor.emit("bf-cache-navigation-pagehide", {
windowGlobal: browsingContext.currentWindowGlobal,
});
}
return null;
}
default:
throw new Error(
"Unsupported message in DevToolsProcessParent: " + message.name
);
}
}
/**
* Called by the JSProcessActor API when this content process is destroyed.
*/
didDestroy() {
this.#destroy();
}
}
export class BrowserToolboxDevToolsProcessParent extends DevToolsProcessParent {}