Bug 993029: Add a way for the add-on manager to pass add-on globals to the debug actors. r=panos, r=Unfocused
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 17 Apr 2014 08:23:42 -0700
changeset 197559 5a9d701731a1143118ad6258517545ab17615029
parent 197558 36bc9e63aba710eb6b0c4ac72553d04259c073c6
child 197560 67c1405cd110853a2664fe3365158cfd9648c17b
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspanos, Unfocused
bugs993029
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 993029: Add a way for the add-on manager to pass add-on globals to the debug actors. r=panos, r=Unfocused
b2g/chrome/content/shell.js
browser/devtools/framework/ToolboxProcess.jsm
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/main.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1162,19 +1162,19 @@ let RemoteDebugger = {
           } : DebuggerServer.globalActorFactories
         };
         let root = new DebuggerServer.RootActor(connection, parameters);
         root.applicationType = "operating-system";
         return root;
       };
 
 #ifdef MOZ_WIDGET_GONK
-      DebuggerServer.onConnectionChange = function(what) {
+      DebuggerServer.on("connectionchange", function() {
         AdbController.updateState();
-      }
+      });
 #endif
     }
 
     let path = Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
                "/data/local/debugger-socket";
     try {
       DebuggerServer.openListener(path);
       // Temporary event, until bug 942756 lands and offers a way to know
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -11,59 +11,104 @@ const DBG_XUL = "chrome://browser/conten
 const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 let require = devtools.require;
 let Telemetry = require("devtools/shared/telemetry");
+let EventEmitter = require("devtools/toolkit/event-emitter");
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
 
+let processes = Set();
+
 /**
  * Constructor for creating a process that will hold a chrome toolbox.
  *
  * @param function aOnClose [optional]
  *        A function called when the process stops running.
  * @param function aOnRun [optional]
  *        A function called when the process starts running.
  * @param object aOptions [optional]
  *        An object with properties for configuring BrowserToolboxProcess.
  */
 this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+  let emitter = new EventEmitter();
+  this.on = emitter.on.bind(emitter);
+  this.off = emitter.off.bind(emitter);
+  this.once = emitter.once.bind(emitter);
+  // Forward any events to the shared emitter.
+  this.emit = function(...args) {
+    emitter.emit(...args);
+    BrowserToolboxProcess.emit(...args);
+  }
+
   // If first argument is an object, use those properties instead of
   // all three arguments
   if (typeof aOnClose === "object") {
-    this._closeCallback = aOnClose.onClose;
-    this._runCallback = aOnClose.onRun;
+    if (aOnClose.onClose) {
+      this.on("close", aOnClose.onClose);
+    }
+    if (aOnClose.onRun) {
+      this.on("run", aOnClose.onRun);
+    }
     this._options = aOnClose;
   } else {
-    this._closeCallback = aOnClose;
-    this._runCallback = aOnRun;
+    if (aOnClose) {
+      this.on("close", aOnClose);
+    }
+    if (aOnRun) {
+      this.on("run", aOnRun);
+    }
     this._options = aOptions || {};
   }
 
   this._telemetry = new Telemetry();
 
   this.close = this.close.bind(this);
   Services.obs.addObserver(this.close, "quit-application", false);
   this._initServer();
   this._initProfile();
   this._create();
+
+  processes.add(this);
 };
 
+EventEmitter.decorate(BrowserToolboxProcess);
+
 /**
  * Initializes and starts a chrome toolbox process.
  * @return object
  */
 BrowserToolboxProcess.init = function(aOnClose, aOnRun, aOptions) {
   return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
 };
 
+/**
+ * Passes a set of options to the BrowserAddonActors for the given ID.
+ *
+ * @param aId string
+ *        The ID of the add-on to pass the options to
+ * @param aOptions object
+ *        The options.
+ * @return a promise that will be resolved when complete.
+ */
+BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
+  let promises = [];
+
+  for (let process of processes.values()) {
+    promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
+  }
+
+  return promise.all(promises);
+};
+
 BrowserToolboxProcess.prototype = {
   /**
    * Initializes the debugger server.
    */
   _initServer: function() {
     dumpn("Initializing the chrome toolbox server.");
 
     if (!this.loader) {
@@ -72,16 +117,19 @@ BrowserToolboxProcess.prototype = {
       // This allows us to safely use the tools against even the actors and
       // DebuggingServer itself, especially since we can mark this loader as
       // invisible to the debugger (unlike the usual loader settings).
       this.loader = new DevToolsLoader();
       this.loader.invisibleToDebugger = true;
       this.loader.main("devtools/server/main");
       this.debuggerServer = this.loader.DebuggerServer;
       dumpn("Created a separate loader instance for the DebuggerServer.");
+
+      // Forward interesting events.
+      this.debuggerServer.on("connectionchange", this.emit.bind(this));
     }
 
     if (!this.debuggerServer.initialized) {
       this.debuggerServer.init();
       this.debuggerServer.addBrowserActors();
       dumpn("initialized and added the browser actors for the DebuggerServer.");
     }
 
@@ -164,19 +212,17 @@ BrowserToolboxProcess.prototype = {
     dumpn("Running chrome debugging process.");
     let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", xulURI];
 
     process.runwAsync(args, args.length, { observe: () => this.close() });
 
     this._telemetry.toolOpened("jsbrowserdebugger");
 
     dumpn("Chrome toolbox is now running...");
-    if (typeof this._runCallback == "function") {
-      this._runCallback.call({}, this);
-    }
+    this.emit("run", this);
   },
 
   /**
    * Closes the remote debugging server and kills the toolbox process.
    */
   close: function() {
     if (this.closed) {
       return;
@@ -191,19 +237,18 @@ BrowserToolboxProcess.prototype = {
 
     this._telemetry.toolClosed("jsbrowserdebugger");
     if (this.debuggerServer) {
       this.debuggerServer.destroy();
     }
 
     dumpn("Chrome toolbox is now closed...");
     this.closed = true;
-    if (typeof this._closeCallback == "function") {
-      this._closeCallback.call({}, this);
-    }
+    this.emit("close", this);
+    processes.delete(this);
   }
 };
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
   chromeDebuggingHost: ["Char", "chrome-debugging-host"],
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1108,16 +1108,17 @@ BrowserAddonList.prototype.onUninstalled
   this._onListChanged();
 };
 
 function BrowserAddonActor(aConnection, aAddon) {
   this.conn = aConnection;
   this._addon = aAddon;
   this._contextPool = null;
   this._threadActor = null;
+  this._global = null;
   AddonManager.addAddonListener(this);
 }
 
 BrowserAddonActor.prototype = {
   actorPrefix: "addon",
 
   get exited() {
     return !this._addon;
@@ -1130,42 +1131,62 @@ BrowserAddonActor.prototype = {
   get url() {
     return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
   },
 
   get attached() {
     return this._threadActor;
   },
 
+  get global() {
+    return this._global;
+  },
+
   form: function BAA_form() {
     dbg_assert(this.actorID, "addon should have an actorID.");
 
     return {
       actor: this.actorID,
       id: this.id,
       name: this._addon.name,
       url: this.url
     };
   },
 
   disconnect: function BAA_disconnect() {
+    this._addon = null;
+    this._global = null;
     AddonManager.removeAddonListener(this);
   },
 
+  setOptions: function BAA_setOptions(aOptions) {
+    if ("global" in aOptions) {
+      this._global = aOptions.global;
+    }
+  },
+
+  onDisabled: function BAA_onDisabled(aAddon) {
+    if (aAddon != this._addon) {
+      return;
+    }
+
+    this._global = null;
+  },
+
   onUninstalled: function BAA_onUninstalled(aAddon) {
-    if (aAddon != this._addon)
+    if (aAddon != this._addon) {
       return;
+    }
 
     if (this.attached) {
       this.onDetach();
       this.conn.send({ from: this.actorID, type: "tabDetached" });
     }
 
-    this._addon = null;
-    AddonManager.removeAddonListener(this);
+    this.disconnect();
   },
 
   onAttach: function BAA_onAttach() {
     if (this.exited) {
       return { type: "exited" };
     }
 
     if (!this.attached) {
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -6,16 +6,17 @@
 
 "use strict";
 /**
  * Toolkit glue for the remote debugging protocol, loaded into the
  * debugging global.
  */
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
 let Services = require("Services");
+let EventEmitter = require("devtools/toolkit/event-emitter");
 
 // Until all Debugger server code is converted to SDK modules,
 // imports Components.* alias from chrome module.
 var { Ci, Cc, CC, Cu, Cr } = require("chrome");
 // On B2G, `this` != Global scope, so `Ci` won't be binded on `this`
 // (i.e. this.Ci is undefined) Then later, when using loadSubScript,
 // Ci,... won't be defined for sub scripts.
 this.Ci = Ci;
@@ -180,29 +181,16 @@ var DebuggerServer = {
   /**
    * 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: null,
 
   /**
-   * Set that to a function that will be called anytime a new connection
-   * is opened or one is closed.
-   */
-  onConnectionChange: null,
-
-  _fireConnectionChange: function(aWhat) {
-    if (this.onConnectionChange &&
-        typeof this.onConnectionChange === "function") {
-      this.onConnectionChange(aWhat);
-    }
-  },
-
-  /**
    * Prompt the user to accept or decline the incoming connection. This is the
    * default implementation that products embedding the debugger server may
    * choose to override.
    *
    * @return true if the connection should be permitted, false otherwise
    */
   _defaultAllowConnection: function DS__defaultAllowConnection() {
     let title = L10N.getStr("remoteIncomingPromptTitle");
@@ -291,18 +279,16 @@ var DebuggerServer = {
 
     this.closeListener();
     this.globalActorFactories = {};
     this.tabActorFactories = {};
     this._allowConnection = null;
     this._transportInitialized = false;
     this._initialized = false;
 
-    this._fireConnectionChange("closed");
-
     dumpn("Debugger server is shut down.");
   },
 
   /**
    * Load a subscript into the debugging global.
    *
    * @param aURL string A url that will be loaded as a subscript into the
    *        debugging global.  The user must load at least one script
@@ -412,16 +398,40 @@ var DebuggerServer = {
     this.registerModule("devtools/server/actors/memory");
     this.registerModule("devtools/server/actors/eventlooplag");
     if ("nsIProfiler" in Ci) {
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
     }
   },
 
   /**
+   * Passes a set of options to the BrowserAddonActors for the given ID.
+   *
+   * @param aId string
+   *        The ID of the add-on to pass the options to
+   * @param aOptions object
+   *        The options.
+   * @return a promise that will be resolved when complete.
+   */
+  setAddonOptions: function DS_setAddonOptions(aId, aOptions) {
+    if (!this._initialized) {
+      return;
+    }
+
+    let promises = [];
+
+    // Pass to all connections
+    for (let connID of Object.getOwnPropertyNames(this._connections)) {
+      promises.push(this._connections[connID].setAddonOptions(aId, aOptions));
+    }
+
+    return all(promises);
+  },
+
+  /**
    * Listens on the given port or socket file for remote debugger connections.
    *
    * @param aPortOrPath int, string
    *        If given an integer, the port to listen on.
    *        Otherwise, the path to the unix socket domain file to listen on.
    */
   openListener: function DS_openListener(aPortOrPath) {
     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
@@ -703,26 +713,26 @@ var DebuggerServer = {
         conn.rootActor.actorID = aForwardingPrefix + ":root";
       else
         conn.rootActor.actorID = "root";
       conn.addActor(conn.rootActor);
       aTransport.send(conn.rootActor.sayHello());
     }
     aTransport.ready();
 
-    this._fireConnectionChange("opened");
+    this.emit("connectionchange", "opened", conn);
     return conn;
   },
 
   /**
    * Remove the connection from the debugging server.
    */
   _connectionClosed: function DS_connectionClosed(aConnection) {
     delete this._connections[aConnection.prefix];
-    this._fireConnectionChange("closed");
+    this.emit("connectionchange", "closed", aConnection);
   },
 
   // DebuggerServer extension API.
 
   /**
    * Registers handlers for new tab-scoped request types defined dynamically.
    * This is used for example by add-ons to augment the functionality of the tab
    * actor. Note that the name or actorPrefix of the request type is not allowed
@@ -807,16 +817,18 @@ var DebuggerServer = {
       let handler = DebuggerServer.globalActorFactories[name];
       if (handler.name == aFunction.name) {
         delete DebuggerServer.globalActorFactories[name];
       }
     }
   }
 };
 
+EventEmitter.decorate(DebuggerServer);
+
 if (this.exports) {
   exports.DebuggerServer = DebuggerServer;
 }
 // Needed on B2G (See header note)
 this.DebuggerServer = DebuggerServer;
 
 /**
  * Construct an ActorPool.
@@ -1056,16 +1068,41 @@ DebuggerServerConnection.prototype = {
     Cu.reportError(errorString);
     dumpn(errorString);
     return {
       error: "unknownError",
       message: errorString
     };
   },
 
+  /**
+   * Passes a set of options to the BrowserAddonActors for the given ID.
+   *
+   * @param aId string
+   *        The ID of the add-on to pass the options to
+   * @param aOptions object
+   *        The options.
+   * @return a promise that will be resolved when complete.
+   */
+  setAddonOptions: function DSC_setAddonOptions(aId, aOptions) {
+    let addonList = this.rootActor._parameters.addonList;
+    if (!addonList) {
+      return resolve();
+    }
+    return addonList.getList().then((addonActors) => {
+      for (let actor of addonActors) {
+        if (actor.id != aId) {
+          continue;
+        }
+        actor.setOptions(aOptions);
+        return;
+      }
+    });
+  },
+
   /* Forwarding packets to other transports based on actor name prefixes. */
 
   /*
    * Arrange to forward packets to another server. This is how we
    * forward debugging connections to child processes.
    *
    * If we receive a packet for an actor whose name begins with |aPrefix|
    * followed by ':', then we will forward that packet to |aTransport|.
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -30,16 +30,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                   "resource://gre/modules/PermissionsUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
+                                  "resource:///modules/devtools/ToolboxProcess.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ChromeRegistry",
                                    "@mozilla.org/chrome/chrome-registry;1",
                                    "nsIChromeRegistry");
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ResProtocolHandler",
                                    "@mozilla.org/network/protocol;1?name=resource",
@@ -1865,16 +1867,24 @@ this.XPIProvider = {
       this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                             null);
       this.enabledAddons = "";
 
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
       Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
 
+      try {
+        BrowserToolboxProcess.on("connectionchange",
+                                 this.onDebugConnectionChange.bind(this));
+      }
+      catch (e) {
+        // BrowserToolboxProcess is not available in all applications
+      }
+
       let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
                                              aOldPlatformVersion);
 
       // Changes to installed extensions may have changed which theme is selected
       this.applyThemeChange();
 
       // If the application has been upgraded and there are add-ons outside the
       // application directory then we may need to synchronize compatibility
@@ -3850,16 +3860,25 @@ this.XPIProvider = {
         logger.warn("Attempting to activate an already active default theme");
       }
     }
     else {
       logger.warn("Unable to activate the default theme");
     }
   },
 
+  onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
+    if (aWhat != "opened")
+      return;
+
+    for (let id of Object.keys(this.bootstrapScopes)) {
+      aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
+    }
+  },
+
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
   observe: function XPI_observe(aSubject, aTopic, aData) {
     if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
       if (!aData || aData == XPI_PERMISSION) {
@@ -4114,30 +4133,44 @@ this.XPIProvider = {
       Components.utils.evalInSandbox(
         "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
                    .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
                    .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
     }
     catch (e) {
       logger.warn("Error loading bootstrap.js for " + aId, e);
     }
+
+    try {
+      BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
+    }
+    catch (e) {
+      // BrowserToolboxProcess is not available in all applications
+    }
   },
 
   /**
    * Unloads a bootstrap scope by dropping all references to it and then
    * updating the list of active add-ons with the crash reporter.
    *
    * @param  aId
    *         The add-on's ID
    */
   unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
     delete this.bootstrapScopes[aId];
     delete this.bootstrappedAddons[aId];
     this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
+
+    try {
+      BrowserToolboxProcess.setAddonOptions(aId, { global: null });
+    }
+    catch (e) {
+      // BrowserToolboxProcess is not available in all applications
+    }
   },
 
   /**
    * Calls a bootstrap method for an add-on.
    *
    * @param  aId
    *         The ID of the add-on
    * @param  aVersion