/* 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 { Ci, Cc } = require("chrome"); var Services = require("Services"); var { ActorRegistry } = require("devtools/server/actors/utils/actor-registry"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); var { dumpn } = DevToolsUtils; loader.lazyRequireGetter( this, "DevToolsServerConnection", "devtools/server/devtools-server-connection", true ); loader.lazyRequireGetter( this, "Authentication", "devtools/shared/security/auth" ); loader.lazyRequireGetter( this, "LocalDebuggerTransport", "devtools/shared/transport/local-transport", true ); loader.lazyRequireGetter( this, "ChildDebuggerTransport", "devtools/shared/transport/child-transport", true ); loader.lazyRequireGetter( this, "JsWindowActorTransport", "devtools/shared/transport/js-window-actor-transport", true ); loader.lazyRequireGetter( this, "WorkerThreadWorkerDebuggerTransport", "devtools/shared/transport/worker-transport", true ); loader.lazyGetter(this, "generateUUID", () => { // eslint-disable-next-line no-shadow const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService( Ci.nsIUUIDGenerator ); return generateUUID; }); const CONTENT_PROCESS_SERVER_STARTUP_SCRIPT = "resource://devtools/server/startup/content-process.js"; loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); /** * 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 FrameTargetActor, 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 // 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("devtools/shared/protocol"); }, get initialized() { return this._initialized; }, hasConnection() { return this._connections && Object.keys(this._connections).length > 0; }, /** * 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; } for (const connID of Object.getOwnPropertyNames(this._connections)) { this._connections[connID].close(); } ActorRegistry.destroy(); this.closeAllSocketListeners(); this._initialized = false; 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("devtools/server/actors/webbrowser"); 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::packet", and all its actors will have names * beginning with "/". */ connectToParent(prefix, scopeOrManager) { this._checkInit(); const transport = isWorker ? new WorkerThreadWorkerDebuggerTransport(scopeOrManager, prefix) : new ChildDebuggerTransport(scopeOrManager, prefix); return this._onConnection(transport, prefix, true); }, connectToParentWindowActor(devtoolsFrameActor, forwardingPrefix) { this._checkInit(); const transport = new JsWindowActorTransport( devtoolsFrameActor, 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 ); }, /** * In a chrome parent process, ask all content child processes * to execute a given module setup helper. * * @param module * The module to be required * @param setupChild * The name of the setup helper exported by the above module * (setup helper signature: function ({mm}) { ... }) * @param waitForEval (optional) * If true, the returned promise only resolves once code in child * is evaluated */ setupInChild({ module, setupChild, args, waitForEval }) { if (this._childMessageManagers.size == 0) { return Promise.resolve(); } return new Promise(done => { // If waitForEval is set, pass a unique id and expect child.js to send // a message back once the code in child is evaluated. if (typeof waitForEval != "boolean") { waitForEval = false; } let count = this._childMessageManagers.size; const id = waitForEval ? generateUUID().toString() : null; this._childMessageManagers.forEach(mm => { if (waitForEval) { // Listen for the end of each child execution const evalListener = msg => { if (msg.data.id !== id) { return; } mm.removeMessageListener( "debug:setup-in-child-response", evalListener ); if (--count === 0) { done(); } }; mm.addMessageListener("debug:setup-in-child-response", evalListener); } mm.sendAsyncMessage("debug:setup-in-child", { module: module, setupChild: setupChild, args: args, id: id, }); }); if (!waitForEval) { done(); } }); }, /** * Live list of all currently attached child's message managers. */ _childMessageManagers: new Set(), /** * 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++ + "."; } 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); }, // 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;