Bumping manifests a=b2g-bump
[gecko.git] / toolkit / devtools / Console.jsm
blobce9532faa61798f1434dd70ca39fb57261668565
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;
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);
109  * A single line stringification of an object designed for use by humans
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;
156  * Create a simple debug representation of a given element.
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       ">";
173  * A multi line stringification of an object, designed for use by humans
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";
259  * Helper for log() which converts a property/value pair into an output
260  * string
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;
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,
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`
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];
317  * Parse a stack trace, returning an array of stack frame objects, where
318  * each has filename/lineNumber/functionName members
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;
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.
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;
376  * Take the output from parseStack() and convert it to nice readable
377  * output
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;
394  * Create a new timer by recording the current time under the specified name.
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) };
415  * Stop the timer with the specified name and retrieve the elapsed time.
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 };
433  * Dump a new message header to stdout by taking care of adding an eventual
434  * prefix
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   );
452  * Create a function which will output a concise level of output when used
453  * as a logging function
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   };
478  * Create a function which will output more detailed level of output when
479  * used as a logging function
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   };
504  * Send a Console API message. This function will send a console-api-log-event
505  * notification through the nsIObserverService.
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 = {})
526   let consoleEvent = {
527     ID: "jsm",
528     innerID: aConsole.innerID || aFrame.filename,
529     consoleID: aConsole.consoleID,
530     level: aLevel,
531     filename: aFrame.filename,
532     lineNumber: aFrame.lineNumber,
533     functionName: aFrame.functionName,
534     timeStamp: Date.now(),
535     arguments: aArgs,
536   };
538   consoleEvent.wrappedJSObject = consoleEvent;
540   switch (aLevel) {
541     case "trace":
542       consoleEvent.stacktrace = aOptions.stacktrace;
543       break;
544     case "time":
545     case "timeEnd":
546       consoleEvent.timer = aOptions.timer;
547       break;
548     case "group":
549     case "groupCollapsed":
550     case "groupEnd":
551       try {
552         consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
553       }
554       catch (ex) {
555         Cu.reportError(ex);
556         Cu.reportError(ex.stack);
557         return;
558       }
559       break;
560   }
562   Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null);
563   let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
564                             .getService(Ci.nsIConsoleAPIStorage);
565   ConsoleAPIStorage.recordEvent("jsm", consoleEvent);
569  * This creates a console object that somewhat replicates Firebug's console
570  * object
572  * @param {object} aConsoleOptions
573  *        Optional dictionary with a set of runtime console options:
574  *        - prefix {string} : An optional prefix string to be printed before
575  *                            the actual logged message
576  *        - maxLogLevel {string} : String identifier (See LOG_LEVELS for
577  *                            possible values) that allows to filter which
578  *                            messages are logged based on their log level.
579  *                            If falsy value, all messages will be logged.
580  *                            If wrong value that doesn't match any key of
581  *                            LOG_LEVELS, no message will be logged
582  *        - dump {function} : An optional function to intercept all strings
583  *                            written to stdout
584  *        - innerID {string}: An ID representing the source of the message.
585  *                            Normally the inner ID of a DOM window.
586  *        - consoleID {string} : String identified for the console, this will
587  *                            be passed through the console notifications
588  * @return {object}
589  *        A console API instance object
590  */
591 function ConsoleAPI(aConsoleOptions = {}) {
592   // Normalize console options to set default values
593   // in order to avoid runtime checks on each console method call.
594   this.dump = aConsoleOptions.dump || dump;
595   this.prefix = aConsoleOptions.prefix || "";
596   this.maxLogLevel = aConsoleOptions.maxLogLevel || "all";
597   this.innerID = aConsoleOptions.innerID || null;
598   this.consoleID = aConsoleOptions.consoleID || "";
600   // Bind all the functions to this object.
601   for (let prop in this) {
602     if (typeof(this[prop]) === "function") {
603       this[prop] = this[prop].bind(this);
604     }
605   }
608 ConsoleAPI.prototype = {
609   debug: createMultiLineDumper("debug"),
610   log: createDumper("log"),
611   info: createDumper("info"),
612   warn: createDumper("warn"),
613   error: createMultiLineDumper("error"),
614   exception: createMultiLineDumper("error"),
616   trace: function Console_trace() {
617     if (!shouldLog("trace", this.maxLogLevel)) {
618       return;
619     }
620     let args = Array.prototype.slice.call(arguments, 0);
621     let trace = getStack(Components.stack.caller);
622     sendConsoleAPIMessage(this, "trace", trace[0], args,
623                           { stacktrace: trace });
624     dumpMessage(this, "trace", "\n" + formatTrace(trace));
625   },
626   clear: function Console_clear() {},
628   dir: createMultiLineDumper("dir"),
629   dirxml: createMultiLineDumper("dirxml"),
630   group: createDumper("group"),
631   groupEnd: createDumper("groupEnd"),
633   time: function Console_time() {
634     if (!shouldLog("time", this.maxLogLevel)) {
635       return;
636     }
637     let args = Array.prototype.slice.call(arguments, 0);
638     let frame = getStack(Components.stack.caller, 1)[0];
639     let timer = startTimer(args[0]);
640     sendConsoleAPIMessage(this, "time", frame, args, { timer: timer });
641     dumpMessage(this, "time",
642                 "'" + timer.name + "' @ " + (new Date()));
643   },
645   timeEnd: function Console_timeEnd() {
646     if (!shouldLog("timeEnd", this.maxLogLevel)) {
647       return;
648     }
649     let args = Array.prototype.slice.call(arguments, 0);
650     let frame = getStack(Components.stack.caller, 1)[0];
651     let timer = stopTimer(args[0]);
652     sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer: timer });
653     dumpMessage(this, "timeEnd",
654                 "'" + timer.name + "' " + timer.duration + "ms");
655   },
658 this.console = new ConsoleAPI();
659 this.ConsoleAPI = ConsoleAPI;