282 lines
9.5 KiB
JavaScript
282 lines
9.5 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/. */
|
|
|
|
/* eslint-env mozilla/process-script */
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* Main entry point for DevTools in content processes.
|
|
*
|
|
* This module is loaded early when a content process is started.
|
|
* Note that (at least) JS XPCOM registered at app-startup, will be running before.
|
|
* It is used by the multiprocess browser toolbox in order to debug privileged resources.
|
|
* When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead
|
|
* (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm).
|
|
*
|
|
* This module won't do anything unless DevTools codebase starts adding some data
|
|
* in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`.
|
|
* Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets.
|
|
*/
|
|
|
|
const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
|
|
|
|
class ContentProcessStartup {
|
|
constructor() {
|
|
// The map is indexed by the Watcher Actor ID.
|
|
// The values are objects containing the following properties:
|
|
// - connection: the DevToolsServerConnection itself
|
|
// - actor: the ContentProcessTargetActor instance
|
|
this._connections = new Map();
|
|
|
|
this.observe = this.observe.bind(this);
|
|
this.receiveMessage = this.receiveMessage.bind(this);
|
|
|
|
this.addListeners();
|
|
this.maybeCreateExistingTargetActors();
|
|
}
|
|
|
|
observe(subject, topic) {
|
|
switch (topic) {
|
|
case "xpcom-shutdown": {
|
|
this.destroy();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy(options) {
|
|
this.removeListeners();
|
|
|
|
for (const [, connectionInfo] of this._connections) {
|
|
connectionInfo.connection.close(options);
|
|
}
|
|
this._connections.clear();
|
|
}
|
|
|
|
addListeners() {
|
|
Services.obs.addObserver(this.observe, "xpcom-shutdown");
|
|
|
|
Services.cpmm.addMessageListener(
|
|
"debug:instantiate-already-available",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.addMessageListener(
|
|
"debug:destroy-target",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.addMessageListener(
|
|
"debug:add-or-set-session-data-entry",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.addMessageListener(
|
|
"debug:remove-session-data-entry",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.addMessageListener(
|
|
"debug:destroy-process-script",
|
|
this.receiveMessage
|
|
);
|
|
}
|
|
|
|
removeListeners() {
|
|
Services.obs.removeObserver(this.observe, "xpcom-shutdown");
|
|
|
|
Services.cpmm.removeMessageListener(
|
|
"debug:instantiate-already-available",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.removeMessageListener(
|
|
"debug:destroy-target",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.removeMessageListener(
|
|
"debug:add-or-set-session-data-entry",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.removeMessageListener(
|
|
"debug:remove-session-data-entry",
|
|
this.receiveMessage
|
|
);
|
|
Services.cpmm.removeMessageListener(
|
|
"debug:destroy-process-script",
|
|
this.receiveMessage
|
|
);
|
|
}
|
|
|
|
receiveMessage(msg) {
|
|
switch (msg.name) {
|
|
case "debug:instantiate-already-available":
|
|
this.createTargetActor(
|
|
msg.data.watcherActorID,
|
|
msg.data.connectionPrefix,
|
|
msg.data.sessionData,
|
|
true
|
|
);
|
|
break;
|
|
case "debug:destroy-target":
|
|
this.destroyTarget(msg.data.watcherActorID);
|
|
break;
|
|
case "debug:add-or-set-session-data-entry":
|
|
this.addOrSetSessionDataEntry(
|
|
msg.data.watcherActorID,
|
|
msg.data.type,
|
|
msg.data.entries,
|
|
msg.data.updateType
|
|
);
|
|
break;
|
|
case "debug:remove-session-data-entry":
|
|
this.removeSessionDataEntry(
|
|
msg.data.watcherActorID,
|
|
msg.data.type,
|
|
msg.data.entries
|
|
);
|
|
break;
|
|
case "debug:destroy-process-script":
|
|
this.destroy(msg.data.options);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported message name ${msg.name}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the content process just started.
|
|
* This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / ParentProcessWatcherRegistry.sys.mjs)
|
|
* put some data in `sharedData` telling us to do so.
|
|
*/
|
|
maybeCreateExistingTargetActors() {
|
|
const { sharedData } = Services.cpmm;
|
|
|
|
// Accessing `sharedData` right off the app-startup returns null.
|
|
// Spinning the event loop with dispatchToMainThread seems enough,
|
|
// but it means that we let some more Javascript code run before
|
|
// instantiating the target actor.
|
|
// So we may miss a few resources and will register the breakpoints late.
|
|
if (!sharedData) {
|
|
Services.tm.dispatchToMainThread(
|
|
this.maybeCreateExistingTargetActors.bind(this)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
|
|
if (!sessionDataByWatcherActor) {
|
|
return;
|
|
}
|
|
|
|
// Create one Target actor for each prefix/client which listen to process
|
|
for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
|
|
const { connectionPrefix, targets } = sessionData;
|
|
// This is where we only do something significant only if DevTools are opened
|
|
// and requesting to create target actor for content processes
|
|
if (targets?.includes("process")) {
|
|
this.createTargetActor(watcherActorID, connectionPrefix, sessionData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiate a new ContentProcessTarget for the given connection.
|
|
* This is where we start doing some significant computation that only occurs when DevTools are opened.
|
|
*
|
|
* @param String watcherActorID
|
|
* The ID of the WatcherActor who requested to observe and create these target actors.
|
|
* @param String parentConnectionPrefix
|
|
* The prefix of the DevToolsServerConnection of the Watcher Actor.
|
|
* This is used to compute a unique ID for the target actor.
|
|
* @param Object sessionData
|
|
* All data managed by the Watcher Actor and ParentProcessWatcherRegistry.jsm, containing
|
|
* target types, resources types to be listened as well as breakpoints and any
|
|
* other data meant to be shared across processes and threads.
|
|
* @param Object options Dictionary with optional values:
|
|
* @param Boolean options.ignoreAlreadyCreated
|
|
* If true, do not throw if the target actor has already been created.
|
|
*/
|
|
createTargetActor(
|
|
watcherActorID,
|
|
parentConnectionPrefix,
|
|
sessionData,
|
|
ignoreAlreadyCreated = false
|
|
) {
|
|
if (this._connections.get(watcherActorID)) {
|
|
if (ignoreAlreadyCreated) {
|
|
return;
|
|
}
|
|
throw new Error(
|
|
"ContentProcessStartup createTargetActor was called more than once" +
|
|
` for the Watcher Actor (ID: "${watcherActorID}")`
|
|
);
|
|
}
|
|
// Compute a unique prefix, just for this content process,
|
|
// which will be used to create a ChildDebuggerTransport 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
|
|
const prefix =
|
|
parentConnectionPrefix + "contentProcess" + Services.appinfo.processID;
|
|
//TODO: probably merge content-process.jsm with this module
|
|
const { initContentProcessTarget } = ChromeUtils.importESModule(
|
|
"resource://devtools/server/startup/content-process.sys.mjs"
|
|
);
|
|
const { actor, connection } = initContentProcessTarget({
|
|
target: Services.cpmm,
|
|
data: {
|
|
watcherActorID,
|
|
parentConnectionPrefix,
|
|
prefix,
|
|
sessionContext: sessionData.sessionContext,
|
|
},
|
|
});
|
|
this._connections.set(watcherActorID, {
|
|
actor,
|
|
connection,
|
|
});
|
|
|
|
// Pass initialization data to the target actor
|
|
for (const type in sessionData) {
|
|
actor.addOrSetSessionDataEntry(type, sessionData[type], false, "set");
|
|
}
|
|
}
|
|
|
|
destroyTarget(watcherActorID) {
|
|
const connectionInfo = this._connections.get(watcherActorID);
|
|
// This connection has already been cleaned?
|
|
if (!connectionInfo) {
|
|
throw new Error(
|
|
`Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
|
|
);
|
|
}
|
|
connectionInfo.connection.close();
|
|
this._connections.delete(watcherActorID);
|
|
}
|
|
|
|
async addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
|
|
const connectionInfo = this._connections.get(watcherActorID);
|
|
if (!connectionInfo) {
|
|
throw new Error(
|
|
`No content process target actor for this Watcher Actor ID:"${watcherActorID}"`
|
|
);
|
|
}
|
|
const { actor } = connectionInfo;
|
|
await actor.addOrSetSessionDataEntry(type, entries, false, updateType);
|
|
Services.cpmm.sendAsyncMessage("debug:add-or-set-session-data-entry-done", {
|
|
watcherActorID,
|
|
});
|
|
}
|
|
|
|
removeSessionDataEntry(watcherActorID, type, entries) {
|
|
const connectionInfo = this._connections.get(watcherActorID);
|
|
if (!connectionInfo) {
|
|
return;
|
|
}
|
|
const { actor } = connectionInfo;
|
|
actor.removeSessionDataEntry(type, entries);
|
|
}
|
|
}
|
|
|
|
// Only start this component for content processes.
|
|
// i.e. explicitely avoid running it for the parent process
|
|
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
|
|
new ContentProcessStartup();
|
|
}
|