devtools-server.js (16315B)
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 { 8 ActorRegistry, 9 } = require("resource://devtools/server/actors/utils/actor-registry.js"); 10 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 11 var { dumpn } = DevToolsUtils; 12 13 loader.lazyRequireGetter( 14 this, 15 "DevToolsServerConnection", 16 "resource://devtools/server/devtools-server-connection.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 "Authentication", 22 "resource://devtools/shared/security/auth.js" 23 ); 24 loader.lazyRequireGetter( 25 this, 26 "LocalDebuggerTransport", 27 "resource://devtools/shared/transport/local-transport.js", 28 true 29 ); 30 loader.lazyRequireGetter( 31 this, 32 "ChildDebuggerTransport", 33 "resource://devtools/shared/transport/child-transport.js", 34 true 35 ); 36 loader.lazyRequireGetter( 37 this, 38 "JsWindowActorTransport", 39 "resource://devtools/shared/transport/js-window-actor-transport.js", 40 true 41 ); 42 loader.lazyRequireGetter( 43 this, 44 "WorkerThreadWorkerDebuggerTransport", 45 "resource://devtools/shared/transport/worker-transport.js", 46 true 47 ); 48 49 const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT = 50 "resource://devtools/server/startup/content-process.js"; 51 52 loader.lazyRequireGetter( 53 this, 54 "EventEmitter", 55 "resource://devtools/shared/event-emitter.js" 56 ); 57 58 /** 59 * DevToolsServer is a singleton that has several responsibilities. It will 60 * register the DevTools server actors that are relevant to the context. 61 * It can also create other DevToolsServer, that will live in the same 62 * environment as the debugged target (content page, worker...). 63 * 64 * For instance a regular Toolbox will be linked to DevToolsClient connected to 65 * a DevToolsServer running in the same process as the Toolbox (main process). 66 * But another DevToolsServer will be created in the same process as the page 67 * targeted by the Toolbox. 68 * 69 * Despite being a singleton, the DevToolsServer still has a lifecycle and a 70 * state. When a consumer needs to spawn a DevToolsServer, the init() method 71 * should be called. Then you should either call registerAllActors or 72 * registerActors to setup the server. 73 * When the server is no longer needed, destroy() should be called. 74 * 75 */ 76 var DevToolsServer = { 77 _listeners: [], 78 _initialized: false, 79 // Map of global actor names to actor constructors. 80 globalActorFactories: {}, 81 // Map of target-scoped actor names to actor constructors. 82 targetScopedActorFactories: {}, 83 84 LONG_STRING_LENGTH: 10000, 85 LONG_STRING_INITIAL_LENGTH: 1000, 86 LONG_STRING_READ_LENGTH: 65 * 1024, 87 88 /** 89 * The windowtype of the chrome window to use for actors that use the global 90 * window (i.e the global style editor). Set this to your main window type, 91 * for example "navigator:browser". 92 */ 93 chromeWindowType: "navigator:browser", 94 95 /** 96 * Allow debugging chrome of (parent or child) processes. 97 */ 98 allowChromeProcess: false, 99 100 /** 101 * Flag used to check if the server can be destroyed when all connections have been 102 * removed. Firefox on Android runs a single shared DevToolsServer, and should not be 103 * closed even if no client is connected. 104 */ 105 keepAlive: false, 106 107 /** 108 * We run a special server in child process whose main actor is an instance 109 * of WindowGlobalTargetActor, but that isn't a root actor. Instead there is no root 110 * actor registered on DevToolsServer. 111 */ 112 get rootlessServer() { 113 return !this.createRootActor; 114 }, 115 116 /** 117 * Initialize the devtools server. 118 */ 119 init() { 120 if (this.initialized) { 121 return; 122 } 123 124 this._connections = {}; 125 ActorRegistry.init(this._connections); 126 this._nextConnID = 0; 127 128 this._initialized = true; 129 this._onSocketListenerAccepted = this._onSocketListenerAccepted.bind(this); 130 131 if (!isWorker) { 132 // Mochitests watch this observable in order to register the custom actor 133 // highlighter-test-actor.js. 134 // Services.obs is not available in workers. 135 const subject = { wrappedJSObject: ActorRegistry }; 136 Services.obs.notifyObservers(subject, "devtools-server-initialized"); 137 } 138 }, 139 140 get protocol() { 141 return require("resource://devtools/shared/protocol.js"); 142 }, 143 144 get initialized() { 145 return this._initialized; 146 }, 147 148 hasConnection() { 149 return this._connections && !!Object.keys(this._connections).length; 150 }, 151 152 hasConnectionForPrefix(prefix) { 153 return this._connections && !!this._connections[prefix + "/"]; 154 }, 155 /** 156 * Performs cleanup tasks before shutting down the devtools server. Such tasks 157 * include clearing any actor constructors added at runtime. This method 158 * should be called whenever a devtools server is no longer useful, to avoid 159 * memory leaks. After this method returns, the devtools server must be 160 * initialized again before use. 161 */ 162 destroy() { 163 if (!this._initialized) { 164 return; 165 } 166 this._initialized = false; 167 168 for (const connection of Object.values(this._connections)) { 169 connection.close(); 170 } 171 172 ActorRegistry.destroy(); 173 this.closeAllSocketListeners(); 174 175 // Unregister all listeners 176 this.off("connectionchange"); 177 178 dumpn("DevTools server is shut down."); 179 }, 180 181 /** 182 * Raises an exception if the server has not been properly initialized. 183 */ 184 _checkInit() { 185 if (!this._initialized) { 186 throw new Error("DevToolsServer has not been initialized."); 187 } 188 189 if (!this.rootlessServer && !this.createRootActor) { 190 throw new Error( 191 "Use DevToolsServer.setRootActor() to add a root actor " + 192 "implementation." 193 ); 194 } 195 }, 196 197 /** 198 * Register different type of actors. Only register the one that are not already 199 * registered. 200 * 201 * @param root boolean 202 * Registers the root actor from webbrowser module, which is used to 203 * connect to and fetch any other actor. 204 * @param browser boolean 205 * Registers all the parent process actors useful for debugging the 206 * runtime itself, like preferences and addons actors. 207 * @param target boolean 208 * Registers all the target-scoped actors like console, script, etc. 209 * for debugging a target context. 210 */ 211 registerActors({ root, browser, target }) { 212 if (browser) { 213 ActorRegistry.addBrowserActors(); 214 } 215 216 if (root) { 217 const { 218 createRootActor, 219 } = require("resource://devtools/server/actors/webbrowser.js"); 220 this.setRootActor(createRootActor); 221 } 222 223 if (target) { 224 ActorRegistry.addTargetScopedActors(); 225 } 226 }, 227 228 /** 229 * Register all possible actors for this DevToolsServer. 230 */ 231 registerAllActors() { 232 this.registerActors({ root: true, browser: true, target: true }); 233 }, 234 235 get listeningSockets() { 236 return this._listeners.length; 237 }, 238 239 /** 240 * Add a SocketListener instance to the server's set of active 241 * SocketListeners. This is called by a SocketListener after it is opened. 242 */ 243 addSocketListener(listener) { 244 if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) { 245 throw new Error("Can't add a SocketListener, remote debugging disabled"); 246 } 247 this._checkInit(); 248 249 listener.on("accepted", this._onSocketListenerAccepted); 250 this._listeners.push(listener); 251 }, 252 253 /** 254 * Remove a SocketListener instance from the server's set of active 255 * SocketListeners. This is called by a SocketListener after it is closed. 256 */ 257 removeSocketListener(listener) { 258 // Remove connections that were accepted in the listener. 259 for (const connID of Object.getOwnPropertyNames(this._connections)) { 260 const connection = this._connections[connID]; 261 // When calling connection.close on a previous element, 262 // this may unregister some of the following other connections in `_connections` 263 // and make them be null here. 264 if (!connection) { 265 continue; 266 } 267 if (connection.isAcceptedBy(listener)) { 268 connection.close(); 269 } 270 } 271 272 this._listeners = this._listeners.filter(l => l !== listener); 273 listener.off("accepted", this._onSocketListenerAccepted); 274 }, 275 276 /** 277 * Closes and forgets all previously opened listeners. 278 * 279 * @return boolean 280 * Whether any listeners were actually closed. 281 */ 282 closeAllSocketListeners() { 283 if (!this.listeningSockets) { 284 return false; 285 } 286 287 for (const listener of this._listeners) { 288 listener.close(); 289 } 290 291 return true; 292 }, 293 294 _onSocketListenerAccepted(transport, listener) { 295 this._onConnection(transport, null, false, listener); 296 }, 297 298 /** 299 * Creates a new connection to the local debugger speaking over a fake 300 * transport. This connection results in straightforward calls to the onPacket 301 * handlers of each side. 302 * 303 * @param prefix string [optional] 304 * If given, all actors in this connection will have names starting 305 * with |prefix + '/'|. 306 * @returns a client-side DebuggerTransport for communicating with 307 * the newly-created connection. 308 */ 309 connectPipe(prefix) { 310 this._checkInit(); 311 312 const serverTransport = new LocalDebuggerTransport(); 313 const clientTransport = new LocalDebuggerTransport(serverTransport); 314 serverTransport.other = clientTransport; 315 const connection = this._onConnection(serverTransport, prefix); 316 317 // I'm putting this here because I trust you. 318 // 319 // There are times, when using a local connection, when you're going 320 // to be tempted to just get direct access to the server. Resist that 321 // temptation! If you succumb to that temptation, you will make the 322 // fine developers that work on Fennec and Firefox OS sad. They're 323 // professionals, they'll try to act like they understand, but deep 324 // down you'll know that you hurt them. 325 // 326 // This reference allows you to give in to that temptation. There are 327 // times this makes sense: tests, for example, and while porting a 328 // previously local-only codebase to the remote protocol. 329 // 330 // But every time you use this, you will feel the shame of having 331 // used a property that starts with a '_'. 332 clientTransport._serverConnection = connection; 333 334 return clientTransport; 335 }, 336 337 /** 338 * In a content child process, create a new connection that exchanges 339 * nsIMessageSender messages with our parent process. 340 * 341 * @param prefix 342 * The prefix we should use in our nsIMessageSender message names and 343 * actor names. This connection will use messages named 344 * "debug:<prefix>:packet", and all its actors will have names 345 * beginning with "<prefix>/". 346 */ 347 connectToParent(prefix, scopeOrManager) { 348 this._checkInit(); 349 350 const transport = isWorker 351 ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix) 352 : new ChildDebuggerTransport(scopeOrManager, prefix); 353 354 return this._onConnection(transport, prefix, true); 355 }, 356 357 connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) { 358 this._checkInit(); 359 const transport = new JsWindowActorTransport( 360 jsWindowChildActor, 361 forwardingPrefix 362 ); 363 364 return this._onConnection(transport, forwardingPrefix, true); 365 }, 366 367 /** 368 * Check if the server is running in the child process. 369 */ 370 get isInChildProcess() { 371 return ( 372 Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT 373 ); 374 }, 375 376 /** 377 * Create a new debugger connection for the given transport. Called after 378 * connectPipe(), from connectToParent, or from an incoming socket 379 * connection handler. 380 * 381 * If present, |forwardingPrefix| is a forwarding prefix that a parent 382 * server is using to recognizes messages intended for this server. Ensure 383 * that all our actors have names beginning with |forwardingPrefix + '/'|. 384 * In particular, the root actor's name will be |forwardingPrefix + '/root'|. 385 */ 386 _onConnection( 387 transport, 388 forwardingPrefix, 389 noRootActor = false, 390 socketListener = null 391 ) { 392 let connID; 393 if (forwardingPrefix) { 394 connID = forwardingPrefix + "/"; 395 } else { 396 // Multiple servers can be started at the same time, and when that's the 397 // case, they are loaded in separate devtools loaders. 398 // So, use the current loader ID to prefix the connection ID and make it 399 // unique. 400 connID = "server" + loader.id + ".conn" + this._nextConnID++ + "."; 401 } 402 403 // Notify the platform code that DevTools is running in the current process 404 // when we are wiring the very first connection 405 if (!this.hasConnection()) { 406 ChromeUtils.notifyDevToolsOpened(); 407 } 408 409 const conn = new DevToolsServerConnection( 410 connID, 411 transport, 412 socketListener 413 ); 414 this._connections[connID] = conn; 415 416 // Create a root actor for the connection and send the hello packet. 417 if (!noRootActor) { 418 conn.rootActor = this.createRootActor(conn); 419 if (forwardingPrefix) { 420 conn.rootActor.actorID = forwardingPrefix + "/root"; 421 } else { 422 conn.rootActor.actorID = "root"; 423 } 424 conn.addActor(conn.rootActor); 425 transport.send(conn.rootActor.sayHello()); 426 } 427 transport.ready(); 428 429 this.emit("connectionchange", "opened", conn); 430 return conn; 431 }, 432 433 /** 434 * Remove the connection from the debugging server. 435 */ 436 _connectionClosed(connection) { 437 delete this._connections[connection.prefix]; 438 this.emit("connectionchange", "closed", connection); 439 440 const hasConnection = this.hasConnection(); 441 442 // Notify the platform code that we stopped running DevTools code in the current process 443 if (!hasConnection) { 444 ChromeUtils.notifyDevToolsClosed(); 445 } 446 447 // If keepAlive isn't explicitely set to true, destroy the server once its 448 // last connection closes. Multiple JSWindowActor may use the same DevToolsServer 449 // and in this case, let the server destroy itself once the last connection closes. 450 // Otherwise we set keepAlive to true when starting a listening server, receiving 451 // client connections. Typically when running server on phones, or on desktop 452 // via `--start-debugger-server`. 453 if (hasConnection || this.keepAlive) { 454 return; 455 } 456 457 this.destroy(); 458 }, 459 460 // DevToolsServer extension API. 461 462 setRootActor(actorFactory) { 463 this.createRootActor = actorFactory; 464 }, 465 466 /** 467 * Called when DevTools are unloaded to remove the contend process server startup script 468 * for the list of scripts loaded for each new content process. Will also remove message 469 * listeners from already loaded scripts. 470 */ 471 removeContentServerScript() { 472 Services.ppmm.removeDelayedProcessScript( 473 CONTENT_PROCESS_SERVER_STARTUP_SCRIPT 474 ); 475 try { 476 Services.ppmm.broadcastAsyncMessage("debug:close-content-server"); 477 } catch (e) { 478 // Nothing to do 479 } 480 }, 481 482 /** 483 * Searches all active connections for an actor matching an ID. 484 * 485 * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠` 486 * 487 * This is helpful for some tests which depend on reaching into the server to check some 488 * properties of an actor, and it is also used by the actors related to the 489 * DevTools WebExtensions API to be able to interact with the actors created for the 490 * panels natively provided by the DevTools Toolbox. 491 */ 492 searchAllConnectionsForActor(actorID) { 493 // NOTE: the actor IDs are generated with the following format: 494 // 495 // `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}` 496 // 497 // as an optimization we can come up with a regexp to query only 498 // the right connection via its id. 499 for (const connID of Object.getOwnPropertyNames(this._connections)) { 500 const actor = this._connections[connID].getActor(actorID); 501 if (actor) { 502 return actor; 503 } 504 } 505 return null; 506 }, 507 }; 508 509 // Expose these to save callers the trouble of importing DebuggerSocket 510 DevToolsUtils.defineLazyGetter(DevToolsServer, "Authenticators", () => { 511 return Authentication.Authenticators; 512 }); 513 DevToolsUtils.defineLazyGetter(DevToolsServer, "AuthenticationResult", () => { 514 return Authentication.AuthenticationResult; 515 }); 516 517 EventEmitter.decorate(DevToolsServer); 518 519 exports.DevToolsServer = DevToolsServer;