зеркало из https://github.com/mozilla/gecko-dev.git
548 строки
16 KiB
JavaScript
548 строки
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 { 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:<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(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;
|