Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / devtools / server / startup / content-process-script.js
blobfa91ab0c28ff3037474c772b0e12cb5873d41163
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 */
7 "use strict";
9 /**
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 {
26 constructor() {
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);
36 this.addListeners();
37 this.maybeCreateExistingTargetActors();
40 observe(subject, topic) {
41 switch (topic) {
42 case "xpcom-shutdown": {
43 this.destroy();
44 break;
49 destroy(options) {
50 this.removeListeners();
52 for (const [, connectionInfo] of this._connections) {
53 connectionInfo.connection.close(options);
55 this._connections.clear();
58 addListeners() {
59 Services.obs.addObserver(this.observe, "xpcom-shutdown");
61 Services.cpmm.addMessageListener(
62 "debug:instantiate-already-available",
63 this.receiveMessage
65 Services.cpmm.addMessageListener(
66 "debug:destroy-target",
67 this.receiveMessage
69 Services.cpmm.addMessageListener(
70 "debug:add-or-set-session-data-entry",
71 this.receiveMessage
73 Services.cpmm.addMessageListener(
74 "debug:remove-session-data-entry",
75 this.receiveMessage
77 Services.cpmm.addMessageListener(
78 "debug:destroy-process-script",
79 this.receiveMessage
83 removeListeners() {
84 Services.obs.removeObserver(this.observe, "xpcom-shutdown");
86 Services.cpmm.removeMessageListener(
87 "debug:instantiate-already-available",
88 this.receiveMessage
90 Services.cpmm.removeMessageListener(
91 "debug:destroy-target",
92 this.receiveMessage
94 Services.cpmm.removeMessageListener(
95 "debug:add-or-set-session-data-entry",
96 this.receiveMessage
98 Services.cpmm.removeMessageListener(
99 "debug:remove-session-data-entry",
100 this.receiveMessage
102 Services.cpmm.removeMessageListener(
103 "debug:destroy-process-script",
104 this.receiveMessage
108 receiveMessage(msg) {
109 switch (msg.name) {
110 case "debug:instantiate-already-available":
111 this.createTargetActor(
112 msg.data.watcherActorID,
113 msg.data.connectionPrefix,
114 msg.data.sessionData,
115 true
117 break;
118 case "debug:destroy-target":
119 this.destroyTarget(msg.data.watcherActorID);
120 break;
121 case "debug:add-or-set-session-data-entry":
122 this.addOrSetSessionDataEntry(
123 msg.data.watcherActorID,
124 msg.data.type,
125 msg.data.entries,
126 msg.data.updateType
128 break;
129 case "debug:remove-session-data-entry":
130 this.removeSessionDataEntry(
131 msg.data.watcherActorID,
132 msg.data.type,
133 msg.data.entries
135 break;
136 case "debug:destroy-process-script":
137 this.destroy(msg.data.options);
138 break;
139 default:
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.
157 if (!sharedData) {
158 Services.tm.dispatchToMainThread(
159 this.maybeCreateExistingTargetActors.bind(this)
161 return;
164 const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
165 if (!sessionDataByWatcherActor) {
166 return;
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.
197 createTargetActor(
198 watcherActorID,
199 parentConnectionPrefix,
200 sessionData,
201 ignoreAlreadyCreated = false
203 if (this._connections.get(watcherActorID)) {
204 if (ignoreAlreadyCreated) {
205 return;
207 throw new Error(
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
216 const prefix =
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,
224 data: {
225 watcherActorID,
226 parentConnectionPrefix,
227 prefix,
228 sessionContext: sessionData.sessionContext,
231 this._connections.set(watcherActorID, {
232 actor,
233 connection,
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) {
246 throw new Error(
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) {
257 throw new Error(
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", {
264 watcherActorID,
268 removeSessionDataEntry(watcherActorID, type, entries) {
269 const connectionInfo = this._connections.get(watcherActorID);
270 if (!connectionInfo) {
271 return;
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();