Front.js (15010B)
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 var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js"); 8 var EventEmitter = require("resource://devtools/shared/event-emitter.js"); 9 10 var { Pool } = require("resource://devtools/shared/protocol/Pool.js"); 11 var { 12 getStack, 13 callFunctionWithAsyncStack, 14 } = require("resource://devtools/shared/platform/stack.js"); 15 16 /** 17 * Base class for client-side actor fronts. 18 * 19 * @param [DevToolsClient|null] conn 20 * The conn must either be DevToolsClient or null. Must have 21 * addActorPool, removeActorPool, and poolFor. 22 * conn can be null if the subclass provides a conn property. 23 * @param [Target|null] target 24 * If we are instantiating a target-scoped front, this is a reference to the front's 25 * Target instance, otherwise this is null. 26 * @param [Front|null] parentFront 27 * The parent front. This is only available if the Front being initialized is a child 28 * of a parent front. 29 * @class 30 */ 31 class Front extends Pool { 32 constructor(conn = null, targetFront = null, parentFront = null) { 33 super(conn); 34 if (!conn) { 35 throw new Error("Front without conn"); 36 } 37 this.actorID = null; 38 // The targetFront attribute represents the debuggable context. Only target-scoped 39 // fronts and their children fronts will have the targetFront attribute set. 40 this.targetFront = targetFront; 41 // The parentFront attribute points to its parent front. Only children of 42 // target-scoped fronts will have the parentFront attribute set. 43 this.parentFront = parentFront; 44 this._requests = []; 45 46 // Front listener functions registered via `watchFronts` 47 this._frontCreationListeners = null; 48 this._frontDestructionListeners = null; 49 50 // List of optional listener for each event, that is processed immediatly on packet 51 // receival, before emitting event via EventEmitter on the Front. 52 // These listeners are register via Front.before function. 53 // Map(Event Name[string] => Event Listener[function]) 54 this._beforeListeners = new Map(); 55 56 // This flag allows to check if the `initialize` method has resolved. 57 // Used to avoid notifying about initialized fronts in `watchFronts`. 58 this._initializeResolved = false; 59 } 60 61 /** 62 * Return the parent front. 63 */ 64 getParent() { 65 return this.parentFront && this.parentFront.actorID 66 ? this.parentFront 67 : null; 68 } 69 70 destroy() { 71 // Prevent destroying twice if a `forwardCancelling` event has already been received 72 // and already called `baseFrontClassDestroy` 73 this.baseFrontClassDestroy(); 74 75 // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be 76 // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests. 77 this.clearEvents(); 78 } 79 80 // This method is also called from `DevToolsClient`, when a connector is destroyed 81 // and we should: 82 // - reject all pending request made to the remote process/target/thread. 83 // - avoid trying to do new request against this remote context. 84 // - unmanage this front, so that DevToolsClient.getFront no longer returns it. 85 // 86 // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server. 87 // This is done in a distinct method from `destroy` in order to do all that immediately, 88 // even if `Front.destroy` is overloaded by an async method. 89 baseFrontClassDestroy() { 90 // Reject all outstanding requests, they won't make sense after 91 // the front is destroyed. 92 while (this._requests.length) { 93 const { deferred, to, type, stack } = this._requests.shift(); 94 // Note: many tests are ignoring `Connection closed` promise rejections, 95 // via PromiseTestUtils.allowMatchingRejectionsGlobally. 96 // Do not update the message without updating the tests. 97 const msg = 98 "Connection closed, pending request to " + 99 to + 100 ", type " + 101 type + 102 " failed" + 103 "\n\nRequest stack:\n" + 104 stack.formattedStack; 105 deferred.reject(new Error(msg)); 106 } 107 108 if (this.actorID) { 109 super.destroy(); 110 this.actorID = null; 111 } 112 this._isDestroyed = true; 113 114 this.targetFront = null; 115 this.parentFront = null; 116 this._frontCreationListeners = null; 117 this._frontDestructionListeners = null; 118 this._beforeListeners = null; 119 } 120 121 async manage(front, form, ctx) { 122 if (!front.actorID) { 123 throw new Error( 124 "Can't manage front without an actor ID.\n" + 125 "Ensure server supports " + 126 front.typeName + 127 "." 128 ); 129 } 130 131 if (front.parentFront && front.parentFront !== this) { 132 throw new Error( 133 `${this.actorID} (${this.typeName}) can't manage ${front.actorID} 134 (${front.typeName}) since it has a different parentFront ${ 135 front.parentFront 136 ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")" 137 : "<no parentFront>" 138 }` 139 ); 140 } 141 142 super.manage(front); 143 144 if (typeof front.initialize == "function") { 145 await front.initialize(); 146 } 147 front._initializeResolved = true; 148 149 // Ensure calling form() *before* notifying about this front being just created. 150 // We exprect the front to be fully initialized, especially via its form attributes. 151 // But do that *after* calling manage() so that the front is already registered 152 // in Pools and can be fetched by its ID, in case a child actor, created in form() 153 // tries to get a reference to its parent via the actor ID. 154 if (form) { 155 front.form(form, ctx); 156 } 157 158 // Call listeners registered via `watchFronts` method 159 // (ignore if this front has been destroyed) 160 if (this._frontCreationListeners) { 161 this._frontCreationListeners.emit(front.typeName, front); 162 } 163 } 164 165 async unmanage(front) { 166 super.unmanage(front); 167 168 // Call listeners registered via `watchFronts` method 169 if (this._frontDestructionListeners) { 170 this._frontDestructionListeners.emit(front.typeName, front); 171 } 172 } 173 174 /** 175 * Listen for the creation and/or destruction of fronts matching one of the provided types. 176 * 177 * @param {string} typeName 178 * Actor type to watch. 179 * @param {Function} onAvailable (optional) 180 * Callback fired when a front has been just created or was already available. 181 * The function is called with one arguments, the front. 182 * @param {Function} onDestroy (optional) 183 * Callback fired in case of front destruction. 184 * The function is called with the same argument than onAvailable. 185 */ 186 watchFronts(typeName, onAvailable, onDestroy) { 187 if (this.isDestroyed()) { 188 // The front was already destroyed, bail out. 189 console.error( 190 `Tried to call watchFronts for the '${typeName}' type on an ` + 191 `already destroyed front '${this.typeName}'.` 192 ); 193 return; 194 } 195 196 if (onAvailable) { 197 // First fire the callback on fronts with the correct type and which have 198 // been initialized. If initialize() is still in progress, the front will 199 // be emitted via _frontCreationListeners shortly after. 200 for (const front of this.poolChildren()) { 201 if (front.typeName == typeName && front._initializeResolved) { 202 onAvailable(front); 203 } 204 } 205 206 if (!this._frontCreationListeners) { 207 this._frontCreationListeners = new EventEmitter(); 208 } 209 // Then register the callback for fronts instantiated in the future 210 this._frontCreationListeners.on(typeName, onAvailable); 211 } 212 213 if (onDestroy) { 214 if (!this._frontDestructionListeners) { 215 this._frontDestructionListeners = new EventEmitter(); 216 } 217 this._frontDestructionListeners.on(typeName, onDestroy); 218 } 219 } 220 221 /** 222 * Stop listening for the creation and/or destruction of a given type of fronts. 223 * See `watchFronts()` for documentation of the arguments. 224 */ 225 unwatchFronts(typeName, onAvailable, onDestroy) { 226 if (this.isDestroyed()) { 227 // The front was already destroyed, bail out. 228 console.error( 229 `Tried to call unwatchFronts for the '${typeName}' type on an ` + 230 `already destroyed front '${this.typeName}'.` 231 ); 232 return; 233 } 234 235 if (onAvailable && this._frontCreationListeners) { 236 this._frontCreationListeners.off(typeName, onAvailable); 237 } 238 if (onDestroy && this._frontDestructionListeners) { 239 this._frontDestructionListeners.off(typeName, onDestroy); 240 } 241 } 242 243 /** 244 * Register an event listener that will be called immediately on packer receival. 245 * The given callback is going to be called before emitting the event via EventEmitter 246 * API on the Front. Event emitting will be delayed if the callback is async. 247 * Only one such listener can be registered per type of event. 248 * 249 * @param String type 250 * Event emitted by the actor to intercept. 251 * @param Function callback 252 * Function that will process the event. 253 */ 254 before(type, callback) { 255 if (this._beforeListeners.has(type)) { 256 throw new Error( 257 `Can't register multiple before listeners for "${type}".` 258 ); 259 } 260 this._beforeListeners.set(type, callback); 261 } 262 263 toString() { 264 return "[Front for " + this.typeName + "/" + this.actorID + "]"; 265 } 266 267 /** 268 * Update the actor from its representation. 269 * Subclasses should override this. 270 */ 271 form() {} 272 273 /** 274 * Send a packet on the connection. 275 * 276 * @param {object} packet 277 * @param {object} options 278 * @param {boolean} options.bulk 279 * To be set to true, if the packet relates to bulk request. 280 * Bulk request allows to send raw bytes over the wire instead of 281 * having to create a JSON string packet. 282 * @param {Function} options.clientBulkCallback 283 * For bulk request, function called with a StreamWriter as only argument. 284 * This is called when the StreamWriter is available in order to send 285 * bytes to the server. 286 */ 287 send(packet, { bulk = false, clientBulkCallback = null } = {}) { 288 // The connection might be closed during the promise resolution 289 if (!this.conn?._transport) { 290 return; 291 } 292 293 if (!bulk) { 294 if (!packet.to) { 295 packet.to = this.actorID; 296 } 297 this.conn._transport.send(packet); 298 } else { 299 if (!packet.actor) { 300 packet.actor = this.actorID; 301 } 302 this.conn._transport.startBulkSend(packet).then(clientBulkCallback); 303 } 304 } 305 306 /** 307 * Send a two-way request on the connection. 308 * 309 * See `send()` jsdoc for parameters definition. 310 */ 311 request(packet, { bulk = false, clientBulkCallback = null } = {}) { 312 const deferred = Promise.withResolvers(); 313 // Save packet basics for debugging 314 const { to, type } = packet; 315 this._requests.push({ 316 deferred, 317 to: to || this.actorID, 318 type, 319 packet, 320 stack: getStack(), 321 clientBulkCallback, 322 }); 323 this.send(packet, { bulk, clientBulkCallback }); 324 return deferred.promise; 325 } 326 327 /** 328 * Handler for incoming packets from the client's actor. 329 */ 330 onPacket(packet) { 331 if (this.isDestroyed()) { 332 // If the Front was already destroyed, all the requests have been purged 333 // and rejected with detailed error messages in baseFrontClassDestroy. 334 return; 335 } 336 337 // Pick off event packets 338 const type = packet.type || undefined; 339 if (this._clientSpec.events && this._clientSpec.events.has(type)) { 340 const event = this._clientSpec.events.get(packet.type); 341 let args; 342 try { 343 args = event.request.read(packet, this); 344 } catch (ex) { 345 console.error("Error reading event: " + packet.type); 346 console.exception(ex); 347 throw ex; 348 } 349 // Check for "pre event" callback to be processed before emitting events on fronts 350 // Use event.name instead of packet.type to use specific event name instead of RDP 351 // packet's type. 352 const beforeEvent = this._beforeListeners.get(event.name); 353 if (beforeEvent) { 354 const result = beforeEvent.apply(this, args); 355 // Check to see if the beforeEvent returned a promise -- if so, 356 // wait for their resolution before emitting. Otherwise, emit synchronously. 357 if (result && typeof result.then == "function") { 358 result.then(() => { 359 super.emit(event.name, ...args); 360 ChromeUtils.addProfilerMarker( 361 "DevTools:RDP Front", 362 null, 363 `${this.typeName}.${event.name}` 364 ); 365 }); 366 return; 367 } 368 } 369 370 super.emit(event.name, ...args); 371 ChromeUtils.addProfilerMarker( 372 "DevTools:RDP Front", 373 null, 374 `${this.typeName}.${event.name}` 375 ); 376 return; 377 } 378 379 // Remaining packets must be responses. 380 if (this._requests.length === 0) { 381 const msg = 382 "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet); 383 const err = Error(msg); 384 console.error(err); 385 throw err; 386 } 387 388 const { deferred, packet: clientPacket, stack } = this._requests.shift(); 389 callFunctionWithAsyncStack( 390 () => { 391 if (packet.error) { 392 let message; 393 if (packet.error && packet.message) { 394 message = 395 "Protocol error (" + packet.error + "): " + packet.message; 396 } else { 397 message = packet.error; 398 } 399 message += " from: " + this.actorID; 400 if (packet.fileName) { 401 const { fileName, columnNumber, lineNumber } = packet; 402 message += ` (${fileName}:${lineNumber}:${columnNumber})`; 403 } 404 const packetError = new Error(message); 405 406 // Pass the packets on the exception object to convey them to AppErrorBoundary 407 packetError.serverPacket = packet; 408 packetError.clientPacket = clientPacket; 409 410 deferred.reject(packetError); 411 } else { 412 deferred.resolve(packet); 413 } 414 }, 415 stack, 416 "DevTools RDP" 417 ); 418 } 419 420 /** 421 * DevToolsClient will notify Fronts about bulk packet via this method. 422 */ 423 onBulkPacket(packet) { 424 // We can actually consider the bulk packet as a regular packet. 425 this.onPacket(packet); 426 } 427 428 hasRequests() { 429 return !!this._requests.length; 430 } 431 432 /** 433 * Wait for all current requests from this front to settle. This is especially useful 434 * for tests and other utility environments that may not have events or mechanisms to 435 * await the completion of requests without this utility. 436 * 437 * @return Promise 438 * Resolved when all requests have settled. 439 */ 440 waitForRequestsToSettle() { 441 return settleAll(this._requests.map(({ deferred }) => deferred.promise)); 442 } 443 } 444 445 exports.Front = Front;