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/. */
9 } = require("resource://devtools/server/actors/utils/actor-registry.js");
10 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
11 var { dumpn } = DevToolsUtils;
13 loader.lazyRequireGetter(
15 "DevToolsServerConnection",
16 "resource://devtools/server/devtools-server-connection.js",
19 loader.lazyRequireGetter(
22 "resource://devtools/shared/security/auth.js"
24 loader.lazyRequireGetter(
26 "LocalDebuggerTransport",
27 "resource://devtools/shared/transport/local-transport.js",
30 loader.lazyRequireGetter(
32 "ChildDebuggerTransport",
33 "resource://devtools/shared/transport/child-transport.js",
36 loader.lazyRequireGetter(
38 "JsWindowActorTransport",
39 "resource://devtools/shared/transport/js-window-actor-transport.js",
42 loader.lazyRequireGetter(
44 "WorkerThreadWorkerDebuggerTransport",
45 "resource://devtools/shared/transport/worker-transport.js",
49 const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
50 "resource://devtools/server/startup/content-process.js";
52 loader.lazyRequireGetter(
55 "resource://devtools/shared/event-emitter.js"
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...).
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.
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.
76 var DevToolsServer = {
79 // Map of global actor names to actor constructors.
80 globalActorFactories: {},
81 // Map of target-scoped actor names to actor constructors.
82 targetScopedActorFactories: {},
84 LONG_STRING_LENGTH: 10000,
85 LONG_STRING_INITIAL_LENGTH: 1000,
86 LONG_STRING_READ_LENGTH: 65 * 1024,
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".
93 chromeWindowType: "navigator:browser",
96 * Allow debugging chrome of (parent or child) processes.
98 allowChromeProcess: false,
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.
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.
112 get rootlessServer() {
113 return !this.createRootActor;
117 * Initialize the devtools server.
120 if (this.initialized) {
124 this._connections = {};
125 ActorRegistry.init(this._connections);
126 this._nextConnID = 0;
128 this._initialized = true;
129 this._onSocketListenerAccepted = this._onSocketListenerAccepted.bind(this);
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");
141 return require("resource://devtools/shared/protocol.js");
145 return this._initialized;
149 return this._connections && !!Object.keys(this._connections).length;
152 hasConnectionForPrefix(prefix) {
153 return this._connections && !!this._connections[prefix + "/"];
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.
163 if (!this._initialized) {
166 this._initialized = false;
168 for (const connection of Object.values(this._connections)) {
172 ActorRegistry.destroy();
173 this.closeAllSocketListeners();
175 // Unregister all listeners
176 this.off("connectionchange");
178 dumpn("DevTools server is shut down.");
182 * Raises an exception if the server has not been properly initialized.
185 if (!this._initialized) {
186 throw new Error("DevToolsServer has not been initialized.");
189 if (!this.rootlessServer && !this.createRootActor) {
191 "Use DevToolsServer.setRootActor() to add a root actor " +
198 * Register different type of actors. Only register the one that are not already
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.
211 registerActors({ root, browser, target }) {
213 ActorRegistry.addBrowserActors();
219 } = require("resource://devtools/server/actors/webbrowser.js");
220 this.setRootActor(createRootActor);
224 ActorRegistry.addTargetScopedActors();
229 * Register all possible actors for this DevToolsServer.
231 registerAllActors() {
232 this.registerActors({ root: true, browser: true, target: true });
235 get listeningSockets() {
236 return this._listeners.length;
240 * Add a SocketListener instance to the server's set of active
241 * SocketListeners. This is called by a SocketListener after it is opened.
243 addSocketListener(listener) {
244 if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
245 throw new Error("Can't add a SocketListener, remote debugging disabled");
249 listener.on("accepted", this._onSocketListenerAccepted);
250 this._listeners.push(listener);
254 * Remove a SocketListener instance from the server's set of active
255 * SocketListeners. This is called by a SocketListener after it is closed.
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.
267 if (connection.isAcceptedBy(listener)) {
272 this._listeners = this._listeners.filter(l => l !== listener);
273 listener.off("accepted", this._onSocketListenerAccepted);
277 * Closes and forgets all previously opened listeners.
280 * Whether any listeners were actually closed.
282 closeAllSocketListeners() {
283 if (!this.listeningSockets) {
287 for (const listener of this._listeners) {
294 _onSocketListenerAccepted(transport, listener) {
295 this._onConnection(transport, null, false, listener);
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.
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.
309 connectPipe(prefix) {
312 const serverTransport = new LocalDebuggerTransport();
313 const clientTransport = new LocalDebuggerTransport(serverTransport);
314 serverTransport.other = clientTransport;
315 const connection = this._onConnection(serverTransport, prefix);
317 // I'm putting this here because I trust you.
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.
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.
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;
334 return clientTransport;
338 * In a content child process, create a new connection that exchanges
339 * nsIMessageSender messages with our parent process.
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>/".
347 connectToParent(prefix, scopeOrManager) {
350 const transport = isWorker
351 ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix)
352 : new ChildDebuggerTransport(scopeOrManager, prefix);
354 return this._onConnection(transport, prefix, true);
357 connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
359 const transport = new JsWindowActorTransport(
364 return this._onConnection(transport, forwardingPrefix, true);
368 * Check if the server is running in the child process.
370 get isInChildProcess() {
372 Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
377 * Create a new debugger connection for the given transport. Called after
378 * connectPipe(), from connectToParent, or from an incoming socket
379 * connection handler.
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'|.
390 socketListener = null
393 if (forwardingPrefix) {
394 connID = forwardingPrefix + "/";
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
400 connID = "server" + loader.id + ".conn" + this._nextConnID++ + ".";
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();
409 const conn = new DevToolsServerConnection(
414 this._connections[connID] = conn;
416 // Create a root actor for the connection and send the hello packet.
418 conn.rootActor = this.createRootActor(conn);
419 if (forwardingPrefix) {
420 conn.rootActor.actorID = forwardingPrefix + "/root";
422 conn.rootActor.actorID = "root";
424 conn.addActor(conn.rootActor);
425 transport.send(conn.rootActor.sayHello());
429 this.emit("connectionchange", "opened", conn);
434 * Remove the connection from the debugging server.
436 _connectionClosed(connection) {
437 delete this._connections[connection.prefix];
438 this.emit("connectionchange", "closed", connection);
440 const hasConnection = this.hasConnection();
442 // Notify the platform code that we stopped running DevTools code in the current process
443 if (!hasConnection) {
444 ChromeUtils.notifyDevToolsClosed();
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) {
460 // DevToolsServer extension API.
462 setRootActor(actorFactory) {
463 this.createRootActor = actorFactory;
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.
471 removeContentServerScript() {
472 Services.ppmm.removeDelayedProcessScript(
473 CONTENT_PROCESS_SERVER_STARTUP_SCRIPT
476 Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
483 * Searches all active connections for an actor matching an ID.
485 * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠`
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.
492 searchAllConnectionsForActor(actorID) {
493 // NOTE: the actor IDs are generated with the following format:
495 // `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}`
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);
509 // Expose these to save callers the trouble of importing DebuggerSocket
510 DevToolsUtils.defineLazyGetter(DevToolsServer, "Authenticators", () => {
511 return Authentication.Authenticators;
513 DevToolsUtils.defineLazyGetter(DevToolsServer, "AuthenticationResult", () => {
514 return Authentication.AuthenticationResult;
517 EventEmitter.decorate(DevToolsServer);
519 exports.DevToolsServer = DevToolsServer;