Bug 1285557 - Create a WebExtensionAddonActor based on ChromeActor and TabActor. r=jryans draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 25 Jul 2016 16:28:49 +0200
changeset 392960 9b8306225d09623d5fdba95cae332a3452ecb83a
parent 392959 5d4c9e9ae90a2824eef4cc6ed175e175e43d1125
child 392961 b27e7eb0c81974c927b01c1b3923de5e5f201a72
push id24157
push userluca.greco@alcacoop.it
push dateTue, 26 Jul 2016 16:01:12 +0000
reviewersjryans
bugs1285557
milestone50.0a1
Bug 1285557 - Create a WebExtensionAddonActor based on ChromeActor and TabActor. r=jryans MozReview-Commit-ID: 70sLUzqHHsl
.eslintignore
devtools/client/framework/attach-thread.js
devtools/client/framework/target.js
devtools/client/framework/toolbox-process-window.js
devtools/client/framework/toolbox.js
devtools/server/actors/addon.js
devtools/server/actors/moz.build
devtools/server/actors/webbrowser.js
devtools/server/actors/webconsole.js
devtools/server/actors/webextension.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -111,16 +111,17 @@ devtools/client/webide/**
 !devtools/client/webide/components/webideCli.js
 devtools/server/**
 !devtools/server/child.js
 !devtools/server/css-logic.js
 !devtools/server/main.js
 !devtools/server/actors/inspector.js
 !devtools/server/actors/highlighters/eye-dropper.js
 !devtools/server/actors/webbrowser.js
+!devtools/server/actors/webextension.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 !devtools/server/actors/csscoverage.js
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
 !devtools/shared/event-emitter.js
 !devtools/shared/task.js
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -84,26 +84,26 @@ function attachThread(toolbox) {
           box.PRIORITY_WARNING_HIGH
         );
       }
 
       deferred.resolve(threadClient);
     });
   };
 
-  if (target.isAddon) {
-    // Attaching an addon
+  if (target.isTabActor) {
+    // Attaching a tab, a browser process, or a WebExtensions add-on.
+    target.activeTab.attachThread(threadOptions, handleResponse);
+  } else if (target.isAddon) {
+    // Attaching a legacy addon.
     target.client.attachAddon(actor, res => {
       target.client.attachThread(res.threadActor, handleResponse);
     });
-  } else if (target.isTabActor) {
-    // Attaching a normal thread
-    target.activeTab.attachThread(threadOptions, handleResponse);
-  } else {
-    // Attaching the browser debugger
+  }  else {
+    // Attaching an old browser debugger or a content process.
     target.client.attachThread(chromeDebugger, handleResponse);
   }
 
   return deferred.promise;
 }
 
 function detachThread(threadClient) {
   threadClient.removeListener("paused");
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -343,18 +343,25 @@ 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+/)
+    ));
+  },
+
+  get isWebExtension() {
     return !!(this._form && this._form.actor &&
-              this._form.actor.match(/conn\d+\.addon\d+/));
+              this._form.actor.match(/conn\d+\.webExtension\d+/));
   },
 
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isMultiProcess() {
     return !this.window;
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -39,17 +39,21 @@ var connect = Task.async(function*() {
   });
   gClient = new DebuggerClient(transport);
   gClient.connect().then(() => {
     let addonID = getParameterByName("addonID");
 
     if (addonID) {
       gClient.listAddons(({addons}) => {
         let addonActor = addons.filter(addon => addon.id === addonID).pop();
-        openToolbox({ form: addonActor, chrome: true, isTabActor: false });
+        openToolbox({
+          form: addonActor,
+          chrome: true,
+          isTabActor: addonActor.isWebExtension ? true : false
+        });
       });
     } else {
       gClient.getProcess().then(aResponse => {
         openToolbox({ form: aResponse.form, chrome: true });
       });
     }
   });
 });
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -923,17 +923,17 @@ Toolbox.prototype = {
       Services.focus.moveFocus(win, elm, type, 0);
     });
   },
 
   /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
   _buildButtons: function () {
-    if (!this.target.isAddon) {
+    if (!this.target.isAddon || this.target.isWebExtension) {
       this._buildPickerButton();
     }
 
     this.setToolboxButtonsVisibility();
 
     // Old servers don't have a GCLI Actor, so just return
     if (!this.target.hasActor("gcli")) {
       return promise.resolve();
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -129,16 +129,19 @@ BrowserAddonActor.prototype = {
 
   onUninstalled: function BAA_onUninstalled(aAddon) {
     if (aAddon != this._addon) {
       return;
     }
 
     if (this.attached) {
       this.onDetach();
+
+      // The BrowserAddonActor is not a TabActor and it has to send
+      // "tabDetached" directly to close the devtools toolbox window.
       this.conn.send({ from: this.actorID, type: "tabDetached" });
     }
 
     this.disconnect();
   },
 
   onAttach: function BAA_onAttach() {
     if (this.exited) {
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -57,11 +57,12 @@ DevToolsModules(
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
     'timeline.js',
     'webapps.js',
     'webaudio.js',
     'webbrowser.js',
     'webconsole.js',
+    'webextension.js',
     'webgl.js',
     'worker.js',
 )
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -18,16 +18,17 @@ var DevToolsUtils = require("devtools/sh
 var { assert } = DevToolsUtils;
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
 
 loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
 loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
+loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
@@ -884,16 +885,24 @@ function TabActor(connection) {
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
+  // Optional console API listener options (e.g. used by the WebExtensionActor to
+  // filter console messages by addonID), set to an empty (no options) object by default.
+  consoleAPIListenerOptions: {},
+
+  // Optional TabSources filter function (e.g. used by the WebExtensionActor to filter
+  // sources by addonID), allow all sources by default.
+  _allowSource() { return true; },
+
   get exited() {
     return this._exited;
   },
 
   get attached() {
     return !!this._attached;
   },
 
@@ -1054,17 +1063,17 @@ TabActor.prototype = {
     }
     // Abrupt closing of the browser window may leave callbacks without a
     // currentURI.
     return null;
   },
 
   get sources() {
     if (!this._sources) {
-      this._sources = new TabSources(this.threadActor);
+      this._sources = new TabSources(this.threadActor, this._allowSource);
     }
     return this._sources;
   },
 
   /**
    * This is called by BrowserTabList.getList for existing tab actors prior to
    * calling |form| below.  It can be used to do any async work that may be
    * needed to assemble the form.
@@ -1359,27 +1368,38 @@ TabActor.prototype = {
       // 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.originAttributes.addonId;
+
       return {
-        id: id,
+        id,
+        parentID,
+        addonID,
         url: window.location.href,
         title: window.document.title,
-        parentID: parentID
       };
     });
   },
 
   _notifyDocShellsUpdate(docshells) {
     let windows = this._docShellsToWindows(docshells);
+
+    // Do not send the `frameUpdate` event if the windows array is empty.
+    if (windows.length == 0) {
+      return;
+    }
+
     this.conn.send({ from: this.actorID,
                      type: "frameUpdate",
                      frames: windows
                    });
   },
 
   _updateChildDocShells() {
     this._notifyDocShellsUpdate(this.docShells);
@@ -2262,17 +2282,22 @@ 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) {
-        actor = new BrowserAddonActor(this._connection, addon);
+        if (addon.isWebExtension) {
+          actor = new WebExtensionActor(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;
 };
 
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -589,18 +589,21 @@ WebConsoleActor.prototype =
             this.consoleServiceListener =
               new ConsoleServiceListener(window, this);
             this.consoleServiceListener.init();
           }
           startedListeners.push(listener);
           break;
         case "ConsoleAPI":
           if (!this.consoleAPIListener) {
+            // Create the consoleAPIListener (and apply the filtering options defined
+            // in the parent actor).
             this.consoleAPIListener =
-              new ConsoleAPIListener(window, this);
+              new ConsoleAPIListener(window, this,
+                                     this.parentActor.consoleAPIListenerOptions);
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           if (!this.networkMonitor) {
             // Create a StackTraceCollector that's going to be shared both by the
             // NetworkMonitorChild (getting messages about requests from parent) and
copy from devtools/server/actors/chrome.js
copy to devtools/server/actors/webextension.js
--- a/devtools/server/actors/chrome.js
+++ b/devtools/server/actors/webextension.js
@@ -1,185 +1,333 @@
 /* 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 } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const Services = require("Services");
-const { DebuggerServer } = require("../main");
-const { getChildDocShells, TabActor } = require("./webbrowser");
+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 chrome content in the
- * current process. Most of the implementation is inherited from TabActor.
- * ChromeActor is a child of RootActor, it can be instanciated via
- * RootActor.getProcess request.
- * ChromeActor exposes all tab actors via its form() request, like TabActor.
+ * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
+ * add-on.
+ * 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.
  *
  * History lecture:
- * All tab actors used to also be registered as global actors,
- * so that the root actor was also exposing tab actors for the main process.
- * Tab actors ended up having RootActor as parent actor,
- * but more and more features of the tab actors were relying on TabActor.
- * So we are now exposing a process actor that offers the same API as TabActor
- * by inheriting its functionality.
- * Global actors are now only the actors that are meant to be global,
- * and are no longer related to any specific scope/document.
+ * 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.),
+ * 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).
  *
- * @param aConnection DebuggerServerConnection
+ * @param conn DebuggerServerConnection
  *        The connection to the client.
+ * @param addon AddonWrapper
+ *        The target addon.
  */
-function ChromeActor(aConnection) {
-  TabActor.call(this, aConnection);
+function WebExtensionActor(conn, addon) {
+  ChromeActor.call(this, conn);
+
+  this.id = addon.id;
+  this.addon = addon;
+
+  // 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 creates a Debugger instance for chrome debugging all globals.
+  // Set the consoleAPIListener filtering options
+  // (retrieved and used in the related webconsole child actor).
+  this.consoleAPIListenerOptions = {
+    addonId: addon.id,
+  };
+
+  // This creates a Debugger instance for debugging all the add-on globals.
   this.makeDebugger = makeDebugger.bind(null, {
-    findDebuggees: dbg => dbg.findAllGlobals(),
-    shouldAddNewGlobalAsDebuggee: () => true
+    findDebuggees: dbg => {
+      return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
+    },
+    shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
   });
 
-  // Ensure catching the creation of any new content docshell
-  this.listenForNewDocShells = true;
+  // Discover the preferred debug global for the target addon
+  this.preferredTargetWindow = null;
+  this._findAddonPreferredTargetWindow();
+
+  AddonManager.addAddonListener(this);
+}
+exports.WebExtensionActor = WebExtensionActor;
+
+WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
+
+WebExtensionActor.prototype.actorPrefix = "webExtension";
+WebExtensionActor.prototype.constructor = WebExtensionActor;
+
+// 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);
 
-  // Defines the default docshell selected for the tab actor
-  let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+  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,
+  });
+};
 
-  // Default to any available top level window if there is no expected window
-  // (for example when we open firefox with -webide argument)
-  if (!window) {
-    window = Services.wm.getMostRecentWindow(null);
+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();
   }
-  // On xpcshell, there is no window/docshell
-  let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDocShell)
-                        : null;
-  Object.defineProperty(this, "docShell", {
-    value: docShell,
-    configurable: true
-  });
-}
-exports.ChromeActor = ChromeActor;
+
+  // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
+  ChromeActor.prototype._attach.apply(this);
+};
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
+WebExtensionActor.prototype._detach = function () {
+  this._destroyFallbackWindow();
 
-ChromeActor.prototype.constructor = ChromeActor;
-
-ChromeActor.prototype.isRootActor = true;
+  // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
+  ChromeActor.prototype._detach.apply(this);
+};
 
 /**
- * Getter for the list of all docshells in this tabActor
- * @return {Array}
+ * Called when the actor is removed from the connection.
+ */
+WebExtensionActor.prototype.exit = function () {
+  AddonManager.removeAddonListener(this);
+
+  this.preferredTargetWindow = null;
+  this.addon = null;
+  this.id = null;
+
+  return ChromeActor.prototype.exit.apply(this);
+};
+
+// Addon Specific Remote Debugging requestTypes and methods.
+
+/**
+ * Reloads the addon.
  */
-Object.defineProperty(ChromeActor.prototype, "docShells", {
-  get: function () {
-    // Iterate over all top-level windows and all their docshells.
-    let docShells = [];
-    let e = Services.ww.getWindowEnumerator();
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
-      let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIWebNavigation)
-                           .QueryInterface(Ci.nsIDocShell);
-      docShells = docShells.concat(getChildDocShells(docShell));
-    }
+WebExtensionActor.prototype.onReload = function () {
+  return this.addon.reload()
+    .then(() => {
+      // send an empty response
+      return {};
+    });
+};
 
-    return docShells;
-  }
-});
-
-ChromeActor.prototype.observe = function (aSubject, aTopic, aData) {
-  TabActor.prototype.observe.call(this, aSubject, aTopic, aData);
-  if (!this.attached) {
-    return;
-  }
-  if (aTopic == "chrome-webnavigation-create") {
-    aSubject.QueryInterface(Ci.nsIDocShell);
-    this._onDocShellCreated(aSubject);
-  } else if (aTopic == "chrome-webnavigation-destroy") {
-    this._onDocShellDestroy(aSubject);
+/**
+ * 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;
   }
 };
 
-ChromeActor.prototype._attach = function () {
-  if (this.attached) {
-    return false;
+// AddonManagerListener callbacks.
+
+WebExtensionActor.prototype.onInstalled = function (addon) {
+  if (addon.id != this.id) {
+    return;
   }
 
-  TabActor.prototype._attach.call(this);
+  // Update the AddonManager's addon object on reload/update.
+  this.addon = addon;
+};
 
-  // Listen for any new/destroyed chrome docshell
-  Services.obs.addObserver(this, "chrome-webnavigation-create", false);
-  Services.obs.addObserver(this, "chrome-webnavigation-destroy", false);
+WebExtensionActor.prototype.onUninstalled = function (addon) {
+  if (addon != this.addon) {
+    return;
+  }
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.watch(docShell);
+  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();
   }
 };
 
-ChromeActor.prototype._detach = function () {
-  if (!this.attached) {
-    return false;
+// Private helpers
+
+WebExtensionActor.prototype._createFallbackWindow = function () {
+  if (this.fallbackWindow) {
+    // Skip if there is already an existent fallback window.
+    return;
   }
 
-  Services.obs.removeObserver(this, "chrome-webnavigation-create");
-  Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
+  // 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
+  );
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.unwatch(docShell);
-  }
+  this.fallbackDocShell = this.fallbackWebNav
+    .QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDocShell);
 
-  TabActor.prototype._detach.call(this);
+  Object.defineProperty(this, "docShell", {
+    value: this.fallbackDocShell,
+    configurable: true
+  });
+
+  // Save the reference to the fallback DOMWindow
+  this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                             .getInterface(Ci.nsIDOMWindow);
 };
 
-/* ThreadActor hooks. */
+WebExtensionActor.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();
 
-/**
- * Prepare to enter a nested event loop by disabling debuggee events.
- */
-ChromeActor.prototype.preNest = function () {
-  // Disable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.suppressEventHandling(true);
-    windowUtils.suspendTimeouts();
+    this.fallbackWebNav = null;
+    this.fallbackWindow = null;
   }
 };
 
 /**
- * Prepare to exit a nested event loop by enabling debuggee events.
+ * Discover the preferred debug global and switch to it if the addon has been attached.
  */
-ChromeActor.prototype.postNest = function (aNestData) {
-  // Enable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.resumeTimeouts();
-    windowUtils.suppressEventHandling(false);
+WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
+  return new Promise(resolve => {
+    let activeAddon = XPIProvider.activeAddons.get(this.id);
+
+    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();
+
+          // Do not use the preferred global if it is not a DOMWindow as expected.
+          if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
+            targetWindow = null;
+          }
+
+          resolve(targetWindow);
+        });
+    }
+  }).then(preferredTargetWindow => {
+    this.preferredTargetWindow = preferredTargetWindow;
+
+    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 an array of the json details related to an array/iterator of docShells.
+ */
+WebExtensionActor.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;
+                    });
+};
+
+/**
+ * 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) {
+  try {
+    let uri = Services.io.newURI(source.url, null, null);
+    let addonID = mapURIToAddonID(uri);
+
+    return addonID == this.id;
+  } catch (e) {
+    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) {
+  const global = unwrapDebuggerObjectGlobal(newGlobal);
+
+  if (global instanceof Ci.nsIDOMWindow) {
+    return global.document.nodePrincipal.originAttributes.addonId == this.id;
+  }
+
+  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;