Bug 1925181 - Properly set small alloc randomization on Android content processes...
[gecko.git] / devtools / server / devtools-server.js
blob89bcab68039ab724379cb2c03205927281c92182
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/. */
5 "use strict";
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;
13 loader.lazyRequireGetter(
14   this,
15   "DevToolsServerConnection",
16   "resource://devtools/server/devtools-server-connection.js",
17   true
19 loader.lazyRequireGetter(
20   this,
21   "Authentication",
22   "resource://devtools/shared/security/auth.js"
24 loader.lazyRequireGetter(
25   this,
26   "LocalDebuggerTransport",
27   "resource://devtools/shared/transport/local-transport.js",
28   true
30 loader.lazyRequireGetter(
31   this,
32   "ChildDebuggerTransport",
33   "resource://devtools/shared/transport/child-transport.js",
34   true
36 loader.lazyRequireGetter(
37   this,
38   "JsWindowActorTransport",
39   "resource://devtools/shared/transport/js-window-actor-transport.js",
40   true
42 loader.lazyRequireGetter(
43   this,
44   "WorkerThreadWorkerDebuggerTransport",
45   "resource://devtools/shared/transport/worker-transport.js",
46   true
49 const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
50   "resource://devtools/server/startup/content-process.js";
52 loader.lazyRequireGetter(
53   this,
54   "EventEmitter",
55   "resource://devtools/shared/event-emitter.js"
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: {},
84   LONG_STRING_LENGTH: 10000,
85   LONG_STRING_INITIAL_LENGTH: 1000,
86   LONG_STRING_READ_LENGTH: 65 * 1024,
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",
95   /**
96    * Allow debugging chrome of (parent or child) processes.
97    */
98   allowChromeProcess: false,
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,
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   },
116   /**
117    * Initialize the devtools server.
118    */
119   init() {
120     if (this.initialized) {
121       return;
122     }
124     this._connections = {};
125     ActorRegistry.init(this._connections);
126     this._nextConnID = 0;
128     this._initialized = true;
129     this._onSocketListenerAccepted = this._onSocketListenerAccepted.bind(this);
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   },
140   get protocol() {
141     return require("resource://devtools/shared/protocol.js");
142   },
144   get initialized() {
145     return this._initialized;
146   },
148   hasConnection() {
149     return this._connections && !!Object.keys(this._connections).length;
150   },
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;
168     for (const connection of Object.values(this._connections)) {
169       connection.close();
170     }
172     ActorRegistry.destroy();
173     this.closeAllSocketListeners();
175     // Unregister all listeners
176     this.off("connectionchange");
178     dumpn("DevTools server is shut down.");
179   },
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     }
189     if (!this.rootlessServer && !this.createRootActor) {
190       throw new Error(
191         "Use DevToolsServer.setRootActor() to add a root actor " +
192           "implementation."
193       );
194     }
195   },
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     }
216     if (root) {
217       const {
218         createRootActor,
219       } = require("resource://devtools/server/actors/webbrowser.js");
220       this.setRootActor(createRootActor);
221     }
223     if (target) {
224       ActorRegistry.addTargetScopedActors();
225     }
226   },
228   /**
229    * Register all possible actors for this DevToolsServer.
230    */
231   registerAllActors() {
232     this.registerActors({ root: true, browser: true, target: true });
233   },
235   get listeningSockets() {
236     return this._listeners.length;
237   },
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();
249     listener.on("accepted", this._onSocketListenerAccepted);
250     this._listeners.push(listener);
251   },
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     }
272     this._listeners = this._listeners.filter(l => l !== listener);
273     listener.off("accepted", this._onSocketListenerAccepted);
274   },
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     }
287     for (const listener of this._listeners) {
288       listener.close();
289     }
291     return true;
292   },
294   _onSocketListenerAccepted(transport, listener) {
295     this._onConnection(transport, null, false, listener);
296   },
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();
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.
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;
334     return clientTransport;
335   },
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();
350     const transport = isWorker
351       ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix)
352       : new ChildDebuggerTransport(scopeOrManager, prefix);
354     return this._onConnection(transport, prefix, true);
355   },
357   connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
358     this._checkInit();
359     const transport = new JsWindowActorTransport(
360       jsWindowChildActor,
361       forwardingPrefix
362     );
364     return this._onConnection(transport, forwardingPrefix, true);
365   },
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   },
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     }
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     }
409     const conn = new DevToolsServerConnection(
410       connID,
411       transport,
412       socketListener
413     );
414     this._connections[connID] = conn;
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();
429     this.emit("connectionchange", "opened", conn);
430     return conn;
431   },
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);
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();
445     }
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     }
457     this.destroy();
458   },
460   // DevToolsServer extension API.
462   setRootActor(actorFactory) {
463     this.createRootActor = actorFactory;
464   },
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   },
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   },
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;