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/. */ 4 5"use strict"; 6 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 */ 22 23var EXPORTED_SYMBOLS = ["console", "ConsoleAPI"]; 24 25ChromeUtils.defineModuleGetter( 26 this, 27 "Services", 28 "resource://gre/modules/Services.jsm" 29); 30 31var gTimerRegistry = new Map(); 32 33/** 34 * String utility to ensure that strings are a specified length. Strings 35 * that are too long are truncated to the max length and the last char is 36 * set to "_". Strings that are too short are padded with spaces. 37 * 38 * @param {string} aStr 39 * The string to format to the correct length 40 * @param {number} aMaxLen 41 * The maximum allowed length of the returned string 42 * @param {number} aMinLen (optional) 43 * The minimum allowed length of the returned string. If undefined, 44 * then aMaxLen will be used 45 * @param {object} aOptions (optional) 46 * An object allowing format customization. Allowed customizations: 47 * 'truncate' - can take the value "start" to truncate strings from 48 * the start as opposed to the end or "center" to truncate 49 * strings in the center. 50 * 'align' - takes an alignment when padding is needed for MinLen, 51 * either "start" or "end". Defaults to "start". 52 * @return {string} 53 * The original string formatted to fit the specified lengths 54 */ 55function fmt(aStr, aMaxLen, aMinLen, aOptions) { 56 if (aMinLen == null) { 57 aMinLen = aMaxLen; 58 } 59 if (aStr == null) { 60 aStr = ""; 61 } 62 if (aStr.length > aMaxLen) { 63 if (aOptions && aOptions.truncate == "start") { 64 return "_" + aStr.substring(aStr.length - aMaxLen + 1); 65 } else if (aOptions && aOptions.truncate == "center") { 66 let start = aStr.substring(0, aMaxLen / 2); 67 68 let end = aStr.substring(aStr.length - aMaxLen / 2 + 1); 69 return start + "_" + end; 70 } 71 return aStr.substring(0, aMaxLen - 1) + "_"; 72 } 73 if (aStr.length < aMinLen) { 74 let padding = Array(aMinLen - aStr.length + 1).join(" "); 75 aStr = aOptions.align === "end" ? padding + aStr : aStr + padding; 76 } 77 return aStr; 78} 79 80/** 81 * Utility to extract the constructor name of an object. 82 * Object.toString gives: "[object ?????]"; we want the "?????". 83 * 84 * @param {object} aObj 85 * The object from which to extract the constructor name 86 * @return {string} 87 * The constructor name 88 */ 89function getCtorName(aObj) { 90 if (aObj === null) { 91 return "null"; 92 } 93 if (aObj === undefined) { 94 return "undefined"; 95 } 96 if (aObj.constructor && aObj.constructor.name) { 97 return aObj.constructor.name; 98 } 99 // If that fails, use Objects toString which sometimes gives something 100 // better than 'Object', and at least defaults to Object if nothing better 101 return Object.prototype.toString.call(aObj).slice(8, -1); 102} 103 104/** 105 * Indicates whether an object is a JS or `Components.Exception` error. 106 * 107 * @param {object} aThing 108 The object to check 109 * @return {boolean} 110 Is this object an error? 111 */ 112function isError(aThing) { 113 return ( 114 aThing && 115 ((typeof aThing.name == "string" && aThing.name.startsWith("NS_ERROR_")) || 116 getCtorName(aThing).endsWith("Error")) 117 ); 118} 119 120/** 121 * A single line stringification of an object designed for use by humans 122 * 123 * @param {any} aThing 124 * The object to be stringified 125 * @param {boolean} aAllowNewLines 126 * @return {string} 127 * A single line representation of aThing, which will generally be at 128 * most 80 chars long 129 */ 130function stringify(aThing, aAllowNewLines) { 131 if (aThing === undefined) { 132 return "undefined"; 133 } 134 135 if (aThing === null) { 136 return "null"; 137 } 138 139 if (isError(aThing)) { 140 return "Message: " + aThing; 141 } 142 143 if (typeof aThing == "object") { 144 let type = getCtorName(aThing); 145 if (Element.isInstance(aThing)) { 146 return debugElement(aThing); 147 } 148 type = type == "Object" ? "" : type + " "; 149 let json; 150 try { 151 json = JSON.stringify(aThing); 152 } catch (ex) { 153 // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled 154 json = "{" + Object.keys(aThing).join(":..,") + ":.., }"; 155 } 156 return type + json; 157 } 158 159 if (typeof aThing == "function") { 160 return aThing.toString().replace(/\s+/g, " "); 161 } 162 163 let str = aThing.toString(); 164 if (!aAllowNewLines) { 165 str = str.replace(/\n/g, "|"); 166 } 167 return str; 168} 169 170/** 171 * Create a simple debug representation of a given element. 172 * 173 * @param {Element} aElement 174 * The element to debug 175 * @return {string} 176 * A simple single line representation of aElement 177 */ 178function debugElement(aElement) { 179 return ( 180 "<" + 181 aElement.tagName + 182 (aElement.id ? "#" + aElement.id : "") + 183 (aElement.className && aElement.className.split 184 ? "." + aElement.className.split(" ").join(" .") 185 : "") + 186 ">" 187 ); 188} 189 190/** 191 * A multi line stringification of an object, designed for use by humans 192 * 193 * @param {any} aThing 194 * The object to be stringified 195 * @return {string} 196 * A multi line representation of aThing 197 */ 198function log(aThing) { 199 if (aThing === null) { 200 return "null\n"; 201 } 202 203 if (aThing === undefined) { 204 return "undefined\n"; 205 } 206 207 if (typeof aThing == "object") { 208 let reply = ""; 209 let type = getCtorName(aThing); 210 if (type == "Map") { 211 reply += "Map\n"; 212 for (let [key, value] of aThing) { 213 reply += logProperty(key, value); 214 } 215 } else if (type == "Set") { 216 let i = 0; 217 reply += "Set\n"; 218 for (let value of aThing) { 219 reply += logProperty("" + i, value); 220 i++; 221 } 222 } else if (isError(aThing)) { 223 reply += " Message: " + aThing + "\n"; 224 if (aThing.stack) { 225 reply += " Stack:\n"; 226 var frame = aThing.stack; 227 while (frame) { 228 reply += " " + frame + "\n"; 229 frame = frame.caller; 230 } 231 } 232 } else if (Element.isInstance(aThing)) { 233 reply += " " + debugElement(aThing) + "\n"; 234 } else { 235 let keys = Object.getOwnPropertyNames(aThing); 236 if (keys.length) { 237 reply += type + "\n"; 238 keys.forEach(function(aProp) { 239 reply += logProperty(aProp, aThing[aProp]); 240 }); 241 } else { 242 reply += type + "\n"; 243 let root = aThing; 244 let logged = []; 245 while (root != null) { 246 let properties = Object.keys(root); 247 properties.sort(); 248 properties.forEach(function(property) { 249 if (!(property in logged)) { 250 logged[property] = property; 251 reply += logProperty(property, aThing[property]); 252 } 253 }); 254 255 root = Object.getPrototypeOf(root); 256 if (root != null) { 257 reply += " - prototype " + getCtorName(root) + "\n"; 258 } 259 } 260 } 261 } 262 263 return reply; 264 } 265 266 return " " + aThing.toString() + "\n"; 267} 268 269/** 270 * Helper for log() which converts a property/value pair into an output 271 * string 272 * 273 * @param {string} aProp 274 * The name of the property to include in the output string 275 * @param {object} aValue 276 * Value assigned to aProp to be converted to a single line string 277 * @return {string} 278 * Multi line output string describing the property/value pair 279 */ 280function logProperty(aProp, aValue) { 281 let reply = ""; 282 if (aProp == "stack" && typeof value == "string") { 283 let trace = parseStack(aValue); 284 reply += formatTrace(trace); 285 } else { 286 reply += " - " + aProp + " = " + stringify(aValue) + "\n"; 287 } 288 return reply; 289} 290 291const LOG_LEVELS = { 292 all: Number.MIN_VALUE, 293 debug: 2, 294 log: 3, 295 info: 3, 296 clear: 3, 297 trace: 3, 298 timeEnd: 3, 299 time: 3, 300 assert: 3, 301 group: 3, 302 groupEnd: 3, 303 profile: 3, 304 profileEnd: 3, 305 dir: 3, 306 dirxml: 3, 307 warn: 4, 308 error: 5, 309 off: Number.MAX_VALUE, 310}; 311 312/** 313 * Helper to tell if a console message of `aLevel` type 314 * should be logged in stdout and sent to consoles given 315 * the current maximum log level being defined in `console.maxLogLevel` 316 * 317 * @param {string} aLevel 318 * Console message log level 319 * @param {string} aMaxLevel {string} 320 * String identifier (See LOG_LEVELS for possible 321 * values) that allows to filter which messages 322 * are logged based on their log level 323 * @return {boolean} 324 * Should this message be logged or not? 325 */ 326function shouldLog(aLevel, aMaxLevel) { 327 return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel]; 328} 329 330/** 331 * Parse a stack trace, returning an array of stack frame objects, where 332 * each has filename/lineNumber/functionName members 333 * 334 * @param {string} aStack 335 * The serialized stack trace 336 * @return {object[]} 337 * Array of { file: "...", line: NNN, call: "..." } objects 338 */ 339function parseStack(aStack) { 340 let trace = []; 341 aStack.split("\n").forEach(function(line) { 342 if (!line) { 343 return; 344 } 345 let at = line.lastIndexOf("@"); 346 let posn = line.substring(at + 1); 347 trace.push({ 348 filename: posn.split(":")[0], 349 lineNumber: posn.split(":")[1], 350 functionName: line.substring(0, at), 351 }); 352 }); 353 return trace; 354} 355 356/** 357 * Format a frame coming from Components.stack such that it can be used by the 358 * Browser Console, via console-api-log-event notifications. 359 * 360 * @param {object} aFrame 361 * The stack frame from which to begin the walk. 362 * @param {number=0} aMaxDepth 363 * Maximum stack trace depth. Default is 0 - no depth limit. 364 * @return {object[]} 365 * An array of {filename, lineNumber, functionName, language} objects. 366 * These objects follow the same format as other console-api-log-event 367 * messages. 368 */ 369function getStack(aFrame, aMaxDepth = 0) { 370 if (!aFrame) { 371 aFrame = Components.stack.caller; 372 } 373 let trace = []; 374 while (aFrame) { 375 trace.push({ 376 filename: aFrame.filename, 377 lineNumber: aFrame.lineNumber, 378 functionName: aFrame.name, 379 language: aFrame.language, 380 }); 381 if (aMaxDepth == trace.length) { 382 break; 383 } 384 aFrame = aFrame.caller; 385 } 386 return trace; 387} 388 389/** 390 * Take the output from parseStack() and convert it to nice readable 391 * output 392 * 393 * @param {object[]} aTrace 394 * Array of trace objects as created by parseStack() 395 * @return {string} Multi line report of the stack trace 396 */ 397function formatTrace(aTrace) { 398 let reply = ""; 399 aTrace.forEach(function(frame) { 400 reply += 401 fmt(frame.filename, 20, 20, { truncate: "start" }) + 402 " " + 403 fmt(frame.lineNumber, 5, 5) + 404 " " + 405 fmt(frame.functionName, 75, 0, { truncate: "center" }) + 406 "\n"; 407 }); 408 return reply; 409} 410 411/** 412 * Create a new timer by recording the current time under the specified name. 413 * 414 * @param {string} aName 415 * The name of the timer. 416 * @param {number} [aTimestamp=Date.now()] 417 * Optional timestamp that tells when the timer was originally started. 418 * @return {object} 419 * The name property holds the timer name and the started property 420 * holds the time the timer was started. In case of error, it returns 421 * an object with the single property "error" that contains the key 422 * for retrieving the localized error message. 423 */ 424function startTimer(aName, aTimestamp) { 425 let key = aName.toString(); 426 if (!gTimerRegistry.has(key)) { 427 gTimerRegistry.set(key, aTimestamp || Date.now()); 428 } 429 return { name: aName, started: gTimerRegistry.get(key) }; 430} 431 432/** 433 * Stop the timer with the specified name and retrieve the elapsed time. 434 * 435 * @param {string} aName 436 * The name of the timer. 437 * @param {number} [aTimestamp=Date.now()] 438 * Optional timestamp that tells when the timer was originally stopped. 439 * @return {object} 440 * The name property holds the timer name and the duration property 441 * holds the number of milliseconds since the timer was started. 442 */ 443function stopTimer(aName, aTimestamp) { 444 let key = aName.toString(); 445 let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key); 446 gTimerRegistry.delete(key); 447 return { name: aName, duration }; 448} 449 450/** 451 * Dump a new message header to stdout by taking care of adding an eventual 452 * prefix 453 * 454 * @param {object} aConsole 455 * ConsoleAPI instance 456 * @param {string} aLevel 457 * The string identifier for the message log level 458 * @param {string} aMessage 459 * The string message to print to stdout 460 */ 461function dumpMessage(aConsole, aLevel, aMessage) { 462 aConsole.dump( 463 "console." + 464 aLevel + 465 ": " + 466 (aConsole.prefix ? aConsole.prefix + ": " : "") + 467 aMessage + 468 "\n" 469 ); 470} 471 472/** 473 * Create a function which will output a concise level of output when used 474 * as a logging function 475 * 476 * @param {string} aLevel 477 * A prefix to all output generated from this function detailing the 478 * level at which output occurred 479 * @return {function} 480 * A logging function 481 * @see createMultiLineDumper() 482 */ 483function createDumper(aLevel) { 484 return function() { 485 if (!shouldLog(aLevel, this.maxLogLevel)) { 486 return; 487 } 488 let args = Array.prototype.slice.call(arguments, 0); 489 let frame = getStack(Components.stack.caller, 1)[0]; 490 sendConsoleAPIMessage(this, aLevel, frame, args); 491 let data = args.map(function(arg) { 492 return stringify(arg, true); 493 }); 494 dumpMessage(this, aLevel, data.join(" ")); 495 }; 496} 497 498/** 499 * Create a function which will output more detailed level of output when 500 * used as a logging function 501 * 502 * @param {string} aLevel 503 * A prefix to all output generated from this function detailing the 504 * level at which output occurred 505 * @return {function} 506 * A logging function 507 * @see createDumper() 508 */ 509function createMultiLineDumper(aLevel) { 510 return function() { 511 if (!shouldLog(aLevel, this.maxLogLevel)) { 512 return; 513 } 514 dumpMessage(this, aLevel, ""); 515 let args = Array.prototype.slice.call(arguments, 0); 516 let frame = getStack(Components.stack.caller, 1)[0]; 517 sendConsoleAPIMessage(this, aLevel, frame, args); 518 args.forEach(function(arg) { 519 this.dump(log(arg)); 520 }, this); 521 }; 522} 523 524/** 525 * Send a Console API message. This function will send a console-api-log-event 526 * notification through the nsIObserverService. 527 * 528 * @param {object} aConsole 529 * The instance of ConsoleAPI performing the logging. 530 * @param {string} aLevel 531 * Message severity level. This is usually the name of the console method 532 * that was called. 533 * @param {object} aFrame 534 * The youngest stack frame coming from Components.stack, as formatted by 535 * getStack(). 536 * @param {array} aArgs 537 * The arguments given to the console method. 538 * @param {object} aOptions 539 * Object properties depend on the console method that was invoked: 540 * - timer: for time() and timeEnd(). Holds the timer information. 541 * - groupName: for group(), groupCollapsed() and groupEnd(). 542 * - stacktrace: for trace(). Holds the array of stack frames as given by 543 * getStack(). 544 */ 545function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) { 546 let consoleEvent = { 547 ID: "jsm", 548 innerID: aConsole.innerID || aFrame.filename, 549 consoleID: aConsole.consoleID, 550 level: aLevel, 551 filename: aFrame.filename, 552 lineNumber: aFrame.lineNumber, 553 functionName: aFrame.functionName, 554 timeStamp: Date.now(), 555 arguments: aArgs, 556 prefix: aConsole.prefix, 557 chromeContext: true, 558 }; 559 560 consoleEvent.wrappedJSObject = consoleEvent; 561 562 switch (aLevel) { 563 case "trace": 564 consoleEvent.stacktrace = aOptions.stacktrace; 565 break; 566 case "time": 567 case "timeEnd": 568 consoleEvent.timer = aOptions.timer; 569 break; 570 case "group": 571 case "groupCollapsed": 572 case "groupEnd": 573 try { 574 consoleEvent.groupName = Array.prototype.join.call(aArgs, " "); 575 } catch (ex) { 576 Cu.reportError(ex); 577 Cu.reportError(ex.stack); 578 return; 579 } 580 break; 581 } 582 583 let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( 584 Ci.nsIConsoleAPIStorage 585 ); 586 if (ConsoleAPIStorage) { 587 ConsoleAPIStorage.recordEvent("jsm", null, consoleEvent); 588 } 589} 590 591/** 592 * This creates a console object that somewhat replicates Firebug's console 593 * object 594 * 595 * @param {object} aConsoleOptions 596 * Optional dictionary with a set of runtime console options: 597 * - prefix {string} : An optional prefix string to be printed before 598 * the actual logged message 599 * - maxLogLevel {string} : String identifier (See LOG_LEVELS for 600 * possible values) that allows to filter which 601 * messages are logged based on their log level. 602 * If falsy value, all messages will be logged. 603 * If wrong value that doesn't match any key of 604 * LOG_LEVELS, no message will be logged 605 * - maxLogLevelPref {string} : String pref name which contains the 606 * level to use for maxLogLevel. If the pref doesn't 607 * exist or gets removed, the maxLogLevel will default 608 * to the value passed to this constructor (or "all" 609 * if it wasn't specified). 610 * - dump {function} : An optional function to intercept all strings 611 * written to stdout 612 * - innerID {string}: An ID representing the source of the message. 613 * Normally the inner ID of a DOM window. 614 * - consoleID {string} : String identified for the console, this will 615 * be passed through the console notifications 616 * @return {object} 617 * A console API instance object 618 */ 619function ConsoleAPI(aConsoleOptions = {}) { 620 // Normalize console options to set default values 621 // in order to avoid runtime checks on each console method call. 622 this.dump = aConsoleOptions.dump || dump; 623 this.prefix = aConsoleOptions.prefix || ""; 624 this.maxLogLevel = aConsoleOptions.maxLogLevel; 625 this.innerID = aConsoleOptions.innerID || null; 626 this.consoleID = aConsoleOptions.consoleID || ""; 627 628 // Setup maxLogLevelPref watching 629 let updateMaxLogLevel = () => { 630 if ( 631 Services.prefs.getPrefType(aConsoleOptions.maxLogLevelPref) == 632 Services.prefs.PREF_STRING 633 ) { 634 this._maxLogLevel = Services.prefs 635 .getCharPref(aConsoleOptions.maxLogLevelPref) 636 .toLowerCase(); 637 } else { 638 this._maxLogLevel = this._maxExplicitLogLevel; 639 } 640 }; 641 642 if (aConsoleOptions.maxLogLevelPref) { 643 updateMaxLogLevel(); 644 Services.prefs.addObserver( 645 aConsoleOptions.maxLogLevelPref, 646 updateMaxLogLevel 647 ); 648 } 649 650 // Bind all the functions to this object. 651 for (let prop in this) { 652 if (typeof this[prop] === "function") { 653 this[prop] = this[prop].bind(this); 654 } 655 } 656} 657 658ConsoleAPI.prototype = { 659 /** 660 * The last log level that was specified via the constructor or setter. This 661 * is used as a fallback if the pref doesn't exist or is removed. 662 */ 663 _maxExplicitLogLevel: null, 664 /** 665 * The current log level via all methods of setting (pref or via the API). 666 */ 667 _maxLogLevel: null, 668 debug: createMultiLineDumper("debug"), 669 assert: createDumper("assert"), 670 log: createDumper("log"), 671 info: createDumper("info"), 672 warn: createDumper("warn"), 673 error: createMultiLineDumper("error"), 674 exception: createMultiLineDumper("error"), 675 676 trace: function Console_trace() { 677 if (!shouldLog("trace", this.maxLogLevel)) { 678 return; 679 } 680 let args = Array.prototype.slice.call(arguments, 0); 681 let trace = getStack(Components.stack.caller); 682 sendConsoleAPIMessage(this, "trace", trace[0], args, { stacktrace: trace }); 683 dumpMessage(this, "trace", "\n" + formatTrace(trace)); 684 }, 685 clear: function Console_clear() {}, 686 687 dir: createMultiLineDumper("dir"), 688 dirxml: createMultiLineDumper("dirxml"), 689 group: createDumper("group"), 690 groupEnd: createDumper("groupEnd"), 691 692 time: function Console_time() { 693 if (!shouldLog("time", this.maxLogLevel)) { 694 return; 695 } 696 let args = Array.prototype.slice.call(arguments, 0); 697 let frame = getStack(Components.stack.caller, 1)[0]; 698 let timer = startTimer(args[0]); 699 sendConsoleAPIMessage(this, "time", frame, args, { timer }); 700 dumpMessage(this, "time", "'" + timer.name + "' @ " + new Date()); 701 }, 702 703 timeEnd: function Console_timeEnd() { 704 if (!shouldLog("timeEnd", this.maxLogLevel)) { 705 return; 706 } 707 let args = Array.prototype.slice.call(arguments, 0); 708 let frame = getStack(Components.stack.caller, 1)[0]; 709 let timer = stopTimer(args[0]); 710 sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer }); 711 dumpMessage( 712 this, 713 "timeEnd", 714 "'" + timer.name + "' " + timer.duration + "ms" 715 ); 716 }, 717 718 profile(profileName) { 719 if (!shouldLog("profile", this.maxLogLevel)) { 720 return; 721 } 722 Services.obs.notifyObservers( 723 { 724 wrappedJSObject: { 725 action: "profile", 726 arguments: [profileName], 727 chromeContext: true, 728 }, 729 }, 730 "console-api-profiler" 731 ); 732 dumpMessage(this, "profile", `'${profileName}'`); 733 }, 734 735 profileEnd(profileName) { 736 if (!shouldLog("profileEnd", this.maxLogLevel)) { 737 return; 738 } 739 Services.obs.notifyObservers( 740 { 741 wrappedJSObject: { 742 action: "profileEnd", 743 arguments: [profileName], 744 chromeContext: true, 745 }, 746 }, 747 "console-api-profiler" 748 ); 749 dumpMessage(this, "profileEnd", `'${profileName}'`); 750 }, 751 752 get maxLogLevel() { 753 return this._maxLogLevel || "all"; 754 }, 755 756 set maxLogLevel(aValue) { 757 this._maxLogLevel = this._maxExplicitLogLevel = aValue; 758 }, 759}; 760 761var console = new ConsoleAPI(); 762