Bug 980481 - DirectorRegistry, DirectorManager, DirectorScript and MessagePort RemoteDebuggerServer actors. r=ochameau
☠☠ backed out by 3580a90de286 ☠ ☠
authorLuca Greco <luca.greco@alcacoop.it>
Tue, 16 Dec 2014 08:43:00 -0500
changeset 247914 724554c093a8e1c621ad3920bdc00c9a3494066e
parent 247913 9e27809d4438e7741d628ad38b385ef19126cd73
child 247915 2a193b7f395c8e6f3c21e83777ce2f540e4c04fe
child 248040 8f91a31d4cec2830c135037fec299490f1cca2ff
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs980481
milestone37.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 980481 - DirectorRegistry, DirectorManager, DirectorScript and MessagePort RemoteDebuggerServer actors. r=ochameau
toolkit/devtools/server/actors/director-manager.js
toolkit/devtools/server/actors/director-registry.js
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/main.js
toolkit/devtools/server/moz.build
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/director-helpers.js
toolkit/devtools/server/tests/mochitest/director-script-target.html
toolkit/devtools/server/tests/mochitest/test_director.html
toolkit/devtools/server/tests/mochitest/test_director_connectToChild.html
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/director-manager.js
@@ -0,0 +1,780 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+
+const { Cu, Ci } = require("chrome");
+
+const { on, once, off, emit } = events;
+const { method, Arg, Option, RetVal, types } = protocol;
+
+const { sandbox, evaluate } = require('sdk/loader/sandbox');
+const { Class } = require("sdk/core/heritage");
+
+const { PlainTextConsole } = require('sdk/console/plain-text');
+
+const { DirectorRegistry } = require("./director-registry");
+
+/**
+ * E10S child setup helper
+ */
+
+const {DebuggerServer} = require("devtools/server/main");
+
+/**
+ * Error Messages
+ */
+
+const ERR_MESSAGEPORT_FINALIZED = "message port finalized";
+
+const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id";
+const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id";
+
+/**
+ * Type describing a messageport event
+ */
+types.addDictType("messageportevent", {
+  isTrusted: "boolean",
+  data: "nullable:primitive",
+  origin: "nullable:string",
+  lastEventId: "nullable:string",
+  source: "messageport",
+  ports: "nullable:array:messageport"
+});
+
+/**
+ * A MessagePort Actor allowing communication through messageport events
+ * over the remote debugging protocol.
+ */
+let MessagePortActor = exports.MessagePortActor = protocol.ActorClass({
+  typeName: "messageport",
+
+  /**
+   * Create a MessagePort actor.
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param MessagePort port
+   *        The wrapped MessagePort.
+   */
+  initialize: function(conn, port) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+
+    // NOTE: can't get a weak reference because we need to subscribe events
+    // using port.onmessage or addEventListener
+    this.port = port;
+  },
+
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Sends a message on the wrapped message port.
+   *
+   * @param Object msg
+   *        The JSON serializable message event payload
+   */
+  postMessage: method(function (msg) {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    this.port.postMessage(msg);
+  }, {
+    oneway: true,
+    request: {
+      msg: Arg(0, "nullable:json")
+    }
+  }),
+
+  /**
+   * Starts to receive and send queued messages on this message port.
+   */
+  start: method(function () {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    // NOTE: set port.onmessage to a function is an implicit start
+    // and starts to send queued messages.
+    // On the client side we should set MessagePortClient.onmessage
+    // to a setter which register an handler to the message event
+    // and call the actor start method to start receiving messages
+    // from the MessagePort's queue.
+    this.port.onmessage = (evt) => {
+      var ports;
+
+      // TODO: test these wrapped ports
+      if (Array.isArray(evt.ports)) {
+        ports = evt.ports.map((port) => {
+          let actor = new MessagePortActor(this.conn, port);
+          this.manage(actor);
+          return actor;
+        });
+      }
+
+      emit(this, "message", {
+        isTrusted: evt.isTrusted,
+        data: evt.data,
+        origin: evt.origin,
+        lastEventId: evt.lastEventId,
+        source: this,
+        ports: ports
+      });
+    };
+  }, {
+    oneway: true,
+    request: {}
+  }),
+
+  /**
+   * Starts to receive and send queued messages on this message port, or
+   * raise an exception if the port is null
+   */
+  close: method(function () {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    this.port.onmessage = null;
+    this.port.close();
+  }, {
+    oneway: true,
+    request: {}
+  }),
+
+  finalize: method(function () {
+    this.close();
+    this.port = null;
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "message": {
+      type: "message",
+      msg: Arg(0, "nullable:messageportevent")
+    }
+  }
+});
+
+/**
+ * The corresponding Front object for the MessagePortActor.
+ */
+let MessagePortFront = exports.MessagePortFront = protocol.FrontClass(MessagePortActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+
+/**
+ * Type describing a director-script error
+ */
+types.addDictType("director-script-error", {
+  directorScriptId: "string",
+  message: "string",
+  stack: "string",
+  fileName: "string",
+  lineNumber: "number",
+  columnNumber: "number"
+});
+
+/**
+ * Type describing a director-script attach event
+ */
+types.addDictType("director-script-attach", {
+  directorScriptId: "string",
+  url: "string",
+  innerId: "number",
+  port: "nullable:messageport"
+});
+
+/**
+ * Type describing a director-script detach event
+ */
+types.addDictType("director-script-detach", {
+  directorScriptId: "string",
+  innerId: "number"
+});
+
+/**
+ * The Director Script Actor manage javascript code running in a non-privileged sandbox with the same
+ * privileges of the target global (browser tab or a firefox os app).
+ *
+ * After retrieving an instance of this actor (from the tab director actor), you'll need to set it up
+ * by calling setup().
+ *
+ * After the setup, this actor will automatically attach/detach the content script (and optionally a
+ * directly connect the debugger client and the content script using a MessageChannel) on tab
+ * navigation.
+ */
+let DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClass({
+  typeName: "director-script",
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "error": {
+      type: "error",
+      data: Arg(0, "director-script-error")
+    },
+    "attach": {
+      type: "attach",
+      data: Arg(0, "director-script-attach")
+    },
+    "detach": {
+      type: "detach",
+      data: Arg(0, "director-script-detach")
+    }
+  },
+
+  /**
+   * Creates the director script actor
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param Actor tabActor
+   *        The tab (or root) actor.
+   * @param String scriptId
+   *        The director-script id.
+   * @param String scriptCode
+   *        The director-script javascript source.
+   * @param Object scriptOptions
+   *        The director-script options object.
+   */
+  initialize: function(conn, tabActor, { scriptId, scriptCode, scriptOptions }) {
+    protocol.Actor.prototype.initialize.call(this, conn, tabActor);
+
+    this.tabActor = tabActor;
+
+    this._scriptId = scriptId;
+    this._scriptCode = scriptCode;
+    this._scriptOptions = scriptOptions;
+    this._setupCalled = false;
+
+    this._onGlobalCreated   = this._onGlobalCreated.bind(this);
+    this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+
+    this.finalize();
+  },
+
+  /**
+   * Starts listening to the tab global created, in order to create the director-script sandbox
+   * using the configured scriptCode, attached/detached automatically to the tab
+   * window on tab navigation.
+   *
+   * @param Boolean reload
+   *        attach the page immediately or reload it first.
+   * @param Boolean skipAttach
+   *        skip the attach
+   */
+  setup: method(function ({ reload, skipAttach }) {
+    if (this._setupCalled) {
+      // do nothing
+      return;
+    }
+
+    this._setupCalled = true;
+
+    on(this.tabActor, "window-ready", this._onGlobalCreated);
+    on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+
+    // optional skip attach (needed by director-manager for director scripts bulk activation)
+    if (skipAttach) {
+      return;
+    }
+
+    if (reload) {
+      this.window.location.reload();
+    } else {
+      // fake a global created event to attach without reload
+      this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true });
+    }
+  }, {
+    request: {
+      reload: Option(0, "boolean"),
+      skipAttach: Option(0, "boolean")
+    },
+    oneway: true
+  }),
+
+  /**
+   * Get the attached MessagePort actor if any
+   */
+  getMessagePort: method(function () {
+    return this._messagePortActor;
+  }, {
+    request: { },
+    response: {
+      port: RetVal("nullable:messageport")
+    }
+  }),
+
+  /**
+   * Stop listening for document global changes, destroy the content worker and puts
+   * this actor to hibernation.
+   */
+  finalize: method(function () {
+    if (!this._setupCalled) {
+      return;
+    }
+
+    off(this.tabActor, "window-ready", this._onGlobalCreated);
+    off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+
+    this._onGlobalDestroyed({ id: this._lastAttachedWinId });
+
+    this._setupCalled = false;
+  }, {
+    oneway: true
+  }),
+
+  // local helpers
+  get window() {
+    return this.tabActor.window;
+  },
+
+  /* event handlers */
+  _onGlobalCreated: function({ id, window, isTopLevel }) {
+     if (!isTopLevel) {
+       // filter iframes
+       return;
+     }
+
+     if (this._lastAttachedWinId) {
+       // if we have received a global created without a previous global destroyed,
+       // it's time to cleanup the previous state
+       this._onGlobalDestroyed(this._lastAttachedWinId);
+     }
+
+     // TODO: check if we want to share a single sandbox per global
+     //       for multiple debugger clients
+
+     // create & attach the new sandbox
+     this._scriptSandbox = new DirectorScriptSandbox({
+       scriptId: this._scriptId,
+       scriptCode: this._scriptCode,
+       scriptOptions: this._scriptOptions
+     });
+
+     try {
+       // attach the global window
+       this._lastAttachedWinId = id;
+       var port = this._scriptSandbox.attach(window, id);
+       this._onDirectorScriptAttach(window, port);
+     } catch(e) {
+       this._onDirectorScriptError(e);
+     }
+  },
+  _onGlobalDestroyed: function({ id }) {
+     if (id !== this._lastAttachedWinId) {
+       // filter destroyed globals
+       return;
+     }
+
+     // unmanage and cleanup the messageport actor
+     if (this._messagePortActor) {
+       this.unmanage(this._messagePortActor);
+       this._messagePortActor = null;
+     }
+
+     // NOTE: destroy here the old worker
+     if (this._scriptSandbox) {
+       this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this));
+
+       // send a detach event to the debugger client
+       emit(this, "detach", {
+         directorScriptId: this._scriptId,
+         innerId: this._lastAttachedWinId
+       });
+
+       this._lastAttachedWinId = null;
+       this._scriptSandbox = null;
+     }
+  },
+  _onDirectorScriptError: function(error) {
+    // route the content script error to the debugger client
+    emit(this, "error", {
+      directorScriptId: this._scriptId,
+      message: error.toString(),
+      stack: error.stack,
+      fileName: error.fileName,
+      lineNumber: error.lineNumber,
+      columnNumber: error.columnNumber
+    });
+  },
+  _onDirectorScriptAttach: function(window, port) {
+    let portActor = new MessagePortActor(this.conn, port);
+    this.manage(portActor);
+    this._messagePortActor = portActor;
+
+    emit(this, "attach", {
+      directorScriptId: this._scriptId,
+      url: (window && window.location) ? window.location.toString() : "",
+      innerId: this._lastAttachedWinId,
+      port: this._messagePortActor
+    });
+  }
+});
+
+/**
+ * The corresponding Front object for the DirectorScriptActor.
+ */
+let DirectorScriptFront = exports.DirectorScriptFront = protocol.FrontClass(DirectorScriptActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts.
+ */
+const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClass({
+  typeName: "director-manager",
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "director-script-error": {
+      type: "error",
+      data: Arg(0, "director-script-error")
+    },
+    "director-script-attach": {
+      type: "attach",
+      data: Arg(0, "director-script-attach")
+    },
+    "director-script-detach": {
+      type: "detach",
+      data: Arg(0, "director-script-detach")
+    }
+  },
+
+  /* init & destroy methods */
+  initialize: function(conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+    this._directorScriptActorsMap = new Map();
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Retrieves the list of installed director-scripts.
+   */
+  list: method(function () {
+    var enabled_script_ids = [for (id of this._directorScriptActorsMap.keys()) id];
+
+    return {
+      installed: DirectorRegistry.list(),
+      enabled: enabled_script_ids
+    };
+  }, {
+    response: {
+      directorScripts: RetVal("json")
+    }
+  }),
+
+  /**
+   * Bulk enabling director-scripts.
+   *
+   * @param Array[String] selectedIds
+   *        The list of director-script ids to be enabled,
+   *        ["*"] will activate all the installed director-scripts
+   * @param Boolean reload
+   *        optionally reload the target window
+   */
+  enableByScriptIds: method(function(selectedIds, { reload }) {
+    if (selectedIds && selectedIds.length === 0) {
+      // filtered all director scripts ids
+      return;
+    }
+
+    for (let scriptId of DirectorRegistry.list()) {
+      // filter director script ids
+      if (selectedIds.indexOf("*") < 0 &&
+          selectedIds.indexOf(scriptId) < 0) {
+        continue;
+      }
+
+      let actor = this.getByScriptId(scriptId);
+
+      // skip attach if reload is true (activated director scripts
+      // will be automatically attached on the final reload)
+      actor.setup({ reload: false, skipAttach: reload });
+    }
+
+    if (reload) {
+      this.tabActor.window.location.reload();
+    }
+  }, {
+    oneway: true,
+    request: {
+      selectedIds: Arg(0, "array:string"),
+      reload: Option(1, "boolean")
+    }
+  }),
+
+  /**
+   * Bulk disabling director-scripts.
+   *
+   * @param Array[String] selectedIds
+   *        The list of director-script ids to be disable,
+   *        ["*"] will de-activate all the enable director-scripts
+   * @param Boolean reload
+   *        optionally reload the target window
+   */
+  disableByScriptIds: method(function(selectedIds, { reload }) {
+    if (selectedIds && selectedIds.length === 0) {
+      // filtered all director scripts ids
+      return;
+    }
+
+    for (let scriptId of this._directorScriptActorsMap.keys()) {
+      // filter director script ids
+      if (selectedIds.indexOf("*") < 0 &&
+          selectedIds.indexOf(scriptId) < 0) {
+        continue;
+      }
+
+      let actor = this._directorScriptActorsMap.get(scriptId);
+      this._directorScriptActorsMap.delete(scriptId);
+
+      // finalize the actor (which will produce director-script-detach event)
+      actor.finalize();
+      // unsubscribe event handlers on the disabled actor
+      off(actor);
+
+      this.unmanage(actor);
+    }
+
+    if (reload) {
+      this.tabActor.window.location.reload();
+    }
+  }, {
+    oneway: true,
+    request: {
+      selectedIds: Arg(0, "array:string"),
+      reload: Option(1, "boolean")
+    }
+  }),
+
+  /**
+   * Retrieves the actor instance of an installed director-script
+   * (and create the actor instance if it doesn't exists yet).
+   */
+  getByScriptId: method(function(scriptId) {
+    var id = scriptId;
+    // raise an unknown director-script id exception
+    if (!DirectorRegistry.checkInstalled(id)) {
+      console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id);
+      throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID);
+    }
+
+    // get a previous created actor instance
+    let actor = this._directorScriptActorsMap.get(id);
+
+    // create a new actor instance
+    if (!actor) {
+      let directorScriptDefinition = DirectorRegistry.get(id);
+
+      // test lazy director-script (e.g. uninstalled in the parent process)
+      if (!directorScriptDefinition) {
+
+        console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id);
+        throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID);
+      }
+
+      actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition);
+      this._directorScriptActorsMap.set(id, actor);
+
+      on(actor, "error", emit.bind(null, this, "director-script-error"));
+      on(actor, "attach", emit.bind(null, this, "director-script-attach"));
+      on(actor, "detach", emit.bind(null, this, "director-script-detach"));
+
+      this.manage(actor);
+    }
+
+    return actor;
+  }, {
+    request: {
+      scriptId: Arg(0, "string")
+    },
+    response: {
+      directorScript: RetVal("director-script")
+    }
+  }),
+
+  finalize: method(function() {
+    this.disableByScriptIds(["*"], false);
+  }, {
+    oneway: true
+  })
+});
+
+/**
+ * The corresponding Front object for the DirectorManagerActor.
+ */
+exports.DirectorManagerFront = protocol.FrontClass(DirectorManagerActor, {
+  initialize: function(client, { directorManagerActor }) {
+    protocol.Front.prototype.initialize.call(this, client, {
+      actor: directorManagerActor
+    });
+    this.manage(this);
+  }
+});
+
+/* private helpers */
+
+/**
+ * DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox
+ * to a target window.
+ */
+const DirectorScriptSandbox = Class({
+  initialize: function({scriptId, scriptCode, scriptOptions}) {
+    this._scriptId = scriptId;
+    this._scriptCode = scriptCode;
+    this._scriptOptions = scriptOptions;
+  },
+
+  attach: function(window, innerId) {
+    this._innerId = innerId,
+    this._window = window;
+    this._proto = Cu.createObjectIn(this._window);
+
+    var id = this._scriptId;
+    var uri = this._scriptCode;
+
+    this._sandbox = sandbox(window, {
+      sandboxName: uri,
+      sandboxPrototype: this._proto,
+      sameZoneAs: window,
+      wantXrays: true,
+      wantComponents: false,
+      wantExportHelpers: false,
+      metadata: {
+        URI: uri,
+        addonID: id,
+        SDKDirectorScript: true,
+        "inner-window-id": innerId
+      }
+    });
+
+    // create a CommonJS module object which match the interface from addon-sdk
+    // (addon-sdk/sources/lib/toolkit/loader.js#L678-L686)
+    var module = Cu.cloneInto(Object.create(null, {
+      id: { enumerable: true, value: id },
+      uri: { enumerable: true, value: uri },
+      exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) }
+    }), this._sandbox);
+
+    // create a console API object
+    let directorScriptConsole = new PlainTextConsole(null, this._innerId);
+
+    // inject CommonJS module globals into the sandbox prototype
+    Object.defineProperties(this._proto, {
+      module: { enumerable: true, value: module },
+      exports: { enumerable: true, value: module.exports },
+      console: {
+        enumerable: true,
+        value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    Object.defineProperties(this._sandbox, {
+      require: {
+        enumerable: true,
+        value: Cu.cloneInto(function() {
+          throw Error("NOT IMPLEMENTED");
+        }, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    // evaluate the director script source in the sandbox
+    evaluate(this._sandbox, this._scriptCode, this._scriptId);
+
+    // prepare the messageport connected to the debugger client
+    let { port1, port2 } = new this._window.MessageChannel();
+
+    // prepare the unload callbacks queue
+    var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = [];
+
+    // create the attach options
+    var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox);
+    Object.defineProperties(attachOptions, {
+      port: { enumerable: true, value: port1 },
+      window: { enumerable: true, value: window },
+      scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) },
+      onUnload: {
+        enumerable: true,
+        value: Cu.cloneInto(function (cb) {
+          // collect unload callbacks
+          if (typeof cb == "function") {
+            sandboxOnUnloadQueue.push(cb);
+          }
+        }, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    // select the attach method
+    var exports = this._proto.module.exports;
+    if ("attachMethod" in this._scriptOptions) {
+      this._sandboxOnAttach = exports[this._scriptOptions.attachMethod];
+    } else {
+      this._sandboxOnAttach = exports;
+    }
+
+    if (typeof this._sandboxOnAttach !== "function") {
+      throw Error("the configured attachMethod '" +
+                  (this._scriptOptions.attachMethod || "module.exports") +
+                  "' is not exported by the directorScript");
+    }
+
+    // call the attach method
+    this._sandboxOnAttach.call(this._sandbox, attachOptions);
+
+    return port2;
+  },
+  destroy:  function(onError) {
+    // evaluate queue unload methods if any
+    while(this._sandboxOnUnloadQueue.length > 0) {
+      let cb = this._sandboxOnUnloadQueue.pop();
+
+      try {
+        cb();
+      } catch(e) {
+        console.error("Exception on DirectorScript Sandbox destroy", e);
+        onError(e);
+      }
+    }
+
+    Cu.nukeSandbox(this._sandbox);
+  }
+});
+
+function getWindowID(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils)
+               .currentInnerWindowID;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/director-registry.js
@@ -0,0 +1,295 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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, Arg, Option, RetVal } = protocol;
+
+const {DebuggerServer} = require("devtools/server/main");
+
+/**
+ * Error Messages
+ */
+
+const ERR_DIRECTOR_INSTALL_TWICE = "Trying to install a director-script twice";
+const ERR_DIRECTOR_INSTALL_EMPTY = "Trying to install an empty director-script";
+const ERR_DIRECTOR_UNINSTALL_UNKNOWN = "Trying to uninstall an unkown director-script";
+
+const ERR_DIRECTOR_PARENT_UNKNOWN_METHOD = "Unknown parent process method";
+const ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD = "Unexpected call to notImplemented method";
+const ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES = "Unexpected multiple replies to called parent method";
+const ERR_DIRECTOR_CHILD_NO_REPLY = "Unexpected no reply to called parent method";
+
+/**
+ * Director Registry
+ */
+
+// Map of director scripts ids to director script definitions
+var gDirectorScripts = Object.create(null);
+
+const DirectorRegistry = exports.DirectorRegistry = {
+  /**
+   * Register a Director Script with the debugger server.
+   * @param id string
+   *    The ID of a director script.
+   * @param directorScriptDef object
+   *    The definition of a director script.
+   */
+  install: function (id, scriptDef) {
+    if (id in gDirectorScripts) {
+      console.error(ERR_DIRECTOR_INSTALL_TWICE,id);
+      return false;
+    }
+
+    if (!scriptDef) {
+      console.error(ERR_DIRECTOR_INSTALL_EMPTY, id);
+      return false;
+    }
+
+    gDirectorScripts[id] = scriptDef;
+
+    return true;
+  },
+
+  /**
+   * Unregister a Director Script with the debugger server.
+   * @param id string
+   *    The ID of a director script.
+   */
+  uninstall: function(id) {
+    if (id in gDirectorScripts) {
+      delete gDirectorScripts[id];
+
+      return true;
+    }
+
+    console.error(ERR_DIRECTOR_UNINSTALL_UNKNOWN, id);
+
+    return false;
+  },
+
+  /**
+   * Returns true if a director script id has been registered.
+   * @param id string
+   *    The ID of a director script.
+   */
+  checkInstalled: function (id) {
+    return (this.list().indexOf(id) >= 0);
+  },
+
+  /**
+   * Returns a registered director script definition by id.
+   * @param id string
+   *    The ID of a director script.
+   */
+  get: function(id) {
+    return gDirectorScripts[id];
+  },
+
+  /**
+   * Returns an array of registered director script ids.
+   */
+  list: function() {
+    return Object.keys(gDirectorScripts);
+  },
+
+  /**
+   * Removes all the registered director scripts.
+   */
+  clear: function() {
+   gDirectorScripts = Object.create(null);
+  }
+};
+
+/**
+ * E10S parent/child setup helpers
+ */
+
+let gTrackedMessageManager = new Set();
+
+exports.setupParentProcess = function setupParentProcess({mm, childID}) {
+  // prevents multiple subscriptions on the same messagemanager
+  if (gTrackedMessageManager.has(mm)) {
+    return;
+  }
+  gTrackedMessageManager.add(mm);
+
+  // listen for director-script requests from the child process
+  mm.addMessageListener("debug:director-registry-request", handleChildRequest);
+
+  DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);
+
+  /* parent process helpers */
+
+  function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
+    // filter out not subscribed message managers
+    if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) {
+      return;
+    }
+
+    gTrackedMessageManager.delete(mm);
+
+    // unregister for director-script requests handlers from the parent process (if any)
+    mm.removeMessageListener("debug:director-registry-request", handleChildRequest);
+  }
+
+  function handleChildRequest(msg) {
+    switch (msg.json.method) {
+    case "get":
+      return DirectorRegistry.get(msg.json.args[0]);
+    case "list":
+      return DirectorRegistry.list();
+    default:
+      console.error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD, msg.json.method);
+      throw new Error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD);
+    }
+  }
+};
+
+// skip child setup if this actor module is not running in a child process
+if (DebuggerServer.isInChildProcess) {
+  setupChildProcess();
+}
+
+function setupChildProcess() {
+  const { sendSyncMessage } = DebuggerServer.parentMessageManager;
+
+  DebuggerServer.setupInParent({
+    module: "devtools/server/actors/director-registry",
+    setupParent: "setupParentProcess"
+  });
+
+  DirectorRegistry.install = notImplemented.bind(null, "install");
+  DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall");
+  DirectorRegistry.clear = notImplemented.bind(null, "clear");
+
+  DirectorRegistry.get = callParentProcess.bind(null, "get");
+  DirectorRegistry.list = callParentProcess.bind(null, "list");
+
+  /* child process helpers */
+
+  function notImplemented(method) {
+    console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method);
+    throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD);
+  }
+
+  function callParentProcess(method, ...args) {
+    var reply = sendSyncMessage("debug:director-registry-request", {
+      method: method,
+      args: args
+    });
+
+    if (reply.length === 0) {
+      console.error(ERR_DIRECTOR_CHILD_NO_REPLY);
+      throw Error(ERR_DIRECTOR_CHILD_NO_REPLY);
+    } else if (reply.length > 1) {
+      console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
+      throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
+    }
+
+    return reply[0];
+  };
+};
+
+/**
+ * The DirectorRegistry Actor is a global actor which manages install/uninstall of
+ * director scripts definitions.
+ */
+const DirectorRegistryActor = exports.DirectorRegistryActor = protocol.ActorClass({
+  typeName: "director-registry",
+
+  /* init & destroy methods */
+  initialize: function(conn, parentActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  finalize: method(function() {
+    // nothing to cleanup
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Install a new director-script definition.
+   *
+   * @param String id
+   *        The director-script definition identifier.
+   * @param String scriptCode
+   *        The director-script javascript source.
+   * @param Object scriptOptions
+   *        The director-script option object.
+   */
+  install: method(function(id, { scriptCode, scriptOptions }) {
+    // TODO: add more checks on id format?
+    if (!id || id.length === 0) {
+      throw Error("director-script id is mandatory");
+    }
+
+    if (!scriptCode) {
+      throw Error("director-script scriptCode is mandatory");
+    }
+
+    return DirectorRegistry.install(id, {
+      scriptId: id,
+      scriptCode: scriptCode,
+      scriptOptions: scriptOptions
+    });
+  }, {
+    request: {
+      scriptId: Arg(0, "string"),
+      scriptCode: Option(1, "string"),
+      scriptOptions: Option(1, "nullable:json")
+    },
+    response: {
+      success: RetVal("boolean")
+    }
+  }),
+
+  /**
+   * Uninstall a director-script definition.
+   *
+   * @param String id
+   *        The identifier of the director-script definition to be removed
+   */
+  uninstall: method(function (id) {
+    return DirectorRegistry.uninstall(id);
+  }, {
+    request: {
+      scritpId: Arg(0, "string")
+    },
+    response: {
+      success: RetVal("boolean")
+    }
+  }),
+
+  /**
+   * Retrieves the list of installed director-scripts.
+   */
+  list: method(function () {
+    return DirectorRegistry.list();
+  }, {
+    response: {
+      directorScripts: RetVal("array:string")
+    }
+  })
+});
+
+/**
+ * The corresponding Front object for the DirectorRegistryActor.
+ */
+exports.DirectorRegistryFront = protocol.FrontClass(DirectorRegistryActor, {
+  initialize: function(client, { directorRegistryActor }) {
+    protocol.Front.prototype.initialize.call(this, client, {
+      actor: directorRegistryActor
+    });
+    this.manage(this);
+  }
+});
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -153,16 +153,18 @@ RootActor.prototype = {
     // Whether the style rule actor implements the modifySelector method
     // that modifies the rule's selector
     selectorEditable: true,
     // Whether the page style actor implements the addNewRule method that
     // adds new rules to the page
     addNewRule: true,
     // Whether the dom node actor implements the getUniqueSelector method
     getUniqueSelector: true,
+    // Whether the director scripts are supported
+    directorScripts: true,
     // Whether the debugger server supports
     // blackboxing/pretty-printing (not supported in Fever Dream yet)
     noBlackBoxing: false,
     noPrettyPrinting: false,
     // Whether the page style actor implements the getUsedFontFaces method
     // that returns the font faces used on a node
     getUsedFontFaces: true
   },
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -384,16 +384,21 @@ var DebuggerServer = {
       constructor: "WebappsActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/device", {
       prefix: "device",
       constructor: "DeviceActor",
       type: { global: true }
     });
+    this.registerModule("devtools/server/actors/director-registry", {
+      prefix: "directorRegistry",
+      constructor: "DirectorRegistryActor",
+      type: { global: true }
+    });
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
     // In case of apps being loaded in parent process, DebuggerServer is already
     // initialized and browser actors are already loaded,
@@ -499,16 +504,21 @@ var DebuggerServer = {
       constructor: "MonitorActor",
       type: { global: true, tab: true }
     });
     this.registerModule("devtools/server/actors/timeline", {
       prefix: "timeline",
       constructor: "TimelineActor",
       type: { global: true, tab: true }
     });
+    this.registerModule("devtools/server/actors/director-manager", {
+      prefix: "directorManager",
+      constructor: "DirectorManagerActor",
+      type: { global: false, tab: true }
+    });
     if ("nsIProfiler" in Ci) {
       this.registerModule("devtools/server/actors/profiler", {
         prefix: "profiler",
         constructor: "ProfilerActor",
         type: { global: true, tab: true }
       });
     }
     this.registerModule("devtools/server/actors/animation", {
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -37,16 +37,18 @@ EXTRA_JS_MODULES.devtools.server.actors 
     'actors/animation.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/director-manager.js',
+    'actors/director-registry.js',
     'actors/eventlooplag.js',
     'actors/framerate.js',
     'actors/gcli.js',
     'actors/highlighter.js',
     'actors/inspector.js',
     'actors/layout.js',
     'actors/memory.js',
     'actors/monitor.js',
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 support-files =
+  director-helpers.js
+  director-script-target.html
   inspector-helpers.js
   inspector-styles-data.css
   inspector-styles-data.html
   inspector-traversal-data.html
   nonchrome_unsafeDereference.html
   inspector_getImageData.html
   large-image.jpg
   memory-helpers.js
@@ -68,10 +70,13 @@ skip-if = buildapp == 'mulet'
 [test_memory_allocations_05.html]
 [test_memory_attach_01.html]
 [test_memory_attach_02.html]
 [test_memory_census.html]
 [test_memory_gc_01.html]
 [test_preference.html]
 [test_connectToChild.html]
 skip-if = buildapp == 'mulet'
+[test_director.html]
+[test_director_connectToChild.html]
++skip-if = buildapp == 'mulet'
 [test_attachProcess.html]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/director-helpers.js
@@ -0,0 +1,100 @@
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+
+const Services = devtools.require("Services");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true);
+
+SimpleTest.registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+  Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled");
+});
+
+const {Class} = devtools.require("sdk/core/heritage");
+
+const {promiseInvoke} = devtools.require("devtools/async-utils");
+
+const { DirectorRegistry,
+        DirectorRegistryFront } = devtools.require("devtools/server/actors/director-registry");
+
+const { DirectorManagerFront } = devtools.require("devtools/server/actors/director-manager");
+const protocol = devtools.require("devtools/server/protocol");
+
+const {Task} = devtools.require("resource://gre/modules/Task.jsm");
+
+/***********************************
+ *  director helpers functions
+ **********************************/
+
+function waitForEvent(target, name) {
+  return new Promise((resolve, reject) => {
+      target.once(name, (...args) => { resolve(args); });
+  });
+}
+
+function* newConnectedDebuggerClient(opts) {
+  var transport = DebuggerServer.connectPipe();
+  var client = new DebuggerClient(transport);
+
+  yield promiseInvoke(client, client.connect);
+
+  var root = yield promiseInvoke(client, client.listTabs);
+
+  return {
+    client: client,
+    root: root,
+    transport: transport
+  };
+}
+
+function* installTestDirectorScript(client, root,  scriptId, scriptDefinition) {
+  var directorRegistryClient = new DirectorRegistryFront(client, root);
+
+  yield directorRegistryClient.install(scriptId, scriptDefinition);
+
+  directorRegistryClient.destroy();
+}
+
+function* getTestDirectorScript(manager, tab, scriptId) {
+  var directorScriptClient = yield manager.getByScriptId(scriptId);
+  return directorScriptClient;
+}
+
+function purgeInstalledDirectorScripts() {
+  DirectorRegistry.clear();
+}
+
+function* installDirectorScriptAndWaitAttachOrError({client, root, manager,
+                                                     scriptId, scriptDefinition}) {
+  yield installTestDirectorScript(client, root, scriptId, scriptDefinition);
+
+  var selectedTab = root.tabs[root.selected];
+  var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, scriptId);
+
+  var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+  var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+  testDirectorScriptClient.setup({reload: false});
+
+  var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
+                                            waitForDirectorScriptError]);
+
+  testDirectorScriptClient.finalize();
+
+  return receivedEvent;
+}
+
+function assertIsDirectorScriptError(error) {
+  ok(!!error, "received error should be defined");
+  ok(!!error.message, "errors should contain a message");
+  ok(!!error.stack, "errors should contain a stack trace");
+  ok(!!error.fileName, "errors should contain a fileName");
+  ok(typeof error.columnNumber == "number", "errors should contain a columnNumber");
+  ok(typeof error.lineNumber == "number", "errors should contain a lineNumber");
+
+  ok(!!error.directorScriptId, "errors should contain a directorScriptId");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/director-script-target.html
@@ -0,0 +1,15 @@
+<html>
+  <head>
+    <script>
+      // change the eval function to ensure the window object in the debug-script is correctly wrapped
+      window.eval = function () {
+        return "unsecure-eval-called";
+      };
+
+      var globalAccessibleVar = "global-value";
+    </script>
+  </head>
+  <body>
+    <h1>debug script target</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_director.html
@@ -0,0 +1,479 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+  <script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  Task.spawn(function* () {
+    SimpleTest.waitForExplicitFinish();
+
+    var tests = [
+      runDirectorScriptModuleExports,
+      runDirectorScriptErrorOnNoAttachExports,
+      runDirectorScriptErrorOnLoadTest,
+      runDirectorScriptErrorOnRequire,
+      runDirectorScriptErrorOnUnloadTest,
+      runDirectorScriptSetupAndReceiveMessagePortTest,
+      runDirectorEnableDirectorScriptsTest,
+      runDirectorScriptDetachEventTest,
+      runDirectorScriptWindowEval
+    ].map((testCase) => {
+      return function* () {
+        setup();
+        yield testCase().then(null, (e) => {
+          console.error("Exception during testCase run", e);
+          ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+        });
+
+        teardown();
+      };
+    });
+
+    for (var test of tests) {
+      yield test();
+    }
+  }).then(
+    function success() {
+      SimpleTest.finish()
+    },
+    function error(e) {
+      console.error("Exception during testCase run", e);
+      ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+
+      SimpleTest.finish();
+    }
+  );
+};
+
+var targetWin = null;
+
+function setup() {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+
+    SimpleTest.registerCleanupFunction(teardown);
+  }
+}
+
+function teardown() {
+  purgeInstalledDirectorScripts();
+
+  DebuggerServer.destroy();
+  if (targetWin) {
+    targetWin.close();
+  }
+}
+
+/***********************************
+ *  test cases
+ **********************************/
+
+function runDirectorScriptModuleExports() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptModuleExports = {
+    scriptCode: "(" + (function() {
+       module.exports = function() {};
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  var testDirectorScriptAttachMethodOption = {
+    scriptCode: "(" + (function() {
+       exports.attach = function() {};
+    }).toString() + ")();",
+    scriptOptions: {
+       attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var receivedEvent1 = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptModuleExports",
+      scriptDefinition: testDirectorScriptModuleExports
+    });
+    ok(!!receivedEvent1.port, "received attach from testDirectorScriptModuleExports");
+
+    var receivedEvent2 = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptAttachMethodOption",
+      scriptDefinition: testDirectorScriptModuleExports
+    });
+    ok(!!receivedEvent2.port, "received attach event from testDirectorScriptAttachMethodOption");
+
+    client.close();
+   })
+}
+
+function runDirectorScriptErrorOnNoAttachExports() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnNoAttachExports = {
+    scriptCode: "(" + (function() {
+      // this director script should raise an error
+      // because it doesn't export any attach method
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var error = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptRaiseErrorOnNoAttachExports",
+      scriptDefinition: testDirectorScriptRaiseErrorOnNoAttachExports
+    });
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnRequire() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnRequire = {
+    scriptCode: "(" + (function() {
+      // this director script should raise an error
+      // because require raise a "not implemented" exception
+      console.log("PROVA", this)
+      require("fake_module");
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var error = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptRaiseErrorOnRequire",
+      scriptDefinition: testDirectorScriptRaiseErrorOnRequire
+    });
+
+    assertIsDirectorScriptError(error);
+    is(error.message, "Error: NOT IMPLEMENTED", "error message should contains the expected error message");
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnLoadTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnLoad = {
+    scriptCode: "(" + (function() {
+       // this will raise an exception on evaluating
+       // the director script
+       raise.an_error.during.content_script.load();
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptRaiseErrorOnLoad);
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+
+    var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+    // activate the director script without window reloading
+    testDirectorScriptClient.setup({reload: false});
+
+    var [error] = yield waitForDirectorScriptError;
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnUnloadTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnUnload = {
+    scriptCode: "(" + (function() {
+       module.exports = function({onUnload}) {
+         // this will raise an exception on unload the director script
+         onUnload(function() {
+           raise_an_error_onunload();
+         });
+       };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptRaiseErrorOnUnload);
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+    var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+
+    // activate the director script without window reloading
+    testDirectorScriptClient.setup({reload: false});
+
+    yield waitForDirectorScriptAttach;
+
+    var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+    testDirectorScriptClient.finalize();
+
+    var [error] = yield waitForDirectorScriptError;
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+
+function runDirectorScriptSetupAndReceiveMessagePortTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+        module.exports = function({port}) {
+          port.onmessage = function(evt) {
+            // echo messages
+            evt.source.postMessage(evt.data);
+          };
+        };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // get a testDirectorScriptClient
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+
+    var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+
+    // activate the director script without window reloading
+    // (and wait for attach)
+    testDirectorScriptClient.setup({reload: false});
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    // call the connectPort method to get a MessagePortClient
+    var port = attachEvent.port;
+
+    ok(!!port && !!port.postMessage, "messageport actor client received");
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortMessage = waitForEvent(port, "message");
+    // needs to explicit start the port
+    port.start();
+
+    var msg = { k1: "v1", k2: [1, 2, 3] };
+    port.postMessage(msg);
+
+    var reply = yield waitForMessagePortMessage;
+
+    ok(JSON.stringify(reply[0].data) === JSON.stringify(msg),
+       "echo reply received on the MessagePortClient");
+
+    yield client.close();
+  });
+}
+
+function runDirectorEnableDirectorScriptsTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+      module.exports = function({port}) {
+        port.onmessage = function(evt) {
+          // echo messages
+          evt.source.postMessage(evt.data);
+        };
+      };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+
+    tabDirectorClient.enableByScriptIds(["*"], { reload: false });
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    is(attachEvent.directorScriptId, "testDirectorScript", "attach event should contains directorScriptId");
+
+    yield client.close();
+  });
+}
+
+function runDirectorScriptDetachEventTest() {
+  targetWin = window.open("director-script-target.html");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+        exports.attach = function({port, onUnload}) {
+            onUnload(function() {
+              port.postMessage("ONUNLOAD");
+            });
+        };
+    }).toString() + ")();",
+    scriptOptions: {
+      attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // NOTE: tab needs to be attached to receive director-script-detach events
+    yield promiseInvoke(client, client.attachTab, selectedTab.actor);
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+    var waitForDirectorScriptDetach = waitForEvent(tabDirectorClient, "director-script-detach");
+
+    tabDirectorClient.enableByScriptIds(["*"], {reload: true});
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortEvent = waitForEvent(attachEvent.port, "message");
+    // needs to explicit start the port
+    attachEvent.port.start();
+
+    tabDirectorClient.disableByScriptIds(["*"], {reload: false});
+
+    // changing the window location should generate a director-script-detach event
+    var [detachEvent] = yield waitForDirectorScriptDetach;
+
+    is(detachEvent.directorScriptId, "testDirectorScript", "detach event should contains directorScriptId");
+
+    var [portEvent] = yield waitForMessagePortEvent;
+
+    is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach");
+
+    yield client.close();
+  });
+}
+
+function runDirectorScriptWindowEval() {
+  targetWin = window.open("http://mochi.test:8888/chrome/toolkit/devtools/server/tests/mochitest/director-script-target.html");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+      exports.attach = function({window, port}) {
+        var onpageloaded = function() {
+          var globalVarValue = window.eval("window.globalAccessibleVar;");
+          port.postMessage(globalVarValue);
+        };
+
+        if (window.document.readyState === "complete") {
+          onpageloaded();
+        } else {
+          window.onload = onpageloaded;
+        }
+      };
+    }).toString() + ")();",
+    scriptOptions: {
+      attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // NOTE: tab needs to be attached to receive director-script-detach events
+    yield promiseInvoke(client, client.attachTab, selectedTab.actor);
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+    var waitForDirectorScriptError = waitForEvent(tabDirectorClient, "director-script-error");
+
+    tabDirectorClient.enableByScriptIds(["*"], {reload: false});
+
+    var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
+                                              waitForDirectorScriptError]);
+
+    ok(!!receivedEvent.port, "received director-script-attach");
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortEvent = waitForEvent(receivedEvent.port, "message");
+    // needs to explicit start the port
+    receivedEvent.port.start();
+
+    var [portEvent] = yield waitForMessagePortEvent;
+
+    ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe");
+
+    is(portEvent.data, "global-value", "window.globalAccessibleVar should be accessible through window.eval");
+
+    yield client.close();
+  });
+}
+
+  </script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_director_connectToChild.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+  <script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  Task.spawn(function* () {
+    SimpleTest.waitForExplicitFinish();
+
+    var tests = [
+      runPropagateDirectorScriptsToChildTest,
+    ].map((testCase) => {
+      return function* () {
+        setup();
+        yield testCase().then(null, (e) => {
+          ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+        });
+
+        teardown();
+      };
+    });
+
+    for (var test of tests) {
+      yield test();
+    }
+
+    SimpleTest.finish();
+  });
+};
+
+function setup() {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+    SimpleTest.registerCleanupFunction(function() {
+      DebuggerServer.destroy();
+    });
+  }
+}
+
+function teardown() {
+  purgeInstalledDirectorScripts();
+  DebuggerServer.destroy();
+}
+
+/***********************************
+ *  test cases
+ **********************************/
+
+function runPropagateDirectorScriptsToChildTest() {
+  let iframe = document.createElement("iframe");
+  iframe.mozbrowser = true;
+
+  document.body.appendChild(iframe);
+
+  return Task.spawn(function* () {
+    var { client, root, transport } = yield newConnectedDebuggerClient();
+
+    var directorRegistryClient = new DirectorRegistryFront(client, root);
+
+    // install a director script
+    yield directorRegistryClient.install("testPropagatedDirectorScript", {
+      scriptCode: "console.log('director script test');",
+      scriptOptions: {}
+    });
+
+    var conn = transport._serverConnection;
+    var childActor = yield DebuggerServer.connectToChild(conn, iframe);
+
+    ok(typeof childActor.directorManagerActor !== "undefined",
+       "childActor.directorActor should be defined");
+
+    var childDirectorManagerClient = new DirectorManagerFront(client, childActor);
+
+    var directorScriptList = yield childDirectorManagerClient.list();
+
+    ok(directorScriptList.installed.length === 1 &&
+       directorScriptList.installed[0] === "testPropagatedDirectorScript",
+       "director scripts propagated correctly")
+
+    yield client.close();
+  });
+}
+  </script>
+</pre>
+</body>
+</html>