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