Bug 993029: Create an add-on console actor that will be displayed in the console tab of the add-on debugger. r=msucan, r=Unfocused, r=past

This commit is contained in:
Dave Townsend 2014-05-01 08:36:01 -07:00
parent eef1c9e982
commit 7f17ca3889
21 changed files with 426 additions and 40 deletions

View File

@ -1,21 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { interfaces: Ci, classes: Cc } = Components;
const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
function notify() {
// Log objects so makeDebuggeeValue can get the global to use
console.log({ msg: "Hello again" });
}
function startup(aParams, aReason) {
Components.utils.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Services.jsm");
let res = Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
res.setSubstitution("browser_dbg_addon4", aParams.resourceURI);
// Load a JS module
Components.utils.import("resource://browser_dbg_addon4/test.jsm");
Cu.import("resource://browser_dbg_addon4/test.jsm");
// Log objects so makeDebuggeeValue can get the global to use
console.log({ msg: "Hello from the test add-on" });
Services.obs.addObserver(notify, "addon-test-ping", false);
}
function shutdown(aParams, aReason) {
Services.obs.removeObserver(notify, "addon-test-ping");
// Unload the JS module
Components.utils.unload("resource://browser_dbg_addon4/test.jsm");
Cu.unload("resource://browser_dbg_addon4/test.jsm");
let res = Services.io.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);

View File

@ -90,6 +90,7 @@ support-files =
[browser_dbg_addon-modules.js]
[browser_dbg_addon-modules-unpacked.js]
[browser_dbg_addon-panels.js]
[browser_dbg_addon-console.js]
[browser_dbg_auto-pretty-print-01.js]
[browser_dbg_auto-pretty-print-02.js]
[browser_dbg_bfcache.js]

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that the we can see console messages from the add-on
const ADDON_URL = EXAMPLE_URL + "addon4.xpi";
function getCachedMessages(webConsole) {
let deferred = promise.defer();
webConsole.getCachedMessages(["ConsoleAPI"], (aResponse) => {
if (aResponse.error) {
deferred.reject(aResponse.error);
return;
}
deferred.resolve(aResponse.messages);
});
return deferred.promise;
}
function test() {
Task.spawn(function () {
let addon = yield addAddon(ADDON_URL);
let addonDebugger = yield initAddonDebugger(ADDON_URL);
let webConsole = addonDebugger.webConsole;
let messages = yield getCachedMessages(webConsole);
is(messages.length, 1, "Should be one cached message");
is(messages[0].arguments[0].type, "object", "Should have logged an object");
is(messages[0].arguments[0].preview.ownProperties.msg.value, "Hello from the test add-on", "Should have got the right message");
let consolePromise = addonDebugger.once("console");
console.log("Bad message");
Services.obs.notifyObservers(null, "addon-test-ping", "");
let messageGrip = yield consolePromise;
is(messageGrip.arguments[0].type, "object", "Should have logged an object");
is(messageGrip.arguments[0].preview.ownProperties.msg.value, "Hello again", "Should have got the right message");
yield addonDebugger.destroy();
yield removeAddon(addon);
finish();
});
}

View File

@ -26,9 +26,9 @@ function test() {
});
let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children;
let expectedTabs = ["options", "jsdebugger"];
let expectedTabs = ["options", "webconsole", "jsdebugger", "scratchpad"];
is(tabs.length, 2, "displaying only 2 tabs in addon debugger");
is(tabs.length, expectedTabs.length, "displaying only " + expectedTabs.length + " tabs in addon debugger");
Array.forEach(tabs, (tab, i) => {
let toolName = expectedTabs[i];
is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);

View File

@ -22,6 +22,7 @@ let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxP
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
let EventEmitter = require("devtools/toolkit/event-emitter");
const { promiseInvoke } = require("devtools/async-utils");
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;
@ -522,6 +523,8 @@ function initAddonDebugger(aUrl) {
function AddonDebugger() {
this._onMessage = this._onMessage.bind(this);
this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
EventEmitter.decorate(this);
}
AddonDebugger.prototype = {
@ -548,7 +551,11 @@ AddonDebugger.prototype = {
let addonActor = yield getAddonActorForUrl(this.client, aUrl);
let targetOptions = {
form: { addonActor: addonActor.actor, title: addonActor.name },
form: {
addonActor: addonActor.actor,
consoleActor: addonActor.consoleActor,
title: addonActor.name
},
client: this.client,
chrome: true
};
@ -557,8 +564,8 @@ AddonDebugger.prototype = {
customIframe: this.frame
};
let target = devtools.TargetFactory.forTab(targetOptions);
let toolbox = yield gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions);
this.target = devtools.TargetFactory.forTab(targetOptions);
let toolbox = yield gDevTools.showToolbox(this.target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions);
info("Addon debugger panel shown successfully.");
@ -567,6 +574,7 @@ AddonDebugger.prototype = {
// Wait for the initial resume...
yield waitForClientEvents(this.debuggerPanel, "resumed");
yield prepareDebugger(this.debuggerPanel);
yield this._attachConsole();
}),
destroy: Task.async(function*() {
@ -578,6 +586,27 @@ AddonDebugger.prototype = {
window.removeEventListener("message", this._onMessage);
}),
_attachConsole: function() {
let deferred = promise.defer();
this.client.attachConsole(this.target.form.consoleActor, ["ConsoleAPI"], (aResponse, aWebConsoleClient) => {
if (aResponse.error) {
deferred.reject(aResponse);
}
else {
this.webConsole = aWebConsoleClient;
this.client.addListener("consoleAPICall", this._onConsoleAPICall);
deferred.resolve();
}
});
return deferred.promise;
},
_onConsoleAPICall: function(aType, aPacket) {
if (aPacket.from != this.webConsole.actor)
return;
this.emit("console", aPacket.message);
},
/**
* Returns a list of the groups and sources in the UI. The returned array
* contains objects for each group with properties name and sources. The

View File

@ -37,7 +37,11 @@ function connect() {
if (addonID) {
gClient.listAddons(({addons}) => {
let addonActor = addons.filter(addon => addon.id === addonID).pop();
openToolbox({ addonActor: addonActor.actor, title: addonActor.name });
openToolbox({
addonActor: addonActor.actor,
consoleActor: addonActor.consoleActor,
title: addonActor.name
});
});
} else {
gClient.listTabs(openToolbox);

View File

@ -105,7 +105,7 @@ Tools.webConsole = {
},
isTargetSupported: function(target) {
return !target.isAddon;
return true;
},
build: function(iframeWindow, toolbox) {
let panel = new WebConsolePanel(iframeWindow, toolbox);
@ -314,7 +314,7 @@ Tools.scratchpad = {
commands: "devtools/scratchpad/scratchpad-commands",
isTargetSupported: function(target) {
return !target.isAddon && target.isRemote;
return target.isRemote;
},
build: function(iframeWindow, toolbox) {

View File

@ -526,6 +526,7 @@ function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {})
let consoleEvent = {
ID: "jsm",
innerID: aConsole.innerID || aFrame.filename,
consoleID: aConsole.consoleID,
level: aLevel,
filename: aFrame.filename,
lineNumber: aFrame.lineNumber,
@ -582,6 +583,8 @@ function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {})
* written to stdout
* - innerID {string}: An ID representing the source of the message.
* Normally the inner ID of a DOM window.
* - consoleID {string} : String identified for the console, this will
* be passed through the console notifications
* @return {object}
* A console API instance object
*/
@ -592,6 +595,7 @@ function ConsoleAPI(aConsoleOptions = {}) {
this.prefix = aConsoleOptions.prefix || "";
this.maxLogLevel = aConsoleOptions.maxLogLevel || "all";
this.innerID = aConsoleOptions.innerID || null;
this.consoleID = aConsoleOptions.consoleID || "";
// Bind all the functions to this object.
for (let prop in this) {

View File

@ -318,3 +318,23 @@ exports.dbg_assert = function dbg_assert(cond, e) {
return e;
}
}
/**
* Utility function for updating an object with the properties of another
* object.
*
* @param aTarget Object
* The object being updated.
* @param aNewAttrs Object
* The new attributes being set on the target.
*/
exports.update = function update(aTarget, aNewAttrs) {
for (let key in aNewAttrs) {
let desc = Object.getOwnPropertyDescriptor(aNewAttrs, key);
if (desc) {
Object.defineProperty(aTarget, key, desc);
}
}
}

View File

@ -12,7 +12,7 @@ const { Cc, Ci, Cu, components } = require("chrome");
const { ActorPool } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn } = DevToolsUtils;
const { dbg_assert, dumpn, update } = DevToolsUtils;
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const { all, defer, resolve } = promise;
@ -5357,25 +5357,6 @@ function getFrameLocation(aFrame) {
}
}
/**
* Utility function for updating an object with the properties of another
* object.
*
* @param aTarget Object
* The object being updated.
* @param aNewAttrs Object
* The new attributes being set on the target.
*/
function update(aTarget, aNewAttrs) {
for (let key in aNewAttrs) {
let desc = Object.getOwnPropertyDescriptor(aNewAttrs, key);
if (desc) {
Object.defineProperty(aTarget, key, desc);
}
}
}
/**
* Returns true if its argument is not null.
*/

View File

@ -1222,7 +1222,8 @@ BrowserAddonList.prototype.onUninstalled = function (aAddon) {
function BrowserAddonActor(aConnection, aAddon) {
this.conn = aConnection;
this._addon = aAddon;
this._contextPool = null;
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = null;
this._global = null;
AddonManager.addAddonListener(this);
@ -1253,6 +1254,11 @@ BrowserAddonActor.prototype = {
form: function BAA_form() {
dbg_assert(this.actorID, "addon should have an actorID.");
if (!this._consoleActor) {
let {AddonConsoleActor} = require("devtools/server/actors/webconsole");
this._consoleActor = new AddonConsoleActor(this._addon, this.conn, this);
this._contextPool.addActor(this._consoleActor);
}
return {
actor: this.actorID,
@ -1260,10 +1266,14 @@ BrowserAddonActor.prototype = {
name: this._addon.name,
url: this.url,
debuggable: this._addon.isDebuggable,
consoleActor: this._consoleActor.actorID,
};
},
disconnect: function BAA_disconnect() {
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
this._consoleActor = null;
this._addon = null;
this._global = null;
AddonManager.removeAddonListener(this);
@ -1302,9 +1312,6 @@ BrowserAddonActor.prototype = {
}
if (!this.attached) {
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = new AddonThreadActor(this.conn, this,
this._addon.id);
this._contextPool.addActor(this._threadActor);
@ -1318,8 +1325,7 @@ BrowserAddonActor.prototype = {
return { error: "wrongState" };
}
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
this._contextPool.remoteActor(this._threadActor);
this._threadActor = null;

View File

@ -10,6 +10,7 @@ const { Cc, Ci, Cu } = require("chrome");
const Debugger = require("Debugger");
const { DebuggerServer, ActorPool } = require("devtools/server/main");
const { EnvironmentActor, LongStringActor, ObjectActor, ThreadActor } = require("devtools/server/actors/script");
const { update } = require("devtools/toolkit/DevToolsUtils");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -46,7 +47,6 @@ for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
});
}
/**
* The WebConsoleActor implements capabilities needed for the Web Console
* feature.
@ -1315,6 +1315,7 @@ WebConsoleActor.prototype =
delete result.wrappedJSObject;
delete result.ID;
delete result.innerID;
delete result.consoleID;
result.arguments = Array.map(aMessage.arguments || [], (aObj) => {
let dbgObj = this.makeDebuggeeValue(aObj, true);
@ -1399,6 +1400,91 @@ WebConsoleActor.prototype.requestTypes =
sendHTTPRequest: WebConsoleActor.prototype.onSendHTTPRequest
};
/**
* The AddonConsoleActor implements capabilities needed for the add-on web
* console feature.
*
* @constructor
* @param object aAddon
* The add-on that this console watches.
* @param object aConnection
* The connection to the client, DebuggerServerConnection.
* @param object aParentActor
* The parent BrowserAddonActor actor.
*/
function AddonConsoleActor(aAddon, aConnection, aParentActor)
{
this.addon = aAddon;
WebConsoleActor.call(this, aConnection, aParentActor);
}
AddonConsoleActor.prototype = Object.create(WebConsoleActor.prototype);
update(AddonConsoleActor.prototype, {
constructor: AddonConsoleActor,
actorPrefix: "addonConsole",
/**
* The add-on that this console watches.
*/
addon: null,
/**
* The main add-on JS global
*/
get window() {
return this.parentActor.global;
},
/**
* Destroy the current AddonConsoleActor instance.
*/
disconnect: function ACA_disconnect()
{
WebConsoleActor.prototype.disconnect.call(this);
this.addon = null;
},
/**
* Handler for the "startListeners" request.
*
* @param object aRequest
* The JSON request object received from the Web Console client.
* @return object
* The response object which holds the startedListeners array.
*/
onStartListeners: function ACA_onStartListeners(aRequest)
{
let startedListeners = [];
while (aRequest.listeners.length > 0) {
let listener = aRequest.listeners.shift();
switch (listener) {
case "ConsoleAPI":
if (!this.consoleAPIListener) {
this.consoleAPIListener =
new ConsoleAPIListener(null, this, "addon/" + this.addon.id);
this.consoleAPIListener.init();
}
startedListeners.push(listener);
break;
}
}
return {
startedListeners: startedListeners,
nativeConsoleAPI: true,
traits: this.traits,
};
},
});
AddonConsoleActor.prototype.requestTypes = Object.create(WebConsoleActor.prototype.requestTypes);
AddonConsoleActor.prototype.requestTypes.startListeners = AddonConsoleActor.prototype.onStartListeners;
exports.AddonConsoleActor = AddonConsoleActor;
/**
* Creates an actor for a network event.
*

View File

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { console, ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const { ConsoleAPIListener } = require("devtools/toolkit/webconsole/utils");
var seenMessages = 0;
var seenTypes = 0;
var callback = {
onConsoleAPICall: function(aMessage) {
switch (aMessage.consoleID) {
case "foo":
do_check_eq(aMessage.level, "warn");
do_check_eq(aMessage.arguments[0], "Warning from foo");
seenTypes |= 1;
break;
case "bar":
do_check_eq(aMessage.level, "error");
do_check_eq(aMessage.arguments[0], "Error from bar");
seenTypes |= 2;
break;
default:
do_check_eq(aMessage.level, "log");
do_check_eq(aMessage.arguments[0], "Hello from default console");
seenTypes |= 4;
break;
}
seenMessages++;
}
};
/**
* Tests that the consoleID property of the ConsoleAPI options gets passed
* through to console messages.
*/
function run_test() {
let console1 = new ConsoleAPI({
consoleID: "foo"
});
let console2 = new ConsoleAPI({
consoleID: "bar"
});
console.log("Hello from default console");
console1.warn("Warning from foo");
console2.error("Error from bar");
let listener = new ConsoleAPIListener(null, callback);
listener.init();
let messages = listener.getCachedMessages();
seenTypes = 0;
seenMessages = 0;
messages.forEach(callback.onConsoleAPICall);
do_check_eq(seenMessages, 3);
do_check_eq(seenTypes, 7);
seenTypes = 0;
seenMessages = 0;
console.log("Hello from default console");
console1.warn("Warning from foo");
console2.error("Error from bar");
do_check_eq(seenMessages, 3);
do_check_eq(seenTypes, 7);
listener.destroy();
listener = new ConsoleAPIListener(null, callback, "foo");
listener.init();
let messages = listener.getCachedMessages();
seenTypes = 0;
seenMessages = 0;
messages.forEach(callback.onConsoleAPICall);
do_check_eq(seenMessages, 2);
do_check_eq(seenTypes, 1);
seenTypes = 0;
seenMessages = 0;
console.log("Hello from default console");
console1.warn("Warning from foo");
console2.error("Error from bar");
do_check_eq(seenMessages, 1);
do_check_eq(seenTypes, 1);
}

View File

@ -7,4 +7,5 @@ tail =
[test_safeErrorString.js]
[test_defineLazyPrototypeGetter.js]
[test_async-utils.js]
[test_consoleID.js]
[test_require_lazy.js]

View File

@ -1291,11 +1291,14 @@ ConsoleServiceListener.prototype =
* - onConsoleAPICall(). This method is invoked with one argument, the
* Console API message that comes from the observer service, whenever
* a relevant console API call is received.
* @param string aConsoleID
* Options - The consoleID that this listener should listen to
*/
function ConsoleAPIListener(aWindow, aOwner)
function ConsoleAPIListener(aWindow, aOwner, aConsoleID)
{
this.window = aWindow;
this.owner = aOwner;
this.consoleID = aConsoleID;
if (this.window) {
this.layoutHelpers = new LayoutHelpers(this.window);
}
@ -1322,6 +1325,12 @@ ConsoleAPIListener.prototype =
*/
owner: null,
/**
* The consoleID that we listen for. If not null then only messages from this
* console will be returned.
*/
consoleID: null,
/**
* Initialize the window.console API observer.
*/
@ -1355,6 +1364,9 @@ ConsoleAPIListener.prototype =
return;
}
}
if (this.consoleID && apiMessage.consoleID != this.consoleID) {
return;
}
this.owner.onConsoleAPICall(apiMessage);
},
@ -1385,6 +1397,10 @@ ConsoleAPIListener.prototype =
});
}
if (this.consoleID) {
messages = messages.filter((m) => m.consoleID == this.consoleID);
}
if (aIncludePrivate) {
return messages;
}

View File

@ -37,6 +37,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
"resource:///modules/devtools/ToolboxProcess.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyServiceGetter(this,
"ChromeRegistry",
@ -4126,6 +4128,9 @@ this.XPIProvider = {
for (let feature of features)
this.bootstrapScopes[aId][feature] = gGlobalScope[feature];
// Define a console for the add-on
this.bootstrapScopes[aId]["console"] = new ConsoleAPI({ consoleID: "addon/" + aId });
// As we don't want our caller to control the JS version used for the
// bootstrap file, we run loadSubScript within the context of the
// sandbox with the latest JS version set explicitly.

View File

@ -0,0 +1,29 @@
Components.utils.import("resource://gre/modules/Services.jsm");
let seenGlobals = new Set();
let scope = this;
function checkGlobal(name, type) {
if (scope[name] && typeof(scope[name]) == type)
seenGlobals.add(name);
}
let wrapped = {};
Services.obs.notifyObservers({ wrappedJSObject: wrapped }, "bootstrap-request-globals", null);
for (let [name, type] of wrapped.expectedGlobals) {
checkGlobal(name, type);
}
function install(data, reason) {
}
function startup(data, reason) {
Services.obs.notifyObservers({
wrappedJSObject: seenGlobals
}, "bootstrap-seen-globals", null);
}
function shutdown(data, reason) {
}
function uninstall(data, reason) {
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>bootstrap_globals@tests.mozilla.org</em:id>
<em:version>1.0</em:version>
<em:bootstrap>true</em:bootstrap>
<!-- Front End MetaData -->
<em:name>Test Bootstrap Globals</em:name>
<em:targetApplication>
<Description>
<em:id>xpcshell@tests.mozilla.org</em:id>
<em:minVersion>1</em:minVersion>
<em:maxVersion>1</em:maxVersion>
</Description>
</em:targetApplication>
</Description>
</RDF>

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// This verifies that bootstrap.js has the expected globals defined
Components.utils.import("resource://gre/modules/Services.jsm");
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
const EXPECTED_GLOBALS = [
["Worker", "function"],
["ChromeWorker", "function"],
["console", "object"]
];
function run_test() {
do_test_pending();
startupManager();
let sawGlobals = false;
Services.obs.addObserver(function(subject) {
subject.wrappedJSObject.expectedGlobals = EXPECTED_GLOBALS;
}, "bootstrap-request-globals", false);
Services.obs.addObserver(function({ wrappedJSObject: seenGlobals }) {
for (let [name,] of EXPECTED_GLOBALS)
do_check_true(seenGlobals.has(name));
sawGlobals = true;
}, "bootstrap-seen-globals", false);
installAllFiles([do_get_addon("bootstrap_globals")], function() {
do_check_true(sawGlobals);
shutdownManager();
do_test_finished();
});
}

View File

@ -265,3 +265,4 @@ run-sequentially = Uses global XCurProcD dir.
[test_overrideblocklist.js]
run-sequentially = Uses global XCurProcD dir.
[test_sourceURI.js]
[test_bootstrap_globals.js]