content-process-script.js (9687B)
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/. */ 4 5 "use strict"; 6 7 /** 8 * Main entry point for DevTools in content processes. 9 * 10 * This module is loaded early when a content process is started. 11 * Note that (at least) JS XPCOM registered at app-startup, will be running before. 12 * It is used by the multiprocess browser toolbox in order to debug privileged resources. 13 * When debugging a Web page loaded in a Tab, DevToolsFrame JS Window Actor is used instead 14 * (DevToolsFrameParent.jsm and DevToolsFrameChild.jsm). 15 * 16 * This module won't do anything unless DevTools codebase starts adding some data 17 * in `Services.cpmm.sharedData` object or send a message manager message via `Services.cpmm`. 18 * Also, this module is only loaded, on-demand from process-helper if devtools are watching for process targets. 19 */ 20 21 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; 22 23 class ContentProcessStartup { 24 constructor() { 25 // The map is indexed by the Watcher Actor ID. 26 // The values are objects containing the following properties: 27 // - connection: the DevToolsServerConnection itself 28 // - actor: the ContentProcessTargetActor instance 29 this._connections = new Map(); 30 31 this.observe = this.observe.bind(this); 32 this.receiveMessage = this.receiveMessage.bind(this); 33 34 this.addListeners(); 35 this.maybeCreateExistingTargetActors(); 36 } 37 38 observe(subject, topic) { 39 switch (topic) { 40 case "xpcom-shutdown": { 41 this.destroy(); 42 break; 43 } 44 } 45 } 46 47 destroy(options) { 48 this.removeListeners(); 49 50 for (const [, connectionInfo] of this._connections) { 51 connectionInfo.connection.close(options); 52 } 53 this._connections.clear(); 54 } 55 56 addListeners() { 57 Services.obs.addObserver(this.observe, "xpcom-shutdown"); 58 59 Services.cpmm.addMessageListener( 60 "debug:instantiate-already-available", 61 this.receiveMessage 62 ); 63 Services.cpmm.addMessageListener( 64 "debug:destroy-target", 65 this.receiveMessage 66 ); 67 Services.cpmm.addMessageListener( 68 "debug:add-or-set-session-data-entry", 69 this.receiveMessage 70 ); 71 Services.cpmm.addMessageListener( 72 "debug:remove-session-data-entry", 73 this.receiveMessage 74 ); 75 Services.cpmm.addMessageListener( 76 "debug:destroy-process-script", 77 this.receiveMessage 78 ); 79 } 80 81 removeListeners() { 82 Services.obs.removeObserver(this.observe, "xpcom-shutdown"); 83 84 Services.cpmm.removeMessageListener( 85 "debug:instantiate-already-available", 86 this.receiveMessage 87 ); 88 Services.cpmm.removeMessageListener( 89 "debug:destroy-target", 90 this.receiveMessage 91 ); 92 Services.cpmm.removeMessageListener( 93 "debug:add-or-set-session-data-entry", 94 this.receiveMessage 95 ); 96 Services.cpmm.removeMessageListener( 97 "debug:remove-session-data-entry", 98 this.receiveMessage 99 ); 100 Services.cpmm.removeMessageListener( 101 "debug:destroy-process-script", 102 this.receiveMessage 103 ); 104 } 105 106 receiveMessage(msg) { 107 switch (msg.name) { 108 case "debug:instantiate-already-available": 109 this.createTargetActor( 110 msg.data.watcherActorID, 111 msg.data.connectionPrefix, 112 msg.data.sessionData, 113 true 114 ); 115 break; 116 case "debug:destroy-target": 117 this.destroyTarget(msg.data.watcherActorID); 118 break; 119 case "debug:add-or-set-session-data-entry": 120 this.addOrSetSessionDataEntry( 121 msg.data.watcherActorID, 122 msg.data.type, 123 msg.data.entries, 124 msg.data.updateType 125 ); 126 break; 127 case "debug:remove-session-data-entry": 128 this.removeSessionDataEntry( 129 msg.data.watcherActorID, 130 msg.data.type, 131 msg.data.entries 132 ); 133 break; 134 case "debug:destroy-process-script": 135 this.destroy(msg.data.options); 136 break; 137 default: 138 throw new Error(`Unsupported message name ${msg.name}`); 139 } 140 } 141 142 /** 143 * Called when the content process just started. 144 * This will start creating ContentProcessTarget actors, but only if DevTools code (WatcherActor / ParentProcessWatcherRegistry.sys.mjs) 145 * put some data in `sharedData` telling us to do so. 146 */ 147 maybeCreateExistingTargetActors() { 148 const { sharedData } = Services.cpmm; 149 150 // Accessing `sharedData` right off the app-startup returns null. 151 // Spinning the event loop with dispatchToMainThread seems enough, 152 // but it means that we let some more Javascript code run before 153 // instantiating the target actor. 154 // So we may miss a few resources and will register the breakpoints late. 155 if (!sharedData) { 156 Services.tm.dispatchToMainThread( 157 this.maybeCreateExistingTargetActors.bind(this) 158 ); 159 return; 160 } 161 162 const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); 163 if (!sessionDataByWatcherActor) { 164 return; 165 } 166 167 // Create one Target actor for each prefix/client which listen to process 168 for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { 169 const { connectionPrefix, targets } = sessionData; 170 // This is where we only do something significant only if DevTools are opened 171 // and requesting to create target actor for content processes 172 if (targets?.includes("process")) { 173 this.createTargetActor(watcherActorID, connectionPrefix, sessionData); 174 } 175 } 176 } 177 178 /** 179 * Instantiate a new ContentProcessTarget for the given connection. 180 * This is where we start doing some significant computation that only occurs when DevTools are opened. 181 * 182 * @param String watcherActorID 183 * The ID of the WatcherActor who requested to observe and create these target actors. 184 * @param String parentConnectionPrefix 185 * The prefix of the DevToolsServerConnection of the Watcher Actor. 186 * This is used to compute a unique ID for the target actor. 187 * @param Object sessionData 188 * All data managed by the Watcher Actor and ParentProcessWatcherRegistry.jsm, containing 189 * target types, resources types to be listened as well as breakpoints and any 190 * other data meant to be shared across processes and threads. 191 * @param Object options Dictionary with optional values: 192 * @param Boolean options.ignoreAlreadyCreated 193 * If true, do not throw if the target actor has already been created. 194 */ 195 createTargetActor( 196 watcherActorID, 197 parentConnectionPrefix, 198 sessionData, 199 ignoreAlreadyCreated = false 200 ) { 201 if (this._connections.get(watcherActorID)) { 202 if (ignoreAlreadyCreated) { 203 return; 204 } 205 throw new Error( 206 "ContentProcessStartup createTargetActor was called more than once" + 207 ` for the Watcher Actor (ID: "${watcherActorID}")` 208 ); 209 } 210 // Compute a unique prefix, just for this content process, 211 // which will be used to create a ChildDebuggerTransport pair between content and parent processes. 212 // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, 213 // but here, we can't have access to any DevTools connection as we are really early in the content process startup 214 const prefix = 215 parentConnectionPrefix + "contentProcess" + Services.appinfo.processID; 216 //TODO: probably merge content-process.jsm with this module 217 const { initContentProcessTarget } = ChromeUtils.importESModule( 218 "resource://devtools/server/startup/content-process.sys.mjs" 219 ); 220 const { actor, connection } = initContentProcessTarget({ 221 target: Services.cpmm, 222 data: { 223 watcherActorID, 224 parentConnectionPrefix, 225 prefix, 226 sessionContext: sessionData.sessionContext, 227 }, 228 }); 229 this._connections.set(watcherActorID, { 230 actor, 231 connection, 232 }); 233 234 // Pass initialization data to the target actor 235 for (const type in sessionData) { 236 actor.addOrSetSessionDataEntry(type, sessionData[type], false, "set"); 237 } 238 } 239 240 destroyTarget(watcherActorID) { 241 const connectionInfo = this._connections.get(watcherActorID); 242 // This connection has already been cleaned? 243 if (!connectionInfo) { 244 throw new Error( 245 `Trying to destroy a content process target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` 246 ); 247 } 248 connectionInfo.connection.close(); 249 this._connections.delete(watcherActorID); 250 } 251 252 async addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) { 253 const connectionInfo = this._connections.get(watcherActorID); 254 if (!connectionInfo) { 255 throw new Error( 256 `No content process target actor for this Watcher Actor ID:"${watcherActorID}"` 257 ); 258 } 259 const { actor } = connectionInfo; 260 await actor.addOrSetSessionDataEntry(type, entries, false, updateType); 261 Services.cpmm.sendAsyncMessage("debug:add-or-set-session-data-entry-done", { 262 watcherActorID, 263 }); 264 } 265 266 removeSessionDataEntry(watcherActorID, type, entries) { 267 const connectionInfo = this._connections.get(watcherActorID); 268 if (!connectionInfo) { 269 return; 270 } 271 const { actor } = connectionInfo; 272 actor.removeSessionDataEntry(type, entries); 273 } 274 } 275 276 // Only start this component for content processes. 277 // i.e. explicitely avoid running it for the parent process 278 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { 279 new ContentProcessStartup(); 280 }