Bug 1005193 - Allow addons to specify a custom global debug context. r=mossop
authorJohann Hofmann <jhofmann@mozilla.com>
Mon, 11 Apr 2016 08:26:20 +0200
changeset 331303 a78ccb08a836784ac24b25b68d833af95b96a5bd
parent 331302 54b5c7592f7337ccb28a288633c0997b752c85e2
child 331304 3c369626af41eec1fc9a3f145a6ddb0d4c475c31
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1005193
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1005193 - Allow addons to specify a custom global debug context. r=mossop
toolkit/components/extensions/ext-backgroundPage.js
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -1,13 +1,15 @@
 "use strict";
 
 var {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
 
 // WeakMap[Extension -> BackgroundPage]
 var backgroundPagesMap = new WeakMap();
 
 // Responsible for the background_page section of the manifest.
 function BackgroundPage(options, extension) {
   this.extension = extension;
   this.scripts = options.scripts || [];
@@ -60,16 +62,21 @@ BackgroundPage.prototype = {
     this.webNav = webNav;
 
     webNav.loadURI(url, 0, null, null, null);
 
     let window = webNav.document.defaultView;
     this.contentWindow = window;
     this.context.contentWindow = window;
 
+    if (this.extension.addonData.instanceID) {
+      AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                  .then(addon => addon.setDebugGlobal(window));
+    }
+
     // TODO: Right now we run onStartup after the background page
     // finishes. See if this is what Chrome does.
     let loadListener = event => {
       if (event.target != window.document) {
         return;
       }
       event.currentTarget.removeEventListener("load", loadListener, true);
 
@@ -94,16 +101,21 @@ BackgroundPage.prototype = {
     // Navigate away from the background page to invalidate any
     // setTimeouts or other callbacks.
     this.webNav.loadURI("about:blank", 0, null, null, null);
     this.webNav = null;
 
     this.chromeWebNav.loadURI("about:blank", 0, null, null, null);
     this.chromeWebNav.close();
     this.chromeWebNav = null;
+
+    if (this.extension.addonData.instanceID) {
+      AddonManager.getAddonByInstanceID(this.extension.addonData.instanceID)
+                  .then(addon => addon.setDebugGlobal(null));
+    }
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_background", (type, directive, extension, manifest) => {
   let bgPage = new BackgroundPage(manifest.background, extension);
   bgPage.build();
   backgroundPagesMap.set(extension, bgPage);
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 support-files =
   file_download.html
   file_download.txt
   interruptible.sjs
   file_sample.html
 
+[test_chrome_ext_background_debug_global.html]
+skip-if = (os == 'android') # android doesn't have devtools
 [test_chrome_ext_downloads_download.html]
 [test_chrome_ext_downloads_misc.html]
 [test_chrome_ext_downloads_search.html]
 [test_chrome_ext_eventpage_warning.html]
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>WebExtension test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+const {
+  utils: Cu,
+} = Components;
+
+Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm");
+
+/**
+ * This test is asserting that ext-backgroundPage.js successfully sets its
+ * debug global in the AddonWrapper provided by XPIProvider.jsm
+ *
+ * It does _not_ test any functionality in devtools and does not guarantee
+ * debugging is actually working correctly end-to-end.
+ */
+
+function backgroundScript() {
+  window.testThing = "test!";
+  browser.test.notifyPass("background script ran");
+}
+
+let extensionData = {
+  useAddonManager: true,
+  background: "(" + backgroundScript.toString() + ")()",
+  manifest: {},
+  files: {},
+};
+
+add_task(function* () {
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  yield extension.startup();
+  info("extension loaded");
+
+  yield extension.awaitFinish("background script ran");
+
+  yield new Promise(function(resolve) {
+    window.BrowserToolboxProcess.emit("connectionchange", "opened", {
+      setAddonOptions(id, options) {
+        if (id === extension.id) {
+          let context = Cu.waiveXrays(options.global);
+          ok(context.chrome, "global context has a chrome object");
+          ok(context.browser, "global context has a browser object");
+          is("test!", context.testThing, "global context is the background script context");
+          resolve();
+        }
+      },
+    });
+  });
+
+  yield extension.unload();
+  info("extension unloaded");
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3926,17 +3926,26 @@ this.XPIProvider = {
    */
    getAddonByInstanceID: function(aInstanceID) {
      if (!aInstanceID || typeof aInstanceID != "symbol")
        throw Components.Exception("aInstanceID must be a Symbol()",
                                   Cr.NS_ERROR_INVALID_ARG);
 
      for (let [id, val] of this.activeAddons) {
        if (aInstanceID == val.instanceID) {
-         return new Promise(resolve => this.getAddonByID(id, resolve));
+         if (val.safeWrapper) {
+           return Promise.resolve(val.safeWrapper);
+         }
+
+         return new Promise(resolve => {
+           this.getAddonByID(id, function(addon) {
+             val.safeWrapper = new PrivateWrapper(addon);
+             resolve(val.safeWrapper);
+           });
+         });
        }
      }
 
      return Promise.resolve(null);
    },
 
   /**
    * Removes an AddonInstall from the list of active installs.
@@ -4213,17 +4222,17 @@ this.XPIProvider = {
   },
 
   onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
     if (aWhat != "opened")
       return;
 
     for (let [id, val] of this.activeAddons) {
       aConnection.setAddonOptions(
-        id, { global: val.bootstrapScope });
+        id, { global: val.debugGlobal || val.bootstrapScope });
     }
   },
 
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
@@ -4502,16 +4511,18 @@ this.XPIProvider = {
       descriptor: aFile.persistentDescriptor,
       multiprocessCompatible: aMultiprocessCompatible,
       runInSafeMode: aRunInSafeMode,
     };
     this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
 
     this.activeAddons.set(aId, {
+      debugGlobal: null,
+      safeWrapper: null,
       bootstrapScope: null,
       // a Symbol passed to this add-on, which it can use to identify itself
       instanceID: Symbol(aId),
     });
     let activeAddon = this.activeAddons.get(aId);
 
     // Locales only contain chrome and can't have bootstrap scripts
     if (aType == "locale") {
@@ -7334,16 +7345,42 @@ AddonWrapper.prototype = {
     let addon = addonFor(this);
     if (!aPath)
       return NetUtil.newURI(addon._sourceBundle);
 
     return getURIForResourceInFile(addon._sourceBundle, aPath);
   }
 };
 
+/**
+ * The PrivateWrapper is used to expose certain functionality only when being
+ * called with the add-on instanceID, disallowing other add-ons to access it.
+ */
+function PrivateWrapper(aAddon) {
+  AddonWrapper.call(this, aAddon);
+}
+
+PrivateWrapper.prototype = Object.create(AddonWrapper.prototype);
+Object.assign(PrivateWrapper.prototype, {
+
+  /**
+   * Defines a global context to be used in the console
+   * of the add-on debugging window.
+   *
+   * @param  global
+   *         The object to set as global context. Must be a window object.
+   */
+  setDebugGlobal(global) {
+    let activeAddon;
+    if (activeAddon = XPIProvider.activeAddons.get(this.id)) {
+      activeAddon.debugGlobal = global;
+    }
+  }
+});
+
 function chooseValue(aAddon, aObj, aProp) {
   let repositoryAddon = aAddon._repositoryAddon;
   let objValue = aObj[aProp];
 
   if (repositoryAddon && (aProp in repositoryAddon) &&
       (objValue === undefined || objValue === null)) {
     return [repositoryAddon[aProp], true];
   }