michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: /** michael@0: * Define a 'console' API to roughly match the implementation provided by michael@0: * Firebug. michael@0: * This module helps cases where code is shared between the web and Firefox. michael@0: * See also Browser.jsm for an implementation of other web constants to help michael@0: * sharing code between the web and firefox; michael@0: * michael@0: * The API is only be a rough approximation for 3 reasons: michael@0: * - The Firebug console API is implemented in many places with differences in michael@0: * the implementations, so there isn't a single reference to adhere to michael@0: * - The Firebug console is a rich display compared with dump(), so there will michael@0: * be many things that we can't replicate michael@0: * - The primary use of this API is debugging and error logging so the perfect michael@0: * implementation isn't always required (or even well defined) michael@0: */ michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "console", "ConsoleAPI" ]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Services", michael@0: "resource://gre/modules/Services.jsm"); michael@0: michael@0: let gTimerRegistry = new Map(); michael@0: michael@0: /** michael@0: * String utility to ensure that strings are a specified length. Strings michael@0: * that are too long are truncated to the max length and the last char is michael@0: * set to "_". Strings that are too short are padded with spaces. michael@0: * michael@0: * @param {string} aStr michael@0: * The string to format to the correct length michael@0: * @param {number} aMaxLen michael@0: * The maximum allowed length of the returned string michael@0: * @param {number} aMinLen (optional) michael@0: * The minimum allowed length of the returned string. If undefined, michael@0: * then aMaxLen will be used michael@0: * @param {object} aOptions (optional) michael@0: * An object allowing format customization. Allowed customizations: michael@0: * 'truncate' - can take the value "start" to truncate strings from michael@0: * the start as opposed to the end or "center" to truncate michael@0: * strings in the center. michael@0: * 'align' - takes an alignment when padding is needed for MinLen, michael@0: * either "start" or "end". Defaults to "start". michael@0: * @return {string} michael@0: * The original string formatted to fit the specified lengths michael@0: */ michael@0: function fmt(aStr, aMaxLen, aMinLen, aOptions) { michael@0: if (aMinLen == null) { michael@0: aMinLen = aMaxLen; michael@0: } michael@0: if (aStr == null) { michael@0: aStr = ""; michael@0: } michael@0: if (aStr.length > aMaxLen) { michael@0: if (aOptions && aOptions.truncate == "start") { michael@0: return "_" + aStr.substring(aStr.length - aMaxLen + 1); michael@0: } michael@0: else if (aOptions && aOptions.truncate == "center") { michael@0: let start = aStr.substring(0, (aMaxLen / 2)); michael@0: michael@0: let end = aStr.substring((aStr.length - (aMaxLen / 2)) + 1); michael@0: return start + "_" + end; michael@0: } michael@0: else { michael@0: return aStr.substring(0, aMaxLen - 1) + "_"; michael@0: } michael@0: } michael@0: if (aStr.length < aMinLen) { michael@0: let padding = Array(aMinLen - aStr.length + 1).join(" "); michael@0: aStr = (aOptions.align === "end") ? padding + aStr : aStr + padding; michael@0: } michael@0: return aStr; michael@0: } michael@0: michael@0: /** michael@0: * Utility to extract the constructor name of an object. michael@0: * Object.toString gives: "[object ?????]"; we want the "?????". michael@0: * michael@0: * @param {object} aObj michael@0: * The object from which to extract the constructor name michael@0: * @return {string} michael@0: * The constructor name michael@0: */ michael@0: function getCtorName(aObj) { michael@0: if (aObj === null) { michael@0: return "null"; michael@0: } michael@0: if (aObj === undefined) { michael@0: return "undefined"; michael@0: } michael@0: if (aObj.constructor && aObj.constructor.name) { michael@0: return aObj.constructor.name; michael@0: } michael@0: // If that fails, use Objects toString which sometimes gives something michael@0: // better than 'Object', and at least defaults to Object if nothing better michael@0: return Object.prototype.toString.call(aObj).slice(8, -1); michael@0: } michael@0: michael@0: /** michael@0: * A single line stringification of an object designed for use by humans michael@0: * michael@0: * @param {any} aThing michael@0: * The object to be stringified michael@0: * @param {boolean} aAllowNewLines michael@0: * @return {string} michael@0: * A single line representation of aThing, which will generally be at michael@0: * most 80 chars long michael@0: */ michael@0: function stringify(aThing, aAllowNewLines) { michael@0: if (aThing === undefined) { michael@0: return "undefined"; michael@0: } michael@0: michael@0: if (aThing === null) { michael@0: return "null"; michael@0: } michael@0: michael@0: if (typeof aThing == "object") { michael@0: let type = getCtorName(aThing); michael@0: if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) { michael@0: return debugElement(aThing); michael@0: } michael@0: type = (type == "Object" ? "" : type + " "); michael@0: let json; michael@0: try { michael@0: json = JSON.stringify(aThing); michael@0: } michael@0: catch (ex) { michael@0: // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled michael@0: json = "{" + Object.keys(aThing).join(":..,") + ":.., " + "}"; michael@0: } michael@0: return type + json; michael@0: } michael@0: michael@0: if (typeof aThing == "function") { michael@0: return aThing.toString().replace(/\s+/g, " "); michael@0: } michael@0: michael@0: let str = aThing.toString(); michael@0: if (!aAllowNewLines) { michael@0: str = str.replace(/\n/g, "|"); michael@0: } michael@0: return str; michael@0: } michael@0: michael@0: /** michael@0: * Create a simple debug representation of a given element. michael@0: * michael@0: * @param {nsIDOMElement} aElement michael@0: * The element to debug michael@0: * @return {string} michael@0: * A simple single line representation of aElement michael@0: */ michael@0: function debugElement(aElement) { michael@0: return "<" + aElement.tagName + michael@0: (aElement.id ? "#" + aElement.id : "") + michael@0: (aElement.className ? michael@0: "." + aElement.className.split(" ").join(" .") : michael@0: "") + michael@0: ">"; michael@0: } michael@0: michael@0: /** michael@0: * A multi line stringification of an object, designed for use by humans michael@0: * michael@0: * @param {any} aThing michael@0: * The object to be stringified michael@0: * @return {string} michael@0: * A multi line representation of aThing michael@0: */ michael@0: function log(aThing) { michael@0: if (aThing === null) { michael@0: return "null\n"; michael@0: } michael@0: michael@0: if (aThing === undefined) { michael@0: return "undefined\n"; michael@0: } michael@0: michael@0: if (typeof aThing == "object") { michael@0: let reply = ""; michael@0: let type = getCtorName(aThing); michael@0: if (type == "Map") { michael@0: reply += "Map\n"; michael@0: for (let [key, value] of aThing) { michael@0: reply += logProperty(key, value); michael@0: } michael@0: } michael@0: else if (type == "Set") { michael@0: let i = 0; michael@0: reply += "Set\n"; michael@0: for (let value of aThing) { michael@0: reply += logProperty('' + i, value); michael@0: i++; michael@0: } michael@0: } michael@0: else if (type.match("Error$") || michael@0: (typeof aThing.name == "string" && michael@0: aThing.name.match("NS_ERROR_"))) { michael@0: reply += " Message: " + aThing + "\n"; michael@0: if (aThing.stack) { michael@0: reply += " Stack:\n"; michael@0: var frame = aThing.stack; michael@0: while (frame) { michael@0: reply += " " + frame + "\n"; michael@0: frame = frame.caller; michael@0: } michael@0: } michael@0: } michael@0: else if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) { michael@0: reply += " " + debugElement(aThing) + "\n"; michael@0: } michael@0: else { michael@0: let keys = Object.getOwnPropertyNames(aThing); michael@0: if (keys.length > 0) { michael@0: reply += type + "\n"; michael@0: keys.forEach(function(aProp) { michael@0: reply += logProperty(aProp, aThing[aProp]); michael@0: }); michael@0: } michael@0: else { michael@0: reply += type + "\n"; michael@0: let root = aThing; michael@0: let logged = []; michael@0: while (root != null) { michael@0: let properties = Object.keys(root); michael@0: properties.sort(); michael@0: properties.forEach(function(property) { michael@0: if (!(property in logged)) { michael@0: logged[property] = property; michael@0: reply += logProperty(property, aThing[property]); michael@0: } michael@0: }); michael@0: michael@0: root = Object.getPrototypeOf(root); michael@0: if (root != null) { michael@0: reply += ' - prototype ' + getCtorName(root) + '\n'; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: return reply; michael@0: } michael@0: michael@0: return " " + aThing.toString() + "\n"; michael@0: } michael@0: michael@0: /** michael@0: * Helper for log() which converts a property/value pair into an output michael@0: * string michael@0: * michael@0: * @param {string} aProp michael@0: * The name of the property to include in the output string michael@0: * @param {object} aValue michael@0: * Value assigned to aProp to be converted to a single line string michael@0: * @return {string} michael@0: * Multi line output string describing the property/value pair michael@0: */ michael@0: function logProperty(aProp, aValue) { michael@0: let reply = ""; michael@0: if (aProp == "stack" && typeof value == "string") { michael@0: let trace = parseStack(aValue); michael@0: reply += formatTrace(trace); michael@0: } michael@0: else { michael@0: reply += " - " + aProp + " = " + stringify(aValue) + "\n"; michael@0: } michael@0: return reply; michael@0: } michael@0: michael@0: const LOG_LEVELS = { michael@0: "all": Number.MIN_VALUE, michael@0: "debug": 2, michael@0: "log": 3, michael@0: "info": 3, michael@0: "trace": 3, michael@0: "timeEnd": 3, michael@0: "time": 3, michael@0: "group": 3, michael@0: "groupEnd": 3, michael@0: "dir": 3, michael@0: "dirxml": 3, michael@0: "warn": 4, michael@0: "error": 5, michael@0: "off": Number.MAX_VALUE, michael@0: }; michael@0: michael@0: /** michael@0: * Helper to tell if a console message of `aLevel` type michael@0: * should be logged in stdout and sent to consoles given michael@0: * the current maximum log level being defined in `console.maxLogLevel` michael@0: * michael@0: * @param {string} aLevel michael@0: * Console message log level michael@0: * @param {string} aMaxLevel {string} michael@0: * String identifier (See LOG_LEVELS for possible michael@0: * values) that allows to filter which messages michael@0: * are logged based on their log level michael@0: * @return {boolean} michael@0: * Should this message be logged or not? michael@0: */ michael@0: function shouldLog(aLevel, aMaxLevel) { michael@0: return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel]; michael@0: } michael@0: michael@0: /** michael@0: * Parse a stack trace, returning an array of stack frame objects, where michael@0: * each has filename/lineNumber/functionName members michael@0: * michael@0: * @param {string} aStack michael@0: * The serialized stack trace michael@0: * @return {object[]} michael@0: * Array of { file: "...", line: NNN, call: "..." } objects michael@0: */ michael@0: function parseStack(aStack) { michael@0: let trace = []; michael@0: aStack.split("\n").forEach(function(line) { michael@0: if (!line) { michael@0: return; michael@0: } michael@0: let at = line.lastIndexOf("@"); michael@0: let posn = line.substring(at + 1); michael@0: trace.push({ michael@0: filename: posn.split(":")[0], michael@0: lineNumber: posn.split(":")[1], michael@0: functionName: line.substring(0, at) michael@0: }); michael@0: }); michael@0: return trace; michael@0: } michael@0: michael@0: /** michael@0: * Format a frame coming from Components.stack such that it can be used by the michael@0: * Browser Console, via console-api-log-event notifications. michael@0: * michael@0: * @param {object} aFrame michael@0: * The stack frame from which to begin the walk. michael@0: * @param {number=0} aMaxDepth michael@0: * Maximum stack trace depth. Default is 0 - no depth limit. michael@0: * @return {object[]} michael@0: * An array of {filename, lineNumber, functionName, language} objects. michael@0: * These objects follow the same format as other console-api-log-event michael@0: * messages. michael@0: */ michael@0: function getStack(aFrame, aMaxDepth = 0) { michael@0: if (!aFrame) { michael@0: aFrame = Components.stack.caller; michael@0: } michael@0: let trace = []; michael@0: while (aFrame) { michael@0: trace.push({ michael@0: filename: aFrame.filename, michael@0: lineNumber: aFrame.lineNumber, michael@0: functionName: aFrame.name, michael@0: language: aFrame.language, michael@0: }); michael@0: if (aMaxDepth == trace.length) { michael@0: break; michael@0: } michael@0: aFrame = aFrame.caller; michael@0: } michael@0: return trace; michael@0: } michael@0: michael@0: /** michael@0: * Take the output from parseStack() and convert it to nice readable michael@0: * output michael@0: * michael@0: * @param {object[]} aTrace michael@0: * Array of trace objects as created by parseStack() michael@0: * @return {string} Multi line report of the stack trace michael@0: */ michael@0: function formatTrace(aTrace) { michael@0: let reply = ""; michael@0: aTrace.forEach(function(frame) { michael@0: reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " + michael@0: fmt(frame.lineNumber, 5, 5) + " " + michael@0: fmt(frame.functionName, 75, 0, { truncate: "center" }) + "\n"; michael@0: }); michael@0: return reply; michael@0: } michael@0: michael@0: /** michael@0: * Create a new timer by recording the current time under the specified name. michael@0: * michael@0: * @param {string} aName michael@0: * The name of the timer. michael@0: * @param {number} [aTimestamp=Date.now()] michael@0: * Optional timestamp that tells when the timer was originally started. michael@0: * @return {object} michael@0: * The name property holds the timer name and the started property michael@0: * holds the time the timer was started. In case of error, it returns michael@0: * an object with the single property "error" that contains the key michael@0: * for retrieving the localized error message. michael@0: */ michael@0: function startTimer(aName, aTimestamp) { michael@0: let key = aName.toString(); michael@0: if (!gTimerRegistry.has(key)) { michael@0: gTimerRegistry.set(key, aTimestamp || Date.now()); michael@0: } michael@0: return { name: aName, started: gTimerRegistry.get(key) }; michael@0: } michael@0: michael@0: /** michael@0: * Stop the timer with the specified name and retrieve the elapsed time. michael@0: * michael@0: * @param {string} aName michael@0: * The name of the timer. michael@0: * @param {number} [aTimestamp=Date.now()] michael@0: * Optional timestamp that tells when the timer was originally stopped. michael@0: * @return {object} michael@0: * The name property holds the timer name and the duration property michael@0: * holds the number of milliseconds since the timer was started. michael@0: */ michael@0: function stopTimer(aName, aTimestamp) { michael@0: let key = aName.toString(); michael@0: let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key); michael@0: gTimerRegistry.delete(key); michael@0: return { name: aName, duration: duration }; michael@0: } michael@0: michael@0: /** michael@0: * Dump a new message header to stdout by taking care of adding an eventual michael@0: * prefix michael@0: * michael@0: * @param {object} aConsole michael@0: * ConsoleAPI instance michael@0: * @param {string} aLevel michael@0: * The string identifier for the message log level michael@0: * @param {string} aMessage michael@0: * The string message to print to stdout michael@0: */ michael@0: function dumpMessage(aConsole, aLevel, aMessage) { michael@0: aConsole.dump( michael@0: "console." + aLevel + ": " + michael@0: aConsole.prefix + michael@0: aMessage + "\n" michael@0: ); michael@0: } michael@0: michael@0: /** michael@0: * Create a function which will output a concise level of output when used michael@0: * as a logging function michael@0: * michael@0: * @param {string} aLevel michael@0: * A prefix to all output generated from this function detailing the michael@0: * level at which output occurred michael@0: * @return {function} michael@0: * A logging function michael@0: * @see createMultiLineDumper() michael@0: */ michael@0: function createDumper(aLevel) { michael@0: return function() { michael@0: if (!shouldLog(aLevel, this.maxLogLevel)) { michael@0: return; michael@0: } michael@0: let args = Array.prototype.slice.call(arguments, 0); michael@0: let frame = getStack(Components.stack.caller, 1)[0]; michael@0: sendConsoleAPIMessage(this, aLevel, frame, args); michael@0: let data = args.map(function(arg) { michael@0: return stringify(arg, true); michael@0: }); michael@0: dumpMessage(this, aLevel, data.join(" ")); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Create a function which will output more detailed level of output when michael@0: * used as a logging function michael@0: * michael@0: * @param {string} aLevel michael@0: * A prefix to all output generated from this function detailing the michael@0: * level at which output occurred michael@0: * @return {function} michael@0: * A logging function michael@0: * @see createDumper() michael@0: */ michael@0: function createMultiLineDumper(aLevel) { michael@0: return function() { michael@0: if (!shouldLog(aLevel, this.maxLogLevel)) { michael@0: return; michael@0: } michael@0: dumpMessage(this, aLevel, ""); michael@0: let args = Array.prototype.slice.call(arguments, 0); michael@0: let frame = getStack(Components.stack.caller, 1)[0]; michael@0: sendConsoleAPIMessage(this, aLevel, frame, args); michael@0: args.forEach(function(arg) { michael@0: this.dump(log(arg)); michael@0: }, this); michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Send a Console API message. This function will send a console-api-log-event michael@0: * notification through the nsIObserverService. michael@0: * michael@0: * @param {object} aConsole michael@0: * The instance of ConsoleAPI performing the logging. michael@0: * @param {string} aLevel michael@0: * Message severity level. This is usually the name of the console method michael@0: * that was called. michael@0: * @param {object} aFrame michael@0: * The youngest stack frame coming from Components.stack, as formatted by michael@0: * getStack(). michael@0: * @param {array} aArgs michael@0: * The arguments given to the console method. michael@0: * @param {object} aOptions michael@0: * Object properties depend on the console method that was invoked: michael@0: * - timer: for time() and timeEnd(). Holds the timer information. michael@0: * - groupName: for group(), groupCollapsed() and groupEnd(). michael@0: * - stacktrace: for trace(). Holds the array of stack frames as given by michael@0: * getStack(). michael@0: */ michael@0: function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) michael@0: { michael@0: let consoleEvent = { michael@0: ID: "jsm", michael@0: innerID: aConsole.innerID || aFrame.filename, michael@0: level: aLevel, michael@0: filename: aFrame.filename, michael@0: lineNumber: aFrame.lineNumber, michael@0: functionName: aFrame.functionName, michael@0: timeStamp: Date.now(), michael@0: arguments: aArgs, michael@0: }; michael@0: michael@0: consoleEvent.wrappedJSObject = consoleEvent; michael@0: michael@0: switch (aLevel) { michael@0: case "trace": michael@0: consoleEvent.stacktrace = aOptions.stacktrace; michael@0: break; michael@0: case "time": michael@0: case "timeEnd": michael@0: consoleEvent.timer = aOptions.timer; michael@0: break; michael@0: case "group": michael@0: case "groupCollapsed": michael@0: case "groupEnd": michael@0: try { michael@0: consoleEvent.groupName = Array.prototype.join.call(aArgs, " "); michael@0: } michael@0: catch (ex) { michael@0: Cu.reportError(ex); michael@0: Cu.reportError(ex.stack); michael@0: return; michael@0: } michael@0: break; michael@0: } michael@0: michael@0: Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null); michael@0: let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] michael@0: .getService(Ci.nsIConsoleAPIStorage); michael@0: ConsoleAPIStorage.recordEvent("jsm", consoleEvent); michael@0: } michael@0: michael@0: /** michael@0: * This creates a console object that somewhat replicates Firebug's console michael@0: * object michael@0: * michael@0: * @param {object} aConsoleOptions michael@0: * Optional dictionary with a set of runtime console options: michael@0: * - prefix {string} : An optional prefix string to be printed before michael@0: * the actual logged message michael@0: * - maxLogLevel {string} : String identifier (See LOG_LEVELS for michael@0: * possible values) that allows to filter which michael@0: * messages are logged based on their log level. michael@0: * If falsy value, all messages will be logged. michael@0: * If wrong value that doesn't match any key of michael@0: * LOG_LEVELS, no message will be logged michael@0: * - dump {function} : An optional function to intercept all strings michael@0: * written to stdout michael@0: * - innerID {string}: An ID representing the source of the message. michael@0: * Normally the inner ID of a DOM window. michael@0: * @return {object} michael@0: * A console API instance object michael@0: */ michael@0: function ConsoleAPI(aConsoleOptions = {}) { michael@0: // Normalize console options to set default values michael@0: // in order to avoid runtime checks on each console method call. michael@0: this.dump = aConsoleOptions.dump || dump; michael@0: this.prefix = aConsoleOptions.prefix || ""; michael@0: this.maxLogLevel = aConsoleOptions.maxLogLevel || "all"; michael@0: this.innerID = aConsoleOptions.innerID || null; michael@0: michael@0: // Bind all the functions to this object. michael@0: for (let prop in this) { michael@0: if (typeof(this[prop]) === "function") { michael@0: this[prop] = this[prop].bind(this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: ConsoleAPI.prototype = { michael@0: debug: createMultiLineDumper("debug"), michael@0: log: createDumper("log"), michael@0: info: createDumper("info"), michael@0: warn: createDumper("warn"), michael@0: error: createMultiLineDumper("error"), michael@0: exception: createMultiLineDumper("error"), michael@0: michael@0: trace: function Console_trace() { michael@0: if (!shouldLog("trace", this.maxLogLevel)) { michael@0: return; michael@0: } michael@0: let args = Array.prototype.slice.call(arguments, 0); michael@0: let trace = getStack(Components.stack.caller); michael@0: sendConsoleAPIMessage(this, "trace", trace[0], args, michael@0: { stacktrace: trace }); michael@0: dumpMessage(this, "trace", "\n" + formatTrace(trace)); michael@0: }, michael@0: clear: function Console_clear() {}, michael@0: michael@0: dir: createMultiLineDumper("dir"), michael@0: dirxml: createMultiLineDumper("dirxml"), michael@0: group: createDumper("group"), michael@0: groupEnd: createDumper("groupEnd"), michael@0: michael@0: time: function Console_time() { michael@0: if (!shouldLog("time", this.maxLogLevel)) { michael@0: return; michael@0: } michael@0: let args = Array.prototype.slice.call(arguments, 0); michael@0: let frame = getStack(Components.stack.caller, 1)[0]; michael@0: let timer = startTimer(args[0]); michael@0: sendConsoleAPIMessage(this, "time", frame, args, { timer: timer }); michael@0: dumpMessage(this, "time", michael@0: "'" + timer.name + "' @ " + (new Date())); michael@0: }, michael@0: michael@0: timeEnd: function Console_timeEnd() { michael@0: if (!shouldLog("timeEnd", this.maxLogLevel)) { michael@0: return; michael@0: } michael@0: let args = Array.prototype.slice.call(arguments, 0); michael@0: let frame = getStack(Components.stack.caller, 1)[0]; michael@0: let timer = stopTimer(args[0]); michael@0: sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer: timer }); michael@0: dumpMessage(this, "timeEnd", michael@0: "'" + timer.name + "' " + timer.duration + "ms"); michael@0: }, michael@0: }; michael@0: michael@0: this.console = new ConsoleAPI(); michael@0: this.ConsoleAPI = ConsoleAPI;