forked from mirrors/gecko-dev
		
	 4f2561f730
			
		
	
	
		4f2561f730
		
	
	
	
	
		
			
			This doesn't help know what particular resource DevTools is currently inspecting, but at least it helps know if it debugs something: * one or many BrowsingContext(s) for regular DevTools (you can use BrowsingContext.watchedByDevTools instead) * the whole process for the Browser Console/Toolbox Differential Revision: https://phabricator.services.mozilla.com/D173295
		
			
				
	
	
		
			513 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			513 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| var {
 | |
|   ActorRegistry,
 | |
| } = require("resource://devtools/server/actors/utils/actor-registry.js");
 | |
| var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
 | |
| var { dumpn } = DevToolsUtils;
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "DevToolsServerConnection",
 | |
|   "resource://devtools/server/devtools-server-connection.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "Authentication",
 | |
|   "resource://devtools/shared/security/auth.js"
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "LocalDebuggerTransport",
 | |
|   "resource://devtools/shared/transport/local-transport.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "ChildDebuggerTransport",
 | |
|   "resource://devtools/shared/transport/child-transport.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "JsWindowActorTransport",
 | |
|   "resource://devtools/shared/transport/js-window-actor-transport.js",
 | |
|   true
 | |
| );
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "WorkerThreadWorkerDebuggerTransport",
 | |
|   "resource://devtools/shared/transport/worker-transport.js",
 | |
|   true
 | |
| );
 | |
| 
 | |
| const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT =
 | |
|   "resource://devtools/server/startup/content-process.js";
 | |
| 
 | |
| loader.lazyRequireGetter(
 | |
|   this,
 | |
|   "EventEmitter",
 | |
|   "resource://devtools/shared/event-emitter.js"
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * DevToolsServer is a singleton that has several responsibilities. It will
 | |
|  * register the DevTools server actors that are relevant to the context.
 | |
|  * It can also create other DevToolsServer, that will live in the same
 | |
|  * environment as the debugged target (content page, worker...).
 | |
|  *
 | |
|  * For instance a regular Toolbox will be linked to DevToolsClient connected to
 | |
|  * a DevToolsServer running in the same process as the Toolbox (main process).
 | |
|  * But another DevToolsServer will be created in the same process as the page
 | |
|  * targeted by the Toolbox.
 | |
|  *
 | |
|  * Despite being a singleton, the DevToolsServer still has a lifecycle and a
 | |
|  * state. When a consumer needs to spawn a DevToolsServer, the init() method
 | |
|  * should be called. Then you should either call registerAllActors or
 | |
|  * registerActors to setup the server.
 | |
|  * When the server is no longer needed, destroy() should be called.
 | |
|  *
 | |
|  */
 | |
| var DevToolsServer = {
 | |
|   _listeners: [],
 | |
|   _initialized: false,
 | |
|   // Map of global actor names to actor constructors.
 | |
|   globalActorFactories: {},
 | |
|   // Map of target-scoped actor names to actor constructors.
 | |
|   targetScopedActorFactories: {},
 | |
| 
 | |
|   LONG_STRING_LENGTH: 10000,
 | |
|   LONG_STRING_INITIAL_LENGTH: 1000,
 | |
|   LONG_STRING_READ_LENGTH: 65 * 1024,
 | |
| 
 | |
|   /**
 | |
|    * The windowtype of the chrome window to use for actors that use the global
 | |
|    * window (i.e the global style editor). Set this to your main window type,
 | |
|    * for example "navigator:browser".
 | |
|    */
 | |
|   chromeWindowType: "navigator:browser",
 | |
| 
 | |
|   /**
 | |
|    * Allow debugging chrome of (parent or child) processes.
 | |
|    */
 | |
|   allowChromeProcess: false,
 | |
| 
 | |
|   /**
 | |
|    * Flag used to check if the server can be destroyed when all connections have been
 | |
|    * removed. Firefox on Android runs a single shared DevToolsServer, and should not be
 | |
|    * closed even if no client is connected.
 | |
|    */
 | |
|   keepAlive: false,
 | |
| 
 | |
|   /**
 | |
|    * We run a special server in child process whose main actor is an instance
 | |
|    * of WindowGlobalTargetActor, but that isn't a root actor. Instead there is no root
 | |
|    * actor registered on DevToolsServer.
 | |
|    */
 | |
|   get rootlessServer() {
 | |
|     return !this.createRootActor;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Initialize the devtools server.
 | |
|    */
 | |
|   init() {
 | |
|     if (this.initialized) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._connections = {};
 | |
|     ActorRegistry.init(this._connections);
 | |
|     this._nextConnID = 0;
 | |
| 
 | |
|     this._initialized = true;
 | |
|     this._onSocketListenerAccepted = this._onSocketListenerAccepted.bind(this);
 | |
| 
 | |
|     if (!isWorker) {
 | |
|       // Mochitests watch this observable in order to register the custom actor
 | |
|       // highlighter-test-actor.js.
 | |
|       // Services.obs is not available in workers.
 | |
|       const subject = { wrappedJSObject: ActorRegistry };
 | |
|       Services.obs.notifyObservers(subject, "devtools-server-initialized");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get protocol() {
 | |
|     return require("resource://devtools/shared/protocol.js");
 | |
|   },
 | |
| 
 | |
|   get initialized() {
 | |
|     return this._initialized;
 | |
|   },
 | |
| 
 | |
|   hasConnection() {
 | |
|     return this._connections && !!Object.keys(this._connections).length;
 | |
|   },
 | |
| 
 | |
|   hasConnectionForPrefix(prefix) {
 | |
|     return this._connections && !!this._connections[prefix + "/"];
 | |
|   },
 | |
|   /**
 | |
|    * Performs cleanup tasks before shutting down the devtools server. Such tasks
 | |
|    * include clearing any actor constructors added at runtime. This method
 | |
|    * should be called whenever a devtools server is no longer useful, to avoid
 | |
|    * memory leaks. After this method returns, the devtools server must be
 | |
|    * initialized again before use.
 | |
|    */
 | |
|   destroy() {
 | |
|     if (!this._initialized) {
 | |
|       return;
 | |
|     }
 | |
|     this._initialized = false;
 | |
| 
 | |
|     for (const connection of Object.values(this._connections)) {
 | |
|       connection.close();
 | |
|     }
 | |
| 
 | |
|     ActorRegistry.destroy();
 | |
|     this.closeAllSocketListeners();
 | |
| 
 | |
|     // Unregister all listeners
 | |
|     this.off("connectionchange");
 | |
| 
 | |
|     dumpn("DevTools server is shut down.");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Raises an exception if the server has not been properly initialized.
 | |
|    */
 | |
|   _checkInit() {
 | |
|     if (!this._initialized) {
 | |
|       throw new Error("DevToolsServer has not been initialized.");
 | |
|     }
 | |
| 
 | |
|     if (!this.rootlessServer && !this.createRootActor) {
 | |
|       throw new Error(
 | |
|         "Use DevToolsServer.setRootActor() to add a root actor " +
 | |
|           "implementation."
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Register different type of actors. Only register the one that are not already
 | |
|    * registered.
 | |
|    *
 | |
|    * @param root boolean
 | |
|    *        Registers the root actor from webbrowser module, which is used to
 | |
|    *        connect to and fetch any other actor.
 | |
|    * @param browser boolean
 | |
|    *        Registers all the parent process actors useful for debugging the
 | |
|    *        runtime itself, like preferences and addons actors.
 | |
|    * @param target boolean
 | |
|    *        Registers all the target-scoped actors like console, script, etc.
 | |
|    *        for debugging a target context.
 | |
|    */
 | |
|   registerActors({ root, browser, target }) {
 | |
|     if (browser) {
 | |
|       ActorRegistry.addBrowserActors();
 | |
|     }
 | |
| 
 | |
|     if (root) {
 | |
|       const {
 | |
|         createRootActor,
 | |
|       } = require("resource://devtools/server/actors/webbrowser.js");
 | |
|       this.setRootActor(createRootActor);
 | |
|     }
 | |
| 
 | |
|     if (target) {
 | |
|       ActorRegistry.addTargetScopedActors();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Register all possible actors for this DevToolsServer.
 | |
|    */
 | |
|   registerAllActors() {
 | |
|     this.registerActors({ root: true, browser: true, target: true });
 | |
|   },
 | |
| 
 | |
|   get listeningSockets() {
 | |
|     return this._listeners.length;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a SocketListener instance to the server's set of active
 | |
|    * SocketListeners.  This is called by a SocketListener after it is opened.
 | |
|    */
 | |
|   addSocketListener(listener) {
 | |
|     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
 | |
|       throw new Error("Can't add a SocketListener, remote debugging disabled");
 | |
|     }
 | |
|     this._checkInit();
 | |
| 
 | |
|     listener.on("accepted", this._onSocketListenerAccepted);
 | |
|     this._listeners.push(listener);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove a SocketListener instance from the server's set of active
 | |
|    * SocketListeners.  This is called by a SocketListener after it is closed.
 | |
|    */
 | |
|   removeSocketListener(listener) {
 | |
|     // Remove connections that were accepted in the listener.
 | |
|     for (const connID of Object.getOwnPropertyNames(this._connections)) {
 | |
|       const connection = this._connections[connID];
 | |
|       if (connection.isAcceptedBy(listener)) {
 | |
|         connection.close();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this._listeners = this._listeners.filter(l => l !== listener);
 | |
|     listener.off("accepted", this._onSocketListenerAccepted);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Closes and forgets all previously opened listeners.
 | |
|    *
 | |
|    * @return boolean
 | |
|    *         Whether any listeners were actually closed.
 | |
|    */
 | |
|   closeAllSocketListeners() {
 | |
|     if (!this.listeningSockets) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     for (const listener of this._listeners) {
 | |
|       listener.close();
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   _onSocketListenerAccepted(transport, listener) {
 | |
|     this._onConnection(transport, null, false, listener);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Creates a new connection to the local debugger speaking over a fake
 | |
|    * transport. This connection results in straightforward calls to the onPacket
 | |
|    * handlers of each side.
 | |
|    *
 | |
|    * @param prefix string [optional]
 | |
|    *    If given, all actors in this connection will have names starting
 | |
|    *    with |prefix + '/'|.
 | |
|    * @returns a client-side DebuggerTransport for communicating with
 | |
|    *    the newly-created connection.
 | |
|    */
 | |
|   connectPipe(prefix) {
 | |
|     this._checkInit();
 | |
| 
 | |
|     const serverTransport = new LocalDebuggerTransport();
 | |
|     const clientTransport = new LocalDebuggerTransport(serverTransport);
 | |
|     serverTransport.other = clientTransport;
 | |
|     const connection = this._onConnection(serverTransport, prefix);
 | |
| 
 | |
|     // I'm putting this here because I trust you.
 | |
|     //
 | |
|     // There are times, when using a local connection, when you're going
 | |
|     // to be tempted to just get direct access to the server.  Resist that
 | |
|     // temptation!  If you succumb to that temptation, you will make the
 | |
|     // fine developers that work on Fennec and Firefox OS sad.  They're
 | |
|     // professionals, they'll try to act like they understand, but deep
 | |
|     // down you'll know that you hurt them.
 | |
|     //
 | |
|     // This reference allows you to give in to that temptation.  There are
 | |
|     // times this makes sense: tests, for example, and while porting a
 | |
|     // previously local-only codebase to the remote protocol.
 | |
|     //
 | |
|     // But every time you use this, you will feel the shame of having
 | |
|     // used a property that starts with a '_'.
 | |
|     clientTransport._serverConnection = connection;
 | |
| 
 | |
|     return clientTransport;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * In a content child process, create a new connection that exchanges
 | |
|    * nsIMessageSender messages with our parent process.
 | |
|    *
 | |
|    * @param prefix
 | |
|    *    The prefix we should use in our nsIMessageSender message names and
 | |
|    *    actor names. This connection will use messages named
 | |
|    *    "debug:<prefix>:packet", and all its actors will have names
 | |
|    *    beginning with "<prefix>/".
 | |
|    */
 | |
|   connectToParent(prefix, scopeOrManager) {
 | |
|     this._checkInit();
 | |
| 
 | |
|     const transport = isWorker
 | |
|       ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix)
 | |
|       : new ChildDebuggerTransport(scopeOrManager, prefix);
 | |
| 
 | |
|     return this._onConnection(transport, prefix, true);
 | |
|   },
 | |
| 
 | |
|   connectToParentWindowActor(jsWindowChildActor, forwardingPrefix) {
 | |
|     this._checkInit();
 | |
|     const transport = new JsWindowActorTransport(
 | |
|       jsWindowChildActor,
 | |
|       forwardingPrefix
 | |
|     );
 | |
| 
 | |
|     return this._onConnection(transport, forwardingPrefix, true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Check if the server is running in the child process.
 | |
|    */
 | |
|   get isInChildProcess() {
 | |
|     return (
 | |
|       Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a new debugger connection for the given transport. Called after
 | |
|    * connectPipe(), from connectToParent, or from an incoming socket
 | |
|    * connection handler.
 | |
|    *
 | |
|    * If present, |forwardingPrefix| is a forwarding prefix that a parent
 | |
|    * server is using to recognizes messages intended for this server. Ensure
 | |
|    * that all our actors have names beginning with |forwardingPrefix + '/'|.
 | |
|    * In particular, the root actor's name will be |forwardingPrefix + '/root'|.
 | |
|    */
 | |
|   _onConnection(
 | |
|     transport,
 | |
|     forwardingPrefix,
 | |
|     noRootActor = false,
 | |
|     socketListener = null
 | |
|   ) {
 | |
|     let connID;
 | |
|     if (forwardingPrefix) {
 | |
|       connID = forwardingPrefix + "/";
 | |
|     } else {
 | |
|       // Multiple servers can be started at the same time, and when that's the
 | |
|       // case, they are loaded in separate devtools loaders.
 | |
|       // So, use the current loader ID to prefix the connection ID and make it
 | |
|       // unique.
 | |
|       connID = "server" + loader.id + ".conn" + this._nextConnID++ + ".";
 | |
|     }
 | |
| 
 | |
|     // Notify the platform code that DevTools is running in the current process
 | |
|     // when we are wiring the very first connection
 | |
|     if (!this.hasConnection()) {
 | |
|       ChromeUtils.notifyDevToolsOpened();
 | |
|     }
 | |
| 
 | |
|     const conn = new DevToolsServerConnection(
 | |
|       connID,
 | |
|       transport,
 | |
|       socketListener
 | |
|     );
 | |
|     this._connections[connID] = conn;
 | |
| 
 | |
|     // Create a root actor for the connection and send the hello packet.
 | |
|     if (!noRootActor) {
 | |
|       conn.rootActor = this.createRootActor(conn);
 | |
|       if (forwardingPrefix) {
 | |
|         conn.rootActor.actorID = forwardingPrefix + "/root";
 | |
|       } else {
 | |
|         conn.rootActor.actorID = "root";
 | |
|       }
 | |
|       conn.addActor(conn.rootActor);
 | |
|       transport.send(conn.rootActor.sayHello());
 | |
|     }
 | |
|     transport.ready();
 | |
| 
 | |
|     this.emit("connectionchange", "opened", conn);
 | |
|     return conn;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove the connection from the debugging server.
 | |
|    */
 | |
|   _connectionClosed(connection) {
 | |
|     delete this._connections[connection.prefix];
 | |
|     this.emit("connectionchange", "closed", connection);
 | |
| 
 | |
|     const hasConnection = this.hasConnection();
 | |
| 
 | |
|     // Notify the platform code that we stopped running DevTools code in the current process
 | |
|     if (!hasConnection) {
 | |
|       ChromeUtils.notifyDevToolsClosed();
 | |
|     }
 | |
| 
 | |
|     // If keepAlive isn't explicitely set to true, destroy the server once its
 | |
|     // last connection closes. Multiple JSWindowActor may use the same DevToolsServer
 | |
|     // and in this case, let the server destroy itself once the last connection closes.
 | |
|     // Otherwise we set keepAlive to true when starting a listening server, receiving
 | |
|     // client connections. Typically when running server on phones, or on desktop
 | |
|     // via `--start-debugger-server`.
 | |
|     if (hasConnection || this.keepAlive) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.destroy();
 | |
|   },
 | |
| 
 | |
|   // DevToolsServer extension API.
 | |
| 
 | |
|   setRootActor(actorFactory) {
 | |
|     this.createRootActor = actorFactory;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called when DevTools are unloaded to remove the contend process server startup script
 | |
|    * for the list of scripts loaded for each new content process. Will also remove message
 | |
|    * listeners from already loaded scripts.
 | |
|    */
 | |
|   removeContentServerScript() {
 | |
|     Services.ppmm.removeDelayedProcessScript(
 | |
|       CONTENT_PROCESS_SERVER_STARTUP_SCRIPT
 | |
|     );
 | |
|     try {
 | |
|       Services.ppmm.broadcastAsyncMessage("debug:close-content-server");
 | |
|     } catch (e) {
 | |
|       // Nothing to do
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Searches all active connections for an actor matching an ID.
 | |
|    *
 | |
|    * ⚠ TO BE USED ONLY FROM SERVER CODE OR TESTING ONLY! ⚠`
 | |
|    *
 | |
|    * This is helpful for some tests which depend on reaching into the server to check some
 | |
|    * properties of an actor, and it is also used by the actors related to the
 | |
|    * DevTools WebExtensions API to be able to interact with the actors created for the
 | |
|    * panels natively provided by the DevTools Toolbox.
 | |
|    */
 | |
|   searchAllConnectionsForActor(actorID) {
 | |
|     // NOTE: the actor IDs are generated with the following format:
 | |
|     //
 | |
|     //   `server${loaderID}.conn${ConnectionID}${ActorPrefix}${ActorID}`
 | |
|     //
 | |
|     // as an optimization we can come up with a regexp to query only
 | |
|     // the right connection via its id.
 | |
|     for (const connID of Object.getOwnPropertyNames(this._connections)) {
 | |
|       const actor = this._connections[connID].getActor(actorID);
 | |
|       if (actor) {
 | |
|         return actor;
 | |
|       }
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| };
 | |
| 
 | |
| // Expose these to save callers the trouble of importing DebuggerSocket
 | |
| DevToolsUtils.defineLazyGetter(DevToolsServer, "Authenticators", () => {
 | |
|   return Authentication.Authenticators;
 | |
| });
 | |
| DevToolsUtils.defineLazyGetter(DevToolsServer, "AuthenticationResult", () => {
 | |
|   return Authentication.AuthenticationResult;
 | |
| });
 | |
| 
 | |
| EventEmitter.decorate(DevToolsServer);
 | |
| 
 | |
| exports.DevToolsServer = DevToolsServer;
 |