ParentProcessWatcherRegistry.sys.mjs (15975B)
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 /** 6 * Helper module around `sharedData` object that helps storing the state 7 * of all observed Targets and Resources, that, for all DevTools connections. 8 * Here is a few words about the C++ implementation of sharedData: 9 * https://searchfox.org/mozilla-central/rev/bc3600def806859c31b2c7ac06e3d69271052a89/dom/ipc/SharedMap.h#30-55 10 * 11 * We may have more than one DevToolsServer and one server may have more than one 12 * client. This module will be the single source of truth in the parent process, 13 * in order to know which targets/resources are currently observed. It will also 14 * be used to declare when something starts/stops being observed. 15 * 16 * `sharedData` is a platform API that helps sharing JS Objects across processes. 17 * We use it in order to communicate to the content process which targets and resources 18 * should be observed. Content processes read this data only once, as soon as they are created. 19 * It isn't used beyond this point. Content processes are not going to update it. 20 * We will notify about changes in observed targets and resources for already running 21 * processes by some other means. (Via JS Window Actor queries "DevTools:(un)watch(Resources|Target)") 22 * This means that only this module will update the "DevTools:watchedPerWatcher" value. 23 * From the parent process, we should be going through this module to fetch the data, 24 * while from the content process, we will read `sharedData` directly. 25 */ 26 27 const { SessionDataHelpers } = ChromeUtils.importESModule( 28 "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", 29 { global: "contextual" } 30 ); 31 32 const { SUPPORTED_DATA } = SessionDataHelpers; 33 const SUPPORTED_DATA_TYPES = Object.values(SUPPORTED_DATA); 34 35 // Define the Map that will be saved in `sharedData`. 36 // It is keyed by WatcherActor ID and values contains following attributes: 37 // - targets: Set of strings, refering to target types to be listened to 38 // - resources: Set of strings, refering to resource types to be observed 39 // - sessionContext Object, The Session Context to help know what is debugged. 40 // See devtools/server/actors/watcher/session-context.js 41 // - connectionPrefix: The DevToolsConnection prefix of the watcher actor. Used to compute new actor ID in the content processes. 42 // 43 // Unfortunately, `sharedData` is subject to race condition and may have side effect 44 // when read/written from multiple places in the same process, 45 // which is why this map should be considered as the single source of truth. 46 const sessionDataByWatcherActor = new Map(); 47 48 // In parallel to the previous map, keep all the WatcherActor keyed by the same WatcherActor ID, 49 // the WatcherActor ID. We don't (and can't) propagate the WatcherActor instances to the content 50 // processes, but still would like to match them by their ID. 51 const watcherActors = new Map(); 52 53 // Name of the attribute into which we save this Map in `sharedData` object. 54 const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; 55 56 /** 57 * Use `sharedData` to allow processes, early during their creation, 58 * to know which resources should be listened to. This will be read 59 * from the Target actor, when it gets created early during process start, 60 * in order to start listening to the expected resource types. 61 */ 62 function persistMapToSharedData() { 63 Services.ppmm.sharedData.set(SHARED_DATA_KEY_NAME, sessionDataByWatcherActor); 64 // Request to immediately flush the data to the content processes in order to prevent 65 // races (bug 1644649). Otherwise content process may have outdated sharedData 66 // and try to create targets for Watcher actor that already stopped watching for targets. 67 Services.ppmm.sharedData.flush(); 68 } 69 70 export const ParentProcessWatcherRegistry = { 71 /** 72 * Tells if a given watcher currently watches for a given target type. 73 * 74 * @param WatcherActor watcher 75 * The WatcherActor which should be listening. 76 * @param string targetType 77 * The new target type to query. 78 * @return boolean 79 * Returns true if already watching. 80 */ 81 isWatchingTargets(watcher, targetType) { 82 const sessionData = this.getSessionData(watcher); 83 return !!sessionData?.targets?.includes(targetType); 84 }, 85 86 /** 87 * Retrieve the data saved into `sharedData` that is used to know 88 * about which type of targets and resources we care listening about. 89 * `sessionDataByWatcherActor` is saved into `sharedData` after each mutation, 90 * but `sessionDataByWatcherActor` is the source of truth. 91 * 92 * @param WatcherActor watcher 93 * The related WatcherActor which starts/stops observing. 94 * @param object options (optional) 95 * A dictionary object with `createData` boolean attribute. 96 * If this attribute is set to true, we create the data structure in the Map 97 * if none exists for this prefix. 98 */ 99 getSessionData(watcher, { createData = false } = {}) { 100 // Use WatcherActor ID as a key as we may have multiple clients willing to watch for targets. 101 // For example, a Browser Toolbox debugging everything and a Content Toolbox debugging 102 // just one tab. We might also have multiple watchers, on the same connection when using about:debugging. 103 const watcherActorID = watcher.actorID; 104 let sessionData = sessionDataByWatcherActor.get(watcherActorID); 105 if (!sessionData && createData) { 106 sessionData = { 107 // The "session context" object help understand what should be debugged and which target should be created. 108 // See WatcherActor constructor for more info. 109 sessionContext: watcher.sessionContext, 110 // The DevToolsServerConnection prefix will be used to compute actor IDs created in the content process. 111 // This prefix is unique per watcher, as we may have many watchers for a single connection. 112 connectionPrefix: watcher.watcherConnectionPrefix, 113 }; 114 sessionDataByWatcherActor.set(watcherActorID, sessionData); 115 watcherActors.set(watcherActorID, watcher); 116 } 117 return sessionData; 118 }, 119 120 /** 121 * Given a Watcher Actor ID, return the related Watcher Actor instance. 122 * 123 * @param String actorID 124 * The Watcher Actor ID to search for. 125 * @return WatcherActor 126 * The Watcher Actor instance. 127 */ 128 getWatcher(actorID) { 129 return watcherActors.get(actorID); 130 }, 131 132 /** 133 * Return an array of the watcher actors that match the passed browserId 134 * 135 * @param {number} browserId 136 * @returns {Array<WatcherActor>} An array of the matching watcher actors 137 */ 138 getWatchersForBrowserId(browserId) { 139 const watchers = []; 140 for (const watcherActor of watcherActors.values()) { 141 if ( 142 watcherActor.sessionContext.type == "browser-element" && 143 watcherActor.sessionContext.browserId === browserId 144 ) { 145 watchers.push(watcherActor); 146 } 147 } 148 149 return watchers; 150 }, 151 152 /** 153 * Notify that a given watcher added or set some entries for given data type. 154 * 155 * @param WatcherActor watcher 156 * The WatcherActor which starts observing. 157 * @param string type 158 * The type of data to be added 159 * @param Array<Object> entries 160 * The values to be added to this type of data 161 * @param String updateType 162 * "add" will only add the new entries in the existing data set. 163 * "set" will update the data set with the new entries. 164 */ 165 addOrSetSessionDataEntry(watcher, type, entries, updateType) { 166 const sessionData = this.getSessionData(watcher, { 167 createData: true, 168 }); 169 170 if (!SUPPORTED_DATA_TYPES.includes(type)) { 171 throw new Error(`Unsupported session data type: ${type}`); 172 } 173 174 SessionDataHelpers.addOrSetSessionDataEntry( 175 sessionData, 176 type, 177 entries, 178 updateType 179 ); 180 181 // Flush sharedData before registering the JS Actors as it is used 182 // during their instantiation. 183 persistMapToSharedData(); 184 185 // Register the JS Window Actor the first time we start watching for something (e.g. resource, target, …). 186 if (watcher.sessionContext.type == "all") { 187 registerBrowserToolboxJSProcessActor(); 188 } else { 189 registerJSProcessActor(); 190 } 191 }, 192 193 /** 194 * Notify that a given watcher removed an entry in a given data type. 195 * 196 * @param WatcherActor watcher 197 * The WatcherActor which stops observing. 198 * @param string type 199 * The type of data to be removed 200 * @param Array<Object> entries 201 * The values to be removed to this type of data 202 * 203 * @return boolean 204 * True if we such entry was already registered, for this watcher actor. 205 */ 206 removeSessionDataEntry(watcher, type, entries) { 207 const sessionData = this.getSessionData(watcher); 208 if (!sessionData) { 209 return false; 210 } 211 212 if (!SUPPORTED_DATA_TYPES.includes(type)) { 213 throw new Error(`Unsupported session data type: ${type}`); 214 } 215 216 if ( 217 !SessionDataHelpers.removeSessionDataEntry(sessionData, type, entries) 218 ) { 219 return false; 220 } 221 222 persistMapToSharedData(); 223 224 return true; 225 }, 226 227 /** 228 * Cleanup everything about a given watcher actor. 229 * Remove it from any registry so that we stop interacting with it. 230 * 231 * The watcher would be automatically unregistered from removeWatcherEntry, 232 * if we remove all entries. But we aren't removing all breakpoints. 233 * So here, we force clearing any reference to the watcher actor when it destroys. 234 */ 235 unregisterWatcher(watcherActorID) { 236 sessionDataByWatcherActor.delete(watcherActorID); 237 watcherActors.delete(watcherActorID); 238 this.maybeUnregisterJSActors(); 239 }, 240 241 /** 242 * Unregister the JS Actors if there is no more DevTools code observing any target/resource. 243 */ 244 maybeUnregisterJSActors() { 245 if (sessionDataByWatcherActor.size == 0) { 246 unregisterBrowserToolboxJSProcessActor(); 247 unregisterJSProcessActor(); 248 } 249 }, 250 251 /** 252 * Notify that a given watcher starts observing a new target type. 253 * 254 * @param WatcherActor watcher 255 * The WatcherActor which starts observing. 256 * @param string targetType 257 * The new target type to start listening to. 258 */ 259 watchTargets(watcher, targetType) { 260 this.addOrSetSessionDataEntry( 261 watcher, 262 SUPPORTED_DATA.TARGETS, 263 [targetType], 264 "add" 265 ); 266 }, 267 268 /** 269 * Notify that a given watcher stops observing a given target type. 270 * 271 * @param WatcherActor watcher 272 * The WatcherActor which stops observing. 273 * @param string targetType 274 * The new target type to stop listening to. 275 * @return boolean 276 * True if we were watching for this target type, for this watcher actor. 277 */ 278 unwatchTargets(watcher, targetType) { 279 return this.removeSessionDataEntry(watcher, SUPPORTED_DATA.TARGETS, [ 280 targetType, 281 ]); 282 }, 283 284 /** 285 * Notify that a given watcher starts observing new resource types. 286 * 287 * @param WatcherActor watcher 288 * The WatcherActor which starts observing. 289 * @param Array<string> resourceTypes 290 * The new resource types to start listening to. 291 */ 292 watchResources(watcher, resourceTypes) { 293 this.addOrSetSessionDataEntry( 294 watcher, 295 SUPPORTED_DATA.RESOURCES, 296 resourceTypes, 297 "add" 298 ); 299 }, 300 301 /** 302 * Notify that a given watcher stops observing given resource types. 303 * 304 * See `watchResources` for argument definition. 305 * 306 * @return boolean 307 * True if we were watching for this resource type, for this watcher actor. 308 */ 309 unwatchResources(watcher, resourceTypes) { 310 return this.removeSessionDataEntry( 311 watcher, 312 SUPPORTED_DATA.RESOURCES, 313 resourceTypes 314 ); 315 }, 316 }; 317 318 // Boolean flag to know if the DevToolsProcess JS Process Actor is currently registered 319 let isJSProcessActorRegistered = false; 320 let isBrowserToolboxJSProcessActorRegistered = false; 321 322 const JSProcessActorConfig = { 323 parent: { 324 esModuleURI: 325 "resource://devtools/server/connectors/js-process-actor/DevToolsProcessParent.sys.mjs", 326 }, 327 child: { 328 esModuleURI: 329 "resource://devtools/server/connectors/js-process-actor/DevToolsProcessChild.sys.mjs", 330 // There is no good observer service notification we can listen to to instantiate the JSProcess Actor 331 // reliably as soon as the process start. 332 // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... 333 observers: ["init-devtools-content-process-actor"], 334 }, 335 // The parent process is handled very differently from content processes 336 // This uses the ParentProcessTarget which inherits from BrowsingContextTarget 337 // and is, for now, manually created by the descriptor as the top level target. 338 includeParent: true, 339 }; 340 341 const BrowserToolboxJSProcessActorConfig = { 342 ...JSProcessActorConfig, 343 344 // This JS Process Actor is used to bootstrap DevTools code debugging the privileged code 345 // in content processes. The privileged code runs in the "shared JSM global" (See mozJSModuleLoader). 346 // DevTools modules should be loaded in a distinct global in order to be able to debug this privileged code. 347 // There is a strong requirement in spidermonkey for the debuggee and debugger to be using distinct compartments. 348 // This flag will force both parent and child modules to be loaded via a dedicated loader (See mozJSModuleLoader::GetOrCreateDevToolsLoader) 349 // 350 // Note that as a side effect, it makes these modules and all their dependencies to be invisible to the debugger. 351 loadInDevToolsLoader: true, 352 }; 353 354 const PROCESS_SCRIPT_URL = 355 "resource://devtools/server/connectors/js-process-actor/content-process-jsprocessactor-startup.js"; 356 357 function registerJSProcessActor() { 358 if (isJSProcessActorRegistered) { 359 return; 360 } 361 isJSProcessActorRegistered = true; 362 ChromeUtils.registerProcessActor("DevToolsProcess", JSProcessActorConfig); 363 364 // There is no good observer service notification we can listen to to instantiate the JSProcess Actor 365 // as soon as the process start. 366 // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... 367 Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); 368 } 369 370 function registerBrowserToolboxJSProcessActor() { 371 if (isBrowserToolboxJSProcessActorRegistered) { 372 return; 373 } 374 isBrowserToolboxJSProcessActorRegistered = true; 375 ChromeUtils.registerProcessActor( 376 "BrowserToolboxDevToolsProcess", 377 BrowserToolboxJSProcessActorConfig 378 ); 379 380 // There is no good observer service notification we can listen to to instantiate the JSProcess Actor 381 // as soon as the process start. 382 // So manually spawn our JSProcessActor from a process script emitting a custom observer service notification... 383 Services.ppmm.loadProcessScript(PROCESS_SCRIPT_URL, true); 384 } 385 386 function unregisterJSProcessActor() { 387 if (!isJSProcessActorRegistered) { 388 return; 389 } 390 isJSProcessActorRegistered = false; 391 try { 392 ChromeUtils.unregisterProcessActor("DevToolsProcess"); 393 } catch (e) { 394 // If any pending query was still ongoing, this would throw 395 } 396 if (isBrowserToolboxJSProcessActorRegistered) { 397 return; 398 } 399 Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); 400 } 401 402 function unregisterBrowserToolboxJSProcessActor() { 403 if (!isBrowserToolboxJSProcessActorRegistered) { 404 return; 405 } 406 isBrowserToolboxJSProcessActorRegistered = false; 407 try { 408 ChromeUtils.unregisterProcessActor("BrowserToolboxDevToolsProcess"); 409 } catch (e) { 410 // If any pending query was still ongoing, this would throw 411 } 412 if (isJSProcessActorRegistered) { 413 return; 414 } 415 Services.ppmm.removeDelayedProcessScript(PROCESS_SCRIPT_URL); 416 }