diff options
Diffstat (limited to 'devtools/server/devtools-server.js')
-rw-r--r-- | devtools/server/devtools-server.js | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/devtools/server/devtools-server.js b/devtools/server/devtools-server.js new file mode 100644 index 0000000000..e1dd7994d1 --- /dev/null +++ b/devtools/server/devtools-server.js @@ -0,0 +1,513 @@ +/* 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; |