Bug 753401 - The debugger server root and tab actors should be easily extensible; r=rcampbell
authorPanos Astithas <past@mozilla.com>
Thu, 20 Sep 2012 09:36:32 +0300
changeset 107594 fc935d3901cd0ad8c2dc35d4932b6fcaa78b269c
parent 107593 47e7a44f9c9123cad29e0d63398ec08504e97071
child 107595 642f820edff86c51f7ca2002181ee63c15c7cdde
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersrcampbell
bugs753401
milestone18.0a1
Bug 753401 - The debugger server root and tab actors should be easily extensible; r=rcampbell
browser/devtools/debugger/DebuggerUI.jsm
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_contextactor-01.js
browser/devtools/debugger/test/browser_dbg_contextactor-02.js
browser/devtools/debugger/test/browser_dbg_globalactor-01.js
browser/devtools/debugger/test/browser_dbg_tabactor-01.js
browser/devtools/debugger/test/browser_dbg_tabactor-02.js
browser/devtools/debugger/test/head.js
browser/devtools/debugger/test/testactors.js
toolkit/devtools/debugger/server/dbg-browser-actors.js
toolkit/devtools/debugger/server/dbg-server.js
toolkit/devtools/debugger/tests/unit/test_dbgsocket.js
--- a/browser/devtools/debugger/DebuggerUI.jsm
+++ b/browser/devtools/debugger/DebuggerUI.jsm
@@ -464,17 +464,16 @@ ChromeDebuggerProcess.prototype = {
   /**
    * Initializes the debugger server.
    */
   _initServer: function RDP__initServer() {
     if (!DebuggerServer.initialized) {
       DebuggerServer.init(this._allowConnection);
       DebuggerServer.addBrowserActors();
     }
-    DebuggerServer.closeListener();
     DebuggerServer.openListener(DebuggerPreferences.remotePort);
   },
 
   /**
    * Prompt the user to accept or decline the incoming connection.
    *
    * @return true if the connection should be permitted, false otherwise
    */
@@ -488,17 +487,17 @@ ChromeDebuggerProcess.prototype = {
                 prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING +
                 prompt.BUTTON_POS_1_DEFAULT;
     let result = prompt.confirmEx(null, title, msg, flags, null, null,
                                   disableButton, null, { value: false });
     if (result == 0) {
       return true;
     }
     if (result == 2) {
-      DebuggerServer.closeListener();
+      DebuggerServer.closeListener(true);
       Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
     }
     return false;
   },
 
   /**
    * Initializes a profile for the remote debugger process.
    */
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -15,18 +15,17 @@ MOCHITEST_BROWSER_TESTS = \
 	browser_dbg_createRemote.js \
 	browser_dbg_createChrome.js \
 	browser_dbg_debugger-tab-switch.js \
 	browser_dbg_debugger-tab-switch-window.js \
 	browser_dbg_debuggerstatement.js \
 	browser_dbg_listtabs.js \
 	browser_dbg_tabactor-01.js \
 	browser_dbg_tabactor-02.js \
-	browser_dbg_contextactor-01.js \
-	browser_dbg_contextactor-02.js \
+	browser_dbg_globalactor-01.js \
 	testactors.js \
 	browser_dbg_nav-01.js \
 	browser_dbg_propertyview-01.js \
 	browser_dbg_propertyview-02.js \
 	browser_dbg_propertyview-03.js \
 	browser_dbg_propertyview-04.js \
 	browser_dbg_propertyview-05.js \
 	browser_dbg_propertyview-06.js \
deleted file mode 100644
--- a/browser/devtools/debugger/test/browser_dbg_contextactor-01.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Check extension-added context actor lifetimes.
- */
-
-var gTab1 = null;
-var gTab1Actor = null;
-
-var gClient = null;
-
-function test()
-{
-  DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
-
-  let transport = DebuggerServer.connectPipe();
-  gClient = new DebuggerClient(transport);
-  gClient.connect(function(aType, aTraits) {
-    is(aType, "browser", "Root actor should identify itself as a browser.");
-    get_tab();
-  });
-}
-
-function get_tab()
-{
-  gTab1 = addTab(TAB1_URL, function() {
-    attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
-      gTab1Actor = aGrip.actor;
-      gClient.request({ to: aGrip.actor, type: "testContextActor1" }, function(aResponse) {
-        ok(aResponse.actor, "testContextActor1 request should return an actor.");
-        ok(aResponse.actor.indexOf("testone") >= 0,
-           "testContextActor's actorPrefix should be used.");
-        gClient.request({ to: aResponse.actor, type: "ping" }, function(aResponse) {
-          is(aResponse.pong, "pong", "Actor should response to requests.");
-          finish_test();
-        });
-      });
-    });
-  });
-}
-
-function finish_test()
-{
-  gClient.close(function() {
-    removeTab(gTab1);
-    finish();
-  });
-};
rename from browser/devtools/debugger/test/browser_dbg_contextactor-02.js
rename to browser/devtools/debugger/test/browser_dbg_globalactor-01.js
--- a/browser/devtools/debugger/test/browser_dbg_contextactor-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_globalactor-01.js
@@ -1,59 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Check extension-added context actor lifetimes.
+ * Check extension-added global actor API.
  */
 
-var gTab1 = null;
-var gTab1Actor = null;
-
 var gClient = null;
 
 function test()
 {
   DebuggerServer.addActors("chrome://mochitests/content/browser/browser/devtools/debugger/test/testactors.js");
 
   let transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect(function(aType, aTraits) {
     is(aType, "browser", "Root actor should identify itself as a browser.");
-    get_tab();
-  });
-}
-
-function get_tab()
-{
-  gTab1 = addTab(TAB1_URL, function() {
-    get_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
-      gTab1Actor = aGrip.actor;
-      gClient.request({ to: gTab1Actor, type: "attach" }, function(aResponse) {
-        gClient.request({ to: gTab1Actor, type: "testContextActor1" }, function(aResponse) {
-          navigate_tab(aResponse.actor);
-        });
+    gClient.listTabs(function(aResponse) {
+      let globalActor = aResponse.testGlobalActor1;
+      ok(globalActor, "Found the test tab actor.")
+      ok(globalActor.indexOf("testone") >= 0,
+         "testTabActor's actorPrefix should be used.");
+      gClient.request({ to: globalActor, type: "ping" }, function(aResponse) {
+        is(aResponse.pong, "pong", "Actor should respond to requests.");
+        finish_test();
       });
     });
   });
 }
 
-function navigate_tab(aTestActor)
-{
-  gClient.addOneTimeListener("tabNavigated", function(aEvent, aResponse) {
-    gClient.request({ to: aTestActor, type: "ping" }, function(aResponse) {
-      // TODO: Currently the client is supposed to clean up after tabNavigated
-      // events. We should remove this check, or even better, remove the whole
-      // test.
-      todo(aResponse.error, "noSuchActor", "testContextActor1 should have gone away with the navigation.");
-      finish_test();
-    });
-  });
-  gTab1.linkedBrowser.loadURI(TAB2_URL);
-}
-
 function finish_test()
 {
   gClient.close(function() {
-    removeTab(gTab1);
     finish();
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_tabactor-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_tabactor-01.js
@@ -19,27 +19,25 @@ function test()
   gClient.connect(function (aType, aTraits) {
     is(aType, "browser", "Root actor should identify itself as a browser.");
     get_tab();
   });
 }
 
 function get_tab()
 {
-  gTab1 = addTab(TAB1_URL, function () {
-    attach_tab_actor_for_url(gClient, TAB1_URL, function (aGrip) {
+  gTab1 = addTab(TAB1_URL, function() {
+    attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
       gTab1Actor = aGrip.actor;
-      gClient.request({ to: aGrip.actor, type: "testTabActor1" }, function (aResponse) {
-        ok(aResponse.actor, "testTabActor1 request should return an actor.");
-        ok(aResponse.actor.indexOf("testone") >= 0,
-           "testTabActor's actorPrefix should be used.");
-        gClient.request({ to: aResponse.actor, type: "ping" }, function (aResponse) {
-          is(aResponse.pong, "pong", "Actor should response to requests.");
-          finish_test();
-        });
+      ok(aGrip.testTabActor1, "Found the test tab actor.")
+      ok(aGrip.testTabActor1.indexOf("testone") >= 0,
+         "testTabActor's actorPrefix should be used.");
+      gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
+        is(aResponse.pong, "pong", "Actor should respond to requests.");
+        finish_test();
       });
     });
   });
 }
 
 function finish_test()
 {
   gClient.close(function() {
--- a/browser/devtools/debugger/test/browser_dbg_tabactor-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_tabactor-02.js
@@ -19,33 +19,43 @@ function test()
   gClient.connect(function (aType, aTraits) {
     is(aType, "browser", "Root actor should identify itself as a browser.");
     get_tab();
   });
 }
 
 function get_tab()
 {
-  gTab1 = addTab(TAB1_URL, function () {
-    attach_tab_actor_for_url(gClient, TAB1_URL, function (aGrip) {
+  gTab1 = addTab(TAB1_URL, function() {
+    attach_tab_actor_for_url(gClient, TAB1_URL, function(aGrip) {
       gTab1Actor = aGrip.actor;
-      gClient.request({ to: aGrip.actor, type: "testTabActor1" }, function (aResponse) {
+      ok(aGrip.testTabActor1, "Found the test tab actor.")
+      ok(aGrip.testTabActor1.indexOf("testone") >= 0,
+         "testTabActor's actorPrefix should be used.");
+      gClient.request({ to: aGrip.testTabActor1, type: "ping" }, function(aResponse) {
+        is(aResponse.pong, "pong", "Actor should respond to requests.");
         close_tab(aResponse.actor);
       });
     });
   });
 }
 
 function close_tab(aTestActor)
 {
   removeTab(gTab1);
-  gClient.request({ to: aTestActor, type: "ping" }, function (aResponse) {
-    is(aResponse.error, "noSuchActor", "testTabActor1 should have gone away with the tab.");
+  try {
+    gClient.request({ to: aTestActor, type: "ping" }, function (aResponse) {
+      is(aResponse, undefined, "testTabActor1 didn't go away with the tab.");
+      finish_test();
+    });
+  } catch (e) {
+    is(e.message, "'ping' request packet has no destination.",
+       "testTabActor1 should have gone away with the tab.");
     finish_test();
-  });
+  }
 }
 
 function finish_test()
 {
   gClient.close(function () {
     finish();
   });
 }
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -19,16 +19,19 @@ const EXAMPLE_URL = "http://example.com/
 const TAB1_URL = EXAMPLE_URL + "browser_dbg_tab1.html";
 const TAB2_URL = EXAMPLE_URL + "browser_dbg_tab2.html";
 const STACK_URL = EXAMPLE_URL + "browser_dbg_stack.html";
 // Enable remote debugging for the relevant tests.
 let gEnableRemote = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 registerCleanupFunction(function() {
   Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote);
+
+  // Properly shut down the server to avoid memory leaks.
+  DebuggerServer.destroy();
 });
 
 if (!DebuggerServer.initialized) {
   DebuggerServer.init(function () { return true; });
   DebuggerServer.addBrowserActors();
 }
 
 waitForExplicitFinish();
--- a/browser/devtools/debugger/test/testactors.js
+++ b/browser/devtools/debugger/test/testactors.js
@@ -1,58 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function TestActor1(aConnection, aTab, aOnDisconnect)
+function TestActor1(aConnection, aTab)
 {
   this.conn = aConnection;
   this.tab = aTab;
-  this.onDisconnect = aOnDisconnect;
 }
 
 TestActor1.prototype = {
   actorPrefix: "testone",
 
-  disconnect: function TA1_disconnect() {
-    this.onDisconnect();
-  },
-
   grip: function TA1_grip() {
     return { actor: this.actorID,
              test: "TestActor1" };
   },
 
   onPing: function TA1_onPing() {
     return { pong: "pong" };
   }
 };
 
 TestActor1.prototype.requestTypes = {
   "ping": TestActor1.prototype.onPing
 };
 
-DebuggerServer.addTabRequest("testTabActor1", function (aTab) {
-  if (aTab._testTabActor1) {
-    return aTab._testTabActor1.grip();
-  }
-
-  let actor = new TestActor1(aTab.conn, aTab.browser, function () {
-    delete aTab._testTabActor1;
-  });
-  aTab.tabActorPool.addActor(actor);
-  aTab._testTabActor1 = actor;
-  return actor.grip();
-});
-
+DebuggerServer.removeTabActor(TestActor1);
+DebuggerServer.removeGlobalActor(TestActor1);
 
-DebuggerServer.addTabRequest("testContextActor1", function (aTab, aRequest) {
-  if (aTab._testContextActor1) {
-    return aTab._testContextActor1.grip();
-  }
-
-  let actor = new TestActor1(aTab.conn, aTab.browser, function () {
-    delete aTab._testContextActor1;
-  });
-  aTab.contextActorPool.addActor(actor);
-  aTab._testContextActor1 = actor;
-  return actor.grip();
-});
-
+DebuggerServer.addTabActor(TestActor1, "testTabActor1");
+DebuggerServer.addGlobalActor(TestActor1, "testGlobalActor1");
--- a/toolkit/devtools/debugger/server/dbg-browser-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-browser-actors.js
@@ -26,37 +26,42 @@ function createRootActor(aConnection)
  * @param aConnection DebuggerServerConnection
  *        The conection to the client.
  */
 function BrowserRootActor(aConnection)
 {
   this.conn = aConnection;
   this._tabActors = new WeakMap();
   this._tabActorPool = null;
-  this._actorFactories = null;
+  // A map of actor names to actor instances provided by extensions.
+  this._extraActors = {};
 
   this.onTabClosed = this.onTabClosed.bind(this);
   windowMediator.addListener(this);
 }
 
 BrowserRootActor.prototype = {
+
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function BRA_sayHello() {
-    return { from: "root",
-             applicationType: "browser",
-             traits: [] };
+    return {
+      from: "root",
+      applicationType: "browser",
+      traits: {}
+    };
   },
 
   /**
    * Disconnects the actor from the browser window.
    */
   disconnect: function BRA_disconnect() {
     windowMediator.removeListener(this);
+    this._extraActors = null;
 
     // We may have registered event listeners on browser windows to
     // watch for tab closes, remove those.
     let e = windowMediator.getEnumerator("navigator:browser");
     while (e.hasMoreElements()) {
       let win = e.getNext();
       this.unwatchWindow(win);
       // Signal our imminent shutdown.
@@ -72,17 +77,17 @@ BrowserRootActor.prototype = {
    * until at least the next listTabs request.
    */
   onListTabs: function BRA_onListTabs() {
     // Get actors for all the currently-running tabs (reusing
     // existing actors where applicable), and store them in
     // an ActorPool.
 
     let actorPool = new ActorPool(this.conn);
-    let actorList = [];
+    let tabActorList = [];
 
     // Walk over open browser windows.
     let e = windowMediator.getEnumerator("navigator:browser");
     let top = windowMediator.getMostRecentWindow("navigator:browser");
     let selected;
     while (e.hasMoreElements()) {
       let win = e.getNext();
 
@@ -90,42 +95,60 @@ BrowserRootActor.prototype = {
       // actors as needed.
       this.watchWindow(win);
 
       // List the tabs in this browser.
       let selectedBrowser = win.getBrowser().selectedBrowser;
       let browsers = win.getBrowser().browsers;
       for each (let browser in browsers) {
         if (browser == selectedBrowser && win == top) {
-          selected = actorList.length;
+          selected = tabActorList.length;
         }
         let actor = this._tabActors.get(browser);
         if (!actor) {
           actor = new BrowserTabActor(this.conn, browser, win.gBrowser);
           actor.parentID = this.actorID;
           this._tabActors.set(browser, actor);
         }
         actorPool.addActor(actor);
-        actorList.push(actor);
+        tabActorList.push(actor);
       }
     }
 
+    // Walk over global actors added by extensions.
+    for (let name in DebuggerServer.globalActorFactories) {
+      let actor = this._extraActors[name];
+      if (!actor) {
+        actor = DebuggerServer.globalActorFactories[name].bind(null, this.conn);
+        actor.prototype = DebuggerServer.globalActorFactories[name].prototype;
+        actor.parentID = this.actorID;
+        this._extraActors[name] = actor;
+      }
+      actorPool.addActor(actor);
+    }
+
     // Now drop the old actorID -> actor map.  Actors that still
     // mattered were added to the new map, others will go
     // away.
     if (this._tabActorPool) {
       this.conn.removeActorPool(this._tabActorPool);
     }
     this._tabActorPool = actorPool;
     this.conn.addActorPool(this._tabActorPool);
 
-    return { "from": "root",
-             "selected": selected,
-             "tabs": [actor.grip()
-                      for each (actor in actorList)] };
+    let response = {
+      "from": "root",
+      "selected": selected,
+      "tabs": [actor.grip() for (actor of tabActorList)]
+    };
+    for (let name in this._extraActors) {
+      let actor = this._extraActors[name];
+      response[name] = actor.actorID;
+    }
+    return response;
   },
 
   /**
    * Watch a window that was visited during onListTabs for
    * tab closures.
    */
   watchWindow: function BRA_watchWindow(aWindow) {
     this.getTabContainer(aWindow).addEventListener("TabClose",
@@ -198,16 +221,19 @@ BrowserRootActor.prototype.requestTypes 
  * @param aTabBrowser tabbrowser
  *        The tabbrowser that can receive nsIWebProgressListener events.
  */
 function BrowserTabActor(aConnection, aBrowser, aTabBrowser)
 {
   this.conn = aConnection;
   this._browser = aBrowser;
   this._tabbrowser = aTabBrowser;
+  this._tabActorPool = null;
+  // A map of actor names to actor instances provided by extensions.
+  this._extraActors = {};
 
   this._onWindowCreated = this.onWindowCreated.bind(this);
 }
 
 // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 BrowserTabActor.prototype = {
@@ -240,37 +266,65 @@ BrowserTabActor.prototype = {
    *
    * @param string aActor
    *        The actor ID.
    */
   removeFromBreakpointPool: function BTA_removeFromBreakpointPool(aActor) {
     this.conn.removeActor(aActor);
   },
 
+  // A constant prefix that will be used to form the actor ID by the server.
   actorPrefix: "tab",
 
   grip: function BTA_grip() {
     dbg_assert(!this.exited,
                "grip() shouldn't be called on exited browser actor.");
     dbg_assert(this.actorID,
                "tab should have an actorID.");
-    return { actor: this.actorID,
-             title: this.browser.contentTitle,
-             url: this.browser.currentURI.spec }
+
+    let response = {
+      actor: this.actorID,
+      title: this.browser.contentTitle,
+      url: this.browser.currentURI.spec
+    };
+
+    // Walk over tab actors added by extensions and add them to a new ActorPool.
+    let actorPool = new ActorPool(this.conn);
+    for (let name in DebuggerServer.tabActorFactories) {
+      let actor = this._extraActors[name];
+      if (!actor) {
+        actor = DebuggerServer.tabActorFactories[name].bind(null, this.conn);
+        actor.prototype = DebuggerServer.tabActorFactories[name].prototype;
+        actor.parentID = this.actorID;
+        this._extraActors[name] = actor;
+      }
+      actorPool.addActor(actor);
+    }
+    if (!actorPool.isEmpty()) {
+      this._tabActorPool = actorPool;
+      this.conn.addActorPool(this._tabActorPool);
+    }
+
+    for (let name in this._extraActors) {
+      let actor = this._extraActors[name];
+      response[name] = actor.actorID;
+    }
+    return response;
   },
 
   /**
    * Called when the actor is removed from the connection.
    */
   disconnect: function BTA_disconnect() {
     this._detach();
 
     if (this._progressListener) {
       this._progressListener.destroy();
     }
+    this._extraActors = null;
   },
 
   /**
    * Called by the root actor when the underlying tab is closed.
    */
   exit: function BTA_exit() {
     if (this.exited) {
       return;
@@ -365,16 +419,20 @@ BrowserTabActor.prototype = {
     this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
     this.browser.removeEventListener("pageshow", this._onWindowCreated, true);
 
     this._popContext();
 
     // Shut down actors that belong to this tab's pool.
     this.conn.removeActorPool(this._tabPool);
     this._tabPool = null;
+    if (this._tabActorPool) {
+      this.conn.removeActorPool(this._tabActorPool);
+      this._tabActorPool = null;
+    }
 
     this._attached = false;
   },
 
   // Protocol Request Handlers
 
   onAttach: function BTA_onAttach(aRequest) {
     if (this.exited) {
@@ -530,25 +588,106 @@ DebuggerProgressListener.prototype = {
    */
   destroy: function DPL_destroy() {
     this._tabActor._tabbrowser.removeProgressListener(this);
     this._tabActor._progressListener = null;
     this._tabActor = null;
   }
 };
 
+// DebuggerServer extension API.
+
 /**
- * Registers handlers for new request types defined dynamically. This is used
- * for example by add-ons to augment the functionality of the tab actor.
+ * 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.
+ * TODO: remove this API in the next release after bug 753401 lands, once all
+ * our experimental add-ons have been converted to the new API.
  *
  * @param aName string
  *        The name of the new request type.
  * @param aFunction function
  *        The handler for this request type.
  */
 DebuggerServer.addTabRequest = function DS_addTabRequest(aName, aFunction) {
   BrowserTabActor.prototype.requestTypes[aName] = function(aRequest) {
     if (!this.attached) {
       return { error: "wrongState" };
     }
     return aFunction(this, aRequest);
   }
 };
+
+/**
+ * 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.
+ *
+ * @param aFunction function
+ *        The constructor function for this request type.
+ * @param aName string [optional]
+ *        The name of the new request type. If this is not present, the
+ *        actorPrefix property of the constructor prototype is used.
+ */
+DebuggerServer.addTabActor = function DS_addTabActor(aFunction, aName) {
+  let name = aName ? aName : aFunction.prototype.actorPrefix;
+  if (["title", "url", "actor"].indexOf(name) != -1) {
+    throw Error(name + " is not allowed");
+  }
+  if (DebuggerServer.tabActorFactories.hasOwnProperty(name)) {
+    throw Error(name + " already exists");
+  }
+  DebuggerServer.tabActorFactories[name] = aFunction;
+};
+
+/**
+ * Unregisters the handler for the specified tab-scoped request type.
+ * This may be used for example by add-ons when shutting down or upgrading.
+ *
+ * @param aFunction function
+ *        The constructor function for this request type.
+ */
+DebuggerServer.removeTabActor = function DS_removeTabActor(aFunction) {
+  for (let name in DebuggerServer.tabActorFactories) {
+    let handler = DebuggerServer.tabActorFactories[name];
+    if (handler.name == aFunction.name) {
+      delete DebuggerServer.tabActorFactories[name];
+    }
+  }
+};
+
+/**
+ * Registers handlers for new browser-scoped request types defined dynamically.
+ * This is used for example by add-ons to augment the functionality of the root
+ * actor.
+ *
+ * @param aFunction function
+ *        The constructor function for this request type.
+ * @param aName string [optional]
+ *        The name of the new request type. If this is not present, the
+ *        actorPrefix property of the constructor prototype is used.
+ */
+DebuggerServer.addGlobalActor = function DS_addGlobalActor(aFunction, aName) {
+  let name = aName ? aName : aFunction.prototype.actorPrefix;
+  if (["from", "tabs", "selected"].indexOf(name) != -1) {
+    throw Error(name + " is not allowed");
+  }
+  if (DebuggerServer.globalActorFactories.hasOwnProperty(name)) {
+    throw Error(name + " already exists");
+  }
+  DebuggerServer.globalActorFactories[name] = aFunction;
+};
+
+/**
+ * Unregisters the handler for the specified browser-scoped request type.
+ * This may be used for example by add-ons when shutting down or upgrading.
+ *
+ * @param aFunction function
+ *        The constructor function for this request type.
+ */
+DebuggerServer.removeGlobalActor = function DS_removeGlobalActor(aFunction) {
+  for (let name in DebuggerServer.globalActorFactories) {
+    let handler = DebuggerServer.globalActorFactories[name];
+    if (handler.name == aFunction.name) {
+      delete DebuggerServer.globalActorFactories[name];
+    }
+  }
+};
--- a/toolkit/devtools/debugger/server/dbg-server.js
+++ b/toolkit/devtools/debugger/server/dbg-server.js
@@ -55,16 +55,22 @@ const ServerSocket = CC("@mozilla.org/ne
 /***
  * Public API
  */
 var DebuggerServer = {
   _listener: null,
   _transportInitialized: false,
   xpcInspector: null,
   _allowConnection: null,
+  // Number of currently open TCP connections.
+  _socketConnections: 0,
+  // Map of global actor names to actor constructors provided by extensions.
+  globalActorFactories: null,
+  // Map of tab actor names to actor constructors provided by extensions.
+  tabActorFactories: null,
 
   LONG_STRING_LENGTH: 10000,
   LONG_STRING_INITIAL_LENGTH: 1000,
 
   /**
    * Initialize the debugger server.
    *
    * @param function aAllowConnectionCallback
@@ -74,16 +80,19 @@ var DebuggerServer = {
   init: function DH_init(aAllowConnectionCallback) {
     if (this.initialized) {
       return;
     }
 
     this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
     this.initTransport(aAllowConnectionCallback);
     this.addActors("chrome://global/content/devtools/dbg-script-actors.js");
+
+    this.globalActorFactories = {};
+    this.tabActorFactories = {};
   },
 
   /**
    * Initialize the debugger server's transport variables.  This can be
    * in place of init() for cases where the jsdebugger isn't needed.
    *
    * @param function aAllowConnectionCallback
    *        The embedder-provider callback, that decides whether an incoming
@@ -95,17 +104,32 @@ var DebuggerServer = {
     }
 
     this._connections = {};
     this._nextConnID = 0;
     this._transportInitialized = true;
     this._allowConnection = aAllowConnectionCallback;
   },
 
-  get initialized() { return !!this.xpcInspector; },
+  get initialized() { return !!this.globalActorFactories; },
+
+  /**
+   * Performs cleanup tasks before shutting down the debugger server, if no
+   * connections are currently open. Such tasks include clearing any actor
+   * constructors added at runtime. This method should be called whenever a
+   * debugger server is no longer useful, to avoid memory leaks. After this
+   * method returns, the debugger server must be initialized again before use.
+   */
+  destroy: function DH_destroy() {
+    if (Object.keys(this._connections).length == 0) {
+      dumpn("Shutting down debugger server.");
+      delete this.globalActorFactories;
+      delete this.tabActorFactories;
+    }
+  },
 
   /**
    * 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
    *        that implements a createRootActor() function to create the
    *        server's root actor.
@@ -128,50 +152,61 @@ var DebuggerServer = {
    *        The port to listen on.
    */
   openListener: function DH_openListener(aPort) {
     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
       return false;
     }
     this._checkInit();
 
+    // Return early if the server is already listening.
     if (this._listener) {
-      throw "Debugging listener already open.";
+      return true;
     }
 
     let localOnly = false;
     // A preference setting can force binding on the loopback interface.
     if (Services.prefs.getBoolPref("devtools.debugger.force-local")) {
       localOnly = true;
     }
 
     try {
       let socket = new ServerSocket(aPort, localOnly, 4);
       socket.asyncListen(this);
       this._listener = socket;
     } catch (e) {
       dumpn("Could not start debugging listener on port " + aPort + ": " + e);
       throw Cr.NS_ERROR_NOT_AVAILABLE;
     }
+    this._socketConnections++;
 
     return true;
   },
 
   /**
    * Close a previously-opened TCP listener.
+   *
+   * @param aForce boolean [optional]
+   *        If set to true, then the socket will be closed, regardless of the
+   *        number of open connections.
    */
-  closeListener: function DH_closeListener() {
+  closeListener: function DH_closeListener(aForce) {
     this._checkInit();
 
-    if (!this._listener) {
+    if (!this._listener || this._socketConnections == 0) {
       return false;
     }
 
-    this._listener.close();
-    this._listener = null;
+    // Only close the listener when the last connection is closed, or if the
+    // aForce flag is passed.
+    if (--this._socketConnections == 0 || aForce) {
+      this._listener.close();
+      this._listener = null;
+      this._socketConnections = 0;
+    }
 
     return true;
   },
 
   /**
    * Creates a new connection to the local debugger speaking over an
    * nsIPipe.
    *
@@ -239,20 +274,22 @@ var DebuggerServer = {
     // Create a root actor for the connection and send the hello packet.
     conn.rootActor = this.createRootActor(conn);
     conn.addActor(conn.rootActor);
     aTransport.send(conn.rootActor.sayHello());
     aTransport.ready();
   },
 
   /**
-   * Remove the connection from the debugging server.
+   * Remove the connection from the debugging server and shut down the server
+   * if no other connections are open.
    */
   _connectionClosed: function DH_connectionClosed(aConnection) {
     delete this._connections[aConnection.prefix];
+    this.destroy();
   }
 };
 
 /**
  * Construct an ActorPool.
  *
  * ActorPools are actorID -> actor mapping and storage.  These are
  * used to accumulate and quickly dispose of groups of actors that
@@ -273,17 +310,21 @@ ActorPool.prototype = {
    * @param aActor object
    *        The actor implementation.  If the object has a
    *        'disconnected' property, it will be called when the actor
    *        pool is cleaned up.
    */
   addActor: function AP_addActor(aActor) {
     aActor.conn = this.conn;
     if (!aActor.actorID) {
-      aActor.actorID = this.conn.allocID(aActor.actorPrefix || undefined);
+      let prefix = aActor.actorPrefix;
+      if (typeof aActor == "function") {
+        prefix = aActor.prototype.actorPrefix;
+      }
+      aActor.actorID = this.conn.allocID(prefix || undefined);
     }
 
     if (aActor.registeredPool) {
       aActor.registeredPool.removeActor(aActor);
     }
     aActor.registeredPool = this;
 
     this._actors[aActor.actorID] = aActor;
@@ -296,16 +337,23 @@ ActorPool.prototype = {
     return this._actors[aActorID];
   },
 
   has: function AP_has(aActorID) {
     return aActorID in this._actors;
   },
 
   /**
+   * Returns true if the pool is empty.
+   */
+  isEmpty: function AP_isEmpty() {
+    return Object.keys(this._actors).length == 0;
+  },
+
+  /**
    * Remove an actor from the actor pool.
    */
   removeActor: function AP_remove(aActorID) {
     delete this._actors[aActorID];
     delete this._cleanups[aActorID];
   },
 
   /**
@@ -432,16 +480,34 @@ DebuggerServerConnection.prototype = {
   onPacket: function DSC_onPacket(aPacket) {
     let actor = this.getActor(aPacket.to);
     if (!actor) {
       this.transport.send({ from: aPacket.to ? aPacket.to : "root",
                             error: "noSuchActor" });
       return;
     }
 
+    // Dyamically-loaded actors have to be created lazily.
+    if (typeof actor == "function") {
+      let instance;
+      try {
+        instance = new actor();
+      } catch (e) {
+        Cu.reportError(e);
+        this.transport.send({
+          error: "unknownError",
+          message: ("error occurred while creating actor '" + actor.name +
+                    "': " + safeErrorString(e))
+        });
+      }
+      actor.registeredPool.addActor(instance);
+      actor.registeredPool.removeActor(actor);
+      actor = instance;
+    }
+
     var ret = null;
 
     // Dispatch the request to the actor.
     if (actor.requestTypes && actor.requestTypes[aPacket.type]) {
       try {
         ret = actor.requestTypes[aPacket.type].bind(actor)(aPacket);
       } catch(e) {
         Cu.reportError(e);
--- a/toolkit/devtools/debugger/tests/unit/test_dbgsocket.js
+++ b/toolkit/devtools/debugger/tests/unit/test_dbgsocket.js
@@ -22,17 +22,22 @@ function really_long() {
   for (let i = 0; i < 18; i++) {
     ret += ret;
   }
   return ret;
 }
 
 function test_socket_conn()
 {
-  DebuggerServer.openListener(2929);
+  do_check_eq(DebuggerServer._socketConnections, 0);
+  do_check_true(DebuggerServer.openListener(2929));
+  do_check_eq(DebuggerServer._socketConnections, 1);
+  // Make sure opening the listener twice does nothing.
+  do_check_true(DebuggerServer.openListener(2929));
+  do_check_eq(DebuggerServer._socketConnections, 1);
 
   let unicodeString = "(╯°□°)╯︵ ┻━┻";
   let transport = debuggerSocketConnect("127.0.0.1", 2929);
   transport.hooks = {
     onPacket: function(aPacket) {
       this.onPacket = function(aPacket) {
         do_check_eq(aPacket.unicode, unicodeString);
         transport.close();
@@ -49,17 +54,22 @@ function test_socket_conn()
       run_next_test();
     },
   };
   transport.ready();
 }
 
 function test_socket_shutdown()
 {
-  DebuggerServer.closeListener();
+  do_check_eq(DebuggerServer._socketConnections, 1);
+  do_check_true(DebuggerServer.closeListener());
+  do_check_eq(DebuggerServer._socketConnections, 0);
+  // Make sure closing the listener twice does nothing.
+  do_check_false(DebuggerServer.closeListener());
+  do_check_eq(DebuggerServer._socketConnections, 0);
 
   let transport = debuggerSocketConnect("127.0.0.1", 2929);
   transport.hooks = {
     onPacket: function(aPacket) {
       // Shouldn't reach this, should never connect.
       do_check_true(false);
     },