Bug 964545 - Add-on SDK page-mods are now debuggable r=dcamp

This commit is contained in:
Erik Vold 2014-01-27 23:21:31 -08:00
parent 6027a6223e
commit 7a50c351d5
17 changed files with 549 additions and 20 deletions

View File

@ -24,6 +24,7 @@ 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();
@ -34,12 +35,13 @@ const sandboxes = new WeakMap();
*/
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';
@ -49,7 +51,7 @@ const WorkerSandbox = Class({
implements: [
EventTarget
],
/**
* Emit a message to the worker content sandbox
*/
@ -131,10 +133,13 @@ const WorkerSandbox = Class({
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.

View File

@ -160,7 +160,10 @@ const WorkerSandbox = EventEmitter.compose({
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

View File

@ -12,6 +12,12 @@ 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
@ -23,7 +29,16 @@ function sandbox(target, options) {
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;

View File

@ -264,7 +264,7 @@ function getTabForWindow(window) {
return tab;
}
}
return null;
return null;
}
function getTabURL(tab) {

View File

@ -0,0 +1,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>Page Mod Debugger Test</title>
</head>
<body></body>
</html>

View File

@ -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');
}

View File

@ -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);

View File

@ -0,0 +1,4 @@
{
"id": "test-page-mod-debugger",
"author": "Erik Vold"
}

View File

@ -0,0 +1,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>Page Mod Debugger Test</title>
</head>
<body></body>
</html>

View File

@ -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');
}

View File

@ -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);

View File

@ -0,0 +1,4 @@
{
"id": "test-page-mod-debugger",
"author": "Erik Vold"
}

View File

@ -9,8 +9,12 @@ 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');
@ -24,6 +28,8 @@ 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.
@ -464,7 +470,7 @@ exports.testExistingOnlyFrameMatchesInclude = function(assert, done) {
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);
@ -602,7 +608,7 @@ exports.testAttachToTabsOnly = function(assert, done) {
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');
@ -850,7 +856,7 @@ exports.testPageModCssAutomaticDestroy = function(assert, done) {
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");
@ -1182,12 +1188,12 @@ exports.testDebugMetadata = function(assert, done) {
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.
@ -1199,4 +1205,16 @@ exports.testDebugMetadata = function(assert, 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);

View File

@ -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);

View File

@ -3,7 +3,6 @@
/* 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",
@ -621,22 +620,45 @@ ThreadActor.prototype = {
*/
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({
@ -5129,3 +5151,8 @@ function makeDebuggeeValueIfNeeded(obj, value) {
}
return value;
}
function getInnerId(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
};

View File

@ -1 +1,2 @@
[test_devtools_extensions.html]
[test_loader_paths.html]

View File

@ -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>