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 var { Pool
} = require("resource://devtools/shared/protocol.js");
8 var DevToolsUtils
= require("resource://devtools/shared/DevToolsUtils.js");
9 var { dumpn
} = DevToolsUtils
;
11 loader
.lazyRequireGetter(
14 "resource://devtools/shared/event-emitter.js"
16 loader
.lazyRequireGetter(
19 "resource://devtools/server/devtools-server.js",
24 * Creates a DevToolsServerConnection.
26 * Represents a connection to this debugging global from a client.
27 * Manages a set of actors and actor pools, allocates actor ids, and
28 * handles incoming requests.
30 * @param prefix string
31 * All actor IDs created by this connection should be prefixed
33 * @param transport transport
34 * Packet transport for the debugging protocol.
35 * @param socketListener SocketListener
36 * SocketListener which accepted the transport.
37 * If this is null, the transport is not that was accepted by SocketListener.
39 function DevToolsServerConnection(prefix
, transport
, socketListener
) {
40 this._prefix
= prefix
;
41 this._transport
= transport
;
42 this._transport
.hooks
= this;
44 this._socketListener
= socketListener
;
46 this._actorPool
= new Pool(this, "server-connection");
47 this._extraPools
= [this._actorPool
];
49 // Responses to a given actor must be returned the the client
50 // in the same order as the requests that they're replying to, but
51 // Implementations might finish serving requests in a different
52 // order. To keep things in order we generate a promise for each
53 // request, chained to the promise for the request before it.
54 // This map stores the latest request promise in the chain, keyed
55 // by an actor ID string.
56 this._actorResponses
= new Map();
59 * We can forward packets to other servers, if the actors on that server
60 * all use a distinct prefix on their names. This is a map from prefixes
61 * to transports: it maps a prefix P to a transport T if T conveys
62 * packets to the server whose actors' names all begin with P + "/".
64 this._forwardingPrefixes
= new Map();
66 EventEmitter
.decorate(this);
68 exports
.DevToolsServerConnection
= DevToolsServerConnection
;
70 DevToolsServerConnection
.prototype = {
78 return this._transport
;
82 if (this._transport
) {
83 this._transport
.close(options
);
88 this.transport
.send(packet
);
92 * Used when sending a bulk reply from an actor.
93 * @see DebuggerTransport.prototype.startBulkSend
95 startBulkSend(header
) {
96 return this.transport
.startBulkSend(header
);
100 return this.prefix
+ (prefix
|| "") + this._nextID
++;
104 * Add a map of actor IDs to the connection.
106 addActorPool(actorPool
) {
107 this._extraPools
.push(actorPool
);
111 * Remove a previously-added pool of actors to the connection.
113 * @param Pool actorPool
114 * The Pool instance you want to remove.
116 removeActorPool(actorPool
) {
117 // When a connection is closed, it removes each of its actor pools. When an
118 // actor pool is removed, it calls the destroy method on each of its
119 // actors. Some actors, such as ThreadActor, manage their own actor pools.
120 // When the destroy method is called on these actors, they manually
121 // remove their actor pools. Consequently, this method is reentrant.
123 // In addition, some actors, such as ThreadActor, perform asynchronous work
124 // (in the case of ThreadActor, because they need to resume), before they
125 // remove each of their actor pools. Since we don't wait for this work to
126 // be completed, we can end up in this function recursively after the
127 // connection already set this._extraPools to null.
129 // This is a bug: if the destroy method can perform asynchronous work,
130 // then we should wait for that work to be completed before setting this.
131 // _extraPools to null. As a temporary solution, it should be acceptable
132 // to just return early (if this._extraPools has been set to null, all
133 // actors pools for this connection should already have been removed).
134 if (this._extraPools
=== null) {
137 const index
= this._extraPools
.lastIndexOf(actorPool
);
139 this._extraPools
.splice(index
, 1);
144 * Add an actor to the default actor pool for this connection.
147 this._actorPool
.manage(actor
);
151 * Remove an actor to the default actor pool for this connection.
154 this._actorPool
.unmanage(actor
);
158 * Match the api expected by the protocol library.
161 return this.removeActor(actor
);
165 * Look up an actor implementation for an actorID. Will search
166 * all the actor pools registered with the connection.
168 * @param actorID string
169 * Actor ID to look up.
172 const pool
= this.poolFor(actorID
);
174 return pool
.getActorByID(actorID
);
177 if (actorID
=== "root") {
178 return this.rootActor
;
184 _getOrCreateActor(actorID
) {
186 const actor
= this.getActor(actorID
);
188 this.transport
.send({
189 from: actorID
? actorID
: "root",
190 error
: "noSuchActor",
191 message
: "No such actor for ID: " + actorID
,
196 if (typeof actor
!== "object") {
197 // Pools should now contain only actor instances (i.e. objects)
199 `Unexpected actor constructor/function in Pool for actorID "${actorID}".`
205 const prefix
= `Error occurred while creating actor' ${actorID}`;
206 this.transport
.send(this._unknownError(actorID
, prefix
, error
));
212 for (const pool
of this._extraPools
) {
213 if (pool
.has(actorID
)) {
220 _unknownError(from, prefix
, error
) {
221 const errorString
= prefix
+ ": " + DevToolsUtils
.safeErrorString(error
);
222 // On worker threads we don't have access to Cu.
224 console
.error(errorString
);
229 error
: "unknownError",
230 message
: errorString
,
234 _queueResponse(from, type
, responseOrPromise
) {
235 const pendingResponse
=
236 this._actorResponses
.get(from) || Promise
.resolve(null);
237 const responsePromise
= pendingResponse
239 return responseOrPromise
;
242 if (!this.transport
) {
244 `Connection closed, pending response from '${from}', ` +
245 `type '${type}' failed`
249 if (!response
.from) {
250 response
.from = from;
253 this.transport
.send(response
);
256 if (!this.transport
) {
258 `Connection closed, pending error from '${from}', ` +
259 `type '${type}' failed`
263 const prefix
= `error occurred while queuing response for '${type}'`;
264 this.transport
.send(this._unknownError(from, prefix
, error
));
267 this._actorResponses
.set(from, responsePromise
);
271 * This function returns whether the connection was accepted by passed SocketListener.
273 * @param {SocketListener} socketListener
274 * @return {Boolean} return true if this connection was accepted by socketListener,
275 * else returns false.
277 isAcceptedBy(socketListener
) {
278 return this._socketListener
=== socketListener
;
281 /* Forwarding packets to other transports based on actor name prefixes. */
284 * Arrange to forward packets to another server. This is how we
285 * forward debugging connections to child processes.
287 * If we receive a packet for an actor whose name begins with |prefix|
288 * followed by '/', then we will forward that packet to |transport|.
290 * This overrides any prior forwarding for |prefix|.
292 * @param prefix string
293 * The actor name prefix, not including the '/'.
294 * @param transport object
295 * A packet transport to which we should forward packets to actors
296 * whose names begin with |(prefix + '/').|
298 setForwarding(prefix
, transport
) {
299 this._forwardingPrefixes
.set(prefix
, transport
);
303 * Stop forwarding messages to actors whose names begin with
304 * |prefix+'/'|. Such messages will now elicit 'noSuchActor' errors.
306 cancelForwarding(prefix
) {
307 this._forwardingPrefixes
.delete(prefix
);
309 // Notify the client that forwarding in now cancelled for this prefix.
310 // There could be requests in progress that the client should abort rather leaving
311 // handing indefinitely.
312 if (this.rootActor
) {
313 this.send(this.rootActor
.forwardingCancelled(prefix
));
317 sendActorEvent(actorID
, eventName
, event
= {}) {
318 event
.from = actorID
;
319 event
.type
= eventName
;
326 * Called by DebuggerTransport to dispatch incoming packets as appropriate.
328 * @param packet object
329 * The incoming packet.
332 // If the actor's name begins with a prefix we've been asked to
335 // Note that the presence of a prefix alone doesn't indicate that
336 // forwarding is needed: in DevToolsServerConnection instances in child
337 // processes, every actor has a prefixed name.
338 if (this._forwardingPrefixes
.size
> 0) {
340 let separator
= to
.lastIndexOf("/");
341 while (separator
>= 0) {
342 to
= to
.substring(0, separator
);
343 const forwardTo
= this._forwardingPrefixes
.get(
344 packet
.to
.substring(0, separator
)
347 forwardTo
.send(packet
);
350 separator
= to
.lastIndexOf("/");
354 const actor
= this._getOrCreateActor(packet
.to
);
361 // handle "requestTypes" RDP request.
362 if (packet
.type
== "requestTypes") {
365 requestTypes
: Object
.keys(actor
.requestTypes
),
367 } else if (actor
.requestTypes
?.[packet
.type
]) {
368 // Dispatch the request to the actor.
370 this.currentPacket
= packet
;
371 ret
= actor
.requestTypes
[packet
.type
].bind(actor
)(packet
, this);
373 // Support legacy errors from old actors such as thread actor which
374 // throw { error, message } objects.
375 let errorMessage
= error
;
376 if (error
?.error
&& error
?.message
) {
377 errorMessage
= `"(${error.error}) ${error.message}"`;
380 const prefix
= `error occurred while processing '${packet.type}'`;
382 this._unknownError(actor
.actorID
, prefix
, errorMessage
)
385 this.currentPacket
= undefined;
389 error
: "unrecognizedPacketType",
390 message
: `Actor ${actor.actorID} does not recognize the packet type '${packet.type}'`,
394 // There will not be a return value if a bulk reply is sent.
396 this._queueResponse(packet
.to
, packet
.type
, ret
);
401 * Called by the DebuggerTransport to dispatch incoming bulk packets as
404 * @param packet object
405 * The incoming packet, which contains:
406 * * actor: Name of actor that will receive the packet
407 * * type: Name of actor's method that should be called on receipt
408 * * length: Size of the data to be read
409 * * stream: This input stream should only be used directly if you can
410 * ensure that you will read exactly |length| bytes and will
411 * not close the stream when reading is complete
412 * * done: If you use the stream directly (instead of |copyTo|
413 * below), you must signal completion by resolving /
414 * rejecting this deferred. If it's rejected, the transport
415 * will be closed. If an Error is supplied as a rejection
416 * value, it will be logged via |dumpn|. If you do use
417 * |copyTo|, resolving is taken care of for you when copying
419 * * copyTo: A helper function for getting your data out of the stream
420 * that meets the stream handling requirements above, and has
421 * the following signature:
422 * @param output nsIAsyncOutputStream
423 * The stream to copy to.
425 * The promise is resolved when copying completes or rejected
426 * if any (unexpected) errors occur.
427 * This object also emits "progress" events for each chunk
428 * that is copied. See stream-utils.js.
430 onBulkPacket(packet
) {
431 const { actor
: actorKey
, type
} = packet
;
433 const actor
= this._getOrCreateActor(actorKey
);
438 // Dispatch the request to the actor.
440 if (actor
.requestTypes
?.[type
]) {
442 ret
= actor
.requestTypes
[type
].call(actor
, packet
);
444 const prefix
= `error occurred while processing bulk packet '${type}'`;
445 this.transport
.send(this._unknownError(actorKey
, prefix
, error
));
446 packet
.done
.reject(error
);
449 const message
= `Actor ${actorKey} does not recognize the bulk packet type '${type}'`;
450 ret
= { error
: "unrecognizedPacketType", message
};
451 packet
.done
.reject(new Error(message
));
454 // If there is a JSON response, queue it for sending back to the client.
456 this._queueResponse(actorKey
, type
, ret
);
461 * Called by DebuggerTransport when the underlying stream is closed.
463 * @param status nsresult
464 * The status code that corresponds to the reason for closing
466 * @param {object} options
467 * @param {boolean} options.isModeSwitching
468 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
470 onTransportClosed(status
, options
) {
471 dumpn("Cleaning up connection.");
472 if (!this._actorPool
) {
473 // Ignore this call if the connection is already closed.
476 this._actorPool
= null;
478 this.emit("closed", status
, this.prefix
);
480 // Use filter in order to create a copy of the extraPools array,
481 // which might be modified by removeActorPool calls.
482 // The isTopLevel check ensures that the pools retrieved here will not be
483 // destroyed by another Pool::destroy. Non top-level pools will be destroyed
484 // by the recursive Pool::destroy mechanism.
485 // See test_connection_closes_all_pools.js for practical examples of Pool
487 const topLevelPools
= this._extraPools
.filter(p
=> p
.isTopPool());
488 topLevelPools
.forEach(p
=> p
.destroy(options
));
490 this._extraPools
= null;
492 this.rootActor
= null;
493 this._transport
= null;
494 DevToolsServer
._connectionClosed(this);
497 dumpPool(pool
, output
= [], dumpedPools
) {
501 if (dumpedPools
.has(pool
)) {
504 dumpedPools
.add(pool
);
506 // TRUE if the pool is a Pool
507 if (!pool
.__poolMap
) {
511 for (const actor
of pool
.poolChildren()) {
512 children
.push(actor
);
513 actorIds
.push(actor
.actorID
);
515 const label
= pool
.label
|| pool
.actorID
;
517 output
.push([label
, actorIds
]);
518 dump(`- ${label}: ${JSON.stringify(actorIds)}\n`);
519 children
.forEach(childPool
=>
520 this.dumpPool(childPool
, output
, dumpedPools
)
525 * Debugging helper for inspecting the state of the actor pools.
529 const dumpedPools
= new Set();
531 this._extraPools
.forEach(pool
=> this.dumpPool(pool
, output
, dumpedPools
));