Bug 964545 Add-on SDK page-mods are now debuggable r=dcamp
authorErik Vold <evold@mozilla.com>
Mon, 27 Jan 2014 23:21:31 -0800
changeset 190642 81c63efd1278db830ac6dbbaedcee3f959e27259
parent 190537 9b1812a7852ea59b33e685b8cba4e0f320bf599d
child 190643 dce96be376bd751874fd28d57c8c68d0aaa28788
push id3503
push userraliiev@mozilla.com
push dateMon, 28 Apr 2014 18:51:11 +0000
treeherdermozilla-beta@c95ac01e332e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs964545
milestone30.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 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/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
@@ -13,32 +13,34 @@ const { on, off, emit } = require('../ev
 const { requiresAddonGlobal } = require('./utils');
 const { delay: async } = require('../lang/functional');
 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 { 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 ],
 
   /**
@@ -123,17 +125,20 @@ 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;
--- a/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
+++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
@@ -152,17 +152,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);
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, handleReadyState } = require("./pagemod-test-helpers");
 const { Loader } = require('sdk/test/loader');
 const tabs = require("sdk/tabs");
 const { setTimeout } = 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) {
@@ -1055,17 +1061,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",
@@ -1386,33 +1392,45 @@ 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();
+    }
+  );
+};
+
 exports.testDetachOnDestroy = function(assert, done) {
   let tab;
   const TEST_URL = 'data:text/html;charset=utf-8,detach';
   const loader = Loader(module);
   const { PageMod } = loader.require('sdk/page-mod');
 
   let mod1 = PageMod({
     include: TEST_URL,
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(globalsCache[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.
@@ -631,32 +630,59 @@ 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 {
+        getContentGlobals({
+          'inner-window-id': getInnerId(this.global)
+        }).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 useGlobal = (aGlobal.hostAnnotations &&
+                       aGlobal.hostAnnotations.type == "document" &&
+                       aGlobal.hostAnnotations.element === this.global);
+
+      // check if the global is a sdk page-mod sandbox
+      if (!useGlobal) {
+        let metadata = {};
+        let id = "";
+        try {
+          id = getInnerId(this.global);
+          metadata = Cu.getSandboxMetadata(aGlobal.unsafeDereference());
+        }
+        catch (e) {}
+
+        useGlobal = (metadata['inner-window-id'] && metadata['inner-window-id'] == id);
+      }
+
       // Content debugging only cares about new globals in the contant window,
       // like iframe children.
-      if (aGlobal.hostAnnotations &&
-          aGlobal.hostAnnotations.type == "document" &&
-          aGlobal.hostAnnotations.element === this.global) {
+      if (useGlobal) {
         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
         });
@@ -5320,8 +5346,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,2 +1,3 @@
 [test_eventemitter_basic.html]
+[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>