mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
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
514 lines
16 KiB
JavaScript
514 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;
|