Bug 1719855 - Clean up code whether to fire a pointercancel event. r=botond
[gecko.git] / devtools / server / devtools-server.js
blobe1dd7994d16dc618cb803148e31728bb8c25a759
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       if (connection.isAcceptedBy(listener)) {
262         connection.close();
263       }
264     }
266     this._listeners = this._listeners.filter(l => l !== listener);
267     listener.off("accepted", this._onSocketListenerAccepted);
268   },
270   /**
271    * Closes and forgets all previously opened listeners.
272    *
273    * @return boolean
274    *         Whether any listeners were actually closed.
275    */
276   closeAllSocketListeners() {
277     if (!this.listeningSockets) {
278       return false;
279     }
281     for (const listener of this._listeners) {
282       listener.close();
283     }
285     return true;
286   },
288   _onSocketListenerAccepted(transport, listener) {
289     this._onConnection(transport, null, false, listener);
290   },
292   /**
293    * Creates a new connection to the local debugger speaking over a fake
294    * transport. This connection results in straightforward calls to the onPacket
295    * handlers of each side.
296    *
297    * @param prefix string [optional]
298    *    If given, all actors in this connection will have names starting
299    *    with |prefix + '/'|.
300    * @returns a client-side DebuggerTransport for communicating with
301    *    the newly-created connection.
302    */
303   connectPipe(prefix) {
304     this._checkInit();
306     const serverTransport = new LocalDebuggerTransport();
307     const clientTransport = new LocalDebuggerTransport(serverTransport);
308     serverTransport.other = clientTransport;
309     const connection = this._onConnection(serverTransport, prefix);
311     // I'm putting this here because I trust you.
312     //
313     // There are times, when using a local connection, when you're going
314     // to be tempted to just get direct access to the server.  Resist that
315     // temptation!  If you succumb to that temptation, you will make the
316     // fine developers that work on Fennec and Firefox OS sad.  They're
317     // professionals, they'll try to act like they understand, but deep
318     // down you'll know that you hurt them.
319     //
320     // This reference allows you to give in to that temptation.  There are
321     // times this makes sense: tests, for example, and while porting a
322     // previously local-only codebase to the remote protocol.
323     //
324     // But every time you use this, you will feel the shame of having
325     // used a property that starts with a '_'.
326     clientTransport._serverConnection = connection;
328     return clientTransport;
329   },
331   /**
332    * In a content child process, create a new connection that exchanges
333    * nsIMessageSender messages with our parent process.
334    *
335    * @param prefix
336    *    The prefix we should use in our nsIMessageSender message names and
337    *    actor names. This connection will use messages named
338    *    "debug:<prefix>:packet", and all its actors will have names
339    *    beginning with "<prefix>/".
340    */
341   connectToParent(prefix, scopeOrManager) {
342     this._checkInit();
344     const transport = isWorker
345       ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix)
346       : new ChildDebuggerTransport(scopeOrManager, prefix);
348     return this._onConnection(transport, prefix, true);
349   },
351   connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
352     this._checkInit();
353     const transport = new JsWindowActorTransport(
354       jsWindowChildActor,
355       forwardingPrefix
356     );
358     return this._onConnection(transport, forwardingPrefix, true);
359   },
361   /**
362    * Check if the server is running in the child process.
363    */
364   get isInChildProcess() {
365     return (
366       Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
367     );
368   },
370   /**
371    * Create a new debugger connection for the given transport. Called after
372    * connectPipe(), from connectToParent, or from an incoming socket
373    * connection handler.
374    *
375    * If present, |forwardingPrefix| is a forwarding prefix that a parent
376    * server is using to recognizes messages intended for this server. Ensure
377    * that all our actors have names beginning with |forwardingPrefix + '/'|.
378    * In particular, the root actor's name will be |forwardingPrefix + '/root'|.
379    */
380   _onConnection(
381     transport,
382     forwardingPrefix,
383     noRootActor = false,
384     socketListener = null
385   ) {
386     let connID;
387     if (forwardingPrefix) {
388       connID = forwardingPrefix + "/";
389     } else {
390       // Multiple servers can be started at the same time, and when that's the
391       // case, they are loaded in separate devtools loaders.
392       // So, use the current loader ID to prefix the connection ID and make it
393       // unique.
394       connID = "server" + loader.id + ".conn" + this._nextConnID++ + ".";
395     }
397     // Notify the platform code that DevTools is running in the current process
398     // when we are wiring the very first connection
399     if (!this.hasConnection()) {
400       ChromeUtils.notifyDevToolsOpened();
401     }
403     const conn = new DevToolsServerConnection(
404       connID,
405       transport,
406       socketListener
407     );
408     this._connections[connID] = conn;
410     // Create a root actor for the connection and send the hello packet.
411     if (!noRootActor) {
412       conn.rootActor = this.createRootActor(conn);
413       if (forwardingPrefix) {
414         conn.rootActor.actorID = forwardingPrefix + "/root";
415       } else {
416         conn.rootActor.actorID = "root";
417       }
418       conn.addActor(conn.rootActor);
419       transport.send(conn.rootActor.sayHello());
420     }
421     transport.ready();
423     this.emit("connectionchange", "opened", conn);
424     return conn;
425   },
427   /**
428    * Remove the connection from the debugging server.
429    */
430   _connectionClosed(connection) {
431     delete this._connections[connection.prefix];
432     this.emit("connectionchange", "closed", connection);
434     const hasConnection = this.hasConnection();
436     // Notify the platform code that we stopped running DevTools code in the current process
437     if (!hasConnection) {
438       ChromeUtils.notifyDevToolsClosed();
439     }
441     // If keepAlive isn't explicitely set to true, destroy the server once its
442     // last connection closes. Multiple JSWindowActor may use the same DevToolsServer
443     // and in this case, let the server destroy itself once the last connection closes.
444     // Otherwise we set keepAlive to true when starting a listening server, receiving
445     // client connections. Typically when running server on phones, or on desktop
446     // via `--start-debugger-server`.
447     if (hasConnection || this.keepAlive) {
448       return;
449     }
451     this.destroy();
452   },
454   // DevToolsServer extension API.
456   setRootActor(actorFactory) {
457     this.createRootActor = actorFactory;
458   },
460   /**
461    * Called when DevTools are unloaded to remove the contend process server startup script
462    * for the list of scripts loaded for each new content process. Will also remove message
463    * listeners from already loaded scripts.
464    */
465   removeContentServerScript() {
466     Services.ppmm.removeDelayedProcessScript(
467       CONTENT_PROCESS_SERVER_STARTUP_SCRIPT
468     );
469     try {
470       Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
471     } catch (e) {
472       // Nothing to do
473     }
474   },
476   /**
477    * Searches all active connections for an actor matching an ID.
478    *
479    * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠`
480    *
481    * This is helpful for some tests which depend on reaching into the server to check some
482    * properties of an actor, and it is also used by the actors related to the
483    * DevTools WebExtensions API to be able to interact with the actors created for the
484    * panels natively provided by the DevTools Toolbox.
485    */
486   searchAllConnectionsForActor(actorID) {
487     // NOTE: the actor IDs are generated with the following format:
488     //
489     //   `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}`
490     //
491     // as an optimization we can come up with a regexp to query only
492     // the right connection via its id.
493     for (const connID of Object.getOwnPropertyNames(this._connections)) {
494       const actor = this._connections[connID].getActor(actorID);
495       if (actor) {
496         return actor;
497       }
498     }
499     return null;
500   },
503 // Expose these to save callers the trouble of importing DebuggerSocket
504 DevToolsUtils.defineLazyGetter(DevToolsServer, "Authenticators", () => {
505   return Authentication.Authenticators;
507 DevToolsUtils.defineLazyGetter(DevToolsServer, "AuthenticationResult", () => {
508   return Authentication.AuthenticationResult;
511 EventEmitter.decorate(DevToolsServer);
513 exports.DevToolsServer = DevToolsServer;