Bug 1302702 - Make WebExtension Addon Debugging oop-compatible. draft
authorLuca Greco <lgreco@mozilla.com>
Tue, 21 Mar 2017 15:55:35 +0100
changeset 579749 c786ebbd4598d836bf3c9347cdf6906b65baa663
parent 579748 7bddadb79113a19eb345873bf1af3c5c9e156e83
child 579750 a3e1180c480e2e6e764747e6bb3a25a0c0def884
push id59363
push userluca.greco@alcacoop.it
push dateWed, 17 May 2017 19:17:20 +0000
bugs1302702
milestone55.0a1
Bug 1302702 - Make WebExtension Addon Debugging oop-compatible. This patch applies all the changes needed to the devtools actors and the toolbox-process-window, to be able to debug a webextension running in an extension child process (as well as a webextension running in the main process). The devtools actor used to debug a webextension is splitted into 3 actors: - the WebExtensionActor is the actor that is created when the "root.listTabs" RDP request is received, it provides the addon metadata (name, icon and addon id) and two RDP methods: - reload: used to reload the addon (e.g. from the "about:debugging#addons" page) - connectAddonDebuggingActor: which provides the actorID of the actor that is connected to the process where the extension is running (used by toolbox-process-window.js to connect the toolbox to the needed devtools actors, e.g. console, inspector etc.) - the WebExtensionParentActor is the actor that connects to the process where the extension is running and ensures that a WebExtensionChildActor instance is created and connected (this actor is only the entrypoint to reach the WebExtensionChildActor, and so it does not provide any RDP request on its own, it only connect itself to its child counterpart and then it returns the RDP "form" of the child actor, and the client is then connected directly to the child actor) - the WebExtensionChildActor is the actor that is running in the same process of the target extension, and it provides the same requestTypes of a tab actor. By splitting the WebExtensionActor from the WebExtensionParentActor, we are able to prevent the RemoteDebuggingServer to connect (and create instances of the WebExtensionChildActor) for every addon listed by a root.listAddons() request. MozReview-Commit-ID: L1vxhA6xQkD
devtools/client/framework/connect/connect.js
devtools/client/framework/target.js
devtools/client/framework/toolbox-process-window.js
devtools/server/actors/moz.build
devtools/server/actors/root.js
devtools/server/actors/tab.js
devtools/server/actors/webbrowser.js
devtools/server/actors/webextension-parent.js
devtools/server/actors/webextension.js
devtools/server/child.js
devtools/server/main.js
devtools/shared/specs/moz.build
devtools/shared/specs/webextension-parent.js
--- a/devtools/client/framework/connect/connect.js
+++ b/devtools/client/framework/connect/connect.js
@@ -159,18 +159,19 @@ var onConnectionReady = Task.async(funct
   }
 });
 
 /**
  * Build one button for an add-on actor.
  */
 function buildAddonLink(addon, parent) {
   let a = document.createElement("a");
-  a.onclick = function () {
-    openToolbox(addon, true, "jsdebugger", false);
+  a.onclick = async function () {
+    const isTabActor = addon.isWebExtension;
+    openToolbox(addon, true, "webconsole", isTabActor);
   };
 
   a.textContent = addon.name;
   a.title = addon.id;
   a.href = "#";
 
   parent.appendChild(a);
 }
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -346,41 +346,41 @@ TabTarget.prototype = {
     return this._url;
   },
 
   get isRemote() {
     return !this.isLocalTab;
   },
 
   get isAddon() {
-    return !!(this._form && this._form.actor && (
-      this._form.actor.match(/conn\d+\.addon\d+/) ||
-      this._form.actor.match(/conn\d+\.webExtension\d+/)
-    ));
+    return !!(this._form && this._form.actor &&
+              this._form.actor.match(/conn\d+\.addon\d+/)) || this.isWebExtension;
   },
 
   get isWebExtension() {
-    return !!(this._form && this._form.actor &&
-              this._form.actor.match(/conn\d+\.webExtension\d+/));
+    return !!(this._form && this._form.actor && (
+      this._form.actor.match(/conn\d+\.webExtension\d+/) ||
+      this._form.actor.match(/child\d+\/webExtension\d+/)
+    ));
   },
 
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isMultiProcess() {
     return !this.window;
   },
 
   /**
    * Adds remote protocol capabilities to the target, so that it can be used
    * for tools that support the Remote Debugging Protocol even for local
    * connections.
    */
-  makeRemote: function () {
+  makeRemote: async function () {
     if (this._remote) {
       return this._remote.promise;
     }
 
     this._remote = defer();
 
     if (this.isLocalTab) {
       // Since a remote protocol connection will be made, let's start the
@@ -393,16 +393,32 @@ TabTarget.prototype = {
       // directly with actors living in the child process.
       // We also need browser actors for actor registry which enabled addons
       // to register custom actors.
       DebuggerServer.registerActors({ root: true, browser: true, tab: false });
 
       this._client = new DebuggerClient(DebuggerServer.connectPipe());
       // A local TabTarget will never perform chrome debugging.
       this._chrome = false;
+    } else if (this._form.isWebExtension &&
+          this.client.mainRoot.traits.webExtensionAddonConnect) {
+      // The addonActor form is related to a WebExtensionParentActor instance,
+      // which isn't a tab actor on its own, it is an actor living in the parent process
+      // with access to the addon metadata, it can control the addon (e.g. reloading it)
+      // and listen to the AddonManager events related to the lifecycle of the addon
+      // (e.g. when the addon is disabled or uninstalled ).
+      // To retrieve the TabActor instance, we call its "connect" method,
+      // (which fetches the TabActor form from a WebExtensionChildActor instance).
+      let {form} = await this._client.request({
+        to: this._form.actor, type: "connect",
+      });
+
+      this._form = form;
+      this._url = form.url;
+      this._title = form.title;
     }
 
     this._setupRemoteListeners();
 
     let attachTab = () => {
       this._client.attachTab(this._form.actor, (response, tabClient) => {
         if (!tabClient) {
           this._remote.reject("Unable to attach to the tab");
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -41,27 +41,21 @@ var connect = Task.async(function*() {
   });
   gClient = new DebuggerClient(transport);
   yield gClient.connect();
   let addonID = getParameterByName("addonID");
 
   if (addonID) {
     let { addons } = yield gClient.listAddons();
     let addonActor = addons.filter(addon => addon.id === addonID).pop();
-    openToolbox({
-      form: addonActor,
-      chrome: true,
-      isTabActor: addonActor.isWebExtension ? true : false
-    });
+    let isTabActor = addonActor.isWebExtension;
+    openToolbox({form: addonActor, chrome: true, isTabActor});
   } else {
     let response = yield gClient.getProcess();
-    openToolbox({
-      form: response.form,
-      chrome: true
-    });
+    openToolbox({form: response.form, chrome: true});
   }
 });
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -57,16 +57,17 @@ DevToolsModules(
     'styles.js',
     'stylesheets.js',
     'tab.js',
     'timeline.js',
     'webaudio.js',
     'webbrowser.js',
     'webconsole.js',
     'webextension-inspected-window.js',
+    'webextension-parent.js',
     'webextension.js',
     'webgl.js',
     'window.js',
     'worker-list.js',
     'worker.js',
 )
 
 with Files('animation.js'):
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -185,17 +185,20 @@ RootActor.prototype = {
     // Whether or not `getProfile()` supports specifying a `startTime`
     // and `endTime` to filter out samples. Fx40+
     profilerDataFilterable: true,
     // Whether or not the MemoryActor's heap snapshot abilities are
     // fully equipped to handle heap snapshots for the memory tool. Fx44+
     heapSnapshots: true,
     // Whether or not the timeline actor can emit DOMContentLoaded and Load
     // markers, currently in use by the network monitor. Fx45+
-    documentLoadingMarkers: true
+    documentLoadingMarkers: true,
+    // Whether or not the webextension addon actor have to be connected
+    // to retrieve the extension child process tab actors.
+    webExtensionAddonConnect: true,
   },
 
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function () {
     return {
       from: this.actorID,
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -592,16 +592,22 @@ TabActor.prototype = {
 
     // We watch for all child docshells under the current document,
     this._progressListener.watch(this.docShell);
 
     // And list all already existing ones.
     this._updateChildDocShells();
   },
 
+  _unwatchDocShell(docShell) {
+    if (this._progressListener) {
+      this._progressListener.unwatch(docShell);
+    }
+  },
+
   onSwitchToFrame(request) {
     let windowId = request.windowId;
     let win;
 
     try {
       win = Services.wm.getOuterWindowWithId(windowId);
     } catch (e) {
       // ignore
@@ -695,61 +701,93 @@ TabActor.prototype = {
       if (this._isRootDocShell(docShell)) {
         this._progressListener.watch(docShell);
       }
       this._notifyDocShellsUpdate([docShell]);
     });
   },
 
   _onDocShellDestroy(docShell) {
+    // Stop watching this docshell (the unwatch() method will check if we
+    // started watching it before).
+    this._unwatchDocShell(docShell);
+
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     this._notifyDocShellDestroy(webProgress);
+
+    if (webProgress.DOMWindow == this._originalWindow) {
+      // If the original top level document we connected to is removed,
+      // we try to switch to any other top level document
+      let rootDocShells = this.docShells
+                              .filter(d => {
+                                return d != this.docShell &&
+                                       this._isRootDocShell(d);
+                              });
+      if (rootDocShells.length > 0) {
+        let newRoot = rootDocShells[0];
+        this._originalWindow = newRoot.DOMWindow;
+        this._changeTopLevelDocument(this._originalWindow);
+      } else {
+        // If for some reason (typically during Firefox shutdown), the original
+        // document is destroyed, and there is no other top level docshell,
+        // we detach the tab actor to unregister all listeners and prevent any
+        // exception
+        this.exit();
+      }
+      return;
+    }
+
+    // If the currently targeted context is destroyed,
+    // and we aren't on the top-level document,
+    // we have to switch to the top-level one.
+    if (webProgress.DOMWindow == this.window &&
+        this.window != this._originalWindow) {
+      this._changeTopLevelDocument(this._originalWindow);
+    }
   },
 
   _isRootDocShell(docShell) {
     // Should report as root docshell:
     //  - New top level window's docshells, when using ChromeActor against a
     // process. It allows tracking iframes of the newly opened windows
     // like Browser console or new browser windows.
     //  - MozActivities or window.open frames on B2G, where a new root docshell
     // is spawn in the child process of the app.
     return !docShell.parent;
   },
 
+  _docShellToWindow(docShell) {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    let window = webProgress.DOMWindow;
+    let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils)
+                   .outerWindowID;
+    let parentID = undefined;
+    // Ignore the parent of the original document on non-e10s firefox,
+    // as we get the xul window as parent and don't care about it.
+    if (window.parent && window != this._originalWindow) {
+      parentID = window.parent
+                       .QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils)
+                       .outerWindowID;
+    }
+
+    return {
+      id,
+      parentID,
+      url: window.location.href,
+      title: window.document.title,
+    };
+  },
+
   // Convert docShell list to windows objects list being sent to the client
   _docShellsToWindows(docshells) {
-    return docshells.map(docShell => {
-      let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIWebProgress);
-      let window = webProgress.DOMWindow;
-      let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                     .getInterface(Ci.nsIDOMWindowUtils)
-                     .outerWindowID;
-      let parentID = undefined;
-      // Ignore the parent of the original document on non-e10s firefox,
-      // as we get the xul window as parent and don't care about it.
-      if (window.parent && window != this._originalWindow) {
-        parentID = window.parent
-                         .QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils)
-                         .outerWindowID;
-      }
-
-      // Collect the addonID from the document origin attributes.
-      let addonID = window.document.nodePrincipal.addonId;
-
-      return {
-        id,
-        parentID,
-        addonID,
-        url: window.location.href,
-        title: window.document.title,
-      };
-    });
+    return docshells.map(docShell => this._docShellToWindow(docShell));
   },
 
   _notifyDocShellsUpdate(docshells) {
     let windows = this._docShellsToWindows(docshells);
 
     // Do not send the `frameUpdate` event if the windows array is empty.
     if (windows.length == 0) {
       return;
@@ -775,51 +813,16 @@ TabActor.prototype = {
     this.conn.send({
       from: this.actorID,
       type: "frameUpdate",
       frames: [{
         id,
         destroy: true
       }]
     });
-
-    // Stop watching this docshell (the unwatch() method will check if we
-    // started watching it before).
-    webProgress.QueryInterface(Ci.nsIDocShell);
-    this._progressListener.unwatch(webProgress);
-
-    if (webProgress.DOMWindow == this._originalWindow) {
-      // If the original top level document we connected to is removed,
-      // we try to switch to any other top level document
-      let rootDocShells = this.docShells
-                              .filter(d => {
-                                return d != this.docShell &&
-                                       this._isRootDocShell(d);
-                              });
-      if (rootDocShells.length > 0) {
-        let newRoot = rootDocShells[0];
-        this._originalWindow = newRoot.DOMWindow;
-        this._changeTopLevelDocument(this._originalWindow);
-      } else {
-        // If for some reason (typically during Firefox shutdown), the original
-        // document is destroyed, and there is no other top level docshell,
-        // we detach the tab actor to unregister all listeners and prevent any
-        // exception
-        this.exit();
-      }
-      return;
-    }
-
-    // If the currently targeted context is destroyed,
-    // and we aren't on the top-level document,
-    // we have to switch to the top-level one.
-    if (webProgress.DOMWindow == this.window &&
-        this.window != this._originalWindow) {
-      this._changeTopLevelDocument(this._originalWindow);
-    }
   },
 
   _notifyDocShellDestroyAll() {
     this.conn.send({
       from: this.actorID,
       type: "frameUpdate",
       destroyAll: true
     });
@@ -861,17 +864,17 @@ TabActor.prototype = {
   _detach() {
     if (!this.attached) {
       return false;
     }
 
     // Check for docShell availability, as it can be already gone
     // during Firefox shutdown.
     if (this.docShell) {
-      this._progressListener.unwatch(this.docShell);
+      this._unwatchDocShell(this.docShell);
       this._restoreDocumentSettings();
     }
     if (this._progressListener) {
       this._progressListener.destroy();
       this._progressListener = null;
       this._originalWindow = null;
 
       // Removes the observers being set in _watchDocShells
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -9,17 +9,17 @@
 var { Ci } = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
 var { DebuggerServer } = require("devtools/server/main");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
 loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
 loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
-loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
+loader.lazyRequireGetter(this, "WebExtensionParentActor", "devtools/server/actors/webextension-parent", true);
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker-list", true);
 loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker-list", true);
 loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
 /**
  * Browser-specific actors.
  */
@@ -830,26 +830,28 @@ function BrowserAddonList(connection) {
 
 BrowserAddonList.prototype.getList = function () {
   let deferred = promise.defer();
   AddonManager.getAllAddons((addons) => {
     for (let addon of addons) {
       let actor = this._actorByAddonId.get(addon.id);
       if (!actor) {
         if (addon.isWebExtension) {
-          actor = new WebExtensionActor(this._connection, addon);
+          actor = new WebExtensionParentActor(this._connection, addon);
         } else {
           actor = new BrowserAddonActor(this._connection, addon);
         }
 
         this._actorByAddonId.set(addon.id, actor);
       }
     }
+
     deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
   });
+
   return deferred.promise;
 };
 
 Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
   enumerable: true,
   configurable: true,
   get() {
     return this._onListChanged;
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/webextension-parent.js
@@ -0,0 +1,210 @@
+/* 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 {DebuggerServer} = require("devtools/server/main");
+const protocol = require("devtools/shared/protocol");
+const {webExtensionSpec} = require("devtools/shared/specs/webextension-parent");
+
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
+
+/**
+ * Creates the actor that represents the addon in the parent process, which connects
+ * itself to a WebExtensionChildActor counterpart which is created in the
+ * extension process (or in the main process if the WebExtensions OOP mode is disabled).
+ *
+ * The WebExtensionParentActor subscribes itself as an AddonListener on the AddonManager
+ * and forwards this events to child actor (e.g. on addon reload or when the addon is
+ * uninstalled completely) and connects to the child extension process using a `browser`
+ * element provided by the extension internals (it is not related to any single extension,
+ * but it will be created automatically to the currently selected "WebExtensions OOP mode"
+ * and it persist across the extension reloads (it is destroyed once the actor exits).
+ * WebExtensionActor is a child of RootActor, it can be retrieved via
+ * RootActor.listAddons request.
+ *
+ * @param {DebuggerServerConnection} conn
+ *        The connection to the client.
+ * @param {AddonWrapper} addon
+ *        The target addon.
+ */
+const WebExtensionParentActor = protocol.ActorClassWithSpec(webExtensionSpec, {
+  initialize(conn, addon) {
+    this.conn = conn;
+    this.addon = addon;
+    this.id = addon.id;
+    this._childFormPromise = null;
+
+    AddonManager.addAddonListener(this);
+  },
+
+  destroy() {
+    AddonManager.removeAddonListener(this);
+
+    this.addon = null;
+    this._childFormPromise = null;
+
+    if (this._destroyProxyChildActor) {
+      this._destroyProxyChildActor();
+      delete this._destroyProxyChildActor;
+    }
+  },
+
+  setOptions() {
+    // NOTE: not used anymore for webextensions, still used in the legacy addons,
+    // addon manager is currently going to call it automatically on every addon.
+  },
+
+  reload() {
+    return this.addon.reload().then(() => {
+      return {};
+    });
+  },
+
+  form() {
+    return {
+      actor: this.actorID,
+      id: this.id,
+      name: this.addon.name,
+      iconURL: this.addon.iconURL,
+      debuggable: this.addon.isDebuggable,
+      temporarilyInstalled: this.addon.temporarilyInstalled,
+      isWebExtension: true,
+    };
+  },
+
+  connect() {
+    if (this._childFormPormise) {
+      return this._childFormPromise;
+    }
+
+    let proxy = new ProxyChildActor(this.conn, this);
+    this._childFormPromise = proxy.connect().then(form => {
+      // Merge into the child actor form, some addon metadata
+      // (e.g. the addon name shown in the addon debugger window title).
+      return Object.assign(form, {
+        id: this.addon.id,
+        name: this.addon.name,
+        iconURL: this.addon.iconURL,
+        // Set the isOOP attribute on the connected child actor form.
+        isOOP: proxy.isOOP,
+      });
+    });
+    this._destroyProxyChildActor = () => proxy.destroy();
+
+    return this._childFormPromise;
+  },
+
+  // ProxyChildActor callbacks.
+
+  onProxyChildActorDestroy() {
+    // Invalidate the cached child actor and form Promise
+    // if the child actor exits.
+    this._childFormPromise = null;
+    delete this._destroyProxyChildActor;
+  },
+
+  // AddonManagerListener callbacks.
+
+  onInstalled(addon) {
+    if (addon.id != this.id) {
+      return;
+    }
+
+    // Update the AddonManager's addon object on reload/update.
+    this.addon = addon;
+  },
+
+  onUninstalled(addon) {
+    if (addon != this.addon) {
+      return;
+    }
+
+    this.destroy();
+  },
+});
+
+exports.WebExtensionParentActor = WebExtensionParentActor;
+
+function ProxyChildActor(connection, parentActor) {
+  this._conn = connection;
+  this._parentActor = parentActor;
+  this.addonId = parentActor.id;
+
+  this._onChildExit = this._onChildExit.bind(this);
+
+  this._form = null;
+  this._browser = null;
+  this._childActorID = null;
+}
+
+ProxyChildActor.prototype = {
+  /**
+   * Connect the webextension child actor.
+   */
+  async connect() {
+    if (this._browser) {
+      throw new Error("This actor is already connected to the extension process");
+    }
+
+    // Called when the debug browser element has been destroyed
+    // (no actor is using it anymore to connect the child extension process).
+    const onDestroy = this.destroy.bind(this);
+
+    this._browser = await ExtensionParent.DebugUtils.getExtensionProcessBrowser(this);
+
+    this._form = await DebuggerServer.connectToChild(this._conn, this._browser, onDestroy,
+                                                     {addonId: this.addonId});
+
+    this._childActorID = this._form.actor;
+
+    // Exit the proxy child actor if the child actor has been destroyed.
+    this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit);
+
+    return this._form;
+  },
+
+  get isOOP() {
+    return this._browser ? this._browser.isRemoteBrowser : undefined;
+  },
+
+  get _mm() {
+    return this._browser && (
+      this._browser.messageManager ||
+      this._browser.frameLoader.messageManager);
+  },
+
+  destroy() {
+    if (this._mm) {
+      this._mm.removeMessageListener("debug:webext_child_exit", this._onChildExit);
+
+      this._mm.sendAsyncMessage("debug:webext_parent_exit", {
+        actor: this._childActorID,
+      });
+
+      ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this);
+    }
+
+    if (this._parentActor) {
+      this._parentActor.onProxyChildActorDestroy();
+    }
+
+    this._parentActor = null;
+    this._browser = null;
+    this._childActorID = null;
+    this._form = null;
+  },
+
+  /**
+   * Handle the child actor exit.
+   */
+  _onChildExit(msg) {
+    if (msg.json.actor !== this._childActorID) {
+      return;
+    }
+
+    this.destroy();
+  },
+};
--- a/devtools/server/actors/webextension.js
+++ b/devtools/server/actors/webextension.js
@@ -1,333 +1,374 @@
 /* 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 { Ci, Cu } = require("chrome");
+const { Ci, Cu, Cc } = require("chrome");
 const Services = require("Services");
+
 const { ChromeActor } = require("./chrome");
 const makeDebugger = require("./utils/make-debugger");
 
-var DevToolsUtils = require("devtools/shared/DevToolsUtils");
-var { assert } = DevToolsUtils;
-
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 
-loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
-loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
-
 const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
 
 /**
  * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
- * add-on.
+ * add-on running in a child extension process.
  * Most of the implementation is inherited from ChromeActor (which inherits most of its
  * implementation from TabActor).
- * WebExtensionActor is a child of RootActor, it can be retrieved via
- * RootActor.listAddons request.
- * WebExtensionActor exposes all tab actors via its form() request, like TabActor.
+ * WebExtensionChildActor is created by a WebExtensionParentActor counterpart, when its
+ * parent actor's `connect` method has been called (on the listAddons RDP package),
+ * it runs in the same process that the extension is running into (which can be the main
+ * process if the extension is running in non-oop mode, or the child extension process
+ * if the extension is running in oop-mode).
+ *
+ * A WebExtensionChildActor contains all tab actors, like a regular ChromeActor
+ * or TabActor.
  *
  * History lecture:
- * The add-on actors used to not inherit TabActor because of the different way the
+ * - The add-on actors used to not inherit TabActor because of the different way the
  * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger
  * has only a sub-set of the feature available in the Tab or in the Browser Toolbox.
- * In a WebExtensions add-on all the provided contexts (background and popup pages etc.),
+ * - In a WebExtensions add-on all the provided contexts (background, popups etc.),
  * besides the Content Scripts which run in the content process, hooked to an existent
  * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can
  * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which
  * filters the visible sources and frames to the one that are related to the target
  * add-on).
+ * - When the WebExtensions OOP mode has been introduced, this actor has been refactored
+ * and moved from the main process to the new child extension process.
  *
- * @param conn DebuggerServerConnection
+ * @param {DebuggerServerConnection} conn
  *        The connection to the client.
- * @param addon AddonWrapper
- *        The target addon.
+ * @param {nsIMessageSender} chromeGlobal.
+ *        The chromeGlobal where this actor has been injected by the
+ *        DebuggerServer.connectToChild method.
+ * @param {string} prefix
+ *        the custom RDP prefix to use.
+ * @param {string} addonId
+ *        the addonId of the target WebExtension.
  */
-function WebExtensionActor(conn, addon) {
+function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) {
   ChromeActor.call(this, conn);
 
-  this.id = addon.id;
-  this.addon = addon;
+  this._chromeGlobal = chromeGlobal;
+  this._prefix = prefix;
+  this.id = addonId;
 
   // Bind the _allowSource helper to this, it is used in the
   // TabActor to lazily create the TabSources instance.
   this._allowSource = this._allowSource.bind(this);
+  this._onParentExit = this._onParentExit.bind(this);
+
+  this._chromeGlobal.addMessageListener("debug:webext_parent_exit", this._onParentExit);
 
   // Set the consoleAPIListener filtering options
   // (retrieved and used in the related webconsole child actor).
   this.consoleAPIListenerOptions = {
-    addonId: addon.id,
+    addonId: this.id,
   };
 
+  this.aps = Cc["@mozilla.org/addons/policy-service;1"]
+               .getService(Ci.nsIAddonPolicyService);
+
   // This creates a Debugger instance for debugging all the add-on globals.
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: dbg => {
       return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
     },
     shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
   });
 
-  // Discover the preferred debug global for the target addon
-  this.preferredTargetWindow = null;
-  this._findAddonPreferredTargetWindow();
+  // Try to discovery an existent extension page to attach (which will provide the initial
+  // URL shown in the window tittle when the addon debugger is opened).
+  let extensionWindow = this._searchForExtensionWindow();
 
-  AddonManager.addAddonListener(this);
+  if (extensionWindow) {
+    this._setWindow(extensionWindow);
+  }
 }
-exports.WebExtensionActor = WebExtensionActor;
+exports.WebExtensionChildActor = WebExtensionChildActor;
 
-WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
+WebExtensionChildActor.prototype = Object.create(ChromeActor.prototype);
 
-WebExtensionActor.prototype.actorPrefix = "webExtension";
-WebExtensionActor.prototype.constructor = WebExtensionActor;
+WebExtensionChildActor.prototype.actorPrefix = "webExtension";
+WebExtensionChildActor.prototype.constructor = WebExtensionChildActor;
 
 // NOTE: This is needed to catch in the webextension webconsole all the
 // errors raised by the WebExtension internals that are not currently
 // associated with any window.
-WebExtensionActor.prototype.isRootActor = true;
-
-WebExtensionActor.prototype.form = function () {
-  assert(this.actorID, "addon should have an actorID.");
-
-  let baseForm = ChromeActor.prototype.form.call(this);
-
-  return Object.assign(baseForm, {
-    actor: this.actorID,
-    id: this.id,
-    name: this.addon.name,
-    url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
-    iconURL: this.addon.iconURL,
-    debuggable: this.addon.isDebuggable,
-    temporarilyInstalled: this.addon.temporarilyInstalled,
-    isWebExtension: this.addon.isWebExtension,
-  });
-};
-
-WebExtensionActor.prototype._attach = function () {
-  // NOTE: we need to be sure that `this.window` can return a
-  // window before calling the ChromeActor.onAttach, or the TabActor
-  // will not be subscribed to the child doc shell updates.
-
-  // If a preferredTargetWindow exists, set it as the target for this actor
-  // when the client request to attach this actor.
-  if (this.preferredTargetWindow) {
-    this._setWindow(this.preferredTargetWindow);
-  } else {
-    this._createFallbackWindow();
-  }
-
-  // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
-  ChromeActor.prototype._attach.apply(this);
-};
-
-WebExtensionActor.prototype._detach = function () {
-  this._destroyFallbackWindow();
-
-  // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
-  ChromeActor.prototype._detach.apply(this);
-};
+WebExtensionChildActor.prototype.isRootActor = true;
 
 /**
  * Called when the actor is removed from the connection.
  */
-WebExtensionActor.prototype.exit = function () {
-  AddonManager.removeAddonListener(this);
+WebExtensionChildActor.prototype.exit = function () {
+  if (this._chromeGlobal) {
+    let chromeGlobal = this._chromeGlobal;
+    this._chromeGlobal = null;
 
-  this.preferredTargetWindow = null;
+    chromeGlobal.removeMessageListener("debug:webext_parent_exit", this._onParentExit);
+
+    chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
+      actor: this.actorID
+    });
+  }
+
   this.addon = null;
   this.id = null;
 
   return ChromeActor.prototype.exit.apply(this);
 };
 
-// Addon Specific Remote Debugging requestTypes and methods.
-
-/**
- * Reloads the addon.
- */
-WebExtensionActor.prototype.onReload = function () {
-  return this.addon.reload()
-    .then(() => {
-      // send an empty response
-      return {};
-    });
-};
-
-/**
- * Set the preferred global for the add-on (called from the AddonManager).
- */
-WebExtensionActor.prototype.setOptions = function (addonOptions) {
-  if ("global" in addonOptions) {
-    // Set the proposed debug global as the preferred target window
-    // (the actor will eventually set it as the target once it is attached)
-    this.preferredTargetWindow = addonOptions.global;
-  }
-};
-
-// AddonManagerListener callbacks.
+// Private helpers.
 
-WebExtensionActor.prototype.onInstalled = function (addon) {
-  if (addon.id != this.id) {
-    return;
-  }
-
-  // Update the AddonManager's addon object on reload/update.
-  this.addon = addon;
-};
-
-WebExtensionActor.prototype.onUninstalled = function (addon) {
-  if (addon != this.addon) {
-    return;
-  }
-
-  this.exit();
-};
-
-WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) {
-  if (addon != this.addon) {
-    return;
-  }
-
-  // Refresh the preferred debug global on disabled/reloaded/upgraded addon.
-  if (changedPropNames.includes("debugGlobal")) {
-    this._findAddonPreferredTargetWindow();
-  }
-};
-
-// Private helpers
-
-WebExtensionActor.prototype._createFallbackWindow = function () {
+WebExtensionChildActor.prototype._createFallbackWindow = function () {
   if (this.fallbackWindow) {
     // Skip if there is already an existent fallback window.
     return;
   }
 
   // Create an empty hidden window as a fallback (e.g. the background page could be
   // not defined for the target add-on or not yet when the actor instance has been
   // created).
   this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
-  this.fallbackWebNav.loadURI(
-    `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`,
-    0, null, null, null
-  );
-
-  this.fallbackDocShell = this.fallbackWebNav
-    .QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDocShell);
 
-  Object.defineProperty(this, "docShell", {
-    value: this.fallbackDocShell,
-    configurable: true
-  });
+  // Save the reference to the fallback DOMWindow.
+  this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
+                                           .getInterface(Ci.nsIDOMWindow);
 
-  // Save the reference to the fallback DOMWindow
-  this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                                             .getInterface(Ci.nsIDOMWindow);
+  // Insert the fallback doc message.
+  this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
 };
 
-WebExtensionActor.prototype._destroyFallbackWindow = function () {
+WebExtensionChildActor.prototype._destroyFallbackWindow = function () {
   if (this.fallbackWebNav) {
     // Explicitly close the fallback windowless browser to prevent it to leak
     // (and to prevent it to freeze devtools xpcshell tests).
     this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
     this.fallbackWebNav.close();
 
     this.fallbackWebNav = null;
     this.fallbackWindow = null;
   }
 };
 
-/**
- * Discover the preferred debug global and switch to it if the addon has been attached.
- */
-WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
-  return new Promise(resolve => {
-    let activeAddon = XPIProvider.activeAddons.get(this.id);
+// Discovery an extension page to use as a default target window.
+// NOTE: This currently fail to discovery an extension page running in a
+// windowless browser when running in non-oop mode, and the background page
+// is set later using _onNewExtensionWindow.
+WebExtensionChildActor.prototype._searchForExtensionWindow = function () {
+  let e = Services.ww.getWindowEnumerator(null);
+  while (e.hasMoreElements()) {
+    let window = e.getNext();
+
+    if (window.document.nodePrincipal.addonId == this.id) {
+      return window;
+    }
+  }
+
+  return undefined;
+};
+
+// Customized ChromeActor/TabActor hooks.
 
-    if (!activeAddon) {
-      // The addon is not active, the background page is going to be destroyed,
-      // navigate to the fallback window (if it already exists).
-      resolve(null);
-    } else {
-      AddonManager.getAddonByInstanceID(activeAddon.instanceID)
-        .then(privateWrapper => {
-          let targetWindow = privateWrapper.getDebugGlobal();
+WebExtensionChildActor.prototype._onDocShellDestroy = function (docShell) {
+  // Stop watching this docshell (the unwatch() method will check if we
+  // started watching it before).
+  this._unwatchDocShell(docShell);
+
+  // Let the _onDocShellDestroy notify that the docShell has been destroyed.
+  let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIWebProgress);
+  this._notifyDocShellDestroy(webProgress);
+
+  // If the destroyed docShell was the current docShell and the actor is
+  // currently attached, switch to the fallback window
+  if (this.attached && docShell == this.docShell) {
+    // Creates a fallback window if it doesn't exist yet.
+    this._createFallbackWindow();
+    this._changeTopLevelDocument(this.fallbackWindow);
+  }
+};
+
+WebExtensionChildActor.prototype._onNewExtensionWindow = function (window) {
+  if (!this.window || this.window === this.fallbackWindow) {
+    this._changeTopLevelDocument(window);
+  }
+};
 
-          // Do not use the preferred global if it is not a DOMWindow as expected.
-          if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
-            targetWindow = null;
-          }
+WebExtensionChildActor.prototype._attach = function () {
+  // NOTE: we need to be sure that `this.window` can return a
+  // window before calling the ChromeActor.onAttach, or the TabActor
+  // will not be subscribed to the child doc shell updates.
 
-          resolve(targetWindow);
-        });
+  if (!this.window || this.window.document.nodePrincipal.addonId !== this.id) {
+    // Discovery an existent extension page to attach.
+    let extensionWindow = this._searchForExtensionWindow();
+
+    if (!extensionWindow) {
+      this._createFallbackWindow();
+      this._setWindow(this.fallbackWindow);
+    } else {
+      this._setWindow(extensionWindow);
     }
-  }).then(preferredTargetWindow => {
-    this.preferredTargetWindow = preferredTargetWindow;
+  }
+
+  // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
+  ChromeActor.prototype._attach.apply(this);
+};
+
+WebExtensionChildActor.prototype._detach = function () {
+  // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
+  ChromeActor.prototype._detach.apply(this);
+
+  // Stop watching for new extension windows.
+  this._destroyFallbackWindow();
+};
 
-    if (!preferredTargetWindow) {
-      // Create a fallback window if no preferred target window has been found.
-      this._createFallbackWindow();
-    } else if (this.attached) {
-      // Change the top level document if the actor is already attached.
-      this._changeTopLevelDocument(preferredTargetWindow);
-    }
+/**
+ * Return the json details related to a docShell.
+ */
+WebExtensionChildActor.prototype._docShellToWindow = function (docShell) {
+  const baseWindowDetails = ChromeActor.prototype._docShellToWindow.call(this, docShell);
+
+  let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIWebProgress);
+  let window = webProgress.DOMWindow;
+
+  // Collect the addonID from the document origin attributes and its sameType top level
+  // frame.
+  let addonID = window.document.nodePrincipal.addonId;
+  let sameTypeRootAddonID = docShell.QueryInterface(Ci.nsIDocShellTreeItem)
+                                    .sameTypeRootTreeItem
+                                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                                    .getInterface(Ci.nsIDOMWindow)
+                                    .document.nodePrincipal.addonId;
+
+  return Object.assign(baseWindowDetails, {
+    addonID,
+    sameTypeRootAddonID,
   });
 };
 
 /**
  * Return an array of the json details related to an array/iterator of docShells.
  */
-WebExtensionActor.prototype._docShellsToWindows = function (docshells) {
+WebExtensionChildActor.prototype._docShellsToWindows = function (docshells) {
   return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
                     .filter(windowDetails => {
-                      // filter the docShells based on the addon id
-                      return windowDetails.addonID == this.id;
+                      // Filter the docShells based on the addon id of the window or
+                      // its sameType top level frame.
+                      return windowDetails.addonID === this.id ||
+                             windowDetails.sameTypeRootAddonID === this.id;
                     });
 };
 
+WebExtensionChildActor.prototype.isExtensionWindow = function (window) {
+  return window.document.nodePrincipal.addonId == this.id;
+};
+
+WebExtensionChildActor.prototype.isExtensionWindowDescendent = function (window) {
+  // Check if the source is coming from a descendant docShell of an extension window.
+  let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDocShell);
+  let rootWin = docShell.sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
+                                             .getInterface(Ci.nsIDOMWindow);
+  return this.isExtensionWindow(rootWin);
+};
+
 /**
  * Return true if the given source is associated with this addon and should be
  * added to the visible sources (retrieved and used by the webbrowser actor module).
  */
-WebExtensionActor.prototype._allowSource = function (source) {
+WebExtensionChildActor.prototype._allowSource = function (source) {
+  // Use the source.element to detect the allowed source, if any.
+  if (source.element) {
+    let domEl = unwrapDebuggerObjectGlobal(source.element);
+    return (this.isExtensionWindow(domEl.ownerGlobal) ||
+            this.isExtensionWindowDescendent(domEl.ownerGlobal));
+  }
+
+  // Fallback to check the uri if there is no source.element associated to the source.
+
+  // Retrieve the first component of source.url in the form "url1 -> url2 -> ...".
+  let url = source.url.split(" -> ").pop();
+
+  // Filter out the code introduced by evaluating code in the webconsole.
+  if (url === "debugger eval code") {
+    return false;
+  }
+
+  let uri;
+
+  // Try to decode the url.
   try {
-    let uri = Services.io.newURI(source.url);
-    let addonID = mapURIToAddonID(uri);
+    uri = Services.io.newURI(url);
+  } catch (err) {
+    Cu.reportError(`Unexpected invalid url: ${url}`);
+    return false;
+  }
+
+  // Filter out resource and chrome sources (which are related to the loaded internals).
+  if (["resource", "chrome", "file"].includes(uri.scheme)) {
+    return false;
+  }
+
+  try {
+    let addonID = this.aps.extensionURIToAddonId(uri);
 
     return addonID == this.id;
-  } catch (e) {
+  } catch (err) {
+    // extensionURIToAddonId raises an exception on non-extension URLs.
     return false;
   }
 };
 
 /**
  * Return true if the given global is associated with this addon and should be
  * added as a debuggee, false otherwise.
  */
-WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
+WebExtensionChildActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
   const global = unwrapDebuggerObjectGlobal(newGlobal);
 
   if (global instanceof Ci.nsIDOMWindow) {
-    return global.document.nodePrincipal.addonId == this.id;
+    // Filter out any global which contains a XUL document.
+    if (global.document instanceof Ci.nsIDOMXULDocument) {
+      return false;
+    }
+
+    // Change top level document as a simulated frame switching.
+    if (global.document.ownerGlobal && this.isExtensionWindow(global)) {
+      this._onNewExtensionWindow(global.document.ownerGlobal);
+    }
+
+    return global.document.ownerGlobal &&
+           this.isExtensionWindowDescendent(global.document.ownerGlobal);
   }
 
   try {
     // This will fail for non-Sandbox objects, hence the try-catch block.
     let metadata = Cu.getSandboxMetadata(global);
     if (metadata) {
       return metadata.addonID === this.id;
     }
   } catch (e) {
     // Unable to retrieve the sandbox metadata.
   }
 
   return false;
 };
 
-/**
- * Override WebExtensionActor requestTypes:
- * - redefined `reload`, which should reload the target addon
- *   (instead of the entire browser as the regular ChromeActor does).
- */
-WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload;
+// Handlers for the messages received from the parent actor.
+
+WebExtensionChildActor.prototype._onParentExit = function (msg) {
+  if (msg.json.actor !== this.actorID) {
+    return;
+  }
+
+  this.exit();
+};
--- a/devtools/server/child.js
+++ b/devtools/server/child.js
@@ -12,39 +12,48 @@ try {
   // Encapsulate in its own scope to allows loading this frame script more than once.
   (function () {
     const Cu = Components.utils;
     const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
     const DevToolsUtils = require("devtools/shared/DevToolsUtils");
     const { dumpn } = DevToolsUtils;
     const { DebuggerServer, ActorPool } = require("devtools/server/main");
-    const { ContentActor } = require("devtools/server/actors/childtab");
 
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
     }
     // We want a special server without any root actor and only tab actors.
     // We are going to spawn a ContentActor instance in the next few lines,
     // it is going to act like a root actor without being one.
     DebuggerServer.registerActors({ root: false, browser: false, tab: true });
 
     let connections = new Map();
 
     let onConnect = DevToolsUtils.makeInfallible(function (msg) {
       removeMessageListener("debug:connect", onConnect);
 
       let mm = msg.target;
       let prefix = msg.data.prefix;
+      let addonId = msg.data.addonId;
 
       let conn = DebuggerServer.connectToParent(prefix, mm);
       conn.parentMessageManager = mm;
       connections.set(prefix, conn);
 
-      let actor = new ContentActor(conn, chromeGlobal, prefix);
+      let actor;
+
+      if (addonId) {
+        const { WebExtensionChildActor } = require("devtools/server/actors/webextension");
+        actor = new WebExtensionChildActor(conn, chromeGlobal, prefix, addonId);
+      } else {
+        const { ContentActor } = require("devtools/server/actors/childtab");
+        actor = new ContentActor(conn, chromeGlobal, prefix);
+      }
+
       let actorPool = new ActorPool(conn);
       actorPool.addActor(actor);
       conn.addActorPool(actorPool);
 
       sendAsyncMessage("debug:actor", {actor: actor.form(), prefix: prefix});
     });
 
     addMessageListener("debug:connect", onConnect);
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -1004,17 +1004,17 @@ var DebuggerServer = {
    * @param function [onDestroy]
    *        Optional function to invoke when the child process closes
    *        or the connection shuts down. (Need to forget about the
    *        related TabActor)
    * @return object
    *         A promise object that is resolved once the connection is
    *         established.
    */
-  connectToChild(connection, frame, onDestroy) {
+  connectToChild(connection, frame, onDestroy, {addonId} = {}) {
     let deferred = SyncPromise.defer();
 
     // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
     // or else fallback to asking the frameLoader itself.
     let mm = frame.messageManager || frame.frameLoader.messageManager;
     mm.loadFrameScript("resource://devtools/server/child.js", false);
 
     let trackMessageManager = () => {
@@ -1117,16 +1117,19 @@ var DebuggerServer = {
       });
 
       if (childTransport) {
         childTransport.swapBrowser(mm);
       }
     };
 
     let destroy = DevToolsUtils.makeInfallible(function () {
+      events.off(connection, "closed", destroy);
+      Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
+
       // provides hook to actor modules that need to exchange messages
       // between e10s parent and child processes
       parentModules.forEach(mod => {
         if (mod.onDisconnected) {
           mod.onDisconnected();
         }
       });
       // TODO: Remove this deprecated path once it's no longer needed by add-ons.
@@ -1163,18 +1166,16 @@ var DebuggerServer = {
       }
 
       if (onDestroy) {
         onDestroy(mm);
       }
 
       // Cleanup all listeners
       untrackMessageManager();
-      Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
-      events.off(connection, "closed", destroy);
     });
 
     // Listen for various messages and frame events
     trackMessageManager();
 
     // Listen for app process exit
     let onMessageManagerClose = function (subject, topic, data) {
       if (subject == mm) {
@@ -1183,17 +1184,17 @@ var DebuggerServer = {
     };
     Services.obs.addObserver(onMessageManagerClose,
                              "message-manager-close");
 
     // Listen for connection close to cleanup things
     // when user unplug the device or we lose the connection somehow.
     events.on(connection, "closed", destroy);
 
-    mm.sendAsyncMessage("debug:connect", { prefix });
+    mm.sendAsyncMessage("debug:connect", { prefix, addonId });
 
     return deferred.promise;
   },
 
   /**
    * Create a new debugger connection for the given transport. Called after
    * connectPipe(), from connectToParent, or from an incoming socket
    * connection handler.
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -38,11 +38,12 @@ DevToolsModules(
     'storage.js',
     'string.js',
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
     'timeline.js',
     'webaudio.js',
     'webextension-inspected-window.js',
+    'webextension-parent.js',
     'webgl.js',
     'worker.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/webextension-parent.js
@@ -0,0 +1,24 @@
+/* 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 {RetVal, generateActorSpec} = require("devtools/shared/protocol");
+
+const webExtensionSpec = generateActorSpec({
+  typeName: "webExtensionAddon",
+
+  methods: {
+    reload: {
+      request: { },
+      response: { addon: RetVal("json") },
+    },
+
+    connect: {
+      request: { },
+      response: { form: RetVal("json") },
+    },
+  },
+});
+
+exports.webExtensionSpec = webExtensionSpec;