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 = {
77 * For a DevToolsServerConnection used in content processes,
78 * returns the prefix of the connection it originates from, from the parent process.
81 this.prefix.replace(/child\d+\//, "");
86 return this._transport;
90 if (this._transport) {
91 this._transport.close(options);
96 this.transport.send(packet);
100 * Used when sending a bulk reply from an actor.
101 * @see DebuggerTransport.prototype.startBulkSend
103 startBulkSend(header) {
104 return this.transport.startBulkSend(header);
108 return this.prefix + (prefix || "") + this._nextID++;
112 * Add a map of actor IDs to the connection.
114 addActorPool(actorPool) {
115 this._extraPools.push(actorPool);
119 * Remove a previously-added pool of actors to the connection.
121 * @param Pool actorPool
122 * The Pool instance you want to remove.
124 removeActorPool(actorPool) {
125 // When a connection is closed, it removes each of its actor pools. When an
126 // actor pool is removed, it calls the destroy method on each of its
127 // actors. Some actors, such as ThreadActor, manage their own actor pools.
128 // When the destroy method is called on these actors, they manually
129 // remove their actor pools. Consequently, this method is reentrant.
131 // In addition, some actors, such as ThreadActor, perform asynchronous work
132 // (in the case of ThreadActor, because they need to resume), before they
133 // remove each of their actor pools. Since we don't wait for this work to
134 // be completed, we can end up in this function recursively after the
135 // connection already set this._extraPools to null.
137 // This is a bug: if the destroy method can perform asynchronous work,
138 // then we should wait for that work to be completed before setting this.
139 // _extraPools to null. As a temporary solution, it should be acceptable
140 // to just return early (if this._extraPools has been set to null, all
141 // actors pools for this connection should already have been removed).
142 if (this._extraPools === null) {
145 const index = this._extraPools.lastIndexOf(actorPool);
147 this._extraPools.splice(index, 1);
152 * Add an actor to the default actor pool for this connection.
155 this._actorPool.manage(actor);
159 * Remove an actor to the default actor pool for this connection.
162 this._actorPool.unmanage(actor);
166 * Match the api expected by the protocol library.
169 return this.removeActor(actor);
173 * Look up an actor implementation for an actorID. Will search
174 * all the actor pools registered with the connection.
176 * @param actorID string
177 * Actor ID to look up.
180 const pool = this.poolFor(actorID);
182 return pool.getActorByID(actorID);
185 if (actorID === "root") {
186 return this.rootActor;
192 _getOrCreateActor(actorID) {
194 const actor = this.getActor(actorID);
196 this.transport.send({
197 from: actorID ? actorID : "root",
198 error: "noSuchActor",
199 message: "No such actor for ID: " + actorID,
204 if (typeof actor !== "object") {
205 // Pools should now contain only actor instances (i.e. objects)
207 `Unexpected actor constructor/function in Pool for actorID "${actorID}".`
213 const prefix = `Error occurred while creating actor' ${actorID}`;
214 this.transport.send(this._unknownError(actorID, prefix, error));
220 for (const pool of this._extraPools) {
221 if (pool.has(actorID)) {
228 _unknownError(from, prefix, error) {
229 const errorString = prefix + ": " + DevToolsUtils.safeErrorString(error);
230 // On worker threads we don't have access to Cu.
232 console.error(errorString);
237 error: "unknownError",
238 message: errorString,
242 _queueResponse(from, type, responseOrPromise) {
243 const pendingResponse =
244 this._actorResponses.get(from) || Promise.resolve(null);
245 const responsePromise = pendingResponse
247 return responseOrPromise;
250 if (!this.transport) {
252 `Connection closed, pending response from '${from}', ` +
253 `type '${type}' failed`
257 if (!response.from) {
258 response.from = from;
261 this.transport.send(response);
264 if (!this.transport) {
266 `Connection closed, pending error from '${from}', ` +
267 `type '${type}' failed`
271 const prefix = `error occurred while queuing response for '${type}'`;
272 this.transport.send(this._unknownError(from, prefix, error));
275 this._actorResponses.set(from, responsePromise);
279 * This function returns whether the connection was accepted by passed SocketListener.
281 * @param {SocketListener} socketListener
282 * @return {Boolean} return true if this connection was accepted by socketListener,
283 * else returns false.
285 isAcceptedBy(socketListener) {
286 return this._socketListener === socketListener;
289 /* Forwarding packets to other transports based on actor name prefixes. */
292 * Arrange to forward packets to another server. This is how we
293 * forward debugging connections to child processes.
295 * If we receive a packet for an actor whose name begins with |prefix|
296 * followed by '/', then we will forward that packet to |transport|.
298 * This overrides any prior forwarding for |prefix|.
300 * @param prefix string
301 * The actor name prefix, not including the '/'.
302 * @param transport object
303 * A packet transport to which we should forward packets to actors
304 * whose names begin with |(prefix + '/').|
306 setForwarding(prefix, transport) {
307 this._forwardingPrefixes.set(prefix, transport);
311 * Stop forwarding messages to actors whose names begin with
312 * |prefix+'/'|. Such messages will now elicit 'noSuchActor' errors.
314 cancelForwarding(prefix) {
315 this._forwardingPrefixes.delete(prefix);
317 // Notify the client that forwarding in now cancelled for this prefix.
318 // There could be requests in progress that the client should abort rather leaving
319 // handing indefinitely.
320 if (this.rootActor) {
321 this.send(this.rootActor.forwardingCancelled(prefix));
325 sendActorEvent(actorID, eventName, event = {}) {
326 event.from = actorID;
327 event.type = eventName;
334 * Called by DebuggerTransport to dispatch incoming packets as appropriate.
336 * @param packet object
337 * The incoming packet.
340 // If the actor's name begins with a prefix we've been asked to
343 // Note that the presence of a prefix alone doesn't indicate that
344 // forwarding is needed: in DevToolsServerConnection instances in child
345 // processes, every actor has a prefixed name.
346 if (this._forwardingPrefixes.size > 0) {
348 let separator = to.lastIndexOf("/");
349 while (separator >= 0) {
350 to = to.substring(0, separator);
351 const forwardTo = this._forwardingPrefixes.get(
352 packet.to.substring(0, separator)
355 forwardTo.send(packet);
358 separator = to.lastIndexOf("/");
362 const actor = this._getOrCreateActor(packet.to);
369 // handle "requestTypes" RDP request.
370 if (packet.type == "requestTypes") {
373 requestTypes: Object.keys(actor.requestTypes),
375 } else if (actor.requestTypes?.[packet.type]) {
376 // Dispatch the request to the actor.
378 this.currentPacket = packet;
379 ret = actor.requestTypes[packet.type].bind(actor)(packet, this);
381 // Support legacy errors from old actors such as thread actor which
382 // throw { error, message } objects.
383 let errorMessage = error;
384 if (error?.error && error?.message) {
385 errorMessage = `"(${error.error}) ${error.message}"`;
388 const prefix = `error occurred while processing '${packet.type}'`;
390 this._unknownError(actor.actorID, prefix, errorMessage)
393 this.currentPacket = undefined;
397 error: "unrecognizedPacketType",
398 message: `Actor ${actor.actorID} does not recognize the packet type '${packet.type}'`,
402 // There will not be a return value if a bulk reply is sent.
404 this._queueResponse(packet.to, packet.type, ret);
409 * Called by the DebuggerTransport to dispatch incoming bulk packets as
412 * @param packet object
413 * The incoming packet, which contains:
414 * * actor: Name of actor that will receive the packet
415 * * type: Name of actor's method that should be called on receipt
416 * * length: Size of the data to be read
417 * * stream: This input stream should only be used directly if you can
418 * ensure that you will read exactly |length| bytes and will
419 * not close the stream when reading is complete
420 * * done: If you use the stream directly (instead of |copyTo|
421 * below), you must signal completion by resolving /
422 * rejecting this deferred. If it's rejected, the transport
423 * will be closed. If an Error is supplied as a rejection
424 * value, it will be logged via |dumpn|. If you do use
425 * |copyTo|, resolving is taken care of for you when copying
427 * * copyTo: A helper function for getting your data out of the stream
428 * that meets the stream handling requirements above, and has
429 * the following signature:
430 * @param output nsIAsyncOutputStream
431 * The stream to copy to.
433 * The promise is resolved when copying completes or rejected
434 * if any (unexpected) errors occur.
435 * This object also emits "progress" events for each chunk
436 * that is copied. See stream-utils.js.
438 onBulkPacket(packet) {
439 const { actor: actorKey, type } = packet;
441 const actor = this._getOrCreateActor(actorKey);
446 // Dispatch the request to the actor.
448 if (actor.requestTypes?.[type]) {
450 ret = actor.requestTypes[type].call(actor, packet);
452 const prefix = `error occurred while processing bulk packet '${type}'`;
453 this.transport.send(this._unknownError(actorKey, prefix, error));
454 packet.done.reject(error);
457 const message = `Actor ${actorKey} does not recognize the bulk packet type '${type}'`;
458 ret = { error: "unrecognizedPacketType", message };
459 packet.done.reject(new Error(message));
462 // If there is a JSON response, queue it for sending back to the client.
464 this._queueResponse(actorKey, type, ret);
469 * Called by DebuggerTransport when the underlying stream is closed.
471 * @param status nsresult
472 * The status code that corresponds to the reason for closing
474 * @param {object} options
475 * @param {boolean} options.isModeSwitching
476 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
478 onTransportClosed(status, options) {
479 dumpn("Cleaning up connection.");
480 if (!this._actorPool) {
481 // Ignore this call if the connection is already closed.
484 this._actorPool = null;
486 this.emit("closed", status, this.prefix);
488 // Use filter in order to create a copy of the extraPools array,
489 // which might be modified by removeActorPool calls.
490 // The isTopLevel check ensures that the pools retrieved here will not be
491 // destroyed by another Pool::destroy. Non top-level pools will be destroyed
492 // by the recursive Pool::destroy mechanism.
493 // See test_connection_closes_all_pools.js for practical examples of Pool
495 const topLevelPools = this._extraPools.filter(p => p.isTopPool());
496 topLevelPools.forEach(p => p.destroy(options));
498 this._extraPools = null;
500 this.rootActor = null;
501 this._transport = null;
502 DevToolsServer._connectionClosed(this);
505 dumpPool(pool, output = [], dumpedPools) {
509 if (dumpedPools.has(pool)) {
512 dumpedPools.add(pool);
514 // TRUE if the pool is a Pool
515 if (!pool.__poolMap) {
519 for (const actor of pool.poolChildren()) {
520 children.push(actor);
521 actorIds.push(actor.actorID);
523 const label = pool.label || pool.actorID;
525 output.push([label, actorIds]);
526 dump(`- ${label}: ${JSON.stringify(actorIds)}\n`);
527 children.forEach(childPool =>
528 this.dumpPool(childPool, output, dumpedPools)
533 * Debugging helper for inspecting the state of the actor pools.
537 const dumpedPools = new Set();
539 this._extraPools.forEach(pool => this.dumpPool(pool, output, dumpedPools));