DevToolsProcessParent.sys.mjs (14605B)
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 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; 6 import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs"; 7 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 8 9 const { ParentProcessWatcherRegistry } = ChromeUtils.importESModule( 10 "resource://devtools/server/actors/watcher/ParentProcessWatcherRegistry.sys.mjs", 11 // ParentProcessWatcherRegistry needs to be a true singleton and loads ActorManagerParent 12 // which also has to be a true singleton. 13 { global: "shared" } 14 ); 15 16 const lazy = {}; 17 loader.lazyRequireGetter( 18 lazy, 19 "JsWindowActorTransport", 20 "devtools/shared/transport/js-window-actor-transport", 21 true 22 ); 23 24 export class DevToolsProcessParent extends JSProcessActorParent { 25 constructor() { 26 super(); 27 28 EventEmitter.decorate(this); 29 } 30 31 // Boolean to indicate if the related content process is slow to respond 32 // and may be hanging or frozen. When true, we should avoid waiting for its replies. 33 #frozen = false; 34 35 #destroyed = false; 36 37 // Map of various data specific to each active Watcher Actor. 38 // A Watcher will become "active" once we receive a first target actor 39 // notified by the content process, which happens only once 40 // the watcher starts watching for some target types. 41 // 42 // This Map is keyed by "watcher connection prefix". 43 // This is a unique prefix, per watcher actor, which will 44 // be used in the "forwardingPrefix" documented below. 45 // 46 // Note that We may have multiple Watcher actors, 47 // which will resuse the same DevToolsServerConnection (watcher.conn) 48 // if they are instantiated from the same client connection. 49 // Or be using distinct DevToolsServerConnection, if they 50 // are instantiated via distinct client connections. 51 // 52 // The Map's values are objects containing the following properties: 53 // - watcher: 54 // The watcher actor instance. 55 // - targetActorForms: 56 // The list of all active target actor forms 57 // related to this watcher actor. 58 // - transport: 59 // The JsWindowActorTransport which will receive and emit 60 // the RDP packets from the current JS Process Actor's content process. 61 // We spawn one transpart per Watcher and Content process pair. 62 // - forwardingPrefix: 63 // The forwarding prefix is specific to the transport. 64 // It helps redirect RDP packets between this "transport" and 65 // the DevToolsServerConnection (watcher.conn), in the parent process, 66 // which receives and emits RDP packet from/to the client. 67 68 #watchers = new Map(); 69 70 /** 71 * Request the content process to create all the targets currently watched 72 * and start observing for new ones to be created later. 73 */ 74 watchTargets({ watcherActorID, targetType }) { 75 return this.sendQuery("DevToolsProcessParent:watchTargets", { 76 watcherActorID, 77 targetType, 78 }); 79 } 80 81 /** 82 * Request the content process to stop observing for currently watched targets 83 * and destroy all the currently active ones. 84 */ 85 unwatchTargets({ watcherActorID, targetType, options }) { 86 this.sendAsyncMessage("DevToolsProcessParent:unwatchTargets", { 87 watcherActorID, 88 targetType, 89 options, 90 }); 91 } 92 93 /** 94 * Communicate to the content process that some data have been added or set. 95 */ 96 addOrSetSessionDataEntry({ watcherActorID, type, entries, updateType }) { 97 return this.sendQuery("DevToolsProcessParent:addOrSetSessionDataEntry", { 98 watcherActorID, 99 type, 100 entries, 101 updateType, 102 }); 103 } 104 105 /** 106 * Communicate to the content process that some data have been removed. 107 */ 108 removeSessionDataEntry({ watcherActorID, type, entries }) { 109 this.sendAsyncMessage("DevToolsProcessParent:removeSessionDataEntry", { 110 watcherActorID, 111 type, 112 entries, 113 }); 114 } 115 116 destroyWatcher({ watcherActorID }) { 117 return this.sendAsyncMessage("DevToolsProcessParent:destroyWatcher", { 118 watcherActorID, 119 }); 120 } 121 122 /** 123 * Called when the content process notified us about a new target actor 124 */ 125 #onTargetAvailable({ watcherActorID, forwardingPrefix, targetActorForm }) { 126 const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID); 127 128 if (!watcher) { 129 throw new Error( 130 `Watcher Actor with ID '${watcherActorID}' can't be found.` 131 ); 132 } 133 const connection = watcher.conn; 134 135 // If this is the first target actor for this watcher, 136 // hook up the DevToolsServerConnection which will bridge 137 // communication between the parent process DevToolsServer 138 // and the content process. 139 if (!this.#watchers.get(watcher.watcherConnectionPrefix)) { 140 connection.on("closed", this.#onConnectionClosed); 141 142 // Create a js-window-actor based transport. 143 const transport = new lazy.JsWindowActorTransport( 144 this, 145 forwardingPrefix, 146 "DevToolsProcessParent:packet" 147 ); 148 transport.hooks = { 149 onPacket: connection.send.bind(connection), 150 onClosed() {}, 151 }; 152 transport.ready(); 153 154 connection.setForwarding(forwardingPrefix, transport); 155 156 this.#watchers.set(watcher.watcherConnectionPrefix, { 157 watcher, 158 // This prefix is the prefix of the DevToolsServerConnection, running 159 // in the content process, for which we should forward packets to, based on its prefix. 160 // While `watcher.connection` is also a DevToolsServerConnection, but from this process, 161 // the parent process. It is the one receiving Client packets and the one, from which 162 // we should forward packets from. 163 forwardingPrefix, 164 transport, 165 targetActorForms: [], 166 }); 167 } 168 169 this.#watchers 170 .get(watcher.watcherConnectionPrefix) 171 .targetActorForms.push(targetActorForm); 172 173 watcher.notifyTargetAvailable(targetActorForm); 174 } 175 176 /** 177 * Called when the content process notified us about a target actor that has been destroyed. 178 */ 179 #onTargetDestroyed({ actors, options }) { 180 for (const { watcherActorID, targetActorForm } of actors) { 181 const watcher = ParentProcessWatcherRegistry.getWatcher(watcherActorID); 182 // As we instruct to destroy all targets when the watcher is destroyed, 183 // we may easily receive the target destruction notification *after* 184 // the watcher has been removed from the registry. 185 if (!watcher || watcher.isDestroyed()) { 186 continue; 187 } 188 watcher.notifyTargetDestroyed(targetActorForm, options); 189 const watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix); 190 if (watcherInfo) { 191 const idx = watcherInfo.targetActorForms.findIndex( 192 form => form.actor == targetActorForm.actor 193 ); 194 if (idx != -1) { 195 watcherInfo.targetActorForms.splice(idx, 1); 196 } 197 // Once the last active target is removed, disconnect the DevTools transport 198 // and cleanup everything bound to this DOM Process. We will re-instantiate 199 // a new connection/transport on the next reported target actor. 200 if (!watcherInfo.targetActorForms.length) { 201 this.#unregisterWatcher(watcherInfo.watcher, options); 202 } 203 } 204 } 205 } 206 207 #onConnectionClosed = (status, prefix) => { 208 for (const watcherInfo of this.#watchers.values()) { 209 if (watcherInfo.watcher.conn?.prefix == prefix) { 210 this.#unregisterWatcher(watcherInfo.watcher); 211 } 212 } 213 }; 214 215 /** 216 * Unregister a given watcher. 217 * This will also close and unregister the related given DevToolsServerConnection, 218 * if no other watcher is active on the same, possibly shared, connection. 219 * (when remote debugging many tabs on the same connection) 220 * 221 * @param {WatcherActor} watcher 222 * @param {object} options 223 * @param {boolean} options.isModeSwitching 224 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 225 */ 226 #unregisterWatcher(watcher, options = {}) { 227 const watcherInfo = this.#watchers.get(watcher.watcherConnectionPrefix); 228 if (!watcherInfo) { 229 return; 230 } 231 this.#watchers.delete(watcher.watcherConnectionPrefix); 232 233 for (const actor of watcherInfo.targetActorForms) { 234 watcherInfo.watcher.notifyTargetDestroyed(actor, options); 235 } 236 237 let connectionUsedByAnotherWatcher = false; 238 for (const info of this.#watchers.values()) { 239 if (info.watcher.conn == watcherInfo.watcher.conn) { 240 connectionUsedByAnotherWatcher = true; 241 break; 242 } 243 } 244 245 if (!connectionUsedByAnotherWatcher) { 246 const { forwardingPrefix, transport } = watcherInfo; 247 if (transport) { 248 // If we have a child transport, the actor has already 249 // been created. We need to stop using this transport. 250 transport.close(options); 251 } 252 // When cancelling the forwarding, one RDP event is sent to the client to purge all requests 253 // and actors related to a given prefix. 254 // Be careful that any late RDP event would be ignored by the client passed this call. 255 watcherInfo.watcher.conn.cancelForwarding(forwardingPrefix); 256 } 257 258 if (!this.#watchers.size) { 259 this.#destroy(options); 260 } 261 } 262 263 /** 264 * Destroy and cleanup everything for this DOM Process. 265 * 266 * @param {object} options 267 * @param {boolean} options.isModeSwitching 268 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 269 */ 270 #destroy(options) { 271 if (this.#destroyed) { 272 return; 273 } 274 this.#destroyed = true; 275 276 for (const watcherInfo of this.#watchers.values()) { 277 this.#unregisterWatcher(watcherInfo.watcher, options); 278 } 279 } 280 281 /** 282 * Used by DevTools Transport to send packets to the content process. 283 */ 284 285 sendPacket(packet, prefix) { 286 this.sendAsyncMessage("DevToolsProcessParent:packet", { packet, prefix }); 287 } 288 289 /** 290 * JsProcessActor API 291 */ 292 293 /** 294 * JS Actor override of `sendQuery` method, whose main goal is the ignore possibly freezing processes. 295 * This also prints a warning when the query failed to be sent, or when a process hangs. 296 * 297 * @param String msg 298 * @param Array<json> args 299 * @return Promise<undefined> 300 * We only use sendQuery for two queries ("watchTargets" and "addOrSetSessionDataEntry") and 301 * none of them use any returned value (except a promise to know when their processing is done). 302 */ 303 async sendQuery(msg, args) { 304 // If any preview query timed out and did not reply yet, the process is considered frozen 305 // and are no longer waiting for the process response. 306 if (this.#frozen) { 307 this.sendAsyncMessage(msg, args); 308 return Promise.resolve(); 309 } 310 311 // Cache `osPid` and avoid querying `this.manager` attribute later as it may result into 312 // a `AssertReturnTypeMatchesJitinfo` assertion crash into GenericGetter . 313 const { osPid } = this.manager; 314 315 return new Promise((resolve, reject) => { 316 // The process may be slow to resolve the query, or even be completely frozen. 317 // Use a timeout to detect when it happens. 318 const timeout = setTimeout(() => { 319 this.#frozen = true; 320 console.error( 321 `Content process ${osPid} isn't responsive while sending "${msg}" request. DevTools will ignore this process for now.` 322 ); 323 // Do not consider timeout as an error as it may easily break the frontend. 324 resolve(); 325 }, 1000); 326 327 super.sendQuery(msg, args).then( 328 () => { 329 if (this.#frozen && !this.#destroyed) { 330 console.error( 331 `Content process ${osPid} is responsive again. DevTools resumes operations against it.` 332 ); 333 } 334 clearTimeout(timeout); 335 // If any of the ongoing query resolved, consider the process as responsive again 336 this.#frozen = false; 337 338 resolve(); 339 }, 340 async e => { 341 // Ignore frozen processes when the JS Process Actor is destroyed. 342 // Either the process was shut down or DevTools unregistered the Actor. 343 if (this.#frozen && !this.#destroyed) { 344 console.error( 345 `Content process ${osPid} is responsive again. DevTools resumes operations against it.` 346 ); 347 } 348 clearTimeout(timeout); 349 // If any of the ongoing query resolved, consider the process as responsive again 350 this.#frozen = false; 351 352 console.error("Failed to sendQuery in DevToolsProcessParent", msg); 353 console.error(e.toString()); 354 reject(e); 355 } 356 ); 357 }); 358 } 359 360 /** 361 * Called by the JSProcessActor API when the content process sent us a message 362 */ 363 receiveMessage(message) { 364 switch (message.name) { 365 case "DevToolsProcessChild:targetAvailable": 366 return this.#onTargetAvailable(message.data); 367 case "DevToolsProcessChild:packet": 368 return this.emit("packet-received", message); 369 case "DevToolsProcessChild:targetDestroyed": 370 return this.#onTargetDestroyed(message.data); 371 case "DevToolsProcessChild:bf-cache-navigation-pageshow": { 372 const browsingContext = BrowsingContext.get( 373 message.data.browsingContextId 374 ); 375 for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId( 376 browsingContext.browserId 377 )) { 378 watcherActor.emit("bf-cache-navigation-pageshow", { 379 windowGlobal: browsingContext.currentWindowGlobal, 380 }); 381 } 382 return null; 383 } 384 case "DevToolsProcessChild:bf-cache-navigation-pagehide": { 385 const browsingContext = BrowsingContext.get( 386 message.data.browsingContextId 387 ); 388 for (const watcherActor of ParentProcessWatcherRegistry.getWatchersForBrowserId( 389 browsingContext.browserId 390 )) { 391 watcherActor.emit("bf-cache-navigation-pagehide", { 392 windowGlobal: browsingContext.currentWindowGlobal, 393 }); 394 } 395 return null; 396 } 397 default: 398 throw new Error( 399 "Unsupported message in DevToolsProcessParent: " + message.name 400 ); 401 } 402 } 403 404 /** 405 * Called by the JSProcessActor API when this content process is destroyed. 406 */ 407 didDestroy() { 408 this.#destroy(); 409 } 410 } 411 412 export class BrowserToolboxDevToolsProcessParent extends DevToolsProcessParent {}