devtools-server-connection.js (17634B)
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 { Pool } = require("resource://devtools/shared/protocol.js"); 8 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 9 var { dumpn } = DevToolsUtils; 10 11 loader.lazyRequireGetter( 12 this, 13 "EventEmitter", 14 "resource://devtools/shared/event-emitter.js" 15 ); 16 loader.lazyRequireGetter( 17 this, 18 "DevToolsServer", 19 "resource://devtools/server/devtools-server.js", 20 true 21 ); 22 23 /** 24 * Creates a DevToolsServerConnection. 25 * 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. 29 */ 30 class DevToolsServerConnection { 31 /** 32 * @param {string} prefix 33 * All actor IDs created by this connection should be prefixed 34 * with prefix. 35 * @param {Transport} transport 36 * Packet transport for the debugging protocol. 37 * @param {SocketListener} socketListener 38 * SocketListener which accepted the transport. 39 * If this is null, the transport is not that was accepted by SocketListener. 40 */ 41 constructor(prefix, transport, socketListener) { 42 this._prefix = prefix; 43 this._transport = transport; 44 this._transport.hooks = this; 45 this._nextID = 1; 46 this._socketListener = socketListener; 47 48 this._actorPool = new Pool(this, "server-connection"); 49 this._extraPools = [this._actorPool]; 50 51 // Responses to a given actor must be returned the the client 52 // in the same order as the requests that they're replying to, but 53 // Implementations might finish serving requests in a different 54 // order. To keep things in order we generate a promise for each 55 // request, chained to the promise for the request before it. 56 // This map stores the latest request promise in the chain, keyed 57 // by an actor ID string. 58 this._actorResponses = new Map(); 59 60 /* 61 * We can forward packets to other servers, if the actors on that server 62 * all use a distinct prefix on their names. This is a map from prefixes 63 * to transports: it maps a prefix P to a transport T if T conveys 64 * packets to the server whose actors' names all begin with P + "/". 65 */ 66 this._forwardingPrefixes = new Map(); 67 68 EventEmitter.decorate(this); 69 } 70 71 _prefix = null; 72 get prefix() { 73 return this._prefix; 74 } 75 76 _transport = null; 77 get transport() { 78 return this._transport; 79 } 80 81 close(options) { 82 if (this._transport) { 83 this._transport.close(options); 84 } 85 } 86 87 send(packet) { 88 this.transport.send(packet); 89 } 90 91 /** 92 * Used when sending a bulk reply from an actor. 93 * 94 * @see DebuggerTransport.prototype.startBulkSend 95 */ 96 startBulkSend(header) { 97 return this.transport.startBulkSend(header); 98 } 99 100 allocID(prefix) { 101 return this.prefix + (prefix || "") + this._nextID++; 102 } 103 104 /** 105 * Add a map of actor IDs to the connection. 106 */ 107 addActorPool(actorPool) { 108 this._extraPools.push(actorPool); 109 } 110 111 /** 112 * Remove a previously-added pool of actors to the connection. 113 * 114 * @param {Pool} actorPool 115 * The Pool instance you want to remove. 116 */ 117 removeActorPool(actorPool) { 118 // When a connection is closed, it removes each of its actor pools. When an 119 // actor pool is removed, it calls the destroy method on each of its 120 // actors. Some actors, such as ThreadActor, manage their own actor pools. 121 // When the destroy method is called on these actors, they manually 122 // remove their actor pools. Consequently, this method is reentrant. 123 // 124 // In addition, some actors, such as ThreadActor, perform asynchronous work 125 // (in the case of ThreadActor, because they need to resume), before they 126 // remove each of their actor pools. Since we don't wait for this work to 127 // be completed, we can end up in this function recursively after the 128 // connection already set this._extraPools to null. 129 // 130 // This is a bug: if the destroy method can perform asynchronous work, 131 // then we should wait for that work to be completed before setting this. 132 // _extraPools to null. As a temporary solution, it should be acceptable 133 // to just return early (if this._extraPools has been set to null, all 134 // actors pools for this connection should already have been removed). 135 if (this._extraPools === null) { 136 return; 137 } 138 const index = this._extraPools.lastIndexOf(actorPool); 139 if (index > -1) { 140 this._extraPools.splice(index, 1); 141 } 142 } 143 144 /** 145 * Add an actor to the default actor pool for this connection. 146 */ 147 addActor(actor) { 148 this._actorPool.manage(actor); 149 } 150 151 /** 152 * Remove an actor to the default actor pool for this connection. 153 */ 154 removeActor(actor) { 155 this._actorPool.unmanage(actor); 156 } 157 158 /** 159 * Match the api expected by the protocol library. 160 */ 161 unmanage(actor) { 162 return this.removeActor(actor); 163 } 164 165 /** 166 * Look up an actor implementation for an actorID. Will search 167 * all the actor pools registered with the connection. 168 * 169 * @param actorID string 170 * Actor ID to look up. 171 */ 172 getActor(actorID) { 173 const pool = this.poolFor(actorID); 174 if (pool) { 175 return pool.getActorByID(actorID); 176 } 177 178 if (actorID === "root") { 179 return this.rootActor; 180 } 181 182 return null; 183 } 184 185 _getOrCreateActor(actorID) { 186 try { 187 const actor = this.getActor(actorID); 188 if (!actor) { 189 this.transport.send({ 190 from: actorID ? actorID : "root", 191 error: "noSuchActor", 192 message: "No such actor for ID: " + actorID, 193 }); 194 return null; 195 } 196 197 if (typeof actor !== "object") { 198 // Pools should now contain only actor instances (i.e. objects) 199 throw new Error( 200 `Unexpected actor constructor/function in Pool for actorID "${actorID}".` 201 ); 202 } 203 204 return actor; 205 } catch (error) { 206 const prefix = `Error occurred while creating actor' ${actorID}`; 207 this.transport.send(this._unknownError(actorID, prefix, error)); 208 } 209 return null; 210 } 211 212 poolFor(actorID) { 213 for (const pool of this._extraPools) { 214 if (pool.has(actorID)) { 215 return pool; 216 } 217 } 218 return null; 219 } 220 221 _unknownError(from, prefix, error) { 222 const errorString = prefix + ": " + DevToolsUtils.safeErrorString(error); 223 // On worker threads we don't have access to Cu. 224 if (!isWorker) { 225 console.error(errorString); 226 } 227 dumpn(errorString); 228 return { 229 from, 230 error: "unknownError", 231 message: errorString, 232 }; 233 } 234 235 _queueResponse(from, type, responseOrPromise) { 236 const pendingResponse = 237 this._actorResponses.get(from) || Promise.resolve(null); 238 const responsePromise = pendingResponse 239 .then(() => { 240 return responseOrPromise; 241 }) 242 .then(response => { 243 if (!this.transport) { 244 throw new Error( 245 `Connection closed, pending response from '${from}', ` + 246 `type '${type}' failed` 247 ); 248 } 249 250 if (!response.from) { 251 response.from = from; 252 } 253 254 this.transport.send(response); 255 }) 256 .catch(error => { 257 if (!this.transport) { 258 throw new Error( 259 `Connection closed, pending error from '${from}', ` + 260 `type '${type}' failed` 261 ); 262 } 263 264 const prefix = `error occurred while queuing response for '${type}'`; 265 this.transport.send(this._unknownError(from, prefix, error)); 266 }); 267 268 this._actorResponses.set(from, responsePromise); 269 } 270 271 /** 272 * This function returns whether the connection was accepted by passed SocketListener. 273 * 274 * @param {SocketListener} socketListener 275 * @return {boolean} return true if this connection was accepted by socketListener, 276 * else returns false. 277 */ 278 isAcceptedBy(socketListener) { 279 return this._socketListener === socketListener; 280 } 281 282 /* Forwarding packets to other transports based on actor name prefixes. */ 283 284 /** 285 * Arrange to forward packets to another server. This is how we 286 * forward debugging connections to child processes. 287 * 288 * If we receive a packet for an actor whose name begins with |prefix| 289 * followed by '/', then we will forward that packet to |transport|. 290 * 291 * This overrides any prior forwarding for |prefix|. 292 * 293 * @param {string} prefix 294 * The actor name prefix, not including the '/'. 295 * @param {object} transport 296 * A packet transport to which we should forward packets to actors 297 * whose names begin with |(prefix + '/').| 298 */ 299 setForwarding(prefix, transport) { 300 this._forwardingPrefixes.set(prefix, transport); 301 } 302 303 /* 304 * Stop forwarding messages to actors whose names begin with 305 * |prefix+'/'|. Such messages will now elicit 'noSuchActor' errors. 306 */ 307 cancelForwarding(prefix) { 308 this._forwardingPrefixes.delete(prefix); 309 310 // Notify the client that forwarding in now cancelled for this prefix. 311 // There could be requests in progress that the client should abort rather leaving 312 // handing indefinitely. 313 if (this.rootActor) { 314 this.send(this.rootActor.forwardingCancelled(prefix)); 315 } 316 } 317 318 sendActorEvent(actorID, eventName, event = {}) { 319 event.from = actorID; 320 event.type = eventName; 321 this.send(event); 322 } 323 324 // Transport hooks. 325 326 /** 327 * Called by DebuggerTransport to dispatch incoming packets as appropriate. 328 * 329 * @param {object} packet 330 * The incoming packet. 331 */ 332 onPacket(packet) { 333 // If the actor's name begins with a prefix we've been asked to 334 // forward, do so. 335 // 336 // Note that the presence of a prefix alone doesn't indicate that 337 // forwarding is needed: in DevToolsServerConnection instances in child 338 // processes, every actor has a prefixed name. 339 if (this._forwardingPrefixes.size > 0) { 340 let to = packet.to; 341 let separator = to.lastIndexOf("/"); 342 while (separator >= 0) { 343 to = to.substring(0, separator); 344 const forwardTo = this._forwardingPrefixes.get( 345 packet.to.substring(0, separator) 346 ); 347 if (forwardTo) { 348 forwardTo.send(packet); 349 return; 350 } 351 separator = to.lastIndexOf("/"); 352 } 353 } 354 355 const actor = this._getOrCreateActor(packet.to); 356 if (!actor) { 357 return; 358 } 359 360 let ret = null; 361 362 // handle "requestTypes" RDP request. 363 if (packet.type == "requestTypes") { 364 ret = { 365 from: actor.actorID, 366 requestTypes: Object.keys(actor.requestTypes), 367 }; 368 } else if (actor.requestTypes?.[packet.type]) { 369 // Dispatch the request to the actor. 370 try { 371 this.currentPacket = packet; 372 ret = actor.requestTypes[packet.type].bind(actor)(packet, this); 373 } catch (error) { 374 // Support legacy errors from old actors such as thread actor which 375 // throw { error, message } objects. 376 let errorMessage = error; 377 if (error?.error && error?.message) { 378 errorMessage = `"(${error.error}) ${error.message}"`; 379 } 380 381 const prefix = `error occurred while processing '${packet.type}'`; 382 this.transport.send( 383 this._unknownError(actor.actorID, prefix, errorMessage) 384 ); 385 } finally { 386 this.currentPacket = undefined; 387 } 388 } else { 389 ret = { 390 error: "unrecognizedPacketType", 391 message: `Actor ${actor.actorID} does not recognize the packet type '${packet.type}'`, 392 }; 393 } 394 395 // There will not be a return value if a bulk reply is sent. 396 if (ret) { 397 this._queueResponse(packet.to, packet.type, ret); 398 } 399 } 400 401 /** 402 * Called by the DebuggerTransport to dispatch incoming bulk packets as 403 * appropriate. 404 * 405 * @param packet object 406 * The incoming packet, which contains: 407 * * actor: Name of actor that will receive the packet 408 * * type: Name of actor's method that should be called on receipt 409 * * length: Size of the data to be read 410 * * stream: This input stream should only be used directly if you can 411 * ensure that you will read exactly |length| bytes and will 412 * not close the stream when reading is complete 413 * * done: If you use the stream directly (instead of |copyTo| 414 * or |copyToBuffer| below), you must signal completion 415 * by resolving / rejecting this deferred. If it's 416 * rejected, the transport will be closed. If an Error 417 * is supplied as a rejection value, it will be logged 418 * via |dumpn|. If you do use |copyTo| or 419 * |copyToBuffer|, resolving is taken care of for you 420 * when copying completes. 421 * * copyTo: A helper function for getting your data out of the stream 422 * that meets the stream handling requirements above, and has 423 * the following signature: 424 * @param output nsIAsyncOutputStream 425 * The stream to copy to. 426 * @return Promise 427 * The promise is resolved when copying completes or rejected 428 * if any (unexpected) errors occur. 429 * This object also emits "progress" events for each chunk 430 * that is copied. See stream-utils.js. 431 * * copyToBuffer: a helper function for getting your data out of the stream 432 * that meets the stream handling requirements above, and has 433 * the following signature: 434 * @param output ArrayBuffer 435 * The buffer to copy to. It needs to be the same length as the data 436 * to be transfered. 437 * @return Promise 438 * The promise is resolved when copying completes or rejected if any 439 * (unexpected) error occurs. 440 */ 441 onBulkPacket(packet) { 442 const { actor: actorKey, type } = packet; 443 444 const actor = this._getOrCreateActor(actorKey); 445 if (!actor) { 446 return; 447 } 448 449 // Dispatch the request to the actor. 450 let ret; 451 if (actor.requestTypes?.[type]) { 452 try { 453 // Protocol.js Actor.js expects the connection to be passed as second argument. 454 ret = actor.requestTypes[type].call(actor, packet, this); 455 } catch (error) { 456 const prefix = `error occurred while processing bulk packet '${type}'`; 457 this.transport.send(this._unknownError(actorKey, prefix, error)); 458 packet.done.reject(error); 459 } 460 } else { 461 const message = `Actor ${actorKey} does not recognize the bulk packet type '${type}'`; 462 ret = { error: "unrecognizedPacketType", message }; 463 packet.done.reject(new Error(message)); 464 } 465 466 // If there is a JSON response, queue it for sending back to the client. 467 if (ret) { 468 this._queueResponse(actorKey, type, ret); 469 } 470 } 471 472 /** 473 * Called by DebuggerTransport when the underlying stream is closed. 474 * 475 * @param status nsresult 476 * The status code that corresponds to the reason for closing 477 * the stream. 478 * @param {object} options 479 * @param {boolean} options.isModeSwitching 480 * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref 481 */ 482 onTransportClosed(status, options) { 483 dumpn("Cleaning up connection."); 484 if (!this._actorPool) { 485 // Ignore this call if the connection is already closed. 486 return; 487 } 488 this._actorPool = null; 489 490 this.emit("closed", status, this.prefix); 491 492 // Use filter in order to create a copy of the extraPools array, 493 // which might be modified by removeActorPool calls. 494 // The isTopLevel check ensures that the pools retrieved here will not be 495 // destroyed by another Pool::destroy. Non top-level pools will be destroyed 496 // by the recursive Pool::destroy mechanism. 497 // See test_connection_closes_all_pools.js for practical examples of Pool 498 // hierarchies. 499 const topLevelPools = this._extraPools.filter(p => p.isTopPool()); 500 topLevelPools.forEach(p => p.destroy(options)); 501 502 this._extraPools = null; 503 504 this.rootActor = null; 505 this._transport = null; 506 DevToolsServer._connectionClosed(this); 507 } 508 509 dumpPool(pool, output = [], dumpedPools) { 510 const actorIds = []; 511 const children = []; 512 513 if (dumpedPools.has(pool)) { 514 return; 515 } 516 dumpedPools.add(pool); 517 518 // TRUE if the pool is a Pool 519 if (!pool.__poolMap) { 520 return; 521 } 522 523 for (const actor of pool.poolChildren()) { 524 children.push(actor); 525 actorIds.push(actor.actorID); 526 } 527 const label = pool.label || pool.actorID; 528 529 output.push([label, actorIds]); 530 dump(`- ${label}: ${JSON.stringify(actorIds)}\n`); 531 children.forEach(childPool => 532 this.dumpPool(childPool, output, dumpedPools) 533 ); 534 } 535 536 /* 537 * Debugging helper for inspecting the state of the actor pools. 538 */ 539 dumpPools() { 540 const output = []; 541 const dumpedPools = new Set(); 542 543 this._extraPools.forEach(pool => this.dumpPool(pool, output, dumpedPools)); 544 545 return output; 546 } 547 } 548 549 exports.DevToolsServerConnection = DevToolsServerConnection;