Wed, 31 Dec 2014 13:27:57 +0100
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;