ContentProcessWatcherRegistry.sys.mjs (18256B)
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 const lazy = {}; 6 ChromeUtils.defineESModuleGetters( 7 lazy, 8 { 9 loader: "resource://devtools/shared/loader/Loader.sys.mjs", 10 }, 11 { global: "contextual" } 12 ); 13 14 ChromeUtils.defineESModuleGetters( 15 lazy, 16 { 17 releaseDistinctSystemPrincipalLoader: 18 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", 19 useDistinctSystemPrincipalLoader: 20 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", 21 }, 22 { global: "shared" } 23 ); 24 25 // Name of the attribute into which we save data in `sharedData` object. 26 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; 27 28 // Map(String => Object) 29 // Map storing the data objects for all currently active watcher actors. 30 // The data objects are defined by `createWatcherDataObject()`. 31 // The main attribute of interest is the `sessionData` one which is set alongside 32 // various other attributes necessary to maintain state per watcher in the content process. 33 // 34 // The Session Data object is maintained by ParentProcessWatcherRegistry, in the parent process 35 // and is fetched from the content process via `sharedData` API. 36 // It is then manually maintained via DevToolsProcess JS Actor queries. 37 let gAllWatcherData = null; 38 39 export const ContentProcessWatcherRegistry = { 40 _getAllWatchersDataMap() { 41 if (gAllWatcherData) { 42 return gAllWatcherData; 43 } 44 const { sharedData } = Services.cpmm; 45 const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); 46 if (!sessionDataByWatcherActorID) { 47 throw new Error("Missing session data in `sharedData`"); 48 } 49 50 // Initialize a distinct Map to replicate the one read from `sharedData`. 51 // This distinct Map will be updated via DevToolsProcess JS Actor queries. 52 // This helps better control the execution flow. 53 gAllWatcherData = new Map(); 54 55 // The Browser Toolbox will load its server modules in a distinct global/compartment whose name is "DevTools global". 56 // (See https://searchfox.org/mozilla-central/rev/0e9ea50a999420d93df0e4e27094952af48dd3b8/js/xpconnect/loader/mozJSModuleLoader.cpp#699) 57 // It means that this class will be instantiated twice, one in each global (the shared one and the browser toolbox one). 58 // We then have to distinguish the two subset of watcher actors accordingly within `sharedMap`, 59 // as `sharedMap` will be shared between the two module instances. 60 // Session type "all" relates to the Browser Toolbox. 61 const isInBrowserToolboxLoader = 62 // eslint-disable-next-line mozilla/reject-globalThis-modification 63 Cu.getRealmLocation(globalThis) == "DevTools global"; 64 65 for (const [watcherActorID, sessionData] of sessionDataByWatcherActorID) { 66 // Filter in/out the watchers based on the current module loader and the watcher session type. 67 const isBrowserToolboxWatcher = sessionData.sessionContext.type == "all"; 68 if ( 69 (isInBrowserToolboxLoader && !isBrowserToolboxWatcher) || 70 (!isInBrowserToolboxLoader && isBrowserToolboxWatcher) 71 ) { 72 continue; 73 } 74 75 gAllWatcherData.set( 76 watcherActorID, 77 createWatcherDataObject(watcherActorID, sessionData) 78 ); 79 } 80 81 return gAllWatcherData; 82 }, 83 84 /** 85 * Get all data objects for all currently active watcher actors. 86 * If a specific target type is passed, this will only return objects of watcher actively watching for a given target type. 87 * 88 * @param {string} targetType 89 * Optional target type to filter only a subset of watchers. 90 * @return {Array|Iterator} 91 * List of data objects. (see createWatcherDataObject) 92 */ 93 getAllWatchersDataObjects(targetType) { 94 if (targetType) { 95 const list = []; 96 for (const watcherDataObject of this._getAllWatchersDataMap().values()) { 97 if (watcherDataObject.sessionData.targets?.includes(targetType)) { 98 list.push(watcherDataObject); 99 } 100 } 101 return list; 102 } 103 return this._getAllWatchersDataMap().values(); 104 }, 105 106 /** 107 * Similar to `getAllWatcherDataObjects`, but will only return the already existing registered watchers in this process. 108 */ 109 getAllExistingWatchersDataObjects() { 110 if (!gAllWatcherData) { 111 return []; 112 } 113 return gAllWatcherData.values(); 114 }, 115 116 /** 117 * Get the watcher data object for a given watcher actor. 118 * 119 * @param {string} watcherActorID 120 * @param {boolean} onlyFromCache 121 * If set explicitly to true, will avoid falling back to shared data. 122 * This is typically useful on destructor/removing/cleanup to avoid creating unexpected data. 123 * It is also used to avoid the exception thrown when sharedData is cleared on toolbox destruction. 124 */ 125 getWatcherDataObject(watcherActorID, onlyFromCache = false) { 126 let data = 127 ContentProcessWatcherRegistry._getAllWatchersDataMap().get( 128 watcherActorID 129 ); 130 if (!data && !onlyFromCache) { 131 // When there is more than one DevTools opened, the DevToolsProcess JS Actor spawned by the first DevTools 132 // created a cached Map in `_getAllWatchersDataMap`. 133 // When opening a second DevTools, this cached Map may miss some new SessionData related to this new DevTools instance, 134 // and new Watcher Actor. 135 // When such scenario happens, fallback to `sharedData` which should hopefully be containing the latest DevTools instance SessionData. 136 // 137 // May be the watcher should trigger a very first JS Actor query before any others in order to transfer the base Session Data object? 138 const { sharedData } = Services.cpmm; 139 const sessionDataByWatcherActorID = sharedData.get(SHARED_DATA_KEY_NAME); 140 const sessionData = sessionDataByWatcherActorID.get(watcherActorID); 141 if (!sessionData) { 142 throw new Error("Unable to find data for watcher " + watcherActorID); 143 } 144 data = createWatcherDataObject(watcherActorID, sessionData); 145 gAllWatcherData.set(watcherActorID, data); 146 } 147 return data; 148 }, 149 150 /** 151 * Instantiate a DevToolsServerConnection for a given Watcher. 152 * 153 * This function will be the one forcing to load the first DevTools CommonJS modules 154 * and spawning the DevTools Loader as well as the DevToolsServer. So better call it 155 * only once when it is strictly necessary. 156 * 157 * This connection will be the communication channel for RDP between this content process 158 * and the parent process, which will route RDP packets from/to the client by using 159 * a unique "forwarding prefix". 160 * 161 * @param {string} watcherActorID 162 * @param {boolean} useDistinctLoader 163 * To be set to true when debugging a privileged context running the shared system principal global. 164 * This is a requirement for spidermonkey Debugger API used by the thread actor. 165 * @return {object} 166 * Object with connection (DevToolsServerConnection) and loader (DevToolsLoader) attributes. 167 */ 168 getOrCreateConnectionForWatcher(watcherActorID, useDistinctLoader) { 169 const watcherDataObject = 170 ContentProcessWatcherRegistry.getWatcherDataObject(watcherActorID); 171 let { connection, loader } = watcherDataObject; 172 173 if (connection) { 174 return { connection, loader }; 175 } 176 177 // When debugging a privileged page, like about:addons, this module will run in the same compartment 178 // as the debugged page. Both will run in the shared system compartment. 179 // The thread actor ultimately need to be in a distinct compartments from its debuggees. 180 // So we are using a special loader, which will use a distinct privileged global and compartment 181 // to load itself as well as all its modules. 182 // 183 // Note that when we are running the Browser Toolbox, this module will already be loaded in a special, distinct global and compartment 184 // Thanks to `loadInDevToolsLoader` flag of BrowserToolboxDevToolsProcess's JS Process Actor configuration. 185 // So that the Loader will also be loaded in the right, distinct compartment. 186 loader = 187 useDistinctLoader || watcherDataObject.sessionContext.type == "all" 188 ? lazy.useDistinctSystemPrincipalLoader(watcherDataObject) 189 : lazy.loader; 190 watcherDataObject.loader = loader; 191 192 // Note that this a key step in loading DevTools backend / modules. 193 const { DevToolsServer } = loader.require( 194 "resource://devtools/server/devtools-server.js" 195 ); 196 197 DevToolsServer.init(); 198 199 // Within the content process, we only need the target scoped actors. 200 // (inspector, console, storage,...) 201 DevToolsServer.registerActors({ target: true }); 202 203 // Instantiate a DevToolsServerConnection which will pipe all its outgoing RDP packets 204 // up to the parent process manager via DevToolsProcess JS Actor messages. 205 const { forwardingPrefix } = watcherDataObject; 206 connection = DevToolsServer.connectToParentWindowActor( 207 watcherDataObject.jsProcessActor, 208 forwardingPrefix, 209 "DevToolsProcessChild:packet" 210 ); 211 watcherDataObject.connection = connection; 212 213 return { connection, loader }; 214 }, 215 216 /** 217 * Method to be called each time a new target actor is instantiated. 218 * 219 * @param {object} watcherDataObject 220 * @param {Actor} targetActor 221 * @param {boolean} isDocumentCreation 222 */ 223 onNewTargetActor(watcherDataObject, targetActor, isDocumentCreation = false) { 224 // There is no root actor in content processes and so 225 // the target actor can't be managed by it, but we do have to manage 226 // the actor to have it working and be registered in the DevToolsServerConnection. 227 // We make it manage itself and become a top level actor. 228 targetActor.manage(targetActor); 229 230 const { watcherActorID } = watcherDataObject; 231 targetActor.once("destroyed", options => { 232 // Maintain the registry and notify the parent process 233 ContentProcessWatcherRegistry.destroyTargetActor( 234 watcherDataObject, 235 targetActor, 236 options 237 ); 238 }); 239 240 watcherDataObject.actors.push(targetActor); 241 242 // Immediately queue a message for the parent process, 243 // in order to ensure that the JSWindowActorTransport is instantiated 244 // before any packet is sent from the content process. 245 // As messages are guaranteed to be delivered in the order they 246 // were queued, we don't have to wait for anything around this sendAsyncMessage call. 247 // In theory, the Target Actor may emit events in its constructor. 248 // If it does, such RDP packets may be lost. But in practice, no events 249 // are emitted during its construction. Instead the frontend will start 250 // the communication first. 251 const { forwardingPrefix } = watcherDataObject; 252 watcherDataObject.jsProcessActor.sendAsyncMessage( 253 "DevToolsProcessChild:targetAvailable", 254 { 255 watcherActorID, 256 forwardingPrefix, 257 targetActorForm: targetActor.form(), 258 } 259 ); 260 261 // Pass initialization data to the target actor 262 const { sessionData } = watcherDataObject; 263 for (const type in sessionData) { 264 // `sessionData` will also contain `browserId` as well as entries with empty arrays, 265 // which shouldn't be processed. 266 const entries = sessionData[type]; 267 if (!Array.isArray(entries) || !entries.length) { 268 continue; 269 } 270 targetActor.addOrSetSessionDataEntry( 271 type, 272 sessionData[type], 273 isDocumentCreation, 274 "set" 275 ); 276 } 277 }, 278 279 /** 280 * Method to be called each time a target actor is meant to be destroyed. 281 * 282 * @param {object} watcherDataObject 283 * @param {Actor} targetActor 284 * @param {object} options 285 * @param {boolean} options.isModeSwitching 286 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 287 */ 288 destroyTargetActor(watcherDataObject, targetActor, options) { 289 const idx = watcherDataObject.actors.indexOf(targetActor); 290 if (idx != -1) { 291 watcherDataObject.actors.splice(idx, 1); 292 } 293 const form = targetActor.form(); 294 targetActor.destroy(options); 295 296 // And this will destroy the parent process one 297 try { 298 watcherDataObject.jsProcessActor.sendAsyncMessage( 299 "DevToolsProcessChild:targetDestroyed", 300 { 301 actors: [ 302 { 303 watcherActorID: watcherDataObject.watcherActorID, 304 targetActorForm: form, 305 }, 306 ], 307 options, 308 } 309 ); 310 } catch (e) { 311 // Ignore exception when the JSProcessActorChild has already been destroyed. 312 // We often try to emit this message while the process is being destroyed, 313 // but sendAsyncMessage doesn't have time to complete and throws. 314 if ( 315 !e.message.includes("JSProcessActorChild cannot send at the moment") 316 ) { 317 throw e; 318 } 319 } 320 }, 321 322 /** 323 * Method to know if a given Watcher Actor is still registered. 324 * 325 * @param {string} watcherActorID 326 * @return {boolean} 327 */ 328 has(watcherActorID) { 329 return gAllWatcherData.has(watcherActorID); 330 }, 331 332 /** 333 * Method to unregister a given Watcher Actor. 334 * 335 * @param {object} watcherDataObject 336 */ 337 remove(watcherDataObject) { 338 // We do not need to destroy each actor individually as they 339 // are all registered in this DevToolsServerConnection, which will 340 // destroy all the registered actors. 341 if (watcherDataObject.connection) { 342 watcherDataObject.connection.close(); 343 } 344 // If we were using a distinct and dedicated loader, 345 // we have to manually release it. 346 if (watcherDataObject.loader && watcherDataObject.loader !== lazy.loader) { 347 lazy.releaseDistinctSystemPrincipalLoader(watcherDataObject); 348 } 349 350 if (watcherDataObject.htmlSourcesCache) { 351 watcherDataObject.htmlSourcesCache.destroy(); 352 } 353 gAllWatcherData.delete(watcherDataObject.watcherActorID); 354 if (gAllWatcherData.size == 0) { 355 gAllWatcherData = null; 356 } 357 }, 358 359 /** 360 * Method to know if there is no more Watcher registered. 361 * 362 * @return {boolean} 363 */ 364 isEmpty() { 365 return !gAllWatcherData || gAllWatcherData.size == 0; 366 }, 367 368 /** 369 * Method to unregister all the Watcher Actors 370 */ 371 clear() { 372 if (!gAllWatcherData) { 373 return; 374 } 375 // Query gAllWatcherData internal map directly as we don't want to re-create the map from sharedData 376 for (const watcherDataObject of gAllWatcherData.values()) { 377 ContentProcessWatcherRegistry.remove(watcherDataObject); 378 } 379 gAllWatcherData = null; 380 }, 381 }; 382 383 function createWatcherDataObject(watcherActorID, sessionData) { 384 // The prefix of the DevToolsServerConnection of the Watcher Actor in the parent process. 385 // This is used to compute a unique ID for this process. 386 const parentConnectionPrefix = sessionData.connectionPrefix; 387 388 // Compute a unique prefix, just for this DOM Process. 389 // (nsIDOMProcessChild's childID should be unique across processes) 390 // 391 // This prefix will be used to create a JSWindowActorTransport pair between content and parent processes. 392 // This is slightly hacky as we typically compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, 393 // but here, we can't have access to any DevTools connection as we could run really early in the content process startup. 394 // 395 // Ensure appending a final slash, otherwise the prefix may be the same between childID 1 and 10... 396 const forwardingPrefix = 397 parentConnectionPrefix + 398 ".process" + 399 ChromeUtils.domProcessChild.childID + 400 "/"; 401 402 // The browser toolbox uses a distinct JS Actor, loaded in the "devtools" ESM loader. 403 const jsActorName = 404 sessionData.sessionContext.type == "all" 405 ? "BrowserToolboxDevToolsProcess" 406 : "DevToolsProcess"; 407 const jsProcessActor = ChromeUtils.domProcessChild.getActor(jsActorName); 408 409 return { 410 // {String} 411 // Actor ID for this watcher 412 watcherActorID, 413 414 // {Array<String>} 415 // List of currently watched target types for this watcher 416 watchingTargetTypes: [], 417 418 // {DevtoolsServerConnection} 419 // Connection bridge made from this content process to the parent process. 420 connection: null, 421 422 // {JSActor} 423 // Reference to the related DevToolsProcessChild instance. 424 jsProcessActor, 425 426 // {Object} 427 // Watcher's sessionContext object, which help identify the browser toolbox usecase. 428 sessionContext: sessionData.sessionContext, 429 430 // {Object} 431 // Watcher's sessionData object, which is initiated with `sharedData` version, 432 // but is later updated on each Session Data update (addOrSetSessionDataEntry/removeSessionDataEntry). 433 // `sharedData` isn't timely updated and can be out of date. 434 sessionData, 435 436 // {String} 437 // Prefix used against all RDP packets to route them correctly from/to this content process 438 forwardingPrefix, 439 440 // {Array<Object>} 441 // List of active WindowGlobal and ContentProcess target actor instances. 442 actors: [], 443 444 // {Object<Array<Object>>} 445 // We can't use `actors` list for workers as this code runs in the main thread and the WorkerTargetActors 446 // run in the worker thread. 447 // We store in each array, specific to each worker type (having a dedicated target watcher class), 448 // an object with the following attributes: 449 // - {WorkerDebugger} dbg 450 // - {String} workerThreadServerForwardingPrefix 451 // - {Object} workerTargetForm 452 // - {DevToolsTransport} transport 453 workers: { 454 service_worker: [], 455 shared_worker: [], 456 worker: [], 457 }, 458 459 // {Object<Set<Array<Object>>>} 460 // A Set of arrays which will be populated with concurrent Session Data updates 461 // being done while a worker target is being instantiated. 462 // Each pending worker being initialized register a new dedicated array which will be removed 463 // from the Set once its initialization is over. 464 // We maintain one Set per target type which is managed by a dedicated target watcher class. 465 pendingWorkers: { 466 service_worker: new Set(), 467 shared_worker: new Set(), 468 worker: new Set(), 469 }, 470 }; 471 }