toolkit/devtools/Console.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 /**
     8  * Define a 'console' API to roughly match the implementation provided by
     9  * Firebug.
    10  * This module helps cases where code is shared between the web and Firefox.
    11  * See also Browser.jsm for an implementation of other web constants to help
    12  * sharing code between the web and firefox;
    13  *
    14  * The API is only be a rough approximation for 3 reasons:
    15  * - The Firebug console API is implemented in many places with differences in
    16  *   the implementations, so there isn't a single reference to adhere to
    17  * - The Firebug console is a rich display compared with dump(), so there will
    18  *   be many things that we can't replicate
    19  * - The primary use of this API is debugging and error logging so the perfect
    20  *   implementation isn't always required (or even well defined)
    21  */
    23 this.EXPORTED_SYMBOLS = [ "console", "ConsoleAPI" ];
    25 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    27 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    29 XPCOMUtils.defineLazyModuleGetter(this, "Services",
    30                                   "resource://gre/modules/Services.jsm");
    32 let gTimerRegistry = new Map();
    34 /**
    35  * String utility to ensure that strings are a specified length. Strings
    36  * that are too long are truncated to the max length and the last char is
    37  * set to "_". Strings that are too short are padded with spaces.
    38  *
    39  * @param {string} aStr
    40  *        The string to format to the correct length
    41  * @param {number} aMaxLen
    42  *        The maximum allowed length of the returned string
    43  * @param {number} aMinLen (optional)
    44  *        The minimum allowed length of the returned string. If undefined,
    45  *        then aMaxLen will be used
    46  * @param {object} aOptions (optional)
    47  *        An object allowing format customization. Allowed customizations:
    48  *          'truncate' - can take the value "start" to truncate strings from
    49  *             the start as opposed to the end or "center" to truncate
    50  *             strings in the center.
    51  *          'align' - takes an alignment when padding is needed for MinLen,
    52  *             either "start" or "end".  Defaults to "start".
    53  * @return {string}
    54  *        The original string formatted to fit the specified lengths
    55  */
    56 function fmt(aStr, aMaxLen, aMinLen, aOptions) {
    57   if (aMinLen == null) {
    58     aMinLen = aMaxLen;
    59   }
    60   if (aStr == null) {
    61     aStr = "";
    62   }
    63   if (aStr.length > aMaxLen) {
    64     if (aOptions && aOptions.truncate == "start") {
    65       return "_" + aStr.substring(aStr.length - aMaxLen + 1);
    66     }
    67     else if (aOptions && aOptions.truncate == "center") {
    68       let start = aStr.substring(0, (aMaxLen / 2));
    70       let end = aStr.substring((aStr.length - (aMaxLen / 2)) + 1);
    71       return start + "_" + end;
    72     }
    73     else {
    74       return aStr.substring(0, aMaxLen - 1) + "_";
    75     }
    76   }
    77   if (aStr.length < aMinLen) {
    78     let padding = Array(aMinLen - aStr.length + 1).join(" ");
    79     aStr = (aOptions.align === "end") ? padding + aStr : aStr + padding;
    80   }
    81   return aStr;
    82 }
    84 /**
    85  * Utility to extract the constructor name of an object.
    86  * Object.toString gives: "[object ?????]"; we want the "?????".
    87  *
    88  * @param {object} aObj
    89  *        The object from which to extract the constructor name
    90  * @return {string}
    91  *        The constructor name
    92  */
    93 function getCtorName(aObj) {
    94   if (aObj === null) {
    95     return "null";
    96   }
    97   if (aObj === undefined) {
    98     return "undefined";
    99   }
   100   if (aObj.constructor && aObj.constructor.name) {
   101     return aObj.constructor.name;
   102   }
   103   // If that fails, use Objects toString which sometimes gives something
   104   // better than 'Object', and at least defaults to Object if nothing better
   105   return Object.prototype.toString.call(aObj).slice(8, -1);
   106 }
   108 /**
   109  * A single line stringification of an object designed for use by humans
   110  *
   111  * @param {any} aThing
   112  *        The object to be stringified
   113  * @param {boolean} aAllowNewLines
   114  * @return {string}
   115  *        A single line representation of aThing, which will generally be at
   116  *        most 80 chars long
   117  */
   118 function stringify(aThing, aAllowNewLines) {
   119   if (aThing === undefined) {
   120     return "undefined";
   121   }
   123   if (aThing === null) {
   124     return "null";
   125   }
   127   if (typeof aThing == "object") {
   128     let type = getCtorName(aThing);
   129     if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) {
   130       return debugElement(aThing);
   131     }
   132     type = (type == "Object" ? "" : type + " ");
   133     let json;
   134     try {
   135       json = JSON.stringify(aThing);
   136     }
   137     catch (ex) {
   138       // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled
   139       json = "{" + Object.keys(aThing).join(":..,") + ":.., " + "}";
   140     }
   141     return type + json;
   142   }
   144   if (typeof aThing == "function") {
   145     return aThing.toString().replace(/\s+/g, " ");
   146   }
   148   let str = aThing.toString();
   149   if (!aAllowNewLines) {
   150     str = str.replace(/\n/g, "|");
   151   }
   152   return str;
   153 }
   155 /**
   156  * Create a simple debug representation of a given element.
   157  *
   158  * @param {nsIDOMElement} aElement
   159  *        The element to debug
   160  * @return {string}
   161  *        A simple single line representation of aElement
   162  */
   163 function debugElement(aElement) {
   164   return "<" + aElement.tagName +
   165       (aElement.id ? "#" + aElement.id : "") +
   166       (aElement.className ?
   167           "." + aElement.className.split(" ").join(" .") :
   168           "") +
   169       ">";
   170 }
   172 /**
   173  * A multi line stringification of an object, designed for use by humans
   174  *
   175  * @param {any} aThing
   176  *        The object to be stringified
   177  * @return {string}
   178  *        A multi line representation of aThing
   179  */
   180 function log(aThing) {
   181   if (aThing === null) {
   182     return "null\n";
   183   }
   185   if (aThing === undefined) {
   186     return "undefined\n";
   187   }
   189   if (typeof aThing == "object") {
   190     let reply = "";
   191     let type = getCtorName(aThing);
   192     if (type == "Map") {
   193       reply += "Map\n";
   194       for (let [key, value] of aThing) {
   195         reply += logProperty(key, value);
   196       }
   197     }
   198     else if (type == "Set") {
   199       let i = 0;
   200       reply += "Set\n";
   201       for (let value of aThing) {
   202         reply += logProperty('' + i, value);
   203         i++;
   204       }
   205     }
   206     else if (type.match("Error$") ||
   207              (typeof aThing.name == "string" &&
   208               aThing.name.match("NS_ERROR_"))) {
   209       reply += "  Message: " + aThing + "\n";
   210       if (aThing.stack) {
   211         reply += "  Stack:\n";
   212         var frame = aThing.stack;
   213         while (frame) {
   214           reply += "    " + frame + "\n";
   215           frame = frame.caller;
   216         }
   217       }
   218     }
   219     else if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) {
   220       reply += "  " + debugElement(aThing) + "\n";
   221     }
   222     else {
   223       let keys = Object.getOwnPropertyNames(aThing);
   224       if (keys.length > 0) {
   225         reply += type + "\n";
   226         keys.forEach(function(aProp) {
   227           reply += logProperty(aProp, aThing[aProp]);
   228         });
   229       }
   230       else {
   231         reply += type + "\n";
   232         let root = aThing;
   233         let logged = [];
   234         while (root != null) {
   235           let properties = Object.keys(root);
   236           properties.sort();
   237           properties.forEach(function(property) {
   238             if (!(property in logged)) {
   239               logged[property] = property;
   240               reply += logProperty(property, aThing[property]);
   241             }
   242           });
   244           root = Object.getPrototypeOf(root);
   245           if (root != null) {
   246             reply += '  - prototype ' + getCtorName(root) + '\n';
   247           }
   248         }
   249       }
   250     }
   252     return reply;
   253   }
   255   return "  " + aThing.toString() + "\n";
   256 }
   258 /**
   259  * Helper for log() which converts a property/value pair into an output
   260  * string
   261  *
   262  * @param {string} aProp
   263  *        The name of the property to include in the output string
   264  * @param {object} aValue
   265  *        Value assigned to aProp to be converted to a single line string
   266  * @return {string}
   267  *        Multi line output string describing the property/value pair
   268  */
   269 function logProperty(aProp, aValue) {
   270   let reply = "";
   271   if (aProp == "stack" && typeof value == "string") {
   272     let trace = parseStack(aValue);
   273     reply += formatTrace(trace);
   274   }
   275   else {
   276     reply += "    - " + aProp + " = " + stringify(aValue) + "\n";
   277   }
   278   return reply;
   279 }
   281 const LOG_LEVELS = {
   282   "all": Number.MIN_VALUE,
   283   "debug": 2,
   284   "log": 3,
   285   "info": 3,
   286   "trace": 3,
   287   "timeEnd": 3,
   288   "time": 3,
   289   "group": 3,
   290   "groupEnd": 3,
   291   "dir": 3,
   292   "dirxml": 3,
   293   "warn": 4,
   294   "error": 5,
   295   "off": Number.MAX_VALUE,
   296 };
   298 /**
   299  * Helper to tell if a console message of `aLevel` type
   300  * should be logged in stdout and sent to consoles given
   301  * the current maximum log level being defined in `console.maxLogLevel`
   302  *
   303  * @param {string} aLevel
   304  *        Console message log level
   305  * @param {string} aMaxLevel {string}
   306  *        String identifier (See LOG_LEVELS for possible
   307  *        values) that allows to filter which messages
   308  *        are logged based on their log level
   309  * @return {boolean}
   310  *        Should this message be logged or not?
   311  */
   312 function shouldLog(aLevel, aMaxLevel) {
   313   return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel];
   314 }
   316 /**
   317  * Parse a stack trace, returning an array of stack frame objects, where
   318  * each has filename/lineNumber/functionName members
   319  *
   320  * @param {string} aStack
   321  *        The serialized stack trace
   322  * @return {object[]}
   323  *        Array of { file: "...", line: NNN, call: "..." } objects
   324  */
   325 function parseStack(aStack) {
   326   let trace = [];
   327   aStack.split("\n").forEach(function(line) {
   328     if (!line) {
   329       return;
   330     }
   331     let at = line.lastIndexOf("@");
   332     let posn = line.substring(at + 1);
   333     trace.push({
   334       filename: posn.split(":")[0],
   335       lineNumber: posn.split(":")[1],
   336       functionName: line.substring(0, at)
   337     });
   338   });
   339   return trace;
   340 }
   342 /**
   343  * Format a frame coming from Components.stack such that it can be used by the
   344  * Browser Console, via console-api-log-event notifications.
   345  *
   346  * @param {object} aFrame
   347  *        The stack frame from which to begin the walk.
   348  * @param {number=0} aMaxDepth
   349  *        Maximum stack trace depth. Default is 0 - no depth limit.
   350  * @return {object[]}
   351  *         An array of {filename, lineNumber, functionName, language} objects.
   352  *         These objects follow the same format as other console-api-log-event
   353  *         messages.
   354  */
   355 function getStack(aFrame, aMaxDepth = 0) {
   356   if (!aFrame) {
   357     aFrame = Components.stack.caller;
   358   }
   359   let trace = [];
   360   while (aFrame) {
   361     trace.push({
   362       filename: aFrame.filename,
   363       lineNumber: aFrame.lineNumber,
   364       functionName: aFrame.name,
   365       language: aFrame.language,
   366     });
   367     if (aMaxDepth == trace.length) {
   368       break;
   369     }
   370     aFrame = aFrame.caller;
   371   }
   372   return trace;
   373 }
   375 /**
   376  * Take the output from parseStack() and convert it to nice readable
   377  * output
   378  *
   379  * @param {object[]} aTrace
   380  *        Array of trace objects as created by parseStack()
   381  * @return {string} Multi line report of the stack trace
   382  */
   383 function formatTrace(aTrace) {
   384   let reply = "";
   385   aTrace.forEach(function(frame) {
   386     reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " +
   387              fmt(frame.lineNumber, 5, 5) + " " +
   388              fmt(frame.functionName, 75, 0, { truncate: "center" }) + "\n";
   389   });
   390   return reply;
   391 }
   393 /**
   394  * Create a new timer by recording the current time under the specified name.
   395  *
   396  * @param {string} aName
   397  *        The name of the timer.
   398  * @param {number} [aTimestamp=Date.now()]
   399  *        Optional timestamp that tells when the timer was originally started.
   400  * @return {object}
   401  *         The name property holds the timer name and the started property
   402  *         holds the time the timer was started. In case of error, it returns
   403  *         an object with the single property "error" that contains the key
   404  *         for retrieving the localized error message.
   405  */
   406 function startTimer(aName, aTimestamp) {
   407   let key = aName.toString();
   408   if (!gTimerRegistry.has(key)) {
   409     gTimerRegistry.set(key, aTimestamp || Date.now());
   410   }
   411   return { name: aName, started: gTimerRegistry.get(key) };
   412 }
   414 /**
   415  * Stop the timer with the specified name and retrieve the elapsed time.
   416  *
   417  * @param {string} aName
   418  *        The name of the timer.
   419  * @param {number} [aTimestamp=Date.now()]
   420  *        Optional timestamp that tells when the timer was originally stopped.
   421  * @return {object}
   422  *         The name property holds the timer name and the duration property
   423  *         holds the number of milliseconds since the timer was started.
   424  */
   425 function stopTimer(aName, aTimestamp) {
   426   let key = aName.toString();
   427   let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key);
   428   gTimerRegistry.delete(key);
   429   return { name: aName, duration: duration };
   430 }
   432 /**
   433  * Dump a new message header to stdout by taking care of adding an eventual
   434  * prefix
   435  *
   436  * @param {object} aConsole
   437  *        ConsoleAPI instance
   438  * @param {string} aLevel
   439  *        The string identifier for the message log level
   440  * @param {string} aMessage
   441  *        The string message to print to stdout
   442  */
   443 function dumpMessage(aConsole, aLevel, aMessage) {
   444   aConsole.dump(
   445     "console." + aLevel + ": " +
   446     aConsole.prefix +
   447     aMessage + "\n"
   448   );
   449 }
   451 /**
   452  * Create a function which will output a concise level of output when used
   453  * as a logging function
   454  *
   455  * @param {string} aLevel
   456  *        A prefix to all output generated from this function detailing the
   457  *        level at which output occurred
   458  * @return {function}
   459  *        A logging function
   460  * @see createMultiLineDumper()
   461  */
   462 function createDumper(aLevel) {
   463   return function() {
   464     if (!shouldLog(aLevel, this.maxLogLevel)) {
   465       return;
   466     }
   467     let args = Array.prototype.slice.call(arguments, 0);
   468     let frame = getStack(Components.stack.caller, 1)[0];
   469     sendConsoleAPIMessage(this, aLevel, frame, args);
   470     let data = args.map(function(arg) {
   471       return stringify(arg, true);
   472     });
   473     dumpMessage(this, aLevel, data.join(" "));
   474   };
   475 }
   477 /**
   478  * Create a function which will output more detailed level of output when
   479  * used as a logging function
   480  *
   481  * @param {string} aLevel
   482  *        A prefix to all output generated from this function detailing the
   483  *        level at which output occurred
   484  * @return {function}
   485  *        A logging function
   486  * @see createDumper()
   487  */
   488 function createMultiLineDumper(aLevel) {
   489   return function() {
   490     if (!shouldLog(aLevel, this.maxLogLevel)) {
   491       return;
   492     }
   493     dumpMessage(this, aLevel, "");
   494     let args = Array.prototype.slice.call(arguments, 0);
   495     let frame = getStack(Components.stack.caller, 1)[0];
   496     sendConsoleAPIMessage(this, aLevel, frame, args);
   497     args.forEach(function(arg) {
   498       this.dump(log(arg));
   499     }, this);
   500   };
   501 }
   503 /**
   504  * Send a Console API message. This function will send a console-api-log-event
   505  * notification through the nsIObserverService.
   506  *
   507  * @param {object} aConsole
   508  *        The instance of ConsoleAPI performing the logging.
   509  * @param {string} aLevel
   510  *        Message severity level. This is usually the name of the console method
   511  *        that was called.
   512  * @param {object} aFrame
   513  *        The youngest stack frame coming from Components.stack, as formatted by
   514  *        getStack().
   515  * @param {array} aArgs
   516  *        The arguments given to the console method.
   517  * @param {object} aOptions
   518  *        Object properties depend on the console method that was invoked:
   519  *        - timer: for time() and timeEnd(). Holds the timer information.
   520  *        - groupName: for group(), groupCollapsed() and groupEnd().
   521  *        - stacktrace: for trace(). Holds the array of stack frames as given by
   522  *        getStack().
   523  */
   524 function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {})
   525 {
   526   let consoleEvent = {
   527     ID: "jsm",
   528     innerID: aConsole.innerID || aFrame.filename,
   529     level: aLevel,
   530     filename: aFrame.filename,
   531     lineNumber: aFrame.lineNumber,
   532     functionName: aFrame.functionName,
   533     timeStamp: Date.now(),
   534     arguments: aArgs,
   535   };
   537   consoleEvent.wrappedJSObject = consoleEvent;
   539   switch (aLevel) {
   540     case "trace":
   541       consoleEvent.stacktrace = aOptions.stacktrace;
   542       break;
   543     case "time":
   544     case "timeEnd":
   545       consoleEvent.timer = aOptions.timer;
   546       break;
   547     case "group":
   548     case "groupCollapsed":
   549     case "groupEnd":
   550       try {
   551         consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
   552       }
   553       catch (ex) {
   554         Cu.reportError(ex);
   555         Cu.reportError(ex.stack);
   556         return;
   557       }
   558       break;
   559   }
   561   Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null);
   562   let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
   563                             .getService(Ci.nsIConsoleAPIStorage);
   564   ConsoleAPIStorage.recordEvent("jsm", consoleEvent);
   565 }
   567 /**
   568  * This creates a console object that somewhat replicates Firebug's console
   569  * object
   570  *
   571  * @param {object} aConsoleOptions
   572  *        Optional dictionary with a set of runtime console options:
   573  *        - prefix {string} : An optional prefix string to be printed before
   574  *                            the actual logged message
   575  *        - maxLogLevel {string} : String identifier (See LOG_LEVELS for
   576  *                            possible values) that allows to filter which
   577  *                            messages are logged based on their log level.
   578  *                            If falsy value, all messages will be logged.
   579  *                            If wrong value that doesn't match any key of
   580  *                            LOG_LEVELS, no message will be logged
   581  *        - dump {function} : An optional function to intercept all strings
   582  *                            written to stdout
   583  *        - innerID {string}: An ID representing the source of the message.
   584  *                            Normally the inner ID of a DOM window.
   585  * @return {object}
   586  *        A console API instance object
   587  */
   588 function ConsoleAPI(aConsoleOptions = {}) {
   589   // Normalize console options to set default values
   590   // in order to avoid runtime checks on each console method call.
   591   this.dump = aConsoleOptions.dump || dump;
   592   this.prefix = aConsoleOptions.prefix || "";
   593   this.maxLogLevel = aConsoleOptions.maxLogLevel || "all";
   594   this.innerID = aConsoleOptions.innerID || null;
   596   // Bind all the functions to this object.
   597   for (let prop in this) {
   598     if (typeof(this[prop]) === "function") {
   599       this[prop] = this[prop].bind(this);
   600     }
   601   }
   602 }
   604 ConsoleAPI.prototype = {
   605   debug: createMultiLineDumper("debug"),
   606   log: createDumper("log"),
   607   info: createDumper("info"),
   608   warn: createDumper("warn"),
   609   error: createMultiLineDumper("error"),
   610   exception: createMultiLineDumper("error"),
   612   trace: function Console_trace() {
   613     if (!shouldLog("trace", this.maxLogLevel)) {
   614       return;
   615     }
   616     let args = Array.prototype.slice.call(arguments, 0);
   617     let trace = getStack(Components.stack.caller);
   618     sendConsoleAPIMessage(this, "trace", trace[0], args,
   619                           { stacktrace: trace });
   620     dumpMessage(this, "trace", "\n" + formatTrace(trace));
   621   },
   622   clear: function Console_clear() {},
   624   dir: createMultiLineDumper("dir"),
   625   dirxml: createMultiLineDumper("dirxml"),
   626   group: createDumper("group"),
   627   groupEnd: createDumper("groupEnd"),
   629   time: function Console_time() {
   630     if (!shouldLog("time", this.maxLogLevel)) {
   631       return;
   632     }
   633     let args = Array.prototype.slice.call(arguments, 0);
   634     let frame = getStack(Components.stack.caller, 1)[0];
   635     let timer = startTimer(args[0]);
   636     sendConsoleAPIMessage(this, "time", frame, args, { timer: timer });
   637     dumpMessage(this, "time",
   638                 "'" + timer.name + "' @ " + (new Date()));
   639   },
   641   timeEnd: function Console_timeEnd() {
   642     if (!shouldLog("timeEnd", this.maxLogLevel)) {
   643       return;
   644     }
   645     let args = Array.prototype.slice.call(arguments, 0);
   646     let frame = getStack(Components.stack.caller, 1)[0];
   647     let timer = stopTimer(args[0]);
   648     sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer: timer });
   649     dumpMessage(this, "timeEnd",
   650                 "'" + timer.name + "' " + timer.duration + "ms");
   651   },
   652 };
   654 this.console = new ConsoleAPI();
   655 this.ConsoleAPI = ConsoleAPI;

mercurial