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/. */
7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
10 callFunctionWithAsyncStack,
11 } = require("resource://devtools/shared/platform/stack.js");
12 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
14 UnsolicitedNotifications,
15 } = require("resource://devtools/client/constants.js");
17 loader.lazyRequireGetter(
20 "resource://devtools/shared/security/auth.js"
22 loader.lazyRequireGetter(
25 "resource://devtools/shared/security/socket.js",
28 loader.lazyRequireGetter(
31 "resource://devtools/shared/event-emitter.js"
34 loader.lazyRequireGetter(
36 ["createRootFront", "Front"],
37 "resource://devtools/shared/protocol.js",
41 loader.lazyRequireGetter(
44 "resource://devtools/client/fronts/object.js",
49 * Creates a client for the remote debugging protocol server. This client
50 * provides the means to communicate with the server and exchange the messages
51 * required by the protocol in a traditional JavaScript API.
53 function DevToolsClient(transport) {
54 this._transport = transport;
55 this._transport.hooks = this;
57 this._pendingRequests = new Map();
58 this._activeRequests = new Map();
59 this._eventsEnabled = true;
63 this.request = this.request.bind(this);
66 * As the first thing on the connection, expect a greeting packet from
67 * the connection's root actor.
70 this.expectReply("root", packet => {
72 console.error("Error when waiting for root actor", packet);
76 this.mainRoot = createRootFront(this, packet);
78 this.emit("connected", packet.applicationType, packet.traits);
82 // Expose these to save callers the trouble of importing DebuggerSocket
83 DevToolsClient.socketConnect = function (options) {
84 // Defined here instead of just copying the function to allow lazy-load
85 return DebuggerSocket.connect(options);
87 DevToolsUtils.defineLazyGetter(DevToolsClient, "Authenticators", () => {
88 return Authentication.Authenticators;
90 DevToolsUtils.defineLazyGetter(DevToolsClient, "AuthenticationResult", () => {
91 return Authentication.AuthenticationResult;
94 DevToolsClient.prototype = {
96 * Connect to the server and start exchanging protocol messages.
99 * Resolves once connected with an array whose first element
100 * is the application type, by default "browser", and the second
101 * element is the traits object (help figure out the features
102 * and behaviors of the server we connect to. See RootActor).
105 return new Promise(resolve => {
106 this.once("connected", (applicationType, traits) => {
107 this.traits = traits;
108 resolve([applicationType, traits]);
111 this._transport.ready();
116 * Shut down communication with the debugging server.
119 * Resolves after the underlying transport is closed.
122 if (this._transportClosed) {
123 return Promise.resolve();
125 if (this._closePromise) {
126 return this._closePromise;
128 // Immediately set the destroy promise,
129 // as the following code is fully synchronous and can be reentrant.
130 this._closePromise = this.once("closed");
132 // Disable detach event notifications, because event handlers will be in a
133 // cleared scope by the time they run.
134 this._eventsEnabled = false;
136 if (this._transport) {
137 this._transport.close();
138 this._transport = null;
141 return this._closePromise;
145 * Send a request to the debugging server.
147 * @param packet object
148 * A JSON packet to send to the debugging server.
150 * This object emits a number of events to allow you to respond to
151 * different parts of the request lifecycle.
152 * It is also a Promise object, with a `then` method, that is resolved
153 * whenever a JSON or a Bulk response is received; and is rejected
154 * if the response is an error.
157 * * json-reply: The server replied with a JSON packet, which is
158 * passed as event data.
159 * * bulk-reply: The server replied with bulk data, which you can read
160 * using the event data object containing:
161 * * actor: Name of actor that received the packet
162 * * type: Name of actor's method that was called on receipt
163 * * length: Size of the data to be read
164 * * stream: This input stream should only be used directly if you
165 * can ensure that you will read exactly |length| bytes
166 * and will not close the stream when reading is complete
167 * * done: If you use the stream directly (instead of |copyTo|
168 * below), you must signal completion by resolving /
169 * rejecting this promise. If it's rejected, the
170 * transport will be closed. If an Error is supplied as a
171 * rejection value, it will be logged via |dumpn|. If you
172 * do use |copyTo|, resolving is taken care of for you
173 * when copying completes.
174 * * copyTo: A helper function for getting your data out of the
175 * stream that meets the stream handling requirements
176 * above, and has the following signature:
177 * @param output nsIAsyncOutputStream
178 * The stream to copy to.
180 * The promise is resolved when copying completes or
181 * rejected if any (unexpected) errors occur.
182 * This object also emits "progress" events for each chunk
183 * that is copied. See stream-utils.js.
186 if (!this.mainRoot) {
187 throw Error("Have not yet received a hello packet from the server.");
189 const type = packet.type || "";
191 throw Error("'" + type + "' request packet has no destination.");
194 if (this._transportClosed) {
198 "' request packet to " +
202 "can't be sent as the connection is closed.";
203 return Promise.reject({ error: "connectionClosed", message: msg });
206 const request = new Request(packet);
207 request.format = "json";
208 request.stack = getStack();
210 // Implement a Promise like API on the returned object
211 // that resolves/rejects on request response
212 const promise = new Promise((resolve, reject) => {
213 function listenerJson(resp) {
214 removeRequestListeners();
221 function listenerBulk(resp) {
222 removeRequestListeners();
226 const removeRequestListeners = () => {
227 request.off("json-reply", listenerJson);
228 request.off("bulk-reply", listenerBulk);
231 request.on("json-reply", listenerJson);
232 request.on("bulk-reply", listenerBulk);
235 this._sendOrQueueRequest(request);
236 request.then = promise.then.bind(promise);
237 request.catch = promise.catch.bind(promise);
243 * Transmit streaming data via a bulk request.
245 * This method initiates the bulk send process by queuing up the header data.
246 * The caller receives eventual access to a stream for writing.
248 * Since this opens up more options for how the server might respond (it could
249 * send back either JSON or bulk data), and the returned Request object emits
250 * events for different stages of the request process that you may want to
253 * @param request Object
254 * This is modeled after the format of JSON packets above, but does not
255 * actually contain the data, but is instead just a routing header:
256 * * actor: Name of actor that will receive the packet
257 * * type: Name of actor's method that should be called on receipt
258 * * length: Size of the data to be sent
260 * This object emits a number of events to allow you to respond to
261 * different parts of the request lifecycle.
264 * * bulk-send-ready: Ready to send bulk data to the server, using the
265 * event data object containing:
266 * * stream: This output stream should only be used directly if
267 * you can ensure that you will write exactly |length|
268 * bytes and will not close the stream when writing is
270 * * done: If you use the stream directly (instead of |copyFrom|
271 * below), you must signal completion by resolving /
272 * rejecting this promise. If it's rejected, the
273 * transport will be closed. If an Error is supplied as
274 * a rejection value, it will be logged via |dumpn|. If
275 * you do use |copyFrom|, resolving is taken care of for
276 * you when copying completes.
277 * * copyFrom: A helper function for getting your data onto the
278 * stream that meets the stream handling requirements
279 * above, and has the following signature:
280 * @param input nsIAsyncInputStream
281 * The stream to copy from.
283 * The promise is resolved when copying completes or
284 * rejected if any (unexpected) errors occur.
285 * This object also emits "progress" events for each chunk
286 * that is copied. See stream-utils.js.
287 * * json-reply: The server replied with a JSON packet, which is
288 * passed as event data.
289 * * bulk-reply: The server replied with bulk data, which you can read
290 * using the event data object containing:
291 * * actor: Name of actor that received the packet
292 * * type: Name of actor's method that was called on receipt
293 * * length: Size of the data to be read
294 * * stream: This input stream should only be used directly if you
295 * can ensure that you will read exactly |length| bytes
296 * and will not close the stream when reading is complete
297 * * done: If you use the stream directly (instead of |copyTo|
298 * below), you must signal completion by resolving /
299 * rejecting this promise. If it's rejected, the
300 * transport will be closed. If an Error is supplied as a
301 * rejection value, it will be logged via |dumpn|. If you
302 * do use |copyTo|, resolving is taken care of for you
303 * when copying completes.
304 * * copyTo: A helper function for getting your data out of the
305 * stream that meets the stream handling requirements
306 * above, and has the following signature:
307 * @param output nsIAsyncOutputStream
308 * The stream to copy to.
310 * The promise is resolved when copying completes or
311 * rejected if any (unexpected) errors occur.
312 * This object also emits "progress" events for each chunk
313 * that is copied. See stream-utils.js.
315 startBulkRequest(request) {
316 if (!this.mainRoot) {
317 throw Error("Have not yet received a hello packet from the server.");
320 throw Error("Bulk packet is missing the required 'type' field.");
322 if (!request.actor) {
323 throw Error("'" + request.type + "' bulk packet has no destination.");
325 if (!request.length) {
326 throw Error("'" + request.type + "' bulk packet has no length.");
329 request = new Request(request);
330 request.format = "bulk";
332 this._sendOrQueueRequest(request);
338 * If a new request can be sent immediately, do so. Otherwise, queue it.
340 _sendOrQueueRequest(request) {
341 const actor = request.actor;
342 if (!this._activeRequests.has(actor)) {
343 this._sendRequest(request);
345 this._queueRequest(request);
351 * @throws Error if there is already an active request in flight for the same
354 _sendRequest(request) {
355 const actor = request.actor;
356 this.expectReply(actor, request);
358 if (request.format === "json") {
359 this._transport.send(request.request);
363 this._transport.startBulkSend(request.request).then((...args) => {
364 request.emit("bulk-send-ready", ...args);
369 * Queue a request to be sent later. Queues are only drained when an in
370 * flight request to a given actor completes.
372 _queueRequest(request) {
373 const actor = request.actor;
374 const queue = this._pendingRequests.get(actor) || [];
376 this._pendingRequests.set(actor, queue);
380 * Attempt the next request to a given actor (if any).
382 _attemptNextRequest(actor) {
383 if (this._activeRequests.has(actor)) {
386 const queue = this._pendingRequests.get(actor);
390 const request = queue.shift();
391 if (queue.length === 0) {
392 this._pendingRequests.delete(actor);
394 this._sendRequest(request);
398 * Arrange to hand the next reply from |actor| to the handler bound to
401 * DevToolsClient.prototype.request / startBulkRequest usually takes care of
402 * establishing the handler for a given request, but in rare cases (well,
403 * greetings from new root actors, is the only case at the moment) we must be
404 * prepared for a "reply" that doesn't correspond to any request we sent.
406 expectReply(actor, request) {
407 if (this._activeRequests.has(actor)) {
408 throw Error("clashing handlers for next reply from " + actor);
411 // If a handler is passed directly (as it is with the handler for the root
412 // actor greeting), create a dummy request to bind this to.
413 if (typeof request === "function") {
414 const handler = request;
415 request = new Request();
416 request.on("json-reply", handler);
419 this._activeRequests.set(actor, request);
425 * Called by DebuggerTransport to dispatch incoming packets as appropriate.
427 * @param packet object
428 * The incoming packet.
432 DevToolsUtils.reportException(
435 "Server did not specify an actor, dropping packet: " +
436 JSON.stringify(packet)
442 // Check for "forwardingCancelled" here instead of using a front to handle it.
443 // This is necessary because we might receive this event while the client is closing,
444 // and the fronts have already been removed by that point.
447 packet.from == this.mainRoot.actorID &&
448 packet.type == "forwardingCancelled"
450 this.purgeRequests(packet.prefix);
454 // If we have a registered Front for this actor, let it handle the packet
455 // and skip all the rest of this unpleasantness.
456 const front = this.getFrontByID(packet.from);
458 front.onPacket(packet);
463 // See if we have a handler function waiting for a reply from this
464 // actor. (Don't count unsolicited notifications or pauses as
467 this._activeRequests.has(packet.from) &&
468 !(packet.type in UnsolicitedNotifications)
470 activeRequest = this._activeRequests.get(packet.from);
471 this._activeRequests.delete(packet.from);
474 // If there is a subsequent request for the same actor, hand it off to the
475 // transport. Delivery of packets on the other end is always async, even
476 // in the local transport case.
477 this._attemptNextRequest(packet.from);
479 // Only try to notify listeners on events, not responses to requests
480 // that lack a packet type.
482 this.emit(packet.type, packet);
486 const emitReply = () => activeRequest.emit("json-reply", packet);
487 if (activeRequest.stack) {
488 callFunctionWithAsyncStack(
500 * Called by the DebuggerTransport to dispatch incoming bulk packets as
503 * @param packet object
504 * The incoming packet, which contains:
505 * * actor: Name of actor that will receive the packet
506 * * type: Name of actor's method that should be called on receipt
507 * * length: Size of the data to be read
508 * * stream: This input stream should only be used directly if you can
509 * ensure that you will read exactly |length| bytes and will
510 * not close the stream when reading is complete
511 * * done: If you use the stream directly (instead of |copyTo|
512 * below), you must signal completion by resolving /
513 * rejecting this promise. If it's rejected, the transport
514 * will be closed. If an Error is supplied as a rejection
515 * value, it will be logged via |dumpn|. If you do use
516 * |copyTo|, resolving is taken care of for you when copying
518 * * copyTo: A helper function for getting your data out of the stream
519 * that meets the stream handling requirements above, and has
520 * the following signature:
521 * @param output nsIAsyncOutputStream
522 * The stream to copy to.
524 * The promise is resolved when copying completes or rejected
525 * if any (unexpected) errors occur.
526 * This object also emits "progress" events for each chunk
527 * that is copied. See stream-utils.js.
529 onBulkPacket(packet) {
530 const { actor } = packet;
533 DevToolsUtils.reportException(
536 "Server did not specify an actor, dropping bulk packet: " +
537 JSON.stringify(packet)
543 // See if we have a handler function waiting for a reply from this
545 if (!this._activeRequests.has(actor)) {
549 const activeRequest = this._activeRequests.get(actor);
550 this._activeRequests.delete(actor);
552 // If there is a subsequent request for the same actor, hand it off to the
553 // transport. Delivery of packets on the other end is always async, even
554 // in the local transport case.
555 this._attemptNextRequest(actor);
557 activeRequest.emit("bulk-reply", packet);
561 * Called by DebuggerTransport when the underlying stream is closed.
563 * @param status nsresult
564 * The status code that corresponds to the reason for closing
567 onTransportClosed() {
568 if (this._transportClosed) {
571 this._transportClosed = true;
574 this.purgeRequests();
576 // The |_pools| array on the client-side currently is used only by
577 // protocol.js to store active fronts, mirroring the actor pools found in
578 // the server. So, read all usages of "pool" as "protocol.js front".
580 // In the normal case where we shutdown cleanly, the toolbox tells each tool
581 // to close, and they each call |destroy| on any fronts they were using.
582 // When |destroy| is called on a protocol.js front, it also
583 // removes itself from the |_pools| array. Once the toolbox has shutdown,
584 // the connection is closed, and we reach here. All fronts (should have
585 // been) |destroy|ed, so |_pools| should empty.
587 // If the connection instead aborts unexpectedly, we may end up here with
588 // all fronts used during the life of the connection. So, we call |destroy|
589 // on them clear their state, reject pending requests, and remove themselves
590 // from |_pools|. This saves the toolbox from hanging indefinitely, in case
591 // it waits for some server response before shutdown that will now never
593 for (const pool of this._pools) {
599 * Purge pending and active requests in this client.
601 * @param prefix string (optional)
602 * If a prefix is given, only requests for actor IDs that start with the prefix
603 * will be cleaned up. This is useful when forwarding of a portion of requests
604 * is cancelled on the server.
606 purgeRequests(prefix = "") {
607 const reject = function (type, request) {
608 // Server can send packets on its own and client only pass a callback
609 // to expectReply, so that there is no request object.
611 if (request.request) {
614 request.request.type +
621 "can't be sent as the connection just closed.";
624 "server side packet can't be received as the connection just closed.";
626 const packet = { error: "connectionClosed", message: msg };
627 request.emit("json-reply", packet);
630 let pendingRequestsToReject = [];
631 this._pendingRequests.forEach((requests, actor) => {
632 if (!actor.startsWith(prefix)) {
635 this._pendingRequests.delete(actor);
636 pendingRequestsToReject = pendingRequestsToReject.concat(requests);
638 pendingRequestsToReject.forEach(request => reject("pending", request));
640 let activeRequestsToReject = [];
641 this._activeRequests.forEach((request, actor) => {
642 if (!actor.startsWith(prefix)) {
645 this._activeRequests.delete(actor);
646 activeRequestsToReject = activeRequestsToReject.concat(request);
648 activeRequestsToReject.forEach(request => reject("active", request));
650 // Also purge protocol.js requests
651 const fronts = this.getAllFronts();
653 for (const front of fronts) {
654 if (!front.isDestroyed() && front.actorID.startsWith(prefix)) {
655 // Call Front.baseFrontClassDestroy nstead of Front.destroy in order to flush requests
656 // and nullify front.actorID immediately, even if Front.destroy is overloaded
657 // by an async function which would otherwise be able to try emitting new request
659 front.baseFrontClassDestroy();
665 * Search for all requests in process for this client, including those made via
666 * protocol.js and wait all of them to complete. Since the requests seen when this is
667 * first called may in turn trigger more requests, we keep recursing through this
668 * function until there is no more activity.
670 * This is a fairly heavy weight process, so it's only meant to be used in tests.
673 * Resolved when all requests have settled.
675 waitForRequestsToSettle() {
678 // Gather all pending and active requests in this client
679 // The request object supports a Promise API for completion (it has .then())
680 this._pendingRequests.forEach(requestsForActor => {
681 // Each value is an array of pending requests
682 requests = requests.concat(requestsForActor);
684 this._activeRequests.forEach(requestForActor => {
685 // Each value is a single active request
686 requests = requests.concat(requestForActor);
690 const fronts = this.getAllFronts();
692 // For each front, wait for its requests to settle
693 for (const front of fronts) {
694 if (front.hasRequests()) {
695 requests.push(front.waitForRequestsToSettle());
699 // Abort early if there are no requests
700 if (!requests.length) {
701 return Promise.resolve();
704 return DevToolsUtils.settleAll(requests)
706 // One of the requests might have failed, but ignore that situation here and pipe
707 // both success and failure through the same path. The important part is just that
711 // Repeat, more requests may have started in response to those we just waited for
712 return this.waitForRequestsToSettle();
717 // Use a Set because some fronts (like domwalker) seem to have multiple parents.
718 const fronts = new Set();
719 const poolsToVisit = [...this._pools];
721 // With protocol.js, each front can potentially have its own pools containing child
722 // fronts, forming a tree. Descend through all the pools to locate all child fronts.
723 while (poolsToVisit.length) {
724 const pool = poolsToVisit.shift();
725 // `_pools` contains either Fronts or Pools, we only want to collect Fronts here.
726 // Front inherits from Pool which exposes `poolChildren`.
727 if (pool instanceof Front) {
730 for (const child of pool.poolChildren()) {
731 poolsToVisit.push(child);
738 * Actor lifetime management, echos the server's actor pools.
745 this.__pools = new Set();
750 this._pools.add(pool);
752 removeActorPool(pool) {
753 this._pools.delete(pool);
757 * Return the Front for the Actor whose ID is the one passed in argument.
759 * @param {String} actorID: The actor ID to look for.
761 getFrontByID(actorID) {
762 const pool = this.poolFor(actorID);
763 return pool ? pool.getActorByID(actorID) : null;
767 for (const pool of this._pools) {
768 if (pool.has(actorID)) {
776 * Creates an object front for this DevToolsClient and the grip in parameter,
777 * @param {Object} grip: The grip to create the ObjectFront for.
778 * @param {ThreadFront} threadFront
779 * @param {Front} parentFront: Optional front that will manage the object front.
780 * Defaults to threadFront.
781 * @returns {ObjectFront}
783 createObjectFront(grip, threadFront, parentFront) {
785 parentFront = threadFront;
788 return new ObjectFront(this, threadFront.targetFront, parentFront, grip);
792 return this._transport;
796 for (const pool of this._pools) {
797 console.log(`%c${pool.actorID}`, "font-weight: bold;", [
798 ...pool.__poolMap.keys(),
804 EventEmitter.decorate(DevToolsClient.prototype);
806 class Request extends EventEmitter {
807 constructor(request) {
809 this.request = request;
813 return this.request.to || this.request.actor;