1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-env mozilla/process-script */
10 * Main entry point for DevTools in content processes.
12 * This module is loaded early when a content process is started.
13 * Note that (at least) JS XPCOM registered at app-startup, will be running before.
14 * It is used by the multiprocess browser toolbox in order to debug privileged resources.
15 * When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead
16 * (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm).
18 * This module won't do anything unless DevTools codebase starts adding some data
19 * in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`.
20 * Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets.
23 const SHARED_DATA_KEY_NAME
= "DevTools:watchedPerWatcher";
25 class ContentProcessStartup
{
27 // The map is indexed by the Watcher Actor ID.
28 // The values are objects containing the following properties:
29 // - connection: the DevToolsServerConnection itself
30 // - actor: the ContentProcessTargetActor instance
31 this._connections
= new Map();
33 this.observe
= this.observe
.bind(this);
34 this.receiveMessage
= this.receiveMessage
.bind(this);
37 this.maybeCreateExistingTargetActors();
40 observe(subject
, topic
) {
42 case "xpcom-shutdown": {
50 this.removeListeners();
52 for (const [, connectionInfo
] of this._connections
) {
53 connectionInfo
.connection
.close(options
);
55 this._connections
.clear();
59 Services
.obs
.addObserver(this.observe
, "xpcom-shutdown");
61 Services
.cpmm
.addMessageListener(
62 "debug:instantiate-already-available",
65 Services
.cpmm
.addMessageListener(
66 "debug:destroy-target",
69 Services
.cpmm
.addMessageListener(
70 "debug:add-or-set-session-data-entry",
73 Services
.cpmm
.addMessageListener(
74 "debug:remove-session-data-entry",
77 Services
.cpmm
.addMessageListener(
78 "debug:destroy-process-script",
84 Services
.obs
.removeObserver(this.observe
, "xpcom-shutdown");
86 Services
.cpmm
.removeMessageListener(
87 "debug:instantiate-already-available",
90 Services
.cpmm
.removeMessageListener(
91 "debug:destroy-target",
94 Services
.cpmm
.removeMessageListener(
95 "debug:add-or-set-session-data-entry",
98 Services
.cpmm
.removeMessageListener(
99 "debug:remove-session-data-entry",
102 Services
.cpmm
.removeMessageListener(
103 "debug:destroy-process-script",
108 receiveMessage(msg
) {
110 case "debug:instantiate-already-available":
111 this.createTargetActor(
112 msg
.data
.watcherActorID
,
113 msg
.data
.connectionPrefix
,
114 msg
.data
.sessionData
,
118 case "debug:destroy-target":
119 this.destroyTarget(msg
.data
.watcherActorID
);
121 case "debug:add-or-set-session-data-entry":
122 this.addOrSetSessionDataEntry(
123 msg
.data
.watcherActorID
,
129 case "debug:remove-session-data-entry":
130 this.removeSessionDataEntry(
131 msg
.data
.watcherActorID
,
136 case "debug:destroy-process-script":
137 this.destroy(msg
.data
.options
);
140 throw new Error(`Unsupported message name ${msg.name}`);
145 * Called when the content process just started.
146 * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / ParentProcessWatcherRegistry.sys.mjs)
147 * put some data in `sharedData` telling us to do so.
149 maybeCreateExistingTargetActors() {
150 const { sharedData
} = Services
.cpmm
;
152 // Accessing `sharedData` right off the app-startup returns null.
153 // Spinning the event loop with dispatchToMainThread seems enough,
154 // but it means that we let some more Javascript code run before
155 // instantiating the target actor.
156 // So we may miss a few resources and will register the breakpoints late.
158 Services
.tm
.dispatchToMainThread(
159 this.maybeCreateExistingTargetActors
.bind(this)
164 const sessionDataByWatcherActor
= sharedData
.get(SHARED_DATA_KEY_NAME
);
165 if (!sessionDataByWatcherActor
) {
169 // Create one Target actor for each prefix/client which listen to process
170 for (const [watcherActorID
, sessionData
] of sessionDataByWatcherActor
) {
171 const { connectionPrefix
, targets
} = sessionData
;
172 // This is where we only do something significant only if DevTools are opened
173 // and requesting to create target actor for content processes
174 if (targets
?.includes("process")) {
175 this.createTargetActor(watcherActorID
, connectionPrefix
, sessionData
);
181 * Instantiate a new ContentProcessTarget for the given connection.
182 * This is where we start doing some significant computation that only occurs when DevTools are opened.
184 * @param String watcherActorID
185 * The ID of the WatcherActor who requested to observe and create these target actors.
186 * @param String parentConnectionPrefix
187 * The prefix of the DevToolsServerConnection of the Watcher Actor.
188 * This is used to compute a unique ID for the target actor.
189 * @param Object sessionData
190 * All data managed by the Watcher Actor and ParentProcessWatcherRegistry.jsm, containing
191 * target types, resources types to be listened as well as breakpoints and any
192 * other data meant to be shared across processes and threads.
193 * @param Object options Dictionary with optional values:
194 * @param Boolean options.ignoreAlreadyCreated
195 * If true, do not throw if the target actor has already been created.
199 parentConnectionPrefix
,
201 ignoreAlreadyCreated
= false
203 if (this._connections
.get(watcherActorID
)) {
204 if (ignoreAlreadyCreated
) {
208 "ContentProcessStartup createTargetActor was called more than once" +
209 ` for the Watcher Actor (ID: "${watcherActorID}")`
212 // Compute a unique prefix, just for this content process,
213 // which will be used to create a ChildDebuggerTransport pair between content and parent processes.
214 // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
215 // but here, we can't have access to any DevTools connection as we are really early in the content process startup
217 parentConnectionPrefix
+ "contentProcess" + Services
.appinfo
.processID
;
218 //TODO: probably merge content-process.jsm with this module
219 const { initContentProcessTarget
} = ChromeUtils
.importESModule(
220 "resource://devtools/server/startup/content-process.sys.mjs"
222 const { actor
, connection
} = initContentProcessTarget({
223 target
: Services
.cpmm
,
226 parentConnectionPrefix
,
228 sessionContext
: sessionData
.sessionContext
,
231 this._connections
.set(watcherActorID
, {
236 // Pass initialization data to the target actor
237 for (const type
in sessionData
) {
238 actor
.addOrSetSessionDataEntry(type
, sessionData
[type
], false, "set");
242 destroyTarget(watcherActorID
) {
243 const connectionInfo
= this._connections
.get(watcherActorID
);
244 // This connection has already been cleaned?
245 if (!connectionInfo
) {
247 `Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
250 connectionInfo
.connection
.close();
251 this._connections
.delete(watcherActorID
);
254 async
addOrSetSessionDataEntry(watcherActorID
, type
, entries
, updateType
) {
255 const connectionInfo
= this._connections
.get(watcherActorID
);
256 if (!connectionInfo
) {
258 `No content process target actor for this Watcher Actor ID:"${watcherActorID}"`
261 const { actor
} = connectionInfo
;
262 await actor
.addOrSetSessionDataEntry(type
, entries
, false, updateType
);
263 Services
.cpmm
.sendAsyncMessage("debug:add-or-set-session-data-entry-done", {
268 removeSessionDataEntry(watcherActorID
, type
, entries
) {
269 const connectionInfo
= this._connections
.get(watcherActorID
);
270 if (!connectionInfo
) {
273 const { actor
} = connectionInfo
;
274 actor
.removeSessionDataEntry(type
, entries
);
278 // Only start this component for content processes.
279 // i.e. explicitely avoid running it for the parent process
280 if (Services
.appinfo
.processType
== Services
.appinfo
.PROCESS_TYPE_CONTENT
) {
281 new ContentProcessStartup();