michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: /** michael@0: * Toolkit glue for the remote debugging protocol, loaded into the michael@0: * debugging global. michael@0: */ michael@0: let { Ci, Cc, CC, Cu, Cr } = require("chrome"); michael@0: let Debugger = require("Debugger"); michael@0: let Services = require("Services"); michael@0: let { ActorPool } = require("devtools/server/actors/common"); michael@0: let { DebuggerTransport, LocalDebuggerTransport, ChildDebuggerTransport } = require("devtools/server/transport"); michael@0: let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); michael@0: let { dumpn, dbg_assert } = DevToolsUtils; michael@0: let Services = require("Services"); michael@0: let EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: // Until all Debugger server code is converted to SDK modules, michael@0: // imports Components.* alias from chrome module. michael@0: var { Ci, Cc, CC, Cu, Cr } = require("chrome"); michael@0: // On B2G, `this` != Global scope, so `Ci` won't be binded on `this` michael@0: // (i.e. this.Ci is undefined) Then later, when using loadSubScript, michael@0: // Ci,... won't be defined for sub scripts. michael@0: this.Ci = Ci; michael@0: this.Cc = Cc; michael@0: this.CC = CC; michael@0: this.Cu = Cu; michael@0: this.Cr = Cr; michael@0: this.Debugger = Debugger; michael@0: this.Services = Services; michael@0: this.ActorPool = ActorPool; michael@0: this.DevToolsUtils = DevToolsUtils; michael@0: this.dumpn = dumpn; michael@0: this.dbg_assert = dbg_assert; michael@0: michael@0: // Overload `Components` to prevent SDK loader exception on Components michael@0: // object usage michael@0: Object.defineProperty(this, "Components", { michael@0: get: function () require("chrome").components michael@0: }); michael@0: michael@0: const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; michael@0: michael@0: const nsFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); michael@0: Cu.import("resource://gre/modules/reflect.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: dumpn.wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); michael@0: michael@0: Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js"); michael@0: michael@0: function loadSubScript(aURL) michael@0: { michael@0: try { michael@0: let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] michael@0: .getService(Ci.mozIJSSubScriptLoader); michael@0: loader.loadSubScript(aURL, this); michael@0: } catch(e) { michael@0: let errorStr = "Error loading: " + aURL + ":\n" + michael@0: (e.fileName ? "at " + e.fileName + " : " + e.lineNumber + "\n" : "") + michael@0: e + " - " + e.stack + "\n"; michael@0: dump(errorStr); michael@0: Cu.reportError(errorStr); michael@0: throw e; michael@0: } michael@0: } michael@0: michael@0: let events = require("sdk/event/core"); michael@0: let {defer, resolve, reject, all} = require("devtools/toolkit/deprecated-sync-thenables"); michael@0: this.defer = defer; michael@0: this.resolve = resolve; michael@0: this.reject = reject; michael@0: this.all = all; michael@0: michael@0: Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "NetworkMonitorManager", () => { michael@0: return require("devtools/toolkit/webconsole/network-monitor").NetworkMonitorManager; michael@0: }); michael@0: michael@0: // XPCOM constructors michael@0: const ServerSocket = CC("@mozilla.org/network/server-socket;1", michael@0: "nsIServerSocket", michael@0: "initSpecialConnection"); michael@0: const UnixDomainServerSocket = CC("@mozilla.org/network/server-socket;1", michael@0: "nsIServerSocket", michael@0: "initWithFilename"); michael@0: michael@0: var gRegisteredModules = Object.create(null); michael@0: michael@0: /** michael@0: * The ModuleAPI object is passed to modules loaded using the michael@0: * DebuggerServer.registerModule() API. Modules can use this michael@0: * object to register actor factories. michael@0: * Factories registered through the module API will be removed michael@0: * when the module is unregistered or when the server is michael@0: * destroyed. michael@0: */ michael@0: function ModuleAPI() { michael@0: let activeTabActors = new Set(); michael@0: let activeGlobalActors = new Set(); michael@0: michael@0: return { michael@0: // See DebuggerServer.addGlobalActor for a description. michael@0: addGlobalActor: function(factory, name) { michael@0: DebuggerServer.addGlobalActor(factory, name); michael@0: activeGlobalActors.add(factory); michael@0: }, michael@0: // See DebuggerServer.removeGlobalActor for a description. michael@0: removeGlobalActor: function(factory) { michael@0: DebuggerServer.removeGlobalActor(factory); michael@0: activeGlobalActors.delete(factory); michael@0: }, michael@0: michael@0: // See DebuggerServer.addTabActor for a description. michael@0: addTabActor: function(factory, name) { michael@0: DebuggerServer.addTabActor(factory, name); michael@0: activeTabActors.add(factory); michael@0: }, michael@0: // See DebuggerServer.removeTabActor for a description. michael@0: removeTabActor: function(factory) { michael@0: DebuggerServer.removeTabActor(factory); michael@0: activeTabActors.delete(factory); michael@0: }, michael@0: michael@0: // Destroy the module API object, unregistering any michael@0: // factories registered by the module. michael@0: destroy: function() { michael@0: for (let factory of activeTabActors) { michael@0: DebuggerServer.removeTabActor(factory); michael@0: } michael@0: activeTabActors = null; michael@0: for (let factory of activeGlobalActors) { michael@0: DebuggerServer.removeGlobalActor(factory); michael@0: } michael@0: activeGlobalActors = null; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /*** michael@0: * Public API michael@0: */ michael@0: var DebuggerServer = { michael@0: _listener: null, michael@0: _initialized: false, michael@0: _transportInitialized: false, michael@0: xpcInspector: null, michael@0: // Number of currently open TCP connections. michael@0: _socketConnections: 0, michael@0: // Map of global actor names to actor constructors provided by extensions. michael@0: globalActorFactories: {}, michael@0: // Map of tab actor names to actor constructors provided by extensions. michael@0: tabActorFactories: {}, michael@0: michael@0: LONG_STRING_LENGTH: 10000, michael@0: LONG_STRING_INITIAL_LENGTH: 1000, michael@0: LONG_STRING_READ_LENGTH: 65 * 1024, michael@0: michael@0: /** michael@0: * A handler function that prompts the user to accept or decline the incoming michael@0: * connection. michael@0: */ michael@0: _allowConnection: null, michael@0: michael@0: /** michael@0: * The windowtype of the chrome window to use for actors that use the global michael@0: * window (i.e the global style editor). Set this to your main window type, michael@0: * for example "navigator:browser". michael@0: */ michael@0: chromeWindowType: null, michael@0: michael@0: /** michael@0: * Prompt the user to accept or decline the incoming connection. This is the michael@0: * default implementation that products embedding the debugger server may michael@0: * choose to override. michael@0: * michael@0: * @return true if the connection should be permitted, false otherwise michael@0: */ michael@0: _defaultAllowConnection: function DS__defaultAllowConnection() { michael@0: let title = L10N.getStr("remoteIncomingPromptTitle"); michael@0: let msg = L10N.getStr("remoteIncomingPromptMessage"); michael@0: let disableButton = L10N.getStr("remoteIncomingPromptDisable"); michael@0: let prompt = Services.prompt; michael@0: let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK + michael@0: prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL + michael@0: prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING + michael@0: prompt.BUTTON_POS_1_DEFAULT; michael@0: let result = prompt.confirmEx(null, title, msg, flags, null, null, michael@0: disableButton, null, { value: false }); michael@0: if (result == 0) { michael@0: return true; michael@0: } michael@0: if (result == 2) { michael@0: DebuggerServer.closeListener(true); michael@0: Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false); michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the debugger server. michael@0: * michael@0: * @param function aAllowConnectionCallback michael@0: * The embedder-provider callback, that decides whether an incoming michael@0: * remote protocol conection should be allowed or refused. michael@0: */ michael@0: init: function DS_init(aAllowConnectionCallback) { michael@0: if (this.initialized) { michael@0: return; michael@0: } michael@0: michael@0: this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); michael@0: this.initTransport(aAllowConnectionCallback); michael@0: this.addActors("resource://gre/modules/devtools/server/actors/root.js"); michael@0: michael@0: this._initialized = true; michael@0: }, michael@0: michael@0: protocol: require("devtools/server/protocol"), michael@0: michael@0: /** michael@0: * Initialize the debugger server's transport variables. This can be michael@0: * in place of init() for cases where the jsdebugger isn't needed. michael@0: * michael@0: * @param function aAllowConnectionCallback michael@0: * The embedder-provider callback, that decides whether an incoming michael@0: * remote protocol conection should be allowed or refused. michael@0: */ michael@0: initTransport: function DS_initTransport(aAllowConnectionCallback) { michael@0: if (this._transportInitialized) { michael@0: return; michael@0: } michael@0: michael@0: this._connections = {}; michael@0: this._nextConnID = 0; michael@0: this._transportInitialized = true; michael@0: this._allowConnection = aAllowConnectionCallback ? michael@0: aAllowConnectionCallback : michael@0: this._defaultAllowConnection; michael@0: }, michael@0: michael@0: get initialized() this._initialized, michael@0: michael@0: /** michael@0: * Performs cleanup tasks before shutting down the debugger server. Such tasks michael@0: * include clearing any actor constructors added at runtime. This method michael@0: * should be called whenever a debugger server is no longer useful, to avoid michael@0: * memory leaks. After this method returns, the debugger server must be michael@0: * initialized again before use. michael@0: */ michael@0: destroy: function DS_destroy() { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: for (let connID of Object.getOwnPropertyNames(this._connections)) { michael@0: this._connections[connID].close(); michael@0: } michael@0: michael@0: for (let id of Object.getOwnPropertyNames(gRegisteredModules)) { michael@0: let mod = gRegisteredModules[id]; michael@0: mod.module.unregister(mod.api); michael@0: } michael@0: gRegisteredModules = {}; michael@0: michael@0: this.closeListener(); michael@0: this.globalActorFactories = {}; michael@0: this.tabActorFactories = {}; michael@0: this._allowConnection = null; michael@0: this._transportInitialized = false; michael@0: this._initialized = false; michael@0: michael@0: dumpn("Debugger server is shut down."); michael@0: }, michael@0: michael@0: /** michael@0: * Load a subscript into the debugging global. michael@0: * michael@0: * @param aURL string A url that will be loaded as a subscript into the michael@0: * debugging global. The user must load at least one script michael@0: * that implements a createRootActor() function to create the michael@0: * server's root actor. michael@0: */ michael@0: addActors: function DS_addActors(aURL) { michael@0: loadSubScript.call(this, aURL); michael@0: }, michael@0: michael@0: /** michael@0: * Register a CommonJS module with the debugger server. michael@0: * @param id string michael@0: * The ID of a CommonJS module. This module must export michael@0: * 'register' and 'unregister' functions. michael@0: */ michael@0: registerModule: function(id) { michael@0: if (id in gRegisteredModules) { michael@0: throw new Error("Tried to register a module twice: " + id + "\n"); michael@0: } michael@0: michael@0: let moduleAPI = ModuleAPI(); michael@0: let mod = require(id); michael@0: mod.register(moduleAPI); michael@0: gRegisteredModules[id] = { module: mod, api: moduleAPI }; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if a module id has been registered. michael@0: */ michael@0: isModuleRegistered: function(id) { michael@0: return (id in gRegisteredModules); michael@0: }, michael@0: michael@0: /** michael@0: * Unregister a previously-loaded CommonJS module from the debugger server. michael@0: */ michael@0: unregisterModule: function(id) { michael@0: let mod = gRegisteredModules[id]; michael@0: if (!mod) { michael@0: throw new Error("Tried to unregister a module that was not previously registered."); michael@0: } michael@0: mod.module.unregister(mod.api); michael@0: mod.api.destroy(); michael@0: delete gRegisteredModules[id]; michael@0: }, michael@0: michael@0: /** michael@0: * Install Firefox-specific actors. michael@0: * michael@0: * /!\ Be careful when adding a new actor, especially global actors. michael@0: * Any new global actor will be exposed and returned by the root actor. michael@0: * michael@0: * That's the reason why tab actors aren't loaded on demand via michael@0: * restrictPrivileges=true, to prevent exposing them on b2g parent process's michael@0: * root actor. michael@0: */ michael@0: addBrowserActors: function(aWindowType = "navigator:browser", restrictPrivileges = false) { michael@0: this.chromeWindowType = aWindowType; michael@0: this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); michael@0: michael@0: if (!restrictPrivileges) { michael@0: this.addTabActors(); michael@0: this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger"); michael@0: this.registerModule("devtools/server/actors/preference"); michael@0: } michael@0: michael@0: this.addActors("resource://gre/modules/devtools/server/actors/webapps.js"); michael@0: this.registerModule("devtools/server/actors/device"); michael@0: }, michael@0: michael@0: /** michael@0: * Install tab actors in documents loaded in content childs michael@0: */ michael@0: addChildActors: function () { michael@0: // In case of apps being loaded in parent process, DebuggerServer is already michael@0: // initialized and browser actors are already loaded, michael@0: // but childtab.js hasn't been loaded yet. michael@0: if (!DebuggerServer.tabActorFactories.hasOwnProperty("consoleActor")) { michael@0: this.addTabActors(); michael@0: } michael@0: // But webbrowser.js and childtab.js aren't loaded from shell.js. michael@0: if (!("BrowserTabActor" in this)) { michael@0: this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); michael@0: } michael@0: if (!("ContentActor" in this)) { michael@0: this.addActors("resource://gre/modules/devtools/server/actors/childtab.js"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Install tab actors. michael@0: */ michael@0: addTabActors: function() { michael@0: this.addActors("resource://gre/modules/devtools/server/actors/script.js"); michael@0: this.registerModule("devtools/server/actors/webconsole"); michael@0: this.registerModule("devtools/server/actors/inspector"); michael@0: this.registerModule("devtools/server/actors/call-watcher"); michael@0: this.registerModule("devtools/server/actors/canvas"); michael@0: this.registerModule("devtools/server/actors/webgl"); michael@0: this.registerModule("devtools/server/actors/webaudio"); michael@0: this.registerModule("devtools/server/actors/stylesheets"); michael@0: this.registerModule("devtools/server/actors/styleeditor"); michael@0: this.registerModule("devtools/server/actors/storage"); michael@0: this.registerModule("devtools/server/actors/gcli"); michael@0: this.registerModule("devtools/server/actors/tracer"); michael@0: this.registerModule("devtools/server/actors/memory"); michael@0: this.registerModule("devtools/server/actors/eventlooplag"); michael@0: if ("nsIProfiler" in Ci) { michael@0: this.addActors("resource://gre/modules/devtools/server/actors/profiler.js"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Passes a set of options to the BrowserAddonActors for the given ID. michael@0: * michael@0: * @param aId string michael@0: * The ID of the add-on to pass the options to michael@0: * @param aOptions object michael@0: * The options. michael@0: * @return a promise that will be resolved when complete. michael@0: */ michael@0: setAddonOptions: function DS_setAddonOptions(aId, aOptions) { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: let promises = []; michael@0: michael@0: // Pass to all connections michael@0: for (let connID of Object.getOwnPropertyNames(this._connections)) { michael@0: promises.push(this._connections[connID].setAddonOptions(aId, aOptions)); michael@0: } michael@0: michael@0: return all(promises); michael@0: }, michael@0: michael@0: /** michael@0: * Listens on the given port or socket file for remote debugger connections. michael@0: * michael@0: * @param aPortOrPath int, string michael@0: * If given an integer, the port to listen on. michael@0: * Otherwise, the path to the unix socket domain file to listen on. michael@0: */ michael@0: openListener: function DS_openListener(aPortOrPath) { michael@0: if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) { michael@0: return false; michael@0: } michael@0: this._checkInit(); michael@0: michael@0: // Return early if the server is already listening. michael@0: if (this._listener) { michael@0: return true; michael@0: } michael@0: michael@0: let flags = Ci.nsIServerSocket.KeepWhenOffline; michael@0: // A preference setting can force binding on the loopback interface. michael@0: if (Services.prefs.getBoolPref("devtools.debugger.force-local")) { michael@0: flags |= Ci.nsIServerSocket.LoopbackOnly; michael@0: } michael@0: michael@0: try { michael@0: let backlog = 4; michael@0: let socket; michael@0: let port = Number(aPortOrPath); michael@0: if (port) { michael@0: socket = new ServerSocket(port, flags, backlog); michael@0: } else { michael@0: let file = nsFile(aPortOrPath); michael@0: if (file.exists()) michael@0: file.remove(false); michael@0: socket = new UnixDomainServerSocket(file, parseInt("666", 8), backlog); michael@0: } michael@0: socket.asyncListen(this); michael@0: this._listener = socket; michael@0: } catch (e) { michael@0: dumpn("Could not start debugging listener on '" + aPortOrPath + "': " + e); michael@0: throw Cr.NS_ERROR_NOT_AVAILABLE; michael@0: } michael@0: this._socketConnections++; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Close a previously-opened TCP listener. michael@0: * michael@0: * @param aForce boolean [optional] michael@0: * If set to true, then the socket will be closed, regardless of the michael@0: * number of open connections. michael@0: */ michael@0: closeListener: function DS_closeListener(aForce) { michael@0: if (!this._listener || this._socketConnections == 0) { michael@0: return false; michael@0: } michael@0: michael@0: // Only close the listener when the last connection is closed, or if the michael@0: // aForce flag is passed. michael@0: if (--this._socketConnections == 0 || aForce) { michael@0: this._listener.close(); michael@0: this._listener = null; michael@0: this._socketConnections = 0; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Creates a new connection to the local debugger speaking over a fake michael@0: * transport. This connection results in straightforward calls to the onPacket michael@0: * handlers of each side. michael@0: * michael@0: * @param aPrefix string [optional] michael@0: * If given, all actors in this connection will have names starting michael@0: * with |aPrefix + ':'|. michael@0: * @returns a client-side DebuggerTransport for communicating with michael@0: * the newly-created connection. michael@0: */ michael@0: connectPipe: function DS_connectPipe(aPrefix) { michael@0: this._checkInit(); michael@0: michael@0: let serverTransport = new LocalDebuggerTransport; michael@0: let clientTransport = new LocalDebuggerTransport(serverTransport); michael@0: serverTransport.other = clientTransport; michael@0: let connection = this._onConnection(serverTransport, aPrefix); michael@0: michael@0: // I'm putting this here because I trust you. michael@0: // michael@0: // There are times, when using a local connection, when you're going michael@0: // to be tempted to just get direct access to the server. Resist that michael@0: // temptation! If you succumb to that temptation, you will make the michael@0: // fine developers that work on Fennec and Firefox OS sad. They're michael@0: // professionals, they'll try to act like they understand, but deep michael@0: // down you'll know that you hurt them. michael@0: // michael@0: // This reference allows you to give in to that temptation. There are michael@0: // times this makes sense: tests, for example, and while porting a michael@0: // previously local-only codebase to the remote protocol. michael@0: // michael@0: // But every time you use this, you will feel the shame of having michael@0: // used a property that starts with a '_'. michael@0: clientTransport._serverConnection = connection; michael@0: michael@0: return clientTransport; michael@0: }, michael@0: michael@0: /** michael@0: * In a content child process, create a new connection that exchanges michael@0: * nsIMessageSender messages with our parent process. michael@0: * michael@0: * @param aPrefix michael@0: * The prefix we should use in our nsIMessageSender message names and michael@0: * actor names. This connection will use messages named michael@0: * "debug::packet", and all its actors will have names michael@0: * beginning with ":". michael@0: */ michael@0: connectToParent: function(aPrefix, aMessageManager) { michael@0: this._checkInit(); michael@0: michael@0: let transport = new ChildDebuggerTransport(aMessageManager, aPrefix); michael@0: return this._onConnection(transport, aPrefix, true); michael@0: }, michael@0: michael@0: /** michael@0: * Connect to a child process. michael@0: * michael@0: * @param object aConnection michael@0: * The debugger server connection to use. michael@0: * @param nsIDOMElement aFrame michael@0: * The browser element that holds the child process. michael@0: * @param function [aOnDisconnect] michael@0: * Optional function to invoke when the child is disconnected. michael@0: * @return object michael@0: * A promise object that is resolved once the connection is michael@0: * established. michael@0: */ michael@0: connectToChild: function(aConnection, aFrame, aOnDisconnect) { michael@0: let deferred = defer(); michael@0: michael@0: let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader michael@0: .messageManager; michael@0: mm.loadFrameScript("resource://gre/modules/devtools/server/child.js", false); michael@0: michael@0: let actor, childTransport; michael@0: let prefix = aConnection.allocID("child"); michael@0: let netMonitor = null; michael@0: michael@0: let onActorCreated = DevToolsUtils.makeInfallible(function (msg) { michael@0: mm.removeMessageListener("debug:actor", onActorCreated); michael@0: michael@0: // Pipe Debugger message from/to parent/child via the message manager michael@0: childTransport = new ChildDebuggerTransport(mm, prefix); michael@0: childTransport.hooks = { michael@0: onPacket: aConnection.send.bind(aConnection), michael@0: onClosed: function () {} michael@0: }; michael@0: childTransport.ready(); michael@0: michael@0: aConnection.setForwarding(prefix, childTransport); michael@0: michael@0: dumpn("establishing forwarding for app with prefix " + prefix); michael@0: michael@0: actor = msg.json.actor; michael@0: michael@0: netMonitor = new NetworkMonitorManager(aFrame, actor.actor); michael@0: michael@0: deferred.resolve(actor); michael@0: }).bind(this); michael@0: mm.addMessageListener("debug:actor", onActorCreated); michael@0: michael@0: let onMessageManagerDisconnect = DevToolsUtils.makeInfallible(function (subject, topic, data) { michael@0: if (subject == mm) { michael@0: Services.obs.removeObserver(onMessageManagerDisconnect, topic); michael@0: if (childTransport) { michael@0: // If we have a child transport, the actor has already michael@0: // been created. We need to stop using this message manager. michael@0: childTransport.close(); michael@0: childTransport = null; michael@0: aConnection.cancelForwarding(prefix); michael@0: } else { michael@0: // Otherwise, the app has been closed before the actor michael@0: // had a chance to be created, so we are not able to create michael@0: // the actor. michael@0: deferred.resolve(null); michael@0: } michael@0: if (actor) { michael@0: // The ContentActor within the child process doesn't necessary michael@0: // have to time to uninitialize itself when the app is closed/killed. michael@0: // So ensure telling the client that the related actor is detached. michael@0: aConnection.send({ from: actor.actor, type: "tabDetached" }); michael@0: actor = null; michael@0: } michael@0: michael@0: if (netMonitor) { michael@0: netMonitor.destroy(); michael@0: netMonitor = null; michael@0: } michael@0: michael@0: if (aOnDisconnect) { michael@0: aOnDisconnect(mm); michael@0: } michael@0: } michael@0: }).bind(this); michael@0: Services.obs.addObserver(onMessageManagerDisconnect, michael@0: "message-manager-disconnect", false); michael@0: michael@0: events.once(aConnection, "closed", () => { michael@0: if (childTransport) { michael@0: // When the client disconnects, we have to unplug the dedicated michael@0: // ChildDebuggerTransport... michael@0: childTransport.close(); michael@0: childTransport = null; michael@0: aConnection.cancelForwarding(prefix); michael@0: michael@0: // ... and notify the child process to clean the tab actors. michael@0: mm.sendAsyncMessage("debug:disconnect"); michael@0: } michael@0: Services.obs.removeObserver(onMessageManagerDisconnect, "message-manager-disconnect"); michael@0: }); michael@0: michael@0: mm.sendAsyncMessage("debug:connect", { prefix: prefix }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: // nsIServerSocketListener implementation michael@0: michael@0: onSocketAccepted: michael@0: DevToolsUtils.makeInfallible(function DS_onSocketAccepted(aSocket, aTransport) { michael@0: if (Services.prefs.getBoolPref("devtools.debugger.prompt-connection") && !this._allowConnection()) { michael@0: return; michael@0: } michael@0: dumpn("New debugging connection on " + aTransport.host + ":" + aTransport.port); michael@0: michael@0: let input = aTransport.openInputStream(0, 0, 0); michael@0: let output = aTransport.openOutputStream(0, 0, 0); michael@0: let transport = new DebuggerTransport(input, output); michael@0: DebuggerServer._onConnection(transport); michael@0: }, "DebuggerServer.onSocketAccepted"), michael@0: michael@0: onStopListening: function DS_onStopListening(aSocket, status) { michael@0: dumpn("onStopListening, status: " + status); michael@0: }, michael@0: michael@0: /** michael@0: * Raises an exception if the server has not been properly initialized. michael@0: */ michael@0: _checkInit: function DS_checkInit() { michael@0: if (!this._transportInitialized) { michael@0: throw "DebuggerServer has not been initialized."; michael@0: } michael@0: michael@0: if (!this.createRootActor) { michael@0: throw "Use DebuggerServer.addActors() to add a root actor implementation."; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Create a new debugger connection for the given transport. Called after michael@0: * connectPipe(), from connectToParent, or from an incoming socket michael@0: * connection handler. michael@0: * michael@0: * If present, |aForwardingPrefix| is a forwarding prefix that a parent michael@0: * server is using to recognizes messages intended for this server. Ensure michael@0: * that all our actors have names beginning with |aForwardingPrefix + ':'|. michael@0: * In particular, the root actor's name will be |aForwardingPrefix + ':root'|. michael@0: */ michael@0: _onConnection: function DS_onConnection(aTransport, aForwardingPrefix, aNoRootActor = false) { michael@0: let connID; michael@0: if (aForwardingPrefix) { michael@0: connID = aForwardingPrefix + ":"; michael@0: } else { michael@0: connID = "conn" + this._nextConnID++ + '.'; michael@0: } michael@0: let conn = new DebuggerServerConnection(connID, aTransport); michael@0: this._connections[connID] = conn; michael@0: michael@0: // Create a root actor for the connection and send the hello packet. michael@0: if (!aNoRootActor) { michael@0: conn.rootActor = this.createRootActor(conn); michael@0: if (aForwardingPrefix) michael@0: conn.rootActor.actorID = aForwardingPrefix + ":root"; michael@0: else michael@0: conn.rootActor.actorID = "root"; michael@0: conn.addActor(conn.rootActor); michael@0: aTransport.send(conn.rootActor.sayHello()); michael@0: } michael@0: aTransport.ready(); michael@0: michael@0: this.emit("connectionchange", "opened", conn); michael@0: return conn; michael@0: }, michael@0: michael@0: /** michael@0: * Remove the connection from the debugging server. michael@0: */ michael@0: _connectionClosed: function DS_connectionClosed(aConnection) { michael@0: delete this._connections[aConnection.prefix]; michael@0: this.emit("connectionchange", "closed", aConnection); michael@0: }, michael@0: michael@0: // DebuggerServer extension API. michael@0: michael@0: /** michael@0: * Registers handlers for new tab-scoped request types defined dynamically. michael@0: * This is used for example by add-ons to augment the functionality of the tab michael@0: * actor. Note that the name or actorPrefix of the request type is not allowed michael@0: * to clash with existing protocol packet properties, like 'title', 'url' or michael@0: * 'actor', since that would break the protocol. michael@0: * michael@0: * @param aFunction function michael@0: * The constructor function for this request type. This expects to be michael@0: * called as a constructor (i.e. with 'new'), and passed two michael@0: * arguments: the DebuggerServerConnection, and the BrowserTabActor michael@0: * with which it will be associated. michael@0: * michael@0: * @param aName string [optional] michael@0: * The name of the new request type. If this is not present, the michael@0: * actorPrefix property of the constructor prototype is used. michael@0: */ michael@0: addTabActor: function DS_addTabActor(aFunction, aName) { michael@0: let name = aName ? aName : aFunction.prototype.actorPrefix; michael@0: if (["title", "url", "actor"].indexOf(name) != -1) { michael@0: throw Error(name + " is not allowed"); michael@0: } michael@0: if (DebuggerServer.tabActorFactories.hasOwnProperty(name)) { michael@0: throw Error(name + " already exists"); michael@0: } michael@0: DebuggerServer.tabActorFactories[name] = aFunction; michael@0: }, michael@0: michael@0: /** michael@0: * Unregisters the handler for the specified tab-scoped request type. michael@0: * This may be used for example by add-ons when shutting down or upgrading. michael@0: * michael@0: * @param aFunction function michael@0: * The constructor function for this request type. michael@0: */ michael@0: removeTabActor: function DS_removeTabActor(aFunction) { michael@0: for (let name in DebuggerServer.tabActorFactories) { michael@0: let handler = DebuggerServer.tabActorFactories[name]; michael@0: if (handler.name == aFunction.name) { michael@0: delete DebuggerServer.tabActorFactories[name]; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Registers handlers for new browser-scoped request types defined michael@0: * dynamically. This is used for example by add-ons to augment the michael@0: * functionality of the root actor. Note that the name or actorPrefix of the michael@0: * request type is not allowed to clash with existing protocol packet michael@0: * properties, like 'from', 'tabs' or 'selected', since that would break the michael@0: * protocol. michael@0: * michael@0: * @param aFunction function michael@0: * The constructor function for this request type. This expects to be michael@0: * called as a constructor (i.e. with 'new'), and passed two michael@0: * arguments: the DebuggerServerConnection, and the BrowserRootActor michael@0: * with which it will be associated. michael@0: * michael@0: * @param aName string [optional] michael@0: * The name of the new request type. If this is not present, the michael@0: * actorPrefix property of the constructor prototype is used. michael@0: */ michael@0: addGlobalActor: function DS_addGlobalActor(aFunction, aName) { michael@0: let name = aName ? aName : aFunction.prototype.actorPrefix; michael@0: if (["from", "tabs", "selected"].indexOf(name) != -1) { michael@0: throw Error(name + " is not allowed"); michael@0: } michael@0: if (DebuggerServer.globalActorFactories.hasOwnProperty(name)) { michael@0: throw Error(name + " already exists"); michael@0: } michael@0: DebuggerServer.globalActorFactories[name] = aFunction; michael@0: }, michael@0: michael@0: /** michael@0: * Unregisters the handler for the specified browser-scoped request type. michael@0: * This may be used for example by add-ons when shutting down or upgrading. michael@0: * michael@0: * @param aFunction function michael@0: * The constructor function for this request type. michael@0: */ michael@0: removeGlobalActor: function DS_removeGlobalActor(aFunction) { michael@0: for (let name in DebuggerServer.globalActorFactories) { michael@0: let handler = DebuggerServer.globalActorFactories[name]; michael@0: if (handler.name == aFunction.name) { michael@0: delete DebuggerServer.globalActorFactories[name]; michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: EventEmitter.decorate(DebuggerServer); michael@0: michael@0: if (this.exports) { michael@0: exports.DebuggerServer = DebuggerServer; michael@0: } michael@0: // Needed on B2G (See header note) michael@0: this.DebuggerServer = DebuggerServer; michael@0: michael@0: // Export ActorPool for requirers of main.js michael@0: if (this.exports) { michael@0: exports.ActorPool = ActorPool; michael@0: } michael@0: michael@0: /** michael@0: * Creates a DebuggerServerConnection. michael@0: * michael@0: * Represents a connection to this debugging global from a client. michael@0: * Manages a set of actors and actor pools, allocates actor ids, and michael@0: * handles incoming requests. michael@0: * michael@0: * @param aPrefix string michael@0: * All actor IDs created by this connection should be prefixed michael@0: * with aPrefix. michael@0: * @param aTransport transport michael@0: * Packet transport for the debugging protocol. michael@0: */ michael@0: function DebuggerServerConnection(aPrefix, aTransport) michael@0: { michael@0: this._prefix = aPrefix; michael@0: this._transport = aTransport; michael@0: this._transport.hooks = this; michael@0: this._nextID = 1; michael@0: michael@0: this._actorPool = new ActorPool(this); michael@0: this._extraPools = []; michael@0: michael@0: // Responses to a given actor must be returned the the client michael@0: // in the same order as the requests that they're replying to, but michael@0: // Implementations might finish serving requests in a different michael@0: // order. To keep things in order we generate a promise for each michael@0: // request, chained to the promise for the request before it. michael@0: // This map stores the latest request promise in the chain, keyed michael@0: // by an actor ID string. michael@0: this._actorResponses = new Map; michael@0: michael@0: /* michael@0: * We can forward packets to other servers, if the actors on that server michael@0: * all use a distinct prefix on their names. This is a map from prefixes michael@0: * to transports: it maps a prefix P to a transport T if T conveys michael@0: * packets to the server whose actors' names all begin with P + ":". michael@0: */ michael@0: this._forwardingPrefixes = new Map; michael@0: } michael@0: michael@0: DebuggerServerConnection.prototype = { michael@0: _prefix: null, michael@0: get prefix() { return this._prefix }, michael@0: michael@0: _transport: null, michael@0: get transport() { return this._transport }, michael@0: michael@0: close: function() { michael@0: this._transport.close(); michael@0: }, michael@0: michael@0: send: function DSC_send(aPacket) { michael@0: this.transport.send(aPacket); michael@0: }, michael@0: michael@0: allocID: function DSC_allocID(aPrefix) { michael@0: return this.prefix + (aPrefix || '') + this._nextID++; michael@0: }, michael@0: michael@0: /** michael@0: * Add a map of actor IDs to the connection. michael@0: */ michael@0: addActorPool: function DSC_addActorPool(aActorPool) { michael@0: this._extraPools.push(aActorPool); michael@0: }, michael@0: michael@0: /** michael@0: * Remove a previously-added pool of actors to the connection. michael@0: * michael@0: * @param ActorPool aActorPool michael@0: * The ActorPool instance you want to remove. michael@0: * @param boolean aNoCleanup [optional] michael@0: * True if you don't want to disconnect each actor from the pool, false michael@0: * otherwise. michael@0: */ michael@0: removeActorPool: function DSC_removeActorPool(aActorPool, aNoCleanup) { michael@0: let index = this._extraPools.lastIndexOf(aActorPool); michael@0: if (index > -1) { michael@0: let pool = this._extraPools.splice(index, 1); michael@0: if (!aNoCleanup) { michael@0: pool.map(function(p) { p.cleanup(); }); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add an actor to the default actor pool for this connection. michael@0: */ michael@0: addActor: function DSC_addActor(aActor) { michael@0: this._actorPool.addActor(aActor); michael@0: }, michael@0: michael@0: /** michael@0: * Remove an actor to the default actor pool for this connection. michael@0: */ michael@0: removeActor: function DSC_removeActor(aActor) { michael@0: this._actorPool.removeActor(aActor); michael@0: }, michael@0: michael@0: /** michael@0: * Match the api expected by the protocol library. michael@0: */ michael@0: unmanage: function(aActor) { michael@0: return this.removeActor(aActor); michael@0: }, michael@0: michael@0: /** michael@0: * Look up an actor implementation for an actorID. Will search michael@0: * all the actor pools registered with the connection. michael@0: * michael@0: * @param aActorID string michael@0: * Actor ID to look up. michael@0: */ michael@0: getActor: function DSC_getActor(aActorID) { michael@0: let pool = this.poolFor(aActorID); michael@0: if (pool) { michael@0: return pool.get(aActorID); michael@0: } michael@0: michael@0: if (aActorID === "root") { michael@0: return this.rootActor; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: poolFor: function DSC_actorPool(aActorID) { michael@0: if (this._actorPool && this._actorPool.has(aActorID)) { michael@0: return this._actorPool; michael@0: } michael@0: michael@0: for (let pool of this._extraPools) { michael@0: if (pool.has(aActorID)) { michael@0: return pool; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: _unknownError: function DSC__unknownError(aPrefix, aError) { michael@0: let errorString = aPrefix + ": " + DevToolsUtils.safeErrorString(aError); michael@0: Cu.reportError(errorString); michael@0: dumpn(errorString); michael@0: return { michael@0: error: "unknownError", michael@0: message: errorString michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Passes a set of options to the BrowserAddonActors for the given ID. michael@0: * michael@0: * @param aId string michael@0: * The ID of the add-on to pass the options to michael@0: * @param aOptions object michael@0: * The options. michael@0: * @return a promise that will be resolved when complete. michael@0: */ michael@0: setAddonOptions: function DSC_setAddonOptions(aId, aOptions) { michael@0: let addonList = this.rootActor._parameters.addonList; michael@0: if (!addonList) { michael@0: return resolve(); michael@0: } michael@0: return addonList.getList().then((addonActors) => { michael@0: for (let actor of addonActors) { michael@0: if (actor.id != aId) { michael@0: continue; michael@0: } michael@0: actor.setOptions(aOptions); michael@0: return; michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /* Forwarding packets to other transports based on actor name prefixes. */ michael@0: michael@0: /* michael@0: * Arrange to forward packets to another server. This is how we michael@0: * forward debugging connections to child processes. michael@0: * michael@0: * If we receive a packet for an actor whose name begins with |aPrefix| michael@0: * followed by ':', then we will forward that packet to |aTransport|. michael@0: * michael@0: * This overrides any prior forwarding for |aPrefix|. michael@0: * michael@0: * @param aPrefix string michael@0: * The actor name prefix, not including the ':'. michael@0: * @param aTransport object michael@0: * A packet transport to which we should forward packets to actors michael@0: * whose names begin with |(aPrefix + ':').| michael@0: */ michael@0: setForwarding: function(aPrefix, aTransport) { michael@0: this._forwardingPrefixes.set(aPrefix, aTransport); michael@0: }, michael@0: michael@0: /* michael@0: * Stop forwarding messages to actors whose names begin with michael@0: * |aPrefix+':'|. Such messages will now elicit 'noSuchActor' errors. michael@0: */ michael@0: cancelForwarding: function(aPrefix) { michael@0: this._forwardingPrefixes.delete(aPrefix); michael@0: }, michael@0: michael@0: // Transport hooks. michael@0: michael@0: /** michael@0: * Called by DebuggerTransport to dispatch incoming packets as appropriate. michael@0: * michael@0: * @param aPacket object michael@0: * The incoming packet. michael@0: */ michael@0: onPacket: function DSC_onPacket(aPacket) { michael@0: // If the actor's name begins with a prefix we've been asked to michael@0: // forward, do so. michael@0: // michael@0: // Note that the presence of a prefix alone doesn't indicate that michael@0: // forwarding is needed: in DebuggerServerConnection instances in child michael@0: // processes, every actor has a prefixed name. michael@0: if (this._forwardingPrefixes.size > 0) { michael@0: let colon = aPacket.to.indexOf(':'); michael@0: if (colon >= 0) { michael@0: let forwardTo = this._forwardingPrefixes.get(aPacket.to.substring(0, colon)); michael@0: if (forwardTo) { michael@0: forwardTo.send(aPacket); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let actor = this.getActor(aPacket.to); michael@0: if (!actor) { michael@0: this.transport.send({ from: aPacket.to ? aPacket.to : "root", michael@0: error: "noSuchActor", michael@0: message: "No such actor for ID: " + aPacket.to }); michael@0: return; michael@0: } michael@0: michael@0: // Dyamically-loaded actors have to be created lazily. michael@0: if (typeof actor == "function") { michael@0: let instance; michael@0: try { michael@0: instance = new actor(); michael@0: } catch (e) { michael@0: this.transport.send(this._unknownError( michael@0: "Error occurred while creating actor '" + actor.name, michael@0: e)); michael@0: } michael@0: instance.parentID = actor.parentID; michael@0: // We want the newly-constructed actor to completely replace the factory michael@0: // actor. Reusing the existing actor ID will make sure ActorPool.addActor michael@0: // does the right thing. michael@0: instance.actorID = actor.actorID; michael@0: actor.registeredPool.addActor(instance); michael@0: actor = instance; michael@0: } michael@0: michael@0: var ret = null; michael@0: michael@0: // handle "requestTypes" RDP request. michael@0: if (aPacket.type == "requestTypes") { michael@0: ret = { from: actor.actorID, requestTypes: Object.keys(actor.requestTypes) }; michael@0: } else if (actor.requestTypes && actor.requestTypes[aPacket.type]) { michael@0: // Dispatch the request to the actor. michael@0: try { michael@0: this.currentPacket = aPacket; michael@0: ret = actor.requestTypes[aPacket.type].bind(actor)(aPacket, this); michael@0: } catch(e) { michael@0: this.transport.send(this._unknownError( michael@0: "error occurred while processing '" + aPacket.type, michael@0: e)); michael@0: } finally { michael@0: this.currentPacket = undefined; michael@0: } michael@0: } else { michael@0: ret = { error: "unrecognizedPacketType", michael@0: message: ('Actor "' + actor.actorID + michael@0: '" does not recognize the packet type "' + michael@0: aPacket.type + '"') }; michael@0: } michael@0: michael@0: if (!ret) { michael@0: // This should become an error once we've converted every user michael@0: // of this to promises in bug 794078. michael@0: return; michael@0: } michael@0: michael@0: let pendingResponse = this._actorResponses.get(actor.actorID) || resolve(null); michael@0: let response = pendingResponse.then(() => { michael@0: return ret; michael@0: }).then(aResponse => { michael@0: if (!aResponse.from) { michael@0: aResponse.from = aPacket.to; michael@0: } michael@0: this.transport.send(aResponse); michael@0: }).then(null, (e) => { michael@0: let errorPacket = this._unknownError( michael@0: "error occurred while processing '" + aPacket.type, michael@0: e); michael@0: errorPacket.from = aPacket.to; michael@0: this.transport.send(errorPacket); michael@0: }); michael@0: michael@0: this._actorResponses.set(actor.actorID, response); michael@0: }, michael@0: michael@0: /** michael@0: * Called by DebuggerTransport when the underlying stream is closed. michael@0: * michael@0: * @param aStatus nsresult michael@0: * The status code that corresponds to the reason for closing michael@0: * the stream. michael@0: */ michael@0: onClosed: function DSC_onClosed(aStatus) { michael@0: dumpn("Cleaning up connection."); michael@0: if (!this._actorPool) { michael@0: // Ignore this call if the connection is already closed. michael@0: return; michael@0: } michael@0: events.emit(this, "closed", aStatus); michael@0: michael@0: this._actorPool.cleanup(); michael@0: this._actorPool = null; michael@0: this._extraPools.map(function(p) { p.cleanup(); }); michael@0: this._extraPools = null; michael@0: michael@0: DebuggerServer._connectionClosed(this); michael@0: }, michael@0: michael@0: /* michael@0: * Debugging helper for inspecting the state of the actor pools. michael@0: */ michael@0: _dumpPools: function DSC_dumpPools() { michael@0: dumpn("/-------------------- dumping pools:"); michael@0: if (this._actorPool) { michael@0: dumpn("--------------------- actorPool actors: " + michael@0: uneval(Object.keys(this._actorPool._actors))); michael@0: } michael@0: for each (let pool in this._extraPools) michael@0: dumpn("--------------------- extraPool actors: " + michael@0: uneval(Object.keys(pool._actors))); michael@0: }, michael@0: michael@0: /* michael@0: * Debugging helper for inspecting the state of an actor pool. michael@0: */ michael@0: _dumpPool: function DSC_dumpPools(aPool) { michael@0: dumpn("/-------------------- dumping pool:"); michael@0: dumpn("--------------------- actorPool actors: " + michael@0: uneval(Object.keys(aPool._actors))); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Localization convenience methods. michael@0: */ michael@0: let L10N = { michael@0: michael@0: /** michael@0: * L10N shortcut function. michael@0: * michael@0: * @param string aName michael@0: * @return string michael@0: */ michael@0: getStr: function L10N_getStr(aName) { michael@0: return this.stringBundle.GetStringFromName(aName); michael@0: } michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() { michael@0: return Services.strings.createBundle(DBG_STRINGS_URI); michael@0: });