Bug 964545 - Add-on SDK page-mods are now debuggable r=dcamp
☠☠ backed out by 21597c8c3d3c ☠ ☠
authorErik Vold <evold@mozilla.com>
Mon, 27 Jan 2014 23:21:31 -0800
changeset 167292 72fdce22d68f8fc5d0b8582191c39eb074cc84c4
parent 167114 1da612492939f6d01c282050c4cb36e3033472af
child 167293 c909875fe4221878725ebdfe8d9935e059f9b7cf
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersdcamp
bugs964545
milestone30.0a1
Bug 964545 - Add-on SDK page-mods are now debuggable r=dcamp
addon-sdk/source/lib/sdk/content/sandbox.js
addon-sdk/source/lib/sdk/deprecated/traits-worker.js
addon-sdk/source/lib/sdk/loader/sandbox.js
addon-sdk/source/lib/sdk/tabs/utils.js
addon-sdk/source/test/addons/page-mod-debugger-post/data/index.html
addon-sdk/source/test/addons/page-mod-debugger-post/data/script.js
addon-sdk/source/test/addons/page-mod-debugger-post/main.js
addon-sdk/source/test/addons/page-mod-debugger-post/package.json
addon-sdk/source/test/addons/page-mod-debugger-pre/data/index.html
addon-sdk/source/test/addons/page-mod-debugger-pre/data/script.js
addon-sdk/source/test/addons/page-mod-debugger-pre/main.js
addon-sdk/source/test/addons/page-mod-debugger-pre/package.json
addon-sdk/source/test/test-page-mod.js
toolkit/devtools/DevToolsExtensions.jsm
toolkit/devtools/server/actors/script.js
toolkit/devtools/tests/mochitest/chrome.ini
toolkit/devtools/tests/mochitest/test_devtools_extensions.html
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -19,42 +19,44 @@ const { Ci, Cu, Cc } = require('chrome')
 const timer = require('../timers');
 const { URL } = require('../url');
 const { sandbox, evaluate, load } = require('../loader/sandbox');
 const { merge } = require('../util/object');
 const xulApp = require('../system/xul-app');
 const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
                                               '17.0a2', '*');
 const { getTabForContentWindow } = require('../tabs/utils');
+const { getInnerId } = require('../window/utils');
 
 // WeakMap of sandboxes so we can access private values
 const sandboxes = new WeakMap();
 
 /* Trick the linker in order to ensure shipping these files in the XPI.
   require('./content-worker.js');
   Then, retrieve URL of these files in the XPI:
 */
 let prefix = module.uri.split('sandbox.js')[0];
 const CONTENT_WORKER_URL = prefix + 'content-worker.js';
+const metadata = require('@loader/options').metadata;
 
 // Fetch additional list of domains to authorize access to for each content
 // script. It is stored in manifest `metadata` field which contains
 // package.json data. This list is originaly defined by authors in
 // `permissions` attribute of their package.json addon file.
-const permissions = require('@loader/options').metadata['permissions'] || {};
+const permissions = (metadata && metadata['permissions']) || {};
 const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
 
 const JS_VERSION = '1.8';
 
 const WorkerSandbox = Class({
 
   implements: [
     EventTarget
   ],
-  
+
   /**
    * Emit a message to the worker content sandbox
    */
   emit: function emit(...args) {
     // Ensure having an asynchronous behavior
     let self = this;
     async(function () {
       emitToContent(self, JSON.stringify(args, replacer));
@@ -126,20 +128,23 @@ const WorkerSandbox = Class({
 
     // Create the sandbox and bind it to window in order for content scripts to
     // have access to all standard globals (window, document, ...)
     let content = sandbox(principals, {
       sandboxPrototype: proto,
       wantXrays: true,
       wantGlobalProperties: wantGlobalProperties,
       sameZoneAs: window,
-      metadata: { SDKContentScript: true }
+      metadata: {
+        SDKContentScript: true,
+        'inner-window-id': getInnerId(window)
+      }
     });
     model.sandbox = content;
-    
+
     // We have to ensure that window.top and window.parent are the exact same
     // object than window object, i.e. the sandbox global object. But not
     // always, in case of iframes, top and parent are another window object.
     let top = window.top === window ? content : content.top;
     let parent = window.parent === window ? content : content.parent;
     merge(content, {
       // We need 'this === window === top' to be true in toplevel scope:
       get window() content,
--- a/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
+++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
@@ -155,17 +155,20 @@ const WorkerSandbox = EventEmitter.compo
 
     // Create the sandbox and bind it to window in order for content scripts to
     // have access to all standard globals (window, document, ...)
     let content = this._sandbox = sandbox(principals, {
       sandboxPrototype: proto,
       wantXrays: true,
       wantGlobalProperties: wantGlobalProperties,
       sameZoneAs: window,
-      metadata: { SDKContentScript: true }
+      metadata: {
+        SDKContentScript: true,
+        'inner-window-id': getInnerId(window)
+      }
     });
     // We have to ensure that window.top and window.parent are the exact same
     // object than window object, i.e. the sandbox global object. But not
     // always, in case of iframes, top and parent are another window object.
     let top = window.top === window ? content : content.top;
     let parent = window.parent === window ? content : content.parent;
     merge(content, {
       // We need "this === window === top" to be true in toplevel scope:
--- a/addon-sdk/source/lib/sdk/loader/sandbox.js
+++ b/addon-sdk/source/lib/sdk/loader/sandbox.js
@@ -7,28 +7,43 @@ module.metadata = {
   "stability": "experimental"
 };
 
 const { Cc, Ci, CC, Cu } = require('chrome');
 const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
 const scriptLoader = Cc['@mozilla.org/moz/jssubscript-loader;1'].
                      getService(Ci.mozIJSSubScriptLoader);
 const self = require('sdk/self');
+const { getTabId, getTabForContentWindow } = require('../tabs/utils');
+const { getInnerId } = require('../window/utils');
+
+const { gDevToolsExtensions: {
+  addContentGlobal, removeContentGlobal
+} } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
 
 /**
  * Make a new sandbox that inherits given `source`'s principals. Source can be
  * URI string, DOMWindow or `null` for system principals.
  */
 function sandbox(target, options) {
   options = options || {};
   options.metadata = options.metadata ? options.metadata : {};
   options.metadata.addonID = options.metadata.addonID ?
     options.metadata.addonID : self.id;
 
-  return Cu.Sandbox(target || systemPrincipal, options);
+  let sandbox = Cu.Sandbox(target || systemPrincipal, options);
+  Cu.setSandboxMetadata(sandbox, options.metadata);
+  let innerWindowID = options.metadata['inner-window-id']
+  if (innerWindowID) {
+    addContentGlobal({
+      global: sandbox,
+      'inner-window-id': innerWindowID
+    });
+  }
+  return sandbox;
 }
 exports.sandbox = sandbox;
 
 /**
  * Evaluates given `source` in a given `sandbox` and returns result.
  */
 function evaluate(sandbox, code, uri, line, version) {
   return Cu.evalInSandbox(code, sandbox, version || '1.8', uri || '', line || 1);
--- a/addon-sdk/source/lib/sdk/tabs/utils.js
+++ b/addon-sdk/source/lib/sdk/tabs/utils.js
@@ -259,17 +259,17 @@ function getTabForWindow(window) {
     if (!BrowserApp)
       continue;
 
     for each (let tab in BrowserApp.tabs) {
       if (tab.browser.contentWindow == window.top)
         return tab;
     }
   }
-  return null; 
+  return null;
 }
 
 function getTabURL(tab) {
   if (tab.browser) // fennec
     return String(tab.browser.currentURI.spec);
   return String(getBrowserForTab(tab).currentURI.spec);
 }
 exports.getTabURL = getTabURL;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-post/data/index.html
@@ -0,0 +1,7 @@
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Page Mod Debugger Test</title>
+  </head>
+  <body></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-post/data/script.js
@@ -0,0 +1,7 @@
+'use strict';
+
+unsafeWindow.runDebuggerStatement = function() {
+  window.document.body.setAttribute('style', 'background-color: red');
+  debugger;
+  window.document.body.setAttribute('style', 'background-color: green');
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-post/main.js
@@ -0,0 +1,133 @@
+/* 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 { Cu } = require('chrome');
+const { PageMod } = require('sdk/page-mod');
+const tabs = require('sdk/tabs');
+const promise = require('sdk/core/promise')
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { data } = require('sdk/self');
+const { set } = require('sdk/preferences/service');
+
+const { DebuggerServer } = Cu.import('resource://gre/modules/devtools/dbg-server.jsm', {});
+const { DebuggerClient } = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {});
+
+let gClient;
+let ok;
+let testName = 'testDebugger';
+let iframeURL = 'data:text/html;charset=utf-8,' + testName;
+let TAB_URL = 'data:text/html;charset=utf-8,' + encodeURIComponent('<iframe src="' + iframeURL + '" />');
+TAB_URL = data.url('index.html');
+let mod;
+
+exports.testDebugger = function(assert, done) {
+  ok = assert.ok.bind(assert);
+  assert.pass('starting test');
+  set('devtools.debugger.log', true);
+
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+  }
+
+  let transport = DebuggerServer.connectPipe();
+  gClient = new DebuggerClient(transport);
+  gClient.connect((aType, aTraits) => {
+    tabs.open({
+      url: TAB_URL,
+      onLoad: function(tab) {
+        assert.pass('tab loaded');
+
+        attachTabActorForUrl(gClient, TAB_URL).
+          then(_ => { assert.pass('attachTabActorForUrl called'); return _; }).
+          then(attachThread).
+          then(testDebuggerStatement).
+          then(_ => { assert.pass('testDebuggerStatement called') }).
+          then(closeConnection).
+          then(_ => { assert.pass('closeConnection called') }).
+          then(done).
+          then(null, aError => {
+            ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+          });
+      }
+    });
+  });
+}
+
+function attachThread([aGrip, aResponse]) {
+  let deferred = promise.defer();
+
+  // Now attach and resume...
+  gClient.request({ to: aResponse.threadActor, type: "attach" }, () => {
+    gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+      ok(true, "Pause wasn't called before we've attached.");
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function testDebuggerStatement([aGrip, aResponse]) {
+  let deferred = promise.defer();
+  ok(aGrip, 'aGrip existss')
+
+  gClient.addListener("paused", (aEvent, aPacket) => {
+    ok(true, 'there was a pause event');
+    gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+      ok(true, "The pause handler was triggered on a debugger statement.");
+      deferred.resolve();
+    });
+  });
+
+  mod = PageMod({
+    include: TAB_URL,
+    attachTo: ['existing', 'top', 'frame'],
+    contentScriptFile: data.url('script.js'),
+    onAttach: function(mod) {
+      ok(true, 'the page-mod was attached to ' + mod.tab.url);
+
+      require('sdk/timers').setTimeout(function() {
+        let debuggee = getMostRecentBrowserWindow().gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+        debuggee.runDebuggerStatement();
+        ok(true, 'called runDebuggerStatement');
+      }, 500)
+    }
+  });
+  ok(true, 'PageMod was created');
+
+  return deferred.promise;
+}
+
+function getTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  aClient.listTabs(aResponse => {
+    let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop();
+    deferred.resolve(tabActor);
+  });
+
+  return deferred.promise;
+}
+
+function attachTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  getTabActorForUrl(aClient, aUrl).then(aGrip => {
+    aClient.attachTab(aGrip.actor, aResponse => {
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function closeConnection() {
+  let deferred = promise.defer();
+  gClient.close(deferred.resolve);
+  return deferred.promise;
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-post/package.json
@@ -0,0 +1,4 @@
+{
+  "id": "test-page-mod-debugger",
+  "author": "Erik Vold"
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-pre/data/index.html
@@ -0,0 +1,7 @@
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Page Mod Debugger Test</title>
+  </head>
+  <body></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-pre/data/script.js
@@ -0,0 +1,7 @@
+'use strict';
+
+unsafeWindow.runDebuggerStatement = function() {
+  window.document.body.setAttribute('style', 'background-color: red');
+  debugger;
+  window.document.body.setAttribute('style', 'background-color: green');
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-pre/main.js
@@ -0,0 +1,128 @@
+/* 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 { Cu } = require('chrome');
+const { PageMod } = require('sdk/page-mod');
+const tabs = require('sdk/tabs');
+const promise = require('sdk/core/promise')
+const { getMostRecentBrowserWindow } = require('sdk/window/utils');
+const { data } = require('sdk/self');
+const { set } = require('sdk/preferences/service');
+
+const { DebuggerServer } = Cu.import('resource://gre/modules/devtools/dbg-server.jsm', {});
+const { DebuggerClient } = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {});
+
+let gClient;
+let ok;
+let testName = 'testDebugger';
+let iframeURL = 'data:text/html;charset=utf-8,' + testName;
+let TAB_URL = 'data:text/html;charset=utf-8,' + encodeURIComponent('<iframe src="' + iframeURL + '" />');
+TAB_URL = data.url('index.html');
+let mod;
+
+exports.testDebugger = function(assert, done) {
+  ok = assert.ok.bind(assert);
+  assert.pass('starting test');
+  set('devtools.debugger.log', true);
+
+  mod = PageMod({
+    include: TAB_URL,
+    attachTo: ['existing', 'top', 'frame'],
+    contentScriptFile: data.url('script.js'),
+  });
+  ok(true, 'PageMod was created');
+
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+  }
+
+  let transport = DebuggerServer.connectPipe();
+  gClient = new DebuggerClient(transport);
+  gClient.connect((aType, aTraits) => {
+    tabs.open({
+      url: TAB_URL,
+      onLoad: function(tab) {
+        assert.pass('tab loaded');
+
+        attachTabActorForUrl(gClient, TAB_URL).
+          then(_ => { assert.pass('attachTabActorForUrl called'); return _; }).
+          then(attachThread).
+          then(testDebuggerStatement).
+          then(_ => { assert.pass('testDebuggerStatement called') }).
+          then(closeConnection).
+          then(_ => { assert.pass('closeConnection called') }).
+          then(done).
+          then(null, aError => {
+            ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+          });
+      }
+    });
+  });
+}
+
+function attachThread([aGrip, aResponse]) {
+  let deferred = promise.defer();
+
+  // Now attach and resume...
+  gClient.request({ to: aResponse.threadActor, type: "attach" }, () => {
+    gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+      ok(true, "Pause wasn't called before we've attached.");
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function testDebuggerStatement([aGrip, aResponse]) {
+  let deferred = promise.defer();
+  ok(aGrip, 'aGrip existss')
+
+  gClient.addListener("paused", (aEvent, aPacket) => {
+    ok(true, 'there was a pause event');
+    gClient.request({ to: aResponse.threadActor, type: "resume" }, () => {
+      ok(true, "The pause handler was triggered on a debugger statement.");
+      deferred.resolve();
+    });
+  });
+
+  let debuggee = getMostRecentBrowserWindow().gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+  debuggee.runDebuggerStatement();
+  ok(true, 'called runDebuggerStatement');
+
+  return deferred.promise;
+}
+
+function getTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  aClient.listTabs(aResponse => {
+    let tabActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop();
+    deferred.resolve(tabActor);
+  });
+
+  return deferred.promise;
+}
+
+function attachTabActorForUrl(aClient, aUrl) {
+  let deferred = promise.defer();
+
+  getTabActorForUrl(aClient, aUrl).then(aGrip => {
+    aClient.attachTab(aGrip.actor, aResponse => {
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function closeConnection() {
+  let deferred = promise.defer();
+  gClient.close(deferred.resolve);
+  return deferred.promise;
+}
+
+require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/page-mod-debugger-pre/package.json
@@ -0,0 +1,4 @@
+{
+  "id": "test-page-mod-debugger",
+  "author": "Erik Vold"
+}
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -4,31 +4,37 @@
 "use strict";
 
 const { PageMod } = require("sdk/page-mod");
 const testPageMod = require("./pagemod-test-helpers").testPageMod;
 const { Loader } = require('sdk/test/loader');
 const tabs = require("sdk/tabs");
 const timer = require("sdk/timers");
 const { Cc, Ci, Cu } = require("chrome");
-const { open, getFrames, getMostRecentBrowserWindow } = require('sdk/window/utils');
-const windowUtils = require('sdk/deprecated/window-utils');
+const {
+  open,
+  getFrames,
+  getMostRecentBrowserWindow,
+  getInnerId
+} = require('sdk/window/utils');
 const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = require('sdk/tabs/utils');
 const xulApp = require("sdk/system/xul-app");
 const { isPrivateBrowsingSupported } = require('sdk/self');
 const { isPrivate } = require('sdk/private-browsing');
 const { openWebpage } = require('./private-browsing/helper');
 const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils');
 const promise = require("sdk/core/promise");
 const { pb } = require('./private-browsing/helper');
 const { URL } = require("sdk/url");
 
 const { waitUntil } = require("sdk/test/utils");
 const data = require("./fixtures");
 
+const { gDevToolsExtensions } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
+
 const testPageURI = data.url("test.html");
 
 // The following adds Debugger constructor to the global namespace.
 const { addDebuggerToGlobal } =
   Cu.import('resource://gre/modules/jsdebugger.jsm', {});
 addDebuggerToGlobal(this);
 
 function Isolate(worker) {
@@ -459,17 +465,17 @@ exports.testExistingOnlyFrameMatchesIncl
   let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(iframe);
   tabs.open({
     url: url,
     onReady: function onReady(tab) {
       let pagemod = new PageMod({
         include: iframeURL,
         attachTo: ['existing', 'frame'],
         onAttach: function(worker) {
-          assert.equal(iframeURL, worker.url, 
+          assert.equal(iframeURL, worker.url,
               "PageMod attached to existing iframe when only it matches include rules");
           pagemod.destroy();
           tab.close(done);
         }
       });
     }
   });
 };
@@ -597,17 +603,17 @@ exports.testAttachToTabsOnly = function(
       win.removeEventListener('DOMContentLoaded', onload, false);
       win.close();
       openBrowserIframe();
     }, false);
   }
 
   function openBrowserIframe() {
     console.info('Open iframe in browser window');
-    let window = require('sdk/deprecated/window-utils').activeBrowserWindow;
+    let window = getMostRecentBrowserWindow();
     let document = window.document;
     let iframe = document.createElement('iframe');
     iframe.setAttribute('type', 'content');
     iframe.setAttribute('src', 'data:text/html;charset=utf-8,foobar');
     iframe.addEventListener('DOMContentLoaded', function onload() {
       iframe.removeEventListener('DOMContentLoaded', onload, false);
       iframe.parentNode.removeChild(iframe);
       openTabWithIframes();
@@ -845,17 +851,17 @@ exports.testPageModCssAutomaticDestroy =
     include: "data:*",
     contentStyle: "div { width: 100px!important; }"
   });
 
   tabs.open({
     url: "data:text/html;charset=utf-8,<div style='width:200px'>css test</div>",
 
     onReady: function onReady(tab) {
-      let browserWindow = windowUtils.activeBrowserWindow;
+      let browserWindow = getMostRecentBrowserWindow();
       let win = getTabContentWindow(getActiveTab(browserWindow));
 
       let div = win.document.querySelector("div");
       let style = win.getComputedStyle(div);
 
       assert.equal(
         style.width,
         "100px",
@@ -1177,26 +1183,38 @@ exports.testDebugMetadata = function(ass
   dbg.onNewGlobalObject = function(global) {
     globalDebuggees.push(global);
   }
 
   let mods = testPageMod(assert, done, "about:", [{
       include: "about:",
       contentScriptWhen: "start",
       contentScript: "null;",
-    }],
-    function(win, done) {
+    }], function(win, done) {
       assert.ok(globalDebuggees.some(function(global) {
         try {
           let metadata = Cu.getSandboxMetadata(global.unsafeDereference());
-          return metadata && metadata.addonID && metadata.SDKContentScript;
+          return metadata && metadata.addonID && metadata.SDKContentScript &&
+                 metadata['inner-window-id'] == getInnerId(win);
         } catch(e) {
           // Some of the globals might not be Sandbox instances and thus
           // will cause getSandboxMetadata to fail.
           return false;
         }
       }), "one of the globals is a content script");
       done();
     }
   );
 };
 
+exports.testDevToolsExtensionsGetContentGlobals = function(assert, done) {
+  let mods = testPageMod(assert, done, "about:", [{
+      include: "about:",
+      contentScriptWhen: "start",
+      contentScript: "null;",
+    }], function(win, done) {
+      assert.equal(gDevToolsExtensions.getContentGlobals({ 'inner-window-id': getInnerId(win) }).length, 1);
+      done();
+    }
+  );
+};
+
 require('sdk/test').run(exports);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/DevToolsExtensions.jsm
@@ -0,0 +1,46 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["gDevToolsExtensions"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+let globalsCache = {};
+
+const gDevToolsExtensions = {
+  addContentGlobal: function(options) {
+    if (!options || !options.global || !options['inner-window-id']) {
+      throw Error('Invalid arguments');
+    }
+    let cache = getGlobalCache(options['inner-window-id']);
+    cache.push(options.global);
+    return undefined;
+  },
+  getContentGlobals: function(options) {
+    if (!options || !options['inner-window-id']) {
+      throw Error('Invalid arguments');
+    }
+    return Array.slice(getGlobalCache(options['inner-window-id']));
+  },
+  removeContentGlobal: function(options) {
+    if (!options || !options.global || !options['inner-window-id']) {
+      throw Error('Invalid arguments');
+    }
+    let cache = getGlobalCache(options['inner-window-id']);
+    let index = cache.indexOf(options.global);
+    cache.splice(index, 1);
+    return undefined;
+  }
+};
+
+function getGlobalCache(aInnerWindowID) {
+  return globalsCache[aInnerWindowID] = globalsCache[aInnerWindowID] || [];
+}
+
+// when the window is destroyed, eliminate the associated globals cache
+Services.obs.addObserver(function observer(subject, topic, data) {
+  let id = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data;
+  delete globalsCache[id];
+}, 'inner-window-destroyed', false);
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1,14 +1,13 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
 "use strict";
 
 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
       "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
       "Float64Array"];
 
 // Number of items to preview in objects, arrays, maps, sets, lists,
 // collections, etc.
@@ -616,32 +615,55 @@ ThreadActor.prototype = {
   },
 
   /**
    * An object that will be used by ThreadActors to tailor their behavior
    * depending on the debugging context being required (chrome or content).
    */
   globalManager: {
     findGlobals: function () {
+      const { gDevToolsExtensions: {
+        getContentGlobals
+      } } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
+
       this.globalDebugObject = this._addDebuggees(this.global);
+      // global may not be a window
+      try {
+        let id = getInnerId(this.global);
+        getContentGlobals({ 'inner-window-id': id }).forEach(this.addDebuggee.bind(this));
+      }
+      catch(e) {}
     },
 
     /**
-     * A function that the engine calls when a new global object has been
-     * created.
+     * A function that the engine calls when a new global object
+     * (for example a sandbox) has been created.
      *
      * @param aGlobal Debugger.Object
      *        The new global object that was created.
      */
     onNewGlobal: function (aGlobal) {
+      let metadata = {};
+      try {
+        metadata = Cu.getSandboxMetadata(aGlobal.unsafeDereference());
+      }
+      catch (e) {}
+
+      let id;
+      try {
+        id = getInnerId(this.global);
+      } catch(e) {}
+
       // Content debugging only cares about new globals in the contant window,
       // like iframe children.
-      if (aGlobal.hostAnnotations &&
+      if ((metadata['inner-window-id'] &&
+          metadata['inner-window-id'] == id) ||
+          (aGlobal.hostAnnotations &&
           aGlobal.hostAnnotations.type == "document" &&
-          aGlobal.hostAnnotations.element === this.global) {
+          aGlobal.hostAnnotations.element === this.global)) {
         this.addDebuggee(aGlobal);
         // Notify the client.
         this.conn.send({
           from: this.actorID,
           type: "newGlobal",
           // TODO: after bug 801084 lands see if we need to JSONify this.
           hostAnnotations: aGlobal.hostAnnotations
         });
@@ -5124,8 +5146,13 @@ function positionInNodeList(element, nod
  * @return object
  */
 function makeDebuggeeValueIfNeeded(obj, value) {
   if (value && (typeof value == "object" || typeof value == "function")) {
     return obj.makeDebuggeeValue(value);
   }
   return value;
 }
+
+function getInnerId(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor).
+                getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+};
--- a/toolkit/devtools/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/tests/mochitest/chrome.ini
@@ -1,1 +1,2 @@
+[test_devtools_extensions.html]
 [test_loader_paths.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/mochitest/test_devtools_extensions.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript"
+            src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css"
+          href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+    <script type="application/javascript;version=1.8">
+      const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+      const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+      const { gDevToolsExtensions } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {});
+      Cu.import("resource://gre/modules/devtools/Loader.jsm");
+      const { require } = devtools;
+      const tabs = require('sdk/tabs');
+      const { getMostRecentBrowserWindow, getInnerId } = require('sdk/window/utils');
+      const { PageMod } = require('sdk/page-mod');
+
+      var _tests = [];
+      function addTest(test) {
+        _tests.push(test);
+      }
+
+      function runNextTest() {
+        if (_tests.length == 0) {
+          SimpleTest.finish()
+          return;
+        }
+        _tests.shift()();
+      }
+
+      window.onload = function() {
+        SimpleTest.waitForExplicitFinish();
+        runNextTest();
+      }
+
+      addTest(function () {
+        let TEST_URL = 'data:text/html;charset=utf-8,test';
+
+        let mod = PageMod({
+          include: TEST_URL,
+          contentScriptWhen: 'ready',
+          contentScript: 'null;'
+        });
+
+        tabs.open({
+          url: TEST_URL,
+          onLoad: function(tab) {
+            let id = getInnerId(getMostRecentBrowserWindow().gBrowser.selectedTab.linkedBrowser.contentWindow);
+
+            // getting
+            is(gDevToolsExtensions.getContentGlobals({
+              'inner-window-id': id
+            }).length, 1, 'found a global for inner-id = ' + id);
+
+            Services.obs.addObserver(function observer(subject, topic, data) {
+              if (id == subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data) {
+                Services.obs.removeObserver(observer, 'inner-window-destroyed');
+                setTimeout(function() {
+                  // closing the tab window should have removed the global
+                  is(gDevToolsExtensions.getContentGlobals({
+                    'inner-window-id': id
+                  }).length, 0, 'did not find a global for inner-id = ' + id);
+
+                  mod.destroy();
+                  runNextTest();
+                })
+              }
+            }, 'inner-window-destroyed', false);
+
+            tab.close();
+          }
+        });
+      })
+
+      addTest(function testAddRemoveGlobal() {
+        let global = {};
+        let globalDetails = {
+          global: global,
+          'inner-window-id': 5
+        };
+
+        // adding
+        gDevToolsExtensions.addContentGlobal(globalDetails);
+
+        // getting
+        is(gDevToolsExtensions.getContentGlobals({
+          'inner-window-id': 5
+        }).length, 1, 'found a global for inner-id = 5');
+        is(gDevToolsExtensions.getContentGlobals({
+          'inner-window-id': 4
+        }).length, 0, 'did not find a global for inner-id = 4');
+
+        // remove
+        gDevToolsExtensions.removeContentGlobal(globalDetails);
+
+        // getting again
+        is(gDevToolsExtensions.getContentGlobals({
+          'inner-window-id': 5
+        }).length, 0, 'did not find a global for inner-id = 5');
+
+        runNextTest();
+      });
+
+    </script>
+  </head>
+  <body></body>
+</html>