/* 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(); 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; // Map of various data specific to each active Watcher Actor. // A Watcher will become "active" once we receive a first target actor // notified by the content process, which happens only once // the watcher starts watching for some target types. // // This Map is keyed by "watcher connection prefix". // This is a unique prefix, per watcher actor, which will // be used in the "forwardingPrefix" documented below. // // Note that We may have multiple Watcher actors, // which will resuse the same DevToolsServerConnection (watcher.conn) // if they are instantiated from the same client connection. // Or be using distinct DevToolsServerConnection, if they // are instantiated via distinct client connections. // // The Map's values are objects containing the following properties: // - watcher: // The watcher actor instance. // - targetActorForms: // The list of all active target actor forms // related to this watcher actor. // - transport: // The JsWindowActorTransport which will receive and emit // the RDP packets from the current JS Process Actor's content process. // We spawn one transpart per Watcher and Content process pair. // - forwardingPrefix: // The forwarding prefix is specific to the transport. // It helps redirect RDP packets between this "transport" and // the DevToolsServerConnection (watcher.conn), in the parent process, // which receives and emits RDP packet from/to the client. #watchers = 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.#watchers.get(watcher.watcherConnectionPrefix)) { 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.#watchers.set(watcher.watcherConnectionPrefix, { watcher, // 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.#watchers .get(watcher.watcherConnectionPrefix) .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 watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix); if (watcherInfo) { const idx = watcherInfo.targetActorForms.findIndex( form => form.actor == targetActorForm.actor ); if (idx != -1) { watcherInfo.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 (!watcherInfo.targetActorForms.length) { this.#unregisterWatcher(watcherInfo.watcher, options); } } } } #onConnectionClosed = (status, prefix) => { for (const watcherInfo of this.#watchers.values()) { if (watcherInfo.watcher.conn.prefix == prefix) { this.#unregisterWatcher(watcherInfo.watcher); } } }; /** * Unregister a given watcher. * This will also close and unregister the related given DevToolsServerConnection, * if no other watcher is active on the same, possibly shared, connection. * (when remote debugging many tabs on the same connection) * * @param {WatcherActor} watcher * @param {object} options * @param {boolean} options.isModeSwitching * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref */ #unregisterWatcher(watcher, options = {}) { const watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix); if (!watcherInfo) { return; } this.#watchers.delete(watcher.watcherConnectionPrefix); for (const actor of watcherInfo.targetActorForms) { watcherInfo.watcher.notifyTargetDestroyed(actor, options); } let connectionUsedByAnotherWatcher = false; for (const info of this.#watchers.values()) { if (info.watcher.conn == watcherInfo.watcher.conn) { connectionUsedByAnotherWatcher = true; break; } } if (!connectionUsedByAnotherWatcher) { const { forwardingPrefix, transport } = watcherInfo; 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. watcherInfo.watcher.conn.cancelForwarding(forwardingPrefix); } if (!this.#watchers.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 watcherInfo of this.#watchers.values()) { this.#unregisterWatcher(watcherInfo.watcher, 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 args * @return Promise * 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 {}