412 lines
14 KiB
JavaScript
412 lines
14 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 { 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<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 {}
|