Bug 1285557 - Create a WebExtensionAddonActor based on ChromeActor and TabActor. r?ochameau draft
authorLuca Greco <lgreco@mozilla.com>
Tue, 05 Jul 2016 20:22:17 +0200
changeset 385533 0ebbe2e5f100e480365c90b374dc3092267e8abe
parent 385475 ec19ee6e8abd89768117449c82d7b6165b30eb77
child 524946 16edadfdda30eaf27d42feb92334a6d88a12153f
push id22521
push userluca.greco@alcacoop.it
push dateFri, 08 Jul 2016 15:24:40 +0000
reviewersochameau
bugs1285557
milestone50.0a1
Bug 1285557 - Create a WebExtensionAddonActor based on ChromeActor and TabActor. r?ochameau MozReview-Commit-ID: 70sLUzqHHsl
devtools/client/framework/attach-thread.js
devtools/client/framework/target.js
devtools/client/framework/toolbox-process-window.js
devtools/server/actors/moz.build
devtools/server/actors/webbrowser.js
devtools/server/actors/webextension.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.isAddon && !target.isWebExtension) {
+    // Attaching a legacy addon.
     target.client.attachAddon(actor, res => {
       target.client.attachThread(res.threadActor, handleResponse);
     });
   } else if (target.isTabActor) {
-    // Attaching a normal thread
+    // Attaching a tab, a browser process or a WebExtensions add-on.
     target.activeTab.attachThread(threadOptions, handleResponse);
   } else {
-    // Attaching the browser debugger
+    // 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+\.webext\d+/)
+    ));
+  },
+
+  get isWebExtension() {
     return !!(this._form && this._form.actor &&
-              this._form.actor.match(/conn\d+\.addon\d+/));
+              this._form.actor.match(/conn\d+\.webext\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/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, "WebExtensionAddonActor", "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,
@@ -2294,17 +2295,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 WebExtensionAddonActor(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;
 };
 
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,341 @@
 /* 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 { TabSources } = require("./utils/TabSources");
+
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert, update } = 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");
 
 /**
- * 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).
+ * WebExtensionAddonActor is a child of RootActor, it can be retrieved via
+ * RootActor.listAddons request.
+ * WebExtensionAddonActor 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 pages, popup pages etc.),
+ * besides the Content Scripts which run in the content process, hooked to an existent tab,
+ * by creating a new WebExtensionAddonActor 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
  *        The connection to the client.
+ * @param aAddon AddonWrapper
+ *        The target addon.
  */
-function ChromeActor(aConnection) {
-  TabActor.call(this, aConnection);
+function WebExtensionAddonActor(aConnection, aAddon) {
+  ChromeActor.call(this, aConnection);
+
+  this._addon = aAddon;
+  this.id = aAddon.id;
 
-  // This creates a Debugger instance for chrome debugging all globals.
+  // 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),
+  });
+
+  // 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).
+  // TODO: destroy the chromeWebNav, if any.
+  let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+  this.chromeWebNav = chromeWebNav;
+  this.fallbackDocShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDocShell);
+  chromeWebNav.loadURI("about:blank", 0, null, null, null);
+
+  Object.defineProperty(this, "docShell", {
+    value: this.fallbackDocShell,
+    configurable: true
   });
 
-  // Ensure catching the creation of any new content docshell
-  this.listenForNewDocShells = true;
+  AddonManager.addAddonListener(this);
+}
+exports.WebExtensionAddonActor = WebExtensionAddonActor;
+
+WebExtensionAddonActor.prototype = Object.create(ChromeActor.prototype);
 
-  // Defines the default docshell selected for the tab actor
-  let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+update(WebExtensionAddonActor.prototype, {
+  actorPrefix: "webext",
+  constructor: WebExtensionAddonActor,
+  isRootActor:true,
+
+  form: function () {
+    assert(this.actorID, "addon should have an actorID.");
+
+    // Use the preferredWindow url if any, so that the window
+    // title of the Add-on toolbox can contains its url.
+    let preferredWindow = this._addon.getDebugGlobal();
+
+    let baseForm = ChromeActor.prototype.form.call(this);
 
-  // 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);
-  }
-  // 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;
+    return update(baseForm, {
+      actor: this.actorID,
+      id: this.id,
+      name: this._addon.name,
+      url: preferredWindow instanceof Ci.nsIDOMWindow
+        ? preferredWindow.location.href : this.url,
+      iconURL: this._addon.iconURL,
+      debuggable: this._addon.isDebuggable,
+      temporarilyInstalled: this._addon.temporarilyInstalled,
+      isWebExtension: this._addon.isWebExtension,
+    });
+  },
+
+  onAttach: function () {
+    let res = ChromeActor.prototype.onAttach.apply(this, arguments);
+
+    // TODO: the console and error messages, network requests, storages etc.
+    // should be filtered by addonId.
+
+    return res;
+  },
+
+  /**
+   * Called when the actor is removed from the connection.
+   */
+  disconnect: function () {
+    let res = ChromeActor.prototype.disconnect.apply(this, arguments);
+    AddonManager.removeAddonListener(this);
+
+    if (this.chromeNav) {
+      this.chromeNav.close();
+    }
+    this.chromeNav = null;
+    this.fallbackDocShell = null;
+    this._addon = null;
+    this.id = null;
+
+    return res;
+  },
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
+  /**
+   * Returns the actor that represents all the sources associated to the target addon.
+   */
+  get sources() {
+    if (!this._sources) {
+      this._sources = new TabSources(this.threadActor, (source) => {
+        return WebExtensionAddonActor.prototype._allowSource.call(this, source);
+      });
+    }
+    return this._sources;
+  },
 
-ChromeActor.prototype.constructor = ChromeActor;
+  /**
+   * Returns the list of all the existent frames associated to the target addon.
+   */
+  onListFrames: function () {
+    let windows = this._docShellsToWindows(this.docShells);
+    return {
+      frames: windows.filter(frame => {
+        return frame.addonID == this.id;
+      }),
+    };
+  },
 
-ChromeActor.prototype.isRootActor = true;
+  // Addon Specific Remote Debugging requestTypes.
 
-/**
- * Getter for the list of all docshells in this tabActor
- * @return {Array}
- */
-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));
+  /**
+   * Reloads the addon.
+   */
+  onReload: function () {
+    return this._addon.reload()
+      .then(() => {
+        return {}; // send an empty response
+      });
+  },
+
+  /**
+   * Set the preferred global for the add-on (called from the AddonManager).
+   */
+  setOptions: function (aOptions) {
+    if ("global" in aOptions) {
+      // TODO: propose to the client to switch to the preferred global if any
+    }
+  },
+
+  /**
+   * Update the AddonWrapper associated to the target addon (called from the AddonManager).
+   */
+  updateAddonWrapper: function (aAddon) {
+    if (aAddon.id != this.id) {
+      return;
+    }
+
+    this._addon = aAddon;
+
+    // TODO: propagate the update add-on metadata to the connected client.
+  },
+
+  // AddonManagerListener callbacks.
+
+  onEnabled: function (aAddon) {
+    if (aAddon != this._addon) {
+      return;
     }
 
-    return docShells;
+    // Print a message to the console?
+  },
+
+  onDisabled: function (aAddon) {
+    if (aAddon != this._addon) {
+      return;
+    }
+
+    // Print a message to the console?
+  },
+
+  onUninstalled: function (aAddon) {
+    if (aAddon != this._addon) {
+       return;
+    }
+
+    if (this.attached) {
+       this.onDetach();
+       this.conn.send({ from: this.actorID, type: "tabDetached" });
+    }
+
+    this.disconnect();
+  },
+
+  onPropertyChanged: function (aAddon, aChangedPropNames) {
+    if (aAddon != this._addon) {
+      return;
+    }
+
+    if (aChangedPropNames.includes("debugGlobal")) {
+      if (!aAddon.getDebugGlobal()) {
+        // the background page is going to be closed,
+        // navigate to the original window (which is the fallback window).
+        this._changeTopLevelWindow(this._originalWindow);
+      }
+    }
+  },
+
+  // Private helpers
+
+  /**
+   * Return an array of the json details related to an array/iterator of docShells.
+   */
+  _docShellsToWindows: function (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;
+      }
+      return {
+        id: id,
+        url: window.location.href,
+        title: window.document.title,
+        parentID: parentID,
+        addonID: mapURIToAddonID(window.document.documentURIObject),
+      };
+    });
+  },
+
+  /**
+   * Return true if the given source is associated with this addon and should be
+   * added to the visible sources.
+   */
+  _allowSource: function (aSource) {
+    try {
+      let uri = Services.io.newURI(aSource.url, null, null);
+      let addonID = mapURIToAddonID(uri);
+
+      if (addonID == this.id) {
+        return true;
+      }
+
+      return false;
+    } catch(e) {
+      return false;
+    }
+  },
+
+  /**
+   * Return true if the given global is associated with this addon and should be
+   * added as a debuggee, false otherwise.
+   */
+  _shouldAddNewGlobalAsDebuggee: function (aGlobal) {
+    const global = unwrapDebuggerObjectGlobal(aGlobal);
+    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) {}
+
+    if (global instanceof Ci.nsIDOMWindow) {
+      return global.document.documentURIObject.schemeIs("moz-extension") &&
+        mapURIToAddonID(global.document.documentURIObject) == this.id;
+    }
+
+    return false;
+  },
+
+  /**
+   * notify the toolbox on updates related to the frames list,
+   * which have to be filtered by addon id as the one received from
+   * the onListFrames request.
+   */
+  _notifyDocShellsUpdate: function (docshells) {
+    let windows = this._docShellsToWindows(docshells);
+    this.conn.send({
+      from: this.actorID,
+      type: "frameUpdate",
+      frames: windows.filter(frame => {
+        return frame.addonID == this.id;
+      }),
+    });
   }
 });
 
-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);
-  }
-};
-
-ChromeActor.prototype._attach = function () {
-  if (this.attached) {
-    return false;
-  }
-
-  TabActor.prototype._attach.call(this);
-
-  // Listen for any new/destroyed chrome docshell
-  Services.obs.addObserver(this, "chrome-webnavigation-create", false);
-  Services.obs.addObserver(this, "chrome-webnavigation-destroy", false);
-
-  // 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);
-  }
-};
-
-ChromeActor.prototype._detach = function () {
-  if (!this.attached) {
-    return false;
+/**
+ * Override WebExtensionAddonActor requestTypes:
+ * - redefined `reload`, which have to reload the target addon;
+ * - rededined `listFrames`, which have to be filtered by addon id;
+ * - added `setOptions`, which is a method that is required on Addon Actors.
+ */
+WebExtensionAddonActor.prototype.requestTypes = update(
+  WebExtensionAddonActor.prototype.requestTypes,
+  {
+    "attach": WebExtensionAddonActor.prototype.onAttach,
+    "reload": WebExtensionAddonActor.prototype.onReload,
+    "listFrames": WebExtensionAddonActor.prototype.onListFrames,
+    "setOptions": WebExtensionAddonActor.prototype.setOptions,
   }
-
-  Services.obs.removeObserver(this, "chrome-webnavigation-create");
-  Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
-
-  // 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);
-  }
-
-  TabActor.prototype._detach.call(this);
-};
-
-/* ThreadActor hooks. */
-
-/**
- * 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();
-  }
-};
-
-/**
- * Prepare to exit a nested event loop by enabling debuggee events.
- */
-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);
-  }
-};
+);