devtools-client.js (33256B)
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 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 8 const { 9 getStack, 10 callFunctionWithAsyncStack, 11 } = require("resource://devtools/shared/platform/stack.js"); 12 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 13 const { 14 UnsolicitedNotifications, 15 } = require("resource://devtools/client/constants.js"); 16 const { AppConstants } = ChromeUtils.importESModule( 17 "resource://gre/modules/AppConstants.sys.mjs" 18 ); 19 20 loader.lazyRequireGetter( 21 this, 22 "Authentication", 23 "resource://devtools/shared/security/auth.js" 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "DebuggerSocket", 28 "resource://devtools/shared/security/socket.js", 29 true 30 ); 31 loader.lazyRequireGetter( 32 this, 33 "EventEmitter", 34 "resource://devtools/shared/event-emitter.js" 35 ); 36 37 loader.lazyRequireGetter( 38 this, 39 ["createRootFront", "Front"], 40 "resource://devtools/shared/protocol.js", 41 true 42 ); 43 44 loader.lazyRequireGetter( 45 this, 46 "ObjectFront", 47 "resource://devtools/client/fronts/object.js", 48 true 49 ); 50 51 /** 52 * Creates a client for the remote debugging protocol server. This client 53 * provides the means to communicate with the server and exchange the messages 54 * required by the protocol in a traditional JavaScript API. 55 */ 56 class DevToolsClient extends EventEmitter { 57 constructor(transport) { 58 super(); 59 60 this._transport = transport; 61 this._transport.hooks = this; 62 63 this._pendingRequests = new Map(); 64 this._activeRequests = new Map(); 65 this._eventsEnabled = true; 66 67 this.traits = {}; 68 69 this.request = this.request.bind(this); 70 71 /* 72 * As the first thing on the connection, expect a greeting packet from 73 * the connection's root actor. 74 */ 75 this.mainRoot = null; 76 this.expectReply("root", async packet => { 77 if (packet.error) { 78 console.error("Error when waiting for root actor", packet); 79 return; 80 } 81 82 this.mainRoot = createRootFront(this, packet); 83 84 // Once the root actor has been communicated by the server, 85 // emit a request to it to also push informations down to the server. 86 // 87 // This request has been added in Firefox 133. 88 try { 89 await this.mainRoot.connect({ 90 frontendVersion: AppConstants.MOZ_APP_VERSION, 91 }); 92 } catch (e) { 93 // Ignore errors of unsupported packet as the server may not yet support this request. 94 // The request may also fail to complete in tests when closing DevTools quickly after opening. 95 if (!e.message.includes("unrecognizedPacketType")) { 96 throw e; 97 } 98 } 99 100 this.emit("connected", packet.applicationType, packet.traits); 101 }); 102 } 103 104 // Expose these to save callers the trouble of importing DebuggerSocket 105 static socketConnect(options) { 106 // Defined here instead of just copying the function to allow lazy-load 107 return DebuggerSocket.connect(options); 108 } 109 110 static get Authenticators() { 111 return Authentication.Authenticators; 112 } 113 114 static get AuthenticationResult() { 115 return Authentication.AuthenticationResult; 116 } 117 118 /** 119 * Connect to the server and start exchanging protocol messages. 120 * 121 * @return Promise 122 * Resolves once connected with an array whose first element 123 * is the application type, by default "browser", and the second 124 * element is the traits object (help figure out the features 125 * and behaviors of the server we connect to. See RootActor). 126 */ 127 connect() { 128 return new Promise(resolve => { 129 this.once("connected", (applicationType, traits) => { 130 this.traits = traits; 131 132 resolve([applicationType, traits]); 133 }); 134 135 this._transport.ready(); 136 }); 137 } 138 139 /** 140 * Shut down communication with the debugging server. 141 * 142 * @return Promise 143 * Resolves after the underlying transport is closed. 144 */ 145 close() { 146 if (this._transportClosed) { 147 return Promise.resolve(); 148 } 149 if (this._closePromise) { 150 return this._closePromise; 151 } 152 // Immediately set the destroy promise, 153 // as the following code is fully synchronous and can be reentrant. 154 this._closePromise = this.once("closed"); 155 156 // Disable detach event notifications, because event handlers will be in a 157 // cleared scope by the time they run. 158 this._eventsEnabled = false; 159 160 if (this._transport) { 161 this._transport.close(); 162 this._transport = null; 163 } 164 165 return this._closePromise; 166 } 167 168 /** 169 * Send a request to the debugging server. 170 * 171 * @param packet object 172 * A JSON packet to send to the debugging server. 173 * @return Request 174 * This object emits a number of events to allow you to respond to 175 * different parts of the request lifecycle. 176 * It is also a Promise object, with a `then` method, that is resolved 177 * whenever a JSON or a Bulk response is received; and is rejected 178 * if the response is an error. 179 * 180 * Events emitted: 181 * * json-reply: The server replied with a JSON packet, which is 182 * passed as event data. 183 * * bulk-reply: The server replied with bulk data, which you can read 184 * using the event data object containing: 185 * * actor: Name of actor that received the packet 186 * * type: Name of actor's method that was called on receipt 187 * * length: Size of the data to be read 188 * * stream: This input stream should only be used directly if you 189 * can ensure that you will read exactly |length| bytes 190 * and will not close the stream when reading is complete 191 * * done: If you use the stream directly (instead of |copyTo| 192 * or |copyToBuffer| below), you must signal 193 * completion by resolving / rejecting this promise. 194 * If it's rejected, the transport will be closed. 195 * If an Error is supplied as a rejection value, it 196 * will be logged via |dumpn|. If you do use 197 * |copyTo| or |copyToBuffer|, resolving is taken 198 * care of for you when copying completes. 199 * * copyTo: A helper function for getting your data out of the 200 * stream that meets the stream handling requirements 201 * above, and has the following signature: 202 * @param output nsIAsyncOutputStream 203 * The stream to copy to. 204 * @return Promise 205 * The promise is resolved when copying completes or 206 * rejected if any (unexpected) errors occur. 207 * This object also emits "progress" events for each chunk 208 * that is copied. See stream-utils.js. 209 * * copyToBuffer: a helper function for getting your data out of the stream 210 * that meets the stream handling requirements above, and has 211 * the following signature: 212 * @param output ArrayBuffer 213 * The buffer to copy to. It needs to be the same length as the data 214 * to be transfered. 215 * @return Promise 216 * The promise is resolved when copying completes or rejected if any 217 * (unexpected) error occurs. 218 */ 219 request(packet) { 220 if (!this.mainRoot) { 221 throw Error("Have not yet received a hello packet from the server."); 222 } 223 const type = packet.type || ""; 224 if (!packet.to) { 225 throw Error("'" + type + "' request packet has no destination."); 226 } 227 228 if (this._transportClosed) { 229 const msg = 230 "'" + 231 type + 232 "' request packet to " + 233 "'" + 234 packet.to + 235 "' " + 236 "can't be sent as the connection is closed."; 237 return Promise.reject({ error: "connectionClosed", message: msg }); 238 } 239 240 const request = new Request(packet); 241 request.format = "json"; 242 request.stack = getStack(); 243 244 // Implement a Promise like API on the returned object 245 // that resolves/rejects on request response 246 const promise = new Promise((resolve, reject) => { 247 function listenerJson(resp) { 248 removeRequestListeners(); 249 if (resp.error) { 250 reject(resp); 251 } else { 252 resolve(resp); 253 } 254 } 255 function listenerBulk(resp) { 256 removeRequestListeners(); 257 resolve(resp); 258 } 259 260 const removeRequestListeners = () => { 261 request.off("json-reply", listenerJson); 262 request.off("bulk-reply", listenerBulk); 263 }; 264 265 request.on("json-reply", listenerJson); 266 request.on("bulk-reply", listenerBulk); 267 }); 268 269 this._sendOrQueueRequest(request); 270 request.then = promise.then.bind(promise); 271 request.catch = promise.catch.bind(promise); 272 273 return request; 274 } 275 276 /** 277 * Transmit streaming data via a bulk request. 278 * 279 * This method initiates the bulk send process by queuing up the header data. 280 * The caller receives eventual access to a stream for writing. 281 * 282 * Since this opens up more options for how the server might respond (it could 283 * send back either JSON or bulk data), and the returned Request object emits 284 * events for different stages of the request process that you may want to 285 * react to. 286 * 287 * @param request Object 288 * This is modeled after the format of JSON packets above, but does not 289 * actually contain the data, but is instead just a routing header: 290 * * actor: Name of actor that will receive the packet 291 * * type: Name of actor's method that should be called on receipt 292 * * length: Size of the data to be sent 293 * @return Request 294 * This object emits a number of events to allow you to respond to 295 * different parts of the request lifecycle. 296 * 297 * Events emitted: 298 * * bulk-send-ready: Ready to send bulk data to the server, using the 299 * event data object containing: 300 * * stream: This output stream should only be used directly if 301 * you can ensure that you will write exactly |length| 302 * bytes and will not close the stream when writing is 303 * complete 304 * * done: If you use the stream directly (instead of |copyFrom| 305 * or |copyFromBuffer| below), you must signal 306 * completion by resolving / rejecting this 307 * promise. If it's rejected, the transport will 308 * be closed. If an Error is supplied as a 309 * rejection value, it will be logged via |dumpn|. 310 * If you do use |copyFrom| or |copyFromBuffer|, 311 * resolving is taken care of for you when copying 312 * completes. 313 * * copyFrom: A helper function for getting your data onto the 314 * stream that meets the stream handling requirements 315 * above, and has the following signature: 316 * @param input nsIAsyncInputStream 317 * The stream to copy from. 318 * @return Promise 319 * The promise is resolved when copying completes or 320 * rejected if any (unexpected) errors occur. 321 * This object also emits "progress" events for each chunk 322 * that is copied. See stream-utils.js. 323 * * copyFromBuffer: A helper function for getting your data onto 324 * the stream that meets the stream handling 325 * requirements above, and has the following 326 * signature: 327 * @param input ArrayBuffer 328 * The buffer to read from. It needs to be the same length 329 * as the data to write. 330 * @return Promise 331 * The promise is resolved when copying completes or 332 * rejected if any (unexpected) errors occur. 333 * * json-reply: The server replied with a JSON packet, which is 334 * passed as event data. 335 * * bulk-reply: The server replied with bulk data, which you can read 336 * using the event data object containing: 337 * * actor: Name of actor that received the packet 338 * * type: Name of actor's method that was called on receipt 339 * * length: Size of the data to be read 340 * * stream: This input stream should only be used directly if you 341 * can ensure that you will read exactly |length| bytes 342 * and will not close the stream when reading is complete 343 * * done: If you use the stream directly (instead of |copyTo| 344 * |copyToBuffer| below), you must signal completion 345 * by resolving / rejecting this promise. If it's 346 * rejected, the transport will be closed. If an 347 * Error is supplied as a rejection value, it will 348 * be logged via |dumpn|. If you do use |copyTo| or 349 * |copyToBuffer|, resolving is taken care of for 350 * you when copying completes. 351 * * copyTo: A helper function for getting your data out of the 352 * stream that meets the stream handling requirements 353 * above, and has the following signature: 354 * @param output nsIAsyncOutputStream 355 * The stream to copy to. 356 * @return Promise 357 * The promise is resolved when copying completes or 358 * rejected if any (unexpected) errors occur. 359 * This object also emits "progress" events for each chunk 360 * that is copied. See stream-utils.js. 361 * 362 * * copyToBuffer: a helper function for getting your data out of the stream 363 * that meets the stream handling requirements above, and has 364 * the following signature: 365 * @param output ArrayBuffer 366 * The buffer to copy to. It needs to be the same length as the data 367 * to be transfered. 368 * @return Promise 369 * The promise is resolved when copying completes or rejected if any 370 * (unexpected) error occurs. 371 */ 372 startBulkRequest(request) { 373 if (!this.mainRoot) { 374 throw Error("Have not yet received a hello packet from the server."); 375 } 376 if (!request.type) { 377 throw Error("Bulk packet is missing the required 'type' field."); 378 } 379 if (!request.actor) { 380 throw Error("'" + request.type + "' bulk packet has no destination."); 381 } 382 if (!request.length) { 383 throw Error("'" + request.type + "' bulk packet has no length."); 384 } 385 386 request = new Request(request); 387 request.format = "bulk"; 388 389 this._sendOrQueueRequest(request); 390 391 return request; 392 } 393 394 /** 395 * If a new request can be sent immediately, do so. Otherwise, queue it. 396 */ 397 _sendOrQueueRequest(request) { 398 const actor = request.actor; 399 if (!this._activeRequests.has(actor)) { 400 this._sendRequest(request); 401 } else { 402 this._queueRequest(request); 403 } 404 } 405 406 /** 407 * Send a request. 408 * 409 * @throws Error if there is already an active request in flight for the same 410 * actor. 411 */ 412 _sendRequest(request) { 413 const actor = request.actor; 414 this.expectReply(actor, request); 415 416 if (request.format === "json") { 417 this._transport.send(request.request); 418 return; 419 } 420 421 this._transport.startBulkSend(request.request).then((...args) => { 422 request.emit("bulk-send-ready", ...args); 423 }); 424 } 425 426 /** 427 * Queue a request to be sent later. Queues are only drained when an in 428 * flight request to a given actor completes. 429 */ 430 _queueRequest(request) { 431 const actor = request.actor; 432 const queue = this._pendingRequests.get(actor) || []; 433 queue.push(request); 434 this._pendingRequests.set(actor, queue); 435 } 436 437 /** 438 * Attempt the next request to a given actor (if any). 439 */ 440 _attemptNextRequest(actor) { 441 if (this._activeRequests.has(actor)) { 442 return; 443 } 444 const queue = this._pendingRequests.get(actor); 445 if (!queue) { 446 return; 447 } 448 const request = queue.shift(); 449 if (queue.length === 0) { 450 this._pendingRequests.delete(actor); 451 } 452 this._sendRequest(request); 453 } 454 455 /** 456 * Arrange to hand the next reply from |actor| to the handler bound to 457 * |request|. 458 * 459 * DevToolsClient.prototype.request / startBulkRequest usually takes care of 460 * establishing the handler for a given request, but in rare cases (well, 461 * greetings from new root actors, is the only case at the moment) we must be 462 * prepared for a "reply" that doesn't correspond to any request we sent. 463 */ 464 expectReply(actor, request) { 465 if (this._activeRequests.has(actor)) { 466 throw Error("clashing handlers for next reply from " + actor); 467 } 468 469 // If a handler is passed directly (as it is with the handler for the root 470 // actor greeting), create a dummy request to bind this to. 471 if (typeof request === "function") { 472 const handler = request; 473 request = new Request(); 474 request.on("json-reply", handler); 475 } 476 477 this._activeRequests.set(actor, request); 478 } 479 480 // Transport hooks. 481 482 /** 483 * Called by DebuggerTransport to dispatch incoming packets as appropriate. 484 * 485 * @param packet object 486 * The incoming packet. 487 */ 488 onPacket(packet) { 489 if (!packet.from) { 490 DevToolsUtils.reportException( 491 "onPacket", 492 new Error( 493 "Server did not specify an actor, dropping packet: " + 494 JSON.stringify(packet) 495 ) 496 ); 497 return; 498 } 499 500 // Check for "forwardingCancelled" here instead of using a front to handle it. 501 // This is necessary because we might receive this event while the client is closing, 502 // and the fronts have already been removed by that point. 503 if ( 504 this.mainRoot && 505 packet.from == this.mainRoot.actorID && 506 packet.type == "forwardingCancelled" 507 ) { 508 this.purgeRequests(packet.prefix); 509 return; 510 } 511 512 // If we have a registered Front for this actor, let it handle the packet 513 // and skip all the rest of this unpleasantness. 514 const front = this.getFrontByID(packet.from); 515 if (front) { 516 front.onPacket(packet); 517 return; 518 } 519 520 let activeRequest; 521 // See if we have a handler function waiting for a reply from this 522 // actor. (Don't count unsolicited notifications or pauses as 523 // replies.) 524 if ( 525 this._activeRequests.has(packet.from) && 526 !(packet.type in UnsolicitedNotifications) 527 ) { 528 activeRequest = this._activeRequests.get(packet.from); 529 this._activeRequests.delete(packet.from); 530 } 531 532 // If there is a subsequent request for the same actor, hand it off to the 533 // transport. Delivery of packets on the other end is always async, even 534 // in the local transport case. 535 this._attemptNextRequest(packet.from); 536 537 // Only try to notify listeners on events, not responses to requests 538 // that lack a packet type. 539 if (packet.type) { 540 this.emit(packet.type, packet); 541 } 542 543 if (activeRequest) { 544 const emitReply = () => activeRequest.emit("json-reply", packet); 545 if (activeRequest.stack) { 546 callFunctionWithAsyncStack( 547 emitReply, 548 activeRequest.stack, 549 "DevTools RDP" 550 ); 551 } else { 552 emitReply(); 553 } 554 } 555 } 556 557 /** 558 * Called by the DebuggerTransport to dispatch incoming bulk packets as 559 * appropriate. 560 * 561 * @param packet object 562 * The incoming packet, which contains: 563 * * actor: Name of actor that will receive the packet 564 * * type: Name of actor's method that should be called on receipt 565 * * length: Size of the data to be read 566 * * stream: This input stream should only be used directly if you can 567 * ensure that you will read exactly |length| bytes and will 568 * not close the stream when reading is complete 569 * * done: If you use the stream directly (instead of |copyTo| 570 * or |copyToBuffer| below), you must signal completion 571 * by resolving / rejecting this promise. If it's 572 * rejected, the transport will be closed. If an Error 573 * is supplied as a rejection value, it will be logged 574 * via |dumpn|. If you do use |copyTo| or 575 * |copyToBuffer|, resolving is taken care of for you 576 * when copying completes. 577 * * copyTo: A helper function for getting your data out of the stream 578 * that meets the stream handling requirements above, and has 579 * the following signature: 580 * @param output nsIAsyncOutputStream 581 * The stream to copy to. 582 * @return Promise 583 * The promise is resolved when copying completes or rejected 584 * if any (unexpected) errors occur. 585 * This object also emits "progress" events for each chunk 586 * that is copied. See stream-utils.js. 587 * * copyToBuffer: a helper function for getting your data out of the stream 588 * that meets the stream handling requirements above, and has 589 * the following signature: 590 * @param output ArrayBuffer 591 * The buffer to copy to. It needs to be the same length as the data 592 * to be transfered. 593 * @return Promise 594 * The promise is resolved when copying completes or rejected if any 595 * (unexpected) error occurs. 596 */ 597 onBulkPacket(packet) { 598 const { actor } = packet; 599 600 if (!actor) { 601 DevToolsUtils.reportException( 602 "onBulkPacket", 603 new Error( 604 "Server did not specify an actor, dropping bulk packet: " + 605 JSON.stringify(packet) 606 ) 607 ); 608 return; 609 } 610 611 const front = this.getFrontByID(actor); 612 if (front) { 613 front.onBulkPacket(packet); 614 return; 615 } 616 617 // See if we have a handler function waiting for a reply from this 618 // actor. 619 if (!this._activeRequests.has(actor)) { 620 return; 621 } 622 623 const activeRequest = this._activeRequests.get(actor); 624 this._activeRequests.delete(actor); 625 626 // If there is a subsequent request for the same actor, hand it off to the 627 // transport. Delivery of packets on the other end is always async, even 628 // in the local transport case. 629 this._attemptNextRequest(actor); 630 631 activeRequest.emit("bulk-reply", packet); 632 } 633 634 /** 635 * Called by DebuggerTransport when the underlying stream is closed. 636 * 637 * @param status nsresult 638 * The status code that corresponds to the reason for closing 639 * the stream. 640 */ 641 onTransportClosed() { 642 if (this._transportClosed) { 643 return; 644 } 645 this._transportClosed = true; 646 this.emit("closed"); 647 648 this.purgeRequests(); 649 650 // The |_pools| array on the client-side currently is used only by 651 // protocol.js to store active fronts, mirroring the actor pools found in 652 // the server. So, read all usages of "pool" as "protocol.js front". 653 // 654 // In the normal case where we shutdown cleanly, the toolbox tells each tool 655 // to close, and they each call |destroy| on any fronts they were using. 656 // When |destroy| is called on a protocol.js front, it also 657 // removes itself from the |_pools| array. Once the toolbox has shutdown, 658 // the connection is closed, and we reach here. All fronts (should have 659 // been) |destroy|ed, so |_pools| should empty. 660 // 661 // If the connection instead aborts unexpectedly, we may end up here with 662 // all fronts used during the life of the connection. So, we call |destroy| 663 // on them clear their state, reject pending requests, and remove themselves 664 // from |_pools|. This saves the toolbox from hanging indefinitely, in case 665 // it waits for some server response before shutdown that will now never 666 // arrive. 667 for (const pool of this._pools) { 668 pool.destroy(); 669 } 670 } 671 672 /** 673 * Purge pending and active requests in this client. 674 * 675 * @param prefix string (optional) 676 * If a prefix is given, only requests for actor IDs that start with the prefix 677 * will be cleaned up. This is useful when forwarding of a portion of requests 678 * is cancelled on the server. 679 */ 680 purgeRequests(prefix = "") { 681 const reject = function (type, request) { 682 // Server can send packets on its own and client only pass a callback 683 // to expectReply, so that there is no request object. 684 let msg; 685 if (request.request) { 686 msg = 687 "'" + 688 request.request.type + 689 "' " + 690 type + 691 " request packet" + 692 " to '" + 693 request.actor + 694 "' " + 695 "can't be sent as the connection just closed."; 696 } else { 697 msg = 698 "server side packet can't be received as the connection just closed."; 699 } 700 const packet = { error: "connectionClosed", message: msg }; 701 request.emit("json-reply", packet); 702 }; 703 704 let pendingRequestsToReject = []; 705 this._pendingRequests.forEach((requests, actor) => { 706 if (!actor.startsWith(prefix)) { 707 return; 708 } 709 this._pendingRequests.delete(actor); 710 pendingRequestsToReject = pendingRequestsToReject.concat(requests); 711 }); 712 pendingRequestsToReject.forEach(request => reject("pending", request)); 713 714 let activeRequestsToReject = []; 715 this._activeRequests.forEach((request, actor) => { 716 if (!actor.startsWith(prefix)) { 717 return; 718 } 719 this._activeRequests.delete(actor); 720 activeRequestsToReject = activeRequestsToReject.concat(request); 721 }); 722 activeRequestsToReject.forEach(request => reject("active", request)); 723 724 // Also purge protocol.js requests 725 const fronts = this.getAllFronts(); 726 727 for (const front of fronts) { 728 if (!front.isDestroyed() && front.actorID.startsWith(prefix)) { 729 // Call Front.baseFrontClassDestroy nstead of Front.destroy in order to flush requests 730 // and nullify front.actorID immediately, even if Front.destroy is overloaded 731 // by an async function which would otherwise be able to try emitting new request 732 // after the purge. 733 front.baseFrontClassDestroy(); 734 } 735 } 736 } 737 738 /** 739 * Search for all requests in process for this client, including those made via 740 * protocol.js and wait all of them to complete. Since the requests seen when this is 741 * first called may in turn trigger more requests, we keep recursing through this 742 * function until there is no more activity. 743 * 744 * This is a fairly heavy weight process, so it's only meant to be used in tests. 745 * 746 * @param {object=} options 747 * @param {boolean=} options.ignoreOrphanedFronts 748 * Allow to ignore fronts which can no longer be retrieved via 749 * getFrontByID, as their requests can never be completed now. 750 * Ideally we should rather investigate and address those cases, but 751 * since this is a test helper, allow to bypass them here. Defaults to 752 * false. 753 * 754 * @return Promise 755 * Resolved when all requests have settled. 756 */ 757 waitForRequestsToSettle({ ignoreOrphanedFronts = false } = {}) { 758 let requests = []; 759 760 // Gather all pending and active requests in this client 761 // The request object supports a Promise API for completion (it has .then()) 762 this._pendingRequests.forEach(requestsForActor => { 763 // Each value is an array of pending requests 764 requests = requests.concat(requestsForActor); 765 }); 766 this._activeRequests.forEach(requestForActor => { 767 // Each value is a single active request 768 requests = requests.concat(requestForActor); 769 }); 770 771 // protocol.js 772 const fronts = this.getAllFronts(); 773 774 // For each front, wait for its requests to settle 775 for (const front of fronts) { 776 if (front.hasRequests()) { 777 if (ignoreOrphanedFronts && !this.getFrontByID(front.actorID)) { 778 // If a front was stuck during its destroy but the pool managing it 779 // has been already removed, ignore its pending requests, they can 780 // never resolve. 781 continue; 782 } 783 requests.push(front.waitForRequestsToSettle()); 784 } 785 } 786 787 // Abort early if there are no requests 788 if (!requests.length) { 789 return Promise.resolve(); 790 } 791 792 return DevToolsUtils.settleAll(requests) 793 .catch(() => { 794 // One of the requests might have failed, but ignore that situation here and pipe 795 // both success and failure through the same path. The important part is just that 796 // we waited. 797 }) 798 .then(() => { 799 // Repeat, more requests may have started in response to those we just waited for 800 return this.waitForRequestsToSettle({ ignoreOrphanedFronts }); 801 }); 802 } 803 804 getAllFronts() { 805 // Use a Set because some fronts (like domwalker) seem to have multiple parents. 806 const fronts = new Set(); 807 const poolsToVisit = [...this._pools]; 808 809 // With protocol.js, each front can potentially have its own pools containing child 810 // fronts, forming a tree. Descend through all the pools to locate all child fronts. 811 while (poolsToVisit.length) { 812 const pool = poolsToVisit.shift(); 813 // `_pools` contains either Fronts or Pools, we only want to collect Fronts here. 814 // Front inherits from Pool which exposes `poolChildren`. 815 if (pool instanceof Front) { 816 fronts.add(pool); 817 } 818 for (const child of pool.poolChildren()) { 819 poolsToVisit.push(child); 820 } 821 } 822 return fronts; 823 } 824 825 /** 826 * Actor lifetime management, echos the server's actor pools. 827 */ 828 get _pools() { 829 if (this.__pools) { 830 return this.__pools; 831 } 832 this.__pools = new Set(); 833 return this.__pools; 834 } 835 836 addActorPool(pool) { 837 this._pools.add(pool); 838 } 839 removeActorPool(pool) { 840 this._pools.delete(pool); 841 } 842 843 /** 844 * Return the Front for the Actor whose ID is the one passed in argument. 845 * 846 * @param {string} actorID: The actor ID to look for. 847 */ 848 getFrontByID(actorID) { 849 const pool = this.poolFor(actorID); 850 return pool ? pool.getActorByID(actorID) : null; 851 } 852 853 poolFor(actorID) { 854 for (const pool of this._pools) { 855 if (pool.has(actorID)) { 856 return pool; 857 } 858 } 859 return null; 860 } 861 862 /** 863 * Creates an object front for this DevToolsClient and the grip in parameter, 864 * 865 * @param {object} grip: The grip to create the ObjectFront for. 866 * @param {ThreadFront} threadFront 867 * @param {Front} parentFront: Optional front that will manage the object front. 868 * Defaults to threadFront. 869 * @returns {ObjectFront} 870 */ 871 createObjectFront(grip, threadFront, parentFront) { 872 if (!parentFront) { 873 parentFront = threadFront; 874 } 875 876 return new ObjectFront(this, threadFront.targetFront, parentFront, grip); 877 } 878 879 get transport() { 880 return this._transport; 881 } 882 883 /** 884 * Boolean flag to help identify client connected to the current runtime, 885 * via a LocalDevToolsTransport pipe. 886 */ 887 get isLocalClient() { 888 return !!this._transport.isLocalTransport; 889 } 890 891 dumpPools() { 892 for (const pool of this._pools) { 893 console.log(`%c${pool.actorID}`, "font-weight: bold;", [ 894 ...pool.__poolMap.keys(), 895 ]); 896 } 897 } 898 } 899 900 class Request extends EventEmitter { 901 constructor(request) { 902 super(); 903 this.request = request; 904 } 905 906 get actor() { 907 return this.request.to || this.request.actor; 908 } 909 } 910 911 module.exports = { 912 DevToolsClient, 913 };