/* 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];
// When calling connection.close on a previous element,
// this may unregister some of the following other connections in `_connections`
// and make them be null here.
if (!connection) {
continue;
}
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;