Bug 977443 - Implement an actor that defines new actors. r=ochameau
authorJan Odvarko <odvarko@gmail.com>
Fri, 03 Oct 2014 12:49:00 +0100
changeset 240757 e0ac11eb4bf426201d02486d4ed5f66458f97583
parent 240756 bcc7d75e0758d3e48c6dbbd1ad4d251db407209a
child 240758 ad98a5bdb2e1c8d5c78e42942ecce9685bc487b6
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs977443
milestone36.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 977443 - Implement an actor that defines new actors. r=ochameau
toolkit/devtools/DevToolsUtils.js
toolkit/devtools/server/actors/actor-registry.js
toolkit/devtools/server/actors/common.js
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/actors/webbrowser.js
toolkit/devtools/server/main.js
toolkit/devtools/server/moz.build
toolkit/devtools/server/tests/unit/hello-actor.js
toolkit/devtools/server/tests/unit/registertestactors-02.js
toolkit/devtools/server/tests/unit/test_actor-registry-actor.js
toolkit/devtools/server/tests/unit/testactors.js
toolkit/devtools/server/tests/unit/xpcshell.ini
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -81,21 +81,20 @@ exports.makeInfallible = function makeIn
   return function (/* arguments */) {
     try {
       return aHandler.apply(this, arguments);
     } catch (ex) {
       let who = "Handler function";
       if (aName) {
         who += " " + aName;
       }
-      exports.reportException(who, ex);
+      return exports.reportException(who, ex);
     }
   }
 }
-
 /**
  * Interleaves two arrays element by element, returning the combined array, like
  * a zip. In the case of arrays with different sizes, undefined values will be
  * interleaved at the end along with the extra values of the larger array.
  *
  * @param Array a
  * @param Array b
  * @returns Array
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/actor-registry.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const protocol = require("devtools/server/protocol");
+const { method, custom, Arg, Option, RetVal } = protocol;
+
+const { Cu, CC, components } = require("chrome");
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+const Services = require("Services");
+const { DebuggerServer } = require("devtools/server/main");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+
+/**
+ * The ActorActor gives you a handle to an actor you've dynamically
+ * registered and allows you to unregister it.
+ */
+const ActorActor = protocol.ActorClass({
+  typeName: "actorActor",
+
+  initialize: function (conn, options) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+
+    this.options = options;
+  },
+
+  unregister: method(function () {
+    if (this.options.tab) {
+      DebuggerServer.removeTabActor(this.options);
+    }
+
+    if (this.options.global) {
+      DebuggerServer.removeGlobalActor(this.options);
+    }
+  }, {
+    request: {},
+    response: {}
+  })
+});
+
+const ActorActorFront = protocol.FrontClass(ActorActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+exports.ActorActorFront = ActorActorFront;
+
+/*
+ * The ActorRegistryActor allows clients to define new actors on the
+ * server. This is particularly useful for addons.
+ */
+const ActorRegistryActor = protocol.ActorClass({
+  typeName: "actorRegistry",
+
+  initialize: function (conn) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+
+  registerActor: method(function (sourceText, fileName, options) {
+    const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+    const sandbox = Cu.Sandbox(principal);
+    const exports = sandbox.exports = {};
+    sandbox.require = require;
+
+    Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
+
+    let { prefix, constructor, type } = options;
+
+    if (type.global) {
+      DebuggerServer.addGlobalActor({
+        constructorName: constructor,
+        constructorFun: sandbox[constructor]
+      }, prefix);
+    }
+
+    if (type.tab) {
+      DebuggerServer.addTabActor({
+        constructorName: constructor,
+        constructorFun: sandbox[constructor]
+      }, prefix);
+    }
+
+    return ActorActor(this.conn, {
+      name: constructor,
+      tab: type.tab,
+      global: type.global
+    });
+  }, {
+    request: {
+      sourceText: Arg(0, "string"),
+      filename: Arg(1, "string"),
+      options: Arg(2, "json")
+    },
+
+    response: {
+      actorActor: RetVal("actorActor")
+    }
+  })
+});
+
+exports.ActorRegistryActor = ActorRegistryActor;
+
+function request(uri) {
+  return new Promise((resolve, reject) => {
+    try {
+      uri = Services.io.newURI(uri, null, null);
+    } catch (e) {
+      reject(e);
+    }
+
+    if (uri.scheme != "resource") {
+      reject(new Error(
+        "Can only register actors whose URI scheme is 'resource'."));
+    }
+
+    NetUtil.asyncFetch(uri, (stream, status, req) => {
+      if (!components.isSuccessCode(status)) {
+        reject(new Error("Request failed with status code = "
+                         + status
+                         + " after NetUtil.asyncFetch for url = "
+                         + uri));
+        return;
+      }
+
+      let source = NetUtil.readInputStreamToString(stream, stream.available());
+      stream.close();
+      resolve(source);
+    });
+  });
+}
+
+const ActorRegistryFront = protocol.FrontClass(ActorRegistryActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client,
+      { actor: form.actorRegistryActor });
+
+    this.manage(this);
+  },
+
+  registerActor: custom(function (uri, options) {
+    return request(uri, options)
+      .then(sourceText => {
+        return this._registerActor(sourceText, uri, options);
+      });
+  }, {
+    impl: "_registerActor"
+  })
+});
+exports.ActorRegistryFront = ActorRegistryFront;
--- a/toolkit/devtools/server/actors/common.js
+++ b/toolkit/devtools/server/actors/common.js
@@ -28,45 +28,54 @@
  *          the BrowserTabActor with which it will be associated.
  *          Only used for deprecated eagerly loaded actors.
  *
  */
 function RegisteredActorFactory(options, prefix) {
   // By default the actor name will also be used for the actorID prefix.
   this._prefix = prefix;
   if (typeof(options) != "function") {
-    // Lazy actor definition, where options contains all the information
-    // required to load the actor lazily.
-    this._getConstructor = function () {
-      // Load the module
-      let mod;
-      try {
-        mod = require(options.id);
-      } catch(e) {
-        throw new Error("Unable to load actor module '" + options.id + "'.\n" +
-                        e.message + "\n" + e.stack + "\n");
-      }
-      // Fetch the actor constructor
-      let c = mod[options.constructorName];
-      if (!c) {
-        throw new Error("Unable to find actor constructor named '" +
-                        options.constructorName + "'. (Is it exported?)");
-      }
-      return c;
-    };
+    // actors definition registered by actorRegistryActor
+    if (options.constructorFun) {
+      this._getConstructor = () => options.constructorFun;
+    } else {
+      // Lazy actor definition, where options contains all the information
+      // required to load the actor lazily.
+      this._getConstructor = function () {
+        // Load the module
+        let mod;
+        try {
+          mod = require(options.id);
+        } catch(e) {
+          throw new Error("Unable to load actor module '" + options.id + "'.\n" +
+                          e.message + "\n" + e.stack + "\n");
+        }
+        // Fetch the actor constructor
+        let c = mod[options.constructorName];
+        if (!c) {
+          throw new Error("Unable to find actor constructor named '" +
+                          options.constructorName + "'. (Is it exported?)");
+        }
+        return c;
+      };
+    }
+    // Exposes `name` attribute in order to allow removeXXXActor to match
+    // the actor by its actor constructor name.
+    this.name = options.constructorName;
   } else {
     // Old actor case, where options is a function that is the actor constructor.
     this._getConstructor = () => options;
     // Exposes `name` attribute in order to allow removeXXXActor to match
-    // the actor by its actor contructor name.
+    // the actor by its actor constructor name.
     this.name = options.name;
+
     // For old actors, we allow the use of a different prefix for actorID
     // than for listTabs actor names, by fetching a prefix on the actor prototype.
     // (Used by ChromeDebuggerActor)
-    if (options.prototype.actorPrefix) {
+    if (options.prototype && options.prototype.actorPrefix) {
       this._prefix = options.prototype.actorPrefix;
     }
   }
 }
 RegisteredActorFactory.prototype.createObservedActorFactory = function (conn, parentActor) {
   return new ObservedActorFactory(this._getConstructor, this._prefix, conn, parentActor);
 }
 exports.RegisteredActorFactory = RegisteredActorFactory;
@@ -74,19 +83,19 @@ exports.RegisteredActorFactory = Registe
 /**
  * Creates "observed" actors factory meant for creating real actor instances.
  * These factories lives in actor pools and fake various actor attributes.
  * They will be replaced in actor pools by final actor instances during
  * the first request for the same actorID from DebuggerServer._getOrCreateActor.
  *
  * ObservedActorFactory fakes the following actors attributes:
  *   actorPrefix (string) Used by ActorPool.addActor to compute the actor id
- *   actorID (string) Set by ActorPool.addActor just after being instanciated
+ *   actorID (string) Set by ActorPool.addActor just after being instantiated
  *   registeredPool (object) Set by ActorPool.addActor just after being
- *                           instanciated
+ *                           instantiated
  * And exposes the following method:
  *   createActor (function) Instantiate an actor that is going to replace
  *                          this factory in the actor pool.
  */
 function ObservedActorFactory(getConstructor, prefix, conn, parentActor) {
   this._getConstructor = getConstructor;
   this._conn = conn;
   this._parentActor = parentActor;
@@ -118,17 +127,17 @@ exports.ObservedActorFactory = ObservedA
  */
 
 /**
  * Populate |this._extraActors| as specified by |aFactories|, reusing whatever
  * actors are already there. Add all actors in the final extra actors table to
  * |aPool|.
  *
  * The root actor and the tab actor use this to instantiate actors that other
- * parts of the browser have specified with DebuggerServer.addTabActor antd
+ * parts of the browser have specified with DebuggerServer.addTabActor and
  * DebuggerServer.addGlobalActor.
  *
  * @param aFactories
  *     An object whose own property names are the names of properties to add to
  *     some reply packet (say, a tab actor grip or the "listTabs" response
  *     form), and whose own property values are actor constructor functions, as
  *     documented for addTabActor and addGlobalActor.
  *
@@ -153,21 +162,26 @@ exports.ObservedActorFactory = ObservedA
 exports.createExtraActors = function createExtraActors(aFactories, aPool) {
   // Walk over global actors added by extensions.
   for (let name in aFactories) {
     let actor = this._extraActors[name];
     if (!actor) {
       // Register another factory, but this time specific to this connection.
       // It creates a fake actor that looks like an regular actor in the pool,
       // but without actually instantiating the actor.
-      // It will only be instanciated on the first request made to the actor.
+      // It will only be instantiated on the first request made to the actor.
       actor = aFactories[name].createObservedActorFactory(this.conn, this);
       this._extraActors[name] = actor;
     }
-    aPool.addActor(actor);
+
+    // If the actor already exists in the pool, it may have been instantiated,
+    // so make sure not to overwrite it by a non-instantiated version.
+    if (!aPool.has(actor.actorID)) {
+      aPool.addActor(actor);
+    }
   }
 }
 
 /**
  * Append the extra actors in |this._extraActors|, constructed by a prior call
  * to CommonCreateExtraActors, to |aObject|.
  *
  * @param aObject
@@ -265,17 +279,23 @@ ActorPool.prototype = {
   /**
    * Run all actor cleanups.
    */
   cleanup: function AP_cleanup() {
     for each (let actor in this._cleanups) {
       actor.disconnect();
     }
     this._cleanups = {};
-  }
+  },
+
+  forEach: function(callback) {
+    for (let name in this._actors) {
+      callback(this._actors[name]);
+    }
+  },
 }
 
 exports.ActorPool = ActorPool;
 
 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
 // implemented.
 exports.getOffsetColumn = function getOffsetColumn(aOffset, aScript) {
   let bestOffsetMapping = null;
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -275,24 +275,22 @@ RootActor.prototype = {
       for (let tabActor of tabActors) {
         if (tabActor.selected) {
           selected = tabActorList.length;
         }
         tabActor.parentID = this.actorID;
         newActorPool.addActor(tabActor);
         tabActorList.push(tabActor);
       }
-
       /* DebuggerServer.addGlobalActor support: create actors. */
       if (!this._globalActorPool) {
         this._globalActorPool = new ActorPool(this.conn);
-        this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool);
         this.conn.addActorPool(this._globalActorPool);
       }
-
+      this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool);
       /*
        * 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 = newActorPool;
@@ -431,32 +429,53 @@ RootActor.prototype = {
     }
   },
 
   /**
    * Create or return the StyleSheetActor for a style sheet. This method
    * is here because the Style Editor and Inspector share style sheet actors.
    *
    * @param DOMStyleSheet styleSheet
-   *        The style sheet to creat an actor for.
+   *        The style sheet to create an actor for.
    * @return StyleSheetActor actor
    *         The actor for this style sheet.
    *
    */
   createStyleSheetActor: function(styleSheet) {
     if (this._styleSheetActors.has(styleSheet)) {
       return this._styleSheetActors.get(styleSheet);
     }
     let actor = new StyleSheetActor(styleSheet, this);
     this._styleSheetActors.set(styleSheet, actor);
 
     this._globalActorPool.addActor(actor);
 
     return actor;
-  }
+  },
+
+  /**
+   * Remove the extra actor (added by DebuggerServer.addGlobalActor or
+   * DebuggerServer.addTabActor) name |aName|.
+   */
+  removeActorByName: function(aName) {
+    if (aName in this._extraActors) {
+      const actor = this._extraActors[aName];
+      if (this._globalActorPool.has(actor)) {
+        this._globalActorPool.removeActor(actor);
+      }
+      if (this._tabActorPool) {
+        // Iterate over TabActor instances to also remove tab actors
+        // created during listTabs for each document.
+        this._tabActorPool.forEach(tab => {
+          tab.removeActorByName(aName);
+        });
+      }
+      delete this._extraActors[aName];
+    }
+   }
 };
 
 RootActor.prototype.requestTypes = {
   "listTabs": RootActor.prototype.onListTabs,
   "listAddons": RootActor.prototype.onListAddons,
   "listProcesses": RootActor.prototype.onListProcesses,
   "attachProcess": RootActor.prototype.onAttachProcess,
   "echo": RootActor.prototype.onEcho,
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -851,17 +851,17 @@ TabActor.prototype = {
     return false;
   },
 
   /* Support for DebuggerServer.addTabActor. */
   _createExtraActors: createExtraActors,
   _appendExtraActors: appendExtraActors,
 
   /**
-   * Does the actual work of attching to a tab.
+   * Does the actual work of attaching to a tab.
    */
   _attach: function BTA_attach() {
     if (this._attached) {
       return;
     }
 
     // Create a pool for tab-lifetime actors.
     dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
@@ -1507,32 +1507,42 @@ TabActor.prototype = {
     return isNative;
   },
 
   /**
    * Create or return the StyleSheetActor for a style sheet. This method
    * is here because the Style Editor and Inspector share style sheet actors.
    *
    * @param DOMStyleSheet styleSheet
-   *        The style sheet to creat an actor for.
+   *        The style sheet to create an actor for.
    * @return StyleSheetActor actor
    *         The actor for this style sheet.
    *
    */
   createStyleSheetActor: function BTA_createStyleSheetActor(styleSheet) {
     if (this._styleSheetActors.has(styleSheet)) {
       return this._styleSheetActors.get(styleSheet);
     }
     let actor = new StyleSheetActor(styleSheet, this);
     this._styleSheetActors.set(styleSheet, actor);
 
     this._tabPool.addActor(actor);
 
     return actor;
-  }
+  },
+
+  removeActorByName: function BTA_removeActor(aName) {
+    if (aName in this._extraActors) {
+      const actor = this._extraActors[aName];
+      if (this._tabActorPool.has(actor)) {
+        this._tabActorPool.removeActor(actor);
+      }
+      delete this._extraActors[aName];
+    }
+  },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
   "attach": TabActor.prototype.onAttach,
   "detach": TabActor.prototype.onDetach,
@@ -1545,17 +1555,17 @@ TabActor.prototype.requestTypes = {
 
 exports.TabActor = TabActor;
 
 /**
  * Creates a tab actor for handling requests to a single in-process
  * <browser> tab. Most of the implementation comes from TabActor.
  *
  * @param aConnection DebuggerServerConnection
- *        The conection to the client.
+ *        The connection to the client.
  * @param aBrowser browser
  *        The browser instance that contains this tab.
  * @param aTabBrowser tabbrowser
  *        The tabbrowser that can receive nsIWebProgressListener events.
  */
 function BrowserTabActor(aConnection, aBrowser, aTabBrowser)
 {
   TabActor.call(this, aConnection, aBrowser);
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -319,17 +319,17 @@ var DebuggerServer = {
    *            request to the root actor returns actor IDs. IDs are in
    *            dictionaries, with actor names as keys and actor IDs as values.
    *            The actor name is the prefix to which the "Actor" string is
    *            appended. So for an actor with the `console` prefix, the actor
    *            name will be `consoleActor`.
    *        - constructor (string):
    *          the name of the exported symbol to be used as the actor
    *          constructor.
-   *        - type (a dictionnary of booleans with following attribute names):
+   *        - type (a dictionary of booleans with following attribute names):
    *          - "global"
    *            registers a global actor instance, if true.
    *            A global actor has the root actor as its parent.
    *          - "tab"
    *            registers a tab actor instance, if true.
    *            A new actor will be created for each tab and each app.
    */
   registerModule: function(id, options) {
@@ -342,17 +342,17 @@ var DebuggerServer = {
       let {prefix, constructor, type} = options;
       if (typeof(prefix) !== "string") {
         throw new Error("Lazy actor definition for '" + id + "' requires a string 'prefix' option.");
       }
       if (typeof(constructor) !== "string") {
         throw new Error("Lazy actor definition for '" + id + "' requires a string 'constructor' option.");
       }
       if (!("global" in type) && !("tab" in type)) {
-        throw new Error("Lazy actor definition for '" + id + "' requires a dictionnary 'type' option whose attributes can be 'global' or 'tab'.");
+        throw new Error("Lazy actor definition for '" + id + "' requires a dictionary 'type' option whose attributes can be 'global' or 'tab'.");
       }
       let name = prefix + "Actor";
       let mod = {
         id: id,
         prefix: prefix,
         constructorName: constructor,
         type: type,
         globalActor: type.global,
@@ -428,16 +428,21 @@ var DebuggerServer = {
       this.addTabActors();
       let { ChromeDebuggerActor } = require("devtools/server/actors/script");
       this.addGlobalActor(ChromeDebuggerActor, "chromeDebugger");
       this.registerModule("devtools/server/actors/preference", {
         prefix: "preference",
         constructor: "PreferenceActor",
         type: { global: true }
       });
+      this.registerModule("devtools/server/actors/actor-registry", {
+        prefix: "actorRegistry",
+        constructor: "ActorRegistryActor",
+        type: { global: true }
+      });
     }
 
     this.registerModule("devtools/server/actors/webapps", {
       prefix: "webapps",
       constructor: "WebappsActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/device", {
@@ -974,29 +979,34 @@ var DebuggerServer = {
       throw Error(name + " already exists");
     }
     DebuggerServer.tabActorFactories[name] = new RegisteredActorFactory(aActor, name);
   },
 
   /**
    * Unregisters the handler for the specified tab-scoped request type.
    * This may be used for example by add-ons when shutting down or upgrading.
+   * When unregistering an existing tab actor remove related tab factory
+   * as well as all existing instances of the actor.
    *
    * @param aActor function, object
    *      In case of function:
    *        The constructor function for this request type.
    *      In case of object:
    *        Same object being given to related addTabActor call.
    */
   removeTabActor: function DS_removeTabActor(aActor) {
     for (let name in DebuggerServer.tabActorFactories) {
       let handler = DebuggerServer.tabActorFactories[name];
       if ((handler.name && handler.name == aActor.name) ||
           (handler.id && handler.id == aActor.id)) {
         delete DebuggerServer.tabActorFactories[name];
+        for (let connID of Object.getOwnPropertyNames(this._connections)) {
+          this._connections[connID].rootActor.removeActorByName(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. Note that the name or actorPrefix of the
@@ -1028,29 +1038,34 @@ var DebuggerServer = {
       throw Error(name + " already exists");
     }
     DebuggerServer.globalActorFactories[name] = new RegisteredActorFactory(aActor, name);
   },
 
   /**
    * Unregisters the handler for the specified browser-scoped request type.
    * This may be used for example by add-ons when shutting down or upgrading.
+   * When unregistering an existing global actor remove related global factory
+   * as well as all existing instances of the actor.
    *
    * @param aActor function, object
    *      In case of function:
    *        The constructor function for this request type.
    *      In case of object:
    *        Same object being given to related addGlobalActor call.
    */
   removeGlobalActor: function DS_removeGlobalActor(aActor) {
     for (let name in DebuggerServer.globalActorFactories) {
       let handler = DebuggerServer.globalActorFactories[name];
       if ((handler.name && handler.name == aActor.name) ||
           (handler.id && handler.id == aActor.id)) {
         delete DebuggerServer.globalActorFactories[name];
+        for (let connID of Object.getOwnPropertyNames(this._connections)) {
+          this._connections[connID].rootActor.removeActorByName(name);
+        }
       }
     }
   }
 };
 
 EventEmitter.decorate(DebuggerServer);
 
 if (this.exports) {
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -28,16 +28,17 @@ EXTRA_JS_MODULES.devtools += [
 EXTRA_JS_MODULES.devtools.server += [
     'child.js',
     'content-globals.js',
     'main.js',
     'protocol.js',
 ]
 
 EXTRA_JS_MODULES.devtools.server.actors += [
+    'actors/actor-registry.js',
     'actors/call-watcher.js',
     'actors/canvas.js',
     'actors/child-process.js',
     'actors/childtab.js',
     'actors/common.js',
     'actors/csscoverage.js',
     'actors/device.js',
     'actors/eventlooplag.js',
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/hello-actor.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const protocol = require("devtools/server/protocol");
+
+const HelloActor = protocol.ActorClass({
+  typeName: "helloActor",
+
+  hello: protocol.method(function () {
+    return;
+  }, {
+    request: {},
+    response: {}
+  })
+});
--- a/toolkit/devtools/server/tests/unit/registertestactors-02.js
+++ b/toolkit/devtools/server/tests/unit/registertestactors-02.js
@@ -4,10 +4,12 @@
 function Actor() {}
 
 exports.register = function(handle) {
   handle.addGlobalActor(Actor, "registeredActor2");
   handle.addTabActor(Actor, "registeredActor2");
 }
 
 exports.unregister = function(handle) {
+  handle.removeTabActor(Actor);
+  handle.removeGlobalActor(Actor);
 }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_actor-registry-actor.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that you can register new actors via the ActorRegistrationActor.
+ */
+
+var gClient;
+var gRegistryFront;
+var gActorFront;
+var gOldPref;
+
+const { ActorRegistryFront } = devtools.require("devtools/server/actors/actor-registry");
+
+function run_test()
+{
+  gOldPref = Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps");
+  Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", false);
+  initTestDebuggerServer();
+  DebuggerServer.addBrowserActors();
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(getRegistry);
+  do_test_pending();
+}
+
+function getRegistry() {
+  gClient.listTabs((response) => {
+    gRegistryFront = ActorRegistryFront(gClient, response);
+    registerNewActor();
+  });
+}
+
+function registerNewActor() {
+  let options = {
+    prefix: "helloActor",
+    constructor: "HelloActor",
+    type: { global: true }
+  };
+
+  gRegistryFront
+    .registerActor("resource://test/hello-actor.js", options)
+    .then(actorFront => gActorFront = actorFront)
+    .then(talkToNewActor)
+    .then(null, e => {
+      DevToolsUtils.reportException("registerNewActor", e)
+      do_check_true(false);
+    });
+}
+
+function talkToNewActor() {
+  gClient.listTabs(({ helloActor }) => {
+    do_check_true(!!helloActor);
+    gClient.request({
+      to: helloActor,
+      type: "hello"
+    }, response => {
+      do_check_true(!response.error);
+      unregisterNewActor();
+    });
+  });
+}
+
+function unregisterNewActor() {
+  gActorFront
+    .unregister()
+    .then(testActorIsUnregistered)
+    .then(null, e => {
+      DevToolsUtils.reportException("registerNewActor", e)
+      do_check_true(false);
+    });
+}
+
+function testActorIsUnregistered() {
+  gClient.listTabs(({ helloActor }) => {
+    do_check_true(!helloActor);
+
+    Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", gOldPref);
+    finishClient(gClient);
+  });
+}
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -47,18 +47,21 @@ TestTabList.prototype = {
   constructor: TestTabList,
   getList: function () {
     return Promise.resolve([tabActor for (tabActor of this._tabActors)]);
   }
 };
 
 function createRootActor(aConnection)
 {
-  let root = new RootActor(aConnection,
-                           { tabList: new TestTabList(aConnection) });
+  let root = new RootActor(aConnection, {
+    tabList: new TestTabList(aConnection),
+    globalActorFactories: DebuggerServer.globalActorFactories,
+  });
+
   root.applicationType = "xpcshell-tests";
   return root;
 }
 
 function TestTabActor(aConnection, aGlobal)
 {
   this.conn = aConnection;
   this._global = aGlobal;
@@ -121,16 +124,24 @@ TestTabActor.prototype = {
   },
 
   onReload: function(aRequest) {
     this.threadActor.clearDebuggees();
     this.threadActor.dbg.addDebuggees();
     return {};
   },
 
+  removeActorByName: function(aName) {
+    const actor = this._extraActors[aName];
+    if (this._tabActorPool) {
+      this._tabActorPool.removeActor(actor);
+    }
+    delete this._extraActors[aName];
+  },
+
   /* Support for DebuggerServer.addTabActor. */
   _createExtraActors: createExtraActors,
   _appendExtraActors: appendExtraActors
 };
 
 TestTabActor.prototype.requestTypes = {
   "attach": TestTabActor.prototype.onAttach,
   "detach": TestTabActor.prototype.onDetach,
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -11,17 +11,19 @@ support-files =
   pre_init_global_actors.js
   pre_init_tab_actors.js
   registertestactors-01.js
   registertestactors-02.js
   registertestactors-03.js
   sourcemapped.js
   testactors.js
   tracerlocations.js
+  hello-actor.js
 
+[test_actor-registry-actor.js]
 [test_nesting-01.js]
 [test_nesting-02.js]
 [test_nesting-03.js]
 [test_forwardingprefix.js]
 [test_getyoungestframe.js]
 [test_nsjsinspector.js]
 [test_dbgactor.js]
 [test_dbgglobal.js]