gecko/dom/base/ConsoleAPI.js
2012-05-21 12:12:37 +01:00

324 lines
10 KiB
JavaScript

/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* 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/. */
let Cu = Components.utils;
let Ci = Components.interfaces;
let Cc = Components.classes;
// The maximum allowed number of concurrent timers per page.
const MAX_PAGE_TIMERS = 10000;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm");
function ConsoleAPI() {}
ConsoleAPI.prototype = {
classID: Components.ID("{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
// nsIDOMGlobalPropertyInitializer
init: function CA_init(aWindow) {
Services.obs.addObserver(this, "xpcom-shutdown", false);
Services.obs.addObserver(this, "inner-window-destroyed", false);
let outerID;
let innerID;
try {
let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
outerID = windowUtils.outerWindowID;
innerID = windowUtils.currentInnerWindowID;
}
catch (ex) {
Cu.reportError(ex);
}
let self = this;
let chromeObject = {
// window.console API
log: function CA_log() {
self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments));
},
info: function CA_info() {
self.notifyObservers(outerID, innerID, "info", self.processArguments(arguments));
},
warn: function CA_warn() {
self.notifyObservers(outerID, innerID, "warn", self.processArguments(arguments));
},
error: function CA_error() {
self.notifyObservers(outerID, innerID, "error", self.processArguments(arguments));
},
debug: function CA_debug() {
self.notifyObservers(outerID, innerID, "log", self.processArguments(arguments));
},
trace: function CA_trace() {
self.notifyObservers(outerID, innerID, "trace", self.getStackTrace());
},
// Displays an interactive listing of all the properties of an object.
dir: function CA_dir() {
self.notifyObservers(outerID, innerID, "dir", arguments);
},
group: function CA_group() {
self.notifyObservers(outerID, innerID, "group", self.beginGroup(arguments));
},
groupCollapsed: function CA_groupCollapsed() {
self.notifyObservers(outerID, innerID, "groupCollapsed", self.beginGroup(arguments));
},
groupEnd: function CA_groupEnd() {
self.notifyObservers(outerID, innerID, "groupEnd", arguments);
},
time: function CA_time() {
self.notifyObservers(outerID, innerID, "time", self.startTimer(innerID, arguments[0]));
},
timeEnd: function CA_timeEnd() {
self.notifyObservers(outerID, innerID, "timeEnd", self.stopTimer(innerID, arguments[0]));
},
__exposedProps__: {
log: "r",
info: "r",
warn: "r",
error: "r",
debug: "r",
trace: "r",
dir: "r",
group: "r",
groupCollapsed: "r",
groupEnd: "r",
time: "r",
timeEnd: "r"
}
};
// We need to return an actual content object here, instead of a wrapped
// chrome object. This allows things like console.log.bind() to work.
let contentObj = Cu.createObjectIn(aWindow);
function genPropDesc(fun) {
return { enumerable: true, configurable: true, writable: true,
value: chromeObject[fun].bind(chromeObject) };
}
const properties = {
log: genPropDesc('log'),
info: genPropDesc('info'),
warn: genPropDesc('warn'),
error: genPropDesc('error'),
debug: genPropDesc('debug'),
trace: genPropDesc('trace'),
dir: genPropDesc('dir'),
group: genPropDesc('group'),
groupCollapsed: genPropDesc('groupCollapsed'),
groupEnd: genPropDesc('groupEnd'),
time: genPropDesc('time'),
timeEnd: genPropDesc('timeEnd'),
__noSuchMethod__: { enumerable: true, configurable: true, writable: true,
value: function() {} },
__mozillaConsole__: { value: true }
};
Object.defineProperties(contentObj, properties);
Cu.makeObjectPropsNormal(contentObj);
return contentObj;
},
observe: function CA_observe(aSubject, aTopic, aData)
{
if (aTopic == "xpcom-shutdown") {
Services.obs.removeObserver(this, "xpcom-shutdown");
Services.obs.removeObserver(this, "inner-window-destroyed");
}
else if (aTopic == "inner-window-destroyed") {
let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
delete this.timerRegistry[innerWindowID + ""];
}
},
/**
* Notify all observers of any console API call.
*
* @param number aOuterWindowID
* The outer window ID from where the message came from.
* @param number aInnerWindowID
* The inner window ID from where the message came from.
* @param string aLevel
* The message level.
* @param mixed aArguments
* The arguments given to the console API call.
**/
notifyObservers:
function CA_notifyObservers(aOuterWindowID, aInnerWindowID, aLevel, aArguments) {
if (!aOuterWindowID) {
return;
}
let stack = this.getStackTrace();
// Skip the first frame since it contains an internal call.
let frame = stack[1];
let consoleEvent = {
ID: aOuterWindowID,
innerID: aInnerWindowID,
level: aLevel,
filename: frame.filename,
lineNumber: frame.lineNumber,
functionName: frame.functionName,
arguments: aArguments,
timeStamp: Date.now(),
};
consoleEvent.wrappedJSObject = consoleEvent;
ConsoleAPIStorage.recordEvent(aInnerWindowID, consoleEvent);
Services.obs.notifyObservers(consoleEvent,
"console-api-log-event", aOuterWindowID);
},
/**
* Process the console API call arguments in order to perform printf-like
* string substitution.
* TODO: object substitution should display an interactive property list (bug
* 685815) and width and precision qualifiers should be taken into account
* (bug 685813).
*
* @param mixed aArguments
* The arguments given to the console API call.
**/
processArguments: function CA_processArguments(aArguments) {
if (aArguments.length < 2) {
return aArguments;
}
let args = Array.prototype.slice.call(aArguments);
let format = args.shift();
if (typeof format != "string") {
return aArguments;
}
// Format specification regular expression.
let pattern = /%(\d*).?(\d*)[a-zA-Z]/g;
let processed = format.replace(pattern, function CA_PA_substitute(spec) {
switch (spec[spec.length-1]) {
case "o":
case "s":
return String(args.shift());
case "d":
case "i":
return parseInt(args.shift());
case "f":
return parseFloat(args.shift());
default:
return spec;
};
});
args.unshift(processed);
return args;
},
/**
* Build the stacktrace array for the console.trace() call.
*
* @return array
* Each element is a stack frame that holds the following properties:
* filename, lineNumber, functionName and language.
**/
getStackTrace: function CA_getStackTrace() {
let stack = [];
let frame = Components.stack.caller;
while (frame = frame.caller) {
if (frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT ||
frame.language == Ci.nsIProgrammingLanguage.JAVASCRIPT2) {
stack.push({
filename: frame.filename,
lineNumber: frame.lineNumber,
functionName: frame.name,
language: frame.language,
});
}
}
return stack;
},
/**
* Begin a new group for logging output together.
**/
beginGroup: function CA_beginGroup() {
return Array.prototype.join.call(arguments[0], " ");
},
/*
* A registry of started timers. It contains a map of pages (defined by their
* inner window IDs) to timer maps. Timer maps are key-value pairs of timer
* names to timer start times, for all timers defined in that page. Timer
* names are prepended with the inner window ID in order to avoid conflicts
* with Object.prototype functions.
*/
timerRegistry: {},
/**
* Create a new timer by recording the current time under the specified name.
*
* @param number aWindowId
* The inner ID of the window.
* @param string aName
* The name of the timer.
* @return object
* The name property holds the timer name and the started property
* holds the time the timer was started. In case of error, it returns
* an object with the single property "error" that contains the key
* for retrieving the localized error message.
**/
startTimer: function CA_startTimer(aWindowId, aName) {
if (!aName) {
return;
}
let innerID = aWindowId + "";
if (!this.timerRegistry[innerID]) {
this.timerRegistry[innerID] = {};
}
let pageTimers = this.timerRegistry[innerID];
if (Object.keys(pageTimers).length > MAX_PAGE_TIMERS - 1) {
return { error: "maxTimersExceeded" };
}
let key = aWindowId + "-" + aName.toString();
if (!pageTimers[key]) {
pageTimers[key] = Date.now();
}
return { name: aName, started: pageTimers[key] };
},
/**
* Stop the timer with the specified name and retrieve the elapsed time.
*
* @param number aWindowId
* The inner ID of the window.
* @param string aName
* The name of the timer.
* @return object
* The name property holds the timer name and the duration property
* holds the number of milliseconds since the timer was started.
**/
stopTimer: function CA_stopTimer(aWindowId, aName) {
if (!aName) {
return;
}
let innerID = aWindowId + "";
let pageTimers = this.timerRegistry[innerID];
if (!pageTimers) {
return;
}
let key = aWindowId + "-" + aName.toString();
if (!pageTimers[key]) {
return;
}
let duration = Date.now() - pageTimers[key];
delete pageTimers[key];
return { name: aName, duration: duration };
}
};
let NSGetFactory = XPCOMUtils.generateNSGetFactory([ConsoleAPI]);