michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: #ifdef DEBUG michael@0: michael@0: // Generic logging/debugging functionality that: michael@0: // michael@0: // (*) when disabled compiles to no-ops at worst (for calls to the service) michael@0: // and to nothing at best (calls to G_Debug() and similar are compiled michael@0: // away when you use a jscompiler that strips dead code) michael@0: // michael@0: // (*) has dynamically configurable/creatable debugging "zones" enabling michael@0: // selective logging michael@0: // michael@0: // (*) hides its plumbing so that all calls in different zones are uniform, michael@0: // so you can drop files using this library into other apps that use it michael@0: // without any configuration michael@0: // michael@0: // (*) can be controlled programmatically or via preferences. The michael@0: // preferences that control the service and its zones are under michael@0: // the preference branch "safebrowsing-debug-service." michael@0: // michael@0: // (*) outputs function call traces when the "loggifier" zone is enabled michael@0: // michael@0: // (*) can write output to logfiles so that you can get a call trace michael@0: // from someone who is having a problem michael@0: // michael@0: // Example: michael@0: // michael@0: // var G_GDEBUG = true // Enable this module michael@0: // var G_debugService = new G_DebugService(); // in global context michael@0: // michael@0: // // You can use it with arbitrary primitive first arguement michael@0: // G_Debug("myzone", "Yo yo yo"); // outputs: [myzone] Yo yo yo\n michael@0: // michael@0: // // But it's nice to use it with an object; it will probe for the zone name michael@0: // function Obj() { michael@0: // this.debugZone = "someobj"; michael@0: // } michael@0: // Obj.prototype.foo = function() { michael@0: // G_Debug(this, "foo called"); michael@0: // } michael@0: // (new Obj).foo(); // outputs: [someobj] foo called\n michael@0: // michael@0: // G_debugService.loggifier.loggify(Obj.prototype); // enable call tracing michael@0: // michael@0: // // En/disable specific zones programmatically (you can also use preferences) michael@0: // G_debugService.enableZone("somezone"); michael@0: // G_debugService.disableZone("someotherzone"); michael@0: // G_debugService.enableAllZones(); michael@0: // michael@0: // // We also have asserts and errors: michael@0: // G_Error(this, "Some error occurred"); // will throw michael@0: // G_Assert(this, (x > 3), "x not greater than three!"); // will throw michael@0: // michael@0: // See classes below for more methods. michael@0: // michael@0: // TODO add code to set prefs when not found to the default value of a tristate michael@0: // TODO add error level support michael@0: // TODO add ability to turn off console output michael@0: // michael@0: // -------> TO START DEBUGGING: set G_GDEBUG to true michael@0: michael@0: // These are the functions code will typically call. Everything is michael@0: // wrapped in if's so we can compile it away when G_GDEBUG is false. michael@0: michael@0: michael@0: if (typeof G_GDEBUG == "undefined") { michael@0: throw new Error("G_GDEBUG constant must be set before loading debug.js"); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Write out a debugging message. michael@0: * michael@0: * @param who The thingy to convert into a zone name corresponding to the michael@0: * zone to which this message belongs michael@0: * @param msg Message to output michael@0: */ michael@0: function G_Debug(who, msg) { michael@0: if (G_GDEBUG) { michael@0: G_GetDebugZone(who).debug(msg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Debugs loudly michael@0: */ michael@0: function G_DebugL(who, msg) { michael@0: if (G_GDEBUG) { michael@0: var zone = G_GetDebugZone(who); michael@0: michael@0: if (zone.zoneIsEnabled()) { michael@0: G_debugService.dump( michael@0: "\n************************************************************\n"); michael@0: michael@0: G_Debug(who, msg); michael@0: michael@0: G_debugService.dump( michael@0: "************************************************************\n\n"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Write out a call tracing message michael@0: * michael@0: * @param who The thingy to convert into a zone name corresponding to the michael@0: * zone to which this message belongs michael@0: * @param msg Message to output michael@0: */ michael@0: function G_TraceCall(who, msg) { michael@0: if (G_GDEBUG) { michael@0: if (G_debugService.callTracingEnabled()) { michael@0: G_debugService.dump(msg + "\n"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Write out an error (and throw) michael@0: * michael@0: * @param who The thingy to convert into a zone name corresponding to the michael@0: * zone to which this message belongs michael@0: * @param msg Message to output michael@0: */ michael@0: function G_Error(who, msg) { michael@0: if (G_GDEBUG) { michael@0: G_GetDebugZone(who).error(msg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Assert something as true and signal an error if it's not michael@0: * michael@0: * @param who The thingy to convert into a zone name corresponding to the michael@0: * zone to which this message belongs michael@0: * @param condition Boolean condition to test michael@0: * @param msg Message to output michael@0: */ michael@0: function G_Assert(who, condition, msg) { michael@0: if (G_GDEBUG) { michael@0: G_GetDebugZone(who).assert(condition, msg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Helper function that takes input and returns the DebugZone michael@0: * corresponding to it. michael@0: * michael@0: * @param who Arbitrary input that will be converted into a zone name. Most michael@0: * likely an object that has .debugZone property, or a string. michael@0: * @returns The DebugZone object corresponding to the input michael@0: */ michael@0: function G_GetDebugZone(who) { michael@0: if (G_GDEBUG) { michael@0: var zone = "?"; michael@0: michael@0: if (who && who.debugZone) { michael@0: zone = who.debugZone; michael@0: } else if (typeof who == "string") { michael@0: zone = who; michael@0: } michael@0: michael@0: return G_debugService.getZone(zone); michael@0: } michael@0: } michael@0: michael@0: // Classes that implement the functionality. michael@0: michael@0: /** michael@0: * A debug "zone" is a string derived from arbitrary types (but michael@0: * typically derived from another string or an object). All debugging michael@0: * messages using a particular zone can be enabled or disabled michael@0: * independent of other zones. This enables you to turn on/off logging michael@0: * of particular objects or modules. This object implements a single michael@0: * zone and the methods required to use it. michael@0: * michael@0: * @constructor michael@0: * @param service Reference to the DebugService object we use for michael@0: * registration michael@0: * @param prefix String indicating the unique prefix we should use michael@0: * when creating preferences to control this zone michael@0: * @param zone String indicating the name of the zone michael@0: */ michael@0: function G_DebugZone(service, prefix, zone) { michael@0: if (G_GDEBUG) { michael@0: this.debugService_ = service; michael@0: this.prefix_ = prefix; michael@0: this.zone_ = zone; michael@0: this.zoneEnabledPrefName_ = prefix + ".zone." + this.zone_; michael@0: this.settings_ = new G_DebugSettings(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @returns Boolean indicating if this zone is enabled michael@0: */ michael@0: G_DebugZone.prototype.zoneIsEnabled = function() { michael@0: if (G_GDEBUG) { michael@0: var explicit = this.settings_.getSetting(this.zoneEnabledPrefName_, null); michael@0: michael@0: if (explicit !== null) { michael@0: return explicit; michael@0: } else { michael@0: return this.debugService_.allZonesEnabled(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Enable this logging zone michael@0: */ michael@0: G_DebugZone.prototype.enableZone = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.zoneEnabledPrefName_, true); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Disable this logging zone michael@0: */ michael@0: G_DebugZone.prototype.disableZone = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.zoneEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Write a debugging message to this zone michael@0: * michael@0: * @param msg String of message to write michael@0: */ michael@0: G_DebugZone.prototype.debug = function(msg) { michael@0: if (G_GDEBUG) { michael@0: if (this.zoneIsEnabled()) { michael@0: this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Write an error to this zone and throw michael@0: * michael@0: * @param msg String of error to write michael@0: */ michael@0: G_DebugZone.prototype.error = function(msg) { michael@0: if (G_GDEBUG) { michael@0: this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n"); michael@0: throw new Error(msg); michael@0: debugger; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Assert something as true and error if it is not michael@0: * michael@0: * @param condition Boolean condition to test michael@0: * @param msg String of message to write if is false michael@0: */ michael@0: G_DebugZone.prototype.assert = function(condition, msg) { michael@0: if (G_GDEBUG) { michael@0: if (condition !== true) { michael@0: G_Error(this.zone_, "ASSERT FAILED: " + msg); michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * The debug service handles auto-registration of zones, namespacing michael@0: * the zones preferences, and various global settings such as whether michael@0: * all zones are enabled. michael@0: * michael@0: * @constructor michael@0: * @param opt_prefix Optional string indicating the unique prefix we should michael@0: * use when creating preferences michael@0: */ michael@0: function G_DebugService(opt_prefix) { michael@0: if (G_GDEBUG) { michael@0: this.prefix_ = opt_prefix ? opt_prefix : "safebrowsing-debug-service"; michael@0: this.consoleEnabledPrefName_ = this.prefix_ + ".alsologtoconsole"; michael@0: this.allZonesEnabledPrefName_ = this.prefix_ + ".enableallzones"; michael@0: this.callTracingEnabledPrefName_ = this.prefix_ + ".trace-function-calls"; michael@0: this.logFileEnabledPrefName_ = this.prefix_ + ".logfileenabled"; michael@0: this.logFileErrorLevelPrefName_ = this.prefix_ + ".logfile-errorlevel"; michael@0: this.zones_ = {}; michael@0: michael@0: this.loggifier = new G_Loggifier(); michael@0: this.settings_ = new G_DebugSettings(); michael@0: } michael@0: } michael@0: michael@0: // Error levels for reporting console messages to the log. michael@0: G_DebugService.ERROR_LEVEL_INFO = "INFO"; michael@0: G_DebugService.ERROR_LEVEL_WARNING = "WARNING"; michael@0: G_DebugService.ERROR_LEVEL_EXCEPTION = "EXCEPTION"; michael@0: michael@0: michael@0: /** michael@0: * @returns Boolean indicating if we should send messages to the jsconsole michael@0: */ michael@0: G_DebugService.prototype.alsoDumpToConsole = function() { michael@0: if (G_GDEBUG) { michael@0: return this.settings_.getSetting(this.consoleEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @returns whether to log output to a file as well as the console. michael@0: */ michael@0: G_DebugService.prototype.logFileIsEnabled = function() { michael@0: if (G_GDEBUG) { michael@0: return this.settings_.getSetting(this.logFileEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Turns on file logging. dump() output will also go to the file specified by michael@0: * setLogFile() michael@0: */ michael@0: G_DebugService.prototype.enableLogFile = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.logFileEnabledPrefName_, true); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Turns off file logging michael@0: */ michael@0: G_DebugService.prototype.disableLogFile = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.logFileEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @returns an nsIFile instance pointing to the current log file location michael@0: */ michael@0: G_DebugService.prototype.getLogFile = function() { michael@0: if (G_GDEBUG) { michael@0: return this.logFile_; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sets a new log file location michael@0: */ michael@0: G_DebugService.prototype.setLogFile = function(file) { michael@0: if (G_GDEBUG) { michael@0: this.logFile_ = file; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Enables sending messages to the jsconsole michael@0: */ michael@0: G_DebugService.prototype.enableDumpToConsole = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.consoleEnabledPrefName_, true); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Disables sending messages to the jsconsole michael@0: */ michael@0: G_DebugService.prototype.disableDumpToConsole = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.consoleEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param zone Name of the zone to get michael@0: * @returns The DebugZone object corresopnding to input. If not such michael@0: * zone exists, a new one is created and returned michael@0: */ michael@0: G_DebugService.prototype.getZone = function(zone) { michael@0: if (G_GDEBUG) { michael@0: if (!this.zones_[zone]) michael@0: this.zones_[zone] = new G_DebugZone(this, this.prefix_, zone); michael@0: michael@0: return this.zones_[zone]; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param zone Zone to enable debugging for michael@0: */ michael@0: G_DebugService.prototype.enableZone = function(zone) { michael@0: if (G_GDEBUG) { michael@0: var toEnable = this.getZone(zone); michael@0: toEnable.enableZone(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param zone Zone to disable debugging for michael@0: */ michael@0: G_DebugService.prototype.disableZone = function(zone) { michael@0: if (G_GDEBUG) { michael@0: var toDisable = this.getZone(zone); michael@0: toDisable.disableZone(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @returns Boolean indicating whether debugging is enabled for all zones michael@0: */ michael@0: G_DebugService.prototype.allZonesEnabled = function() { michael@0: if (G_GDEBUG) { michael@0: return this.settings_.getSetting(this.allZonesEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Enables all debugging zones michael@0: */ michael@0: G_DebugService.prototype.enableAllZones = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.allZonesEnabledPrefName_, true); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Disables all debugging zones michael@0: */ michael@0: G_DebugService.prototype.disableAllZones = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.allZonesEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @returns Boolean indicating whether call tracing is enabled michael@0: */ michael@0: G_DebugService.prototype.callTracingEnabled = function() { michael@0: if (G_GDEBUG) { michael@0: return this.settings_.getSetting(this.callTracingEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Enables call tracing michael@0: */ michael@0: G_DebugService.prototype.enableCallTracing = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.callTracingEnabledPrefName_, true); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Disables call tracing michael@0: */ michael@0: G_DebugService.prototype.disableCallTracing = function() { michael@0: if (G_GDEBUG) { michael@0: this.settings_.setDefault(this.callTracingEnabledPrefName_, false); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Gets the minimum error that will be reported to the log. michael@0: */ michael@0: G_DebugService.prototype.getLogFileErrorLevel = function() { michael@0: if (G_GDEBUG) { michael@0: var level = this.settings_.getSetting(this.logFileErrorLevelPrefName_, michael@0: G_DebugService.ERROR_LEVEL_EXCEPTION); michael@0: michael@0: return level.toUpperCase(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sets the minimum error level that will be reported to the log. michael@0: */ michael@0: G_DebugService.prototype.setLogFileErrorLevel = function(level) { michael@0: if (G_GDEBUG) { michael@0: // normalize case just to make it slightly easier to not screw up. michael@0: level = level.toUpperCase(); michael@0: michael@0: if (level != G_DebugService.ERROR_LEVEL_INFO && michael@0: level != G_DebugService.ERROR_LEVEL_WARNING && michael@0: level != G_DebugService.ERROR_LEVEL_EXCEPTION) { michael@0: throw new Error("Invalid error level specified: {" + level + "}"); michael@0: } michael@0: michael@0: this.settings_.setDefault(this.logFileErrorLevelPrefName_, level); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Internal dump() method michael@0: * michael@0: * @param msg String of message to dump michael@0: */ michael@0: G_DebugService.prototype.dump = function(msg) { michael@0: if (G_GDEBUG) { michael@0: dump(msg); michael@0: michael@0: if (this.alsoDumpToConsole()) { michael@0: try { michael@0: var console = Components.classes['@mozilla.org/consoleservice;1'] michael@0: .getService(Components.interfaces.nsIConsoleService); michael@0: console.logStringMessage(msg); michael@0: } catch(e) { michael@0: dump("G_DebugZone ERROR: COULD NOT DUMP TO CONSOLE\n"); michael@0: } michael@0: } michael@0: michael@0: this.maybeDumpToFile(msg); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Writes the specified message to the log file, if file logging is enabled. michael@0: */ michael@0: G_DebugService.prototype.maybeDumpToFile = function(msg) { michael@0: if (this.logFileIsEnabled() && this.logFile_) { michael@0: michael@0: /* try to get the correct line end character for this platform */ michael@0: if (!this._LINE_END_CHAR) michael@0: this._LINE_END_CHAR = michael@0: Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) michael@0: .OS == "WINNT" ? "\r\n" : "\n"; michael@0: if (this._LINE_END_CHAR != "\n") michael@0: msg = msg.replace(/\n/g, this._LINE_END_CHAR); michael@0: michael@0: try { michael@0: var stream = Cc["@mozilla.org/network/file-output-stream;1"] michael@0: .createInstance(Ci.nsIFileOutputStream); michael@0: stream.init(this.logFile_, michael@0: 0x02 | 0x08 | 0x10 /* PR_WRONLY | PR_CREATE_FILE | PR_APPEND */ michael@0: -1 /* default perms */, 0 /* no special behavior */); michael@0: stream.write(msg, msg.length); michael@0: } finally { michael@0: stream.close(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Implements nsIConsoleListener.observe(). Gets called when an error message michael@0: * gets reported to the console and sends it to the log file as well. michael@0: */ michael@0: G_DebugService.prototype.observe = function(consoleMessage) { michael@0: if (G_GDEBUG) { michael@0: var errorLevel = this.getLogFileErrorLevel(); michael@0: michael@0: // consoleMessage can be either nsIScriptError or nsIConsoleMessage. The michael@0: // latter does not have things like line number, etc. So we special case michael@0: // it first. michael@0: if (!(consoleMessage instanceof Ci.nsIScriptError)) { michael@0: // Only report these messages if the error level is INFO. michael@0: if (errorLevel == G_DebugService.ERROR_LEVEL_INFO) { michael@0: this.maybeDumpToFile(G_DebugService.ERROR_LEVEL_INFO + ": " + michael@0: consoleMessage.message + "\n"); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: // We make a local copy of these fields because writing to it doesn't seem michael@0: // to work. michael@0: var flags = consoleMessage.flags; michael@0: var sourceName = consoleMessage.sourceName; michael@0: var lineNumber = consoleMessage.lineNumber; michael@0: michael@0: // Sometimes, a scripterror instance won't have any flags set. We michael@0: // default to exception. michael@0: if (!flags) { michael@0: flags = Ci.nsIScriptError.exceptionFlag; michael@0: } michael@0: michael@0: // Default the filename and line number if they aren't set. michael@0: if (!sourceName) { michael@0: sourceName = ""; michael@0: } michael@0: michael@0: if (!lineNumber) { michael@0: lineNumber = ""; michael@0: } michael@0: michael@0: // Report the error in the log file. michael@0: if (flags & Ci.nsIScriptError.warningFlag) { michael@0: // Only report warnings if the error level is warning or better. michael@0: if (errorLevel == G_DebugService.ERROR_LEVEL_WARNING || michael@0: errorLevel == G_DebugService.ERROR_LEVEL_INFO) { michael@0: this.reportScriptError_(consoleMessage.message, michael@0: sourceName, michael@0: lineNumber, michael@0: G_DebugService.ERROR_LEVEL_WARNING); michael@0: } michael@0: } else if (flags & Ci.nsIScriptError.exceptionFlag) { michael@0: // Always report exceptions. michael@0: this.reportScriptError_(consoleMessage.message, michael@0: sourceName, michael@0: lineNumber, michael@0: G_DebugService.ERROR_LEVEL_EXCEPTION); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Private helper to report an nsIScriptError instance to the log/console. michael@0: */ michael@0: G_DebugService.prototype.reportScriptError_ = function(message, sourceName, michael@0: lineNumber, label) { michael@0: message = "\n------------------------------------------------------------\n" + michael@0: label + ": " + message + michael@0: "\nlocation: " + sourceName + ", " + "line: " + lineNumber + michael@0: "\n------------------------------------------------------------\n\n"; michael@0: michael@0: dump(message); michael@0: this.maybeDumpToFile(message); michael@0: } michael@0: michael@0: michael@0: michael@0: /** michael@0: * A class that instruments methods so they output a call trace, michael@0: * including the values of their actual parameters and return value. michael@0: * This code is mostly stolen from Aaron Boodman's original michael@0: * implementation in clobber utils. michael@0: * michael@0: * Note that this class uses the "loggifier" debug zone, so you'll see michael@0: * a complete call trace when that zone is enabled. michael@0: * michael@0: * @constructor michael@0: */ michael@0: function G_Loggifier() { michael@0: if (G_GDEBUG) { michael@0: // Careful not to loggify ourselves! michael@0: this.mark_(this); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Marks an object as having been loggified. Loggification is not michael@0: * idempotent :) michael@0: * michael@0: * @param obj Object to be marked michael@0: */ michael@0: G_Loggifier.prototype.mark_ = function(obj) { michael@0: if (G_GDEBUG) { michael@0: obj.__loggified_ = true; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * @param obj Object to be examined michael@0: * @returns Boolean indicating if the object has been loggified michael@0: */ michael@0: G_Loggifier.prototype.isLoggified = function(obj) { michael@0: if (G_GDEBUG) { michael@0: return !!obj.__loggified_; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Attempt to extract the class name from the constructor definition. michael@0: * Assumes the object was created using new. michael@0: * michael@0: * @param constructor String containing the definition of a constructor, michael@0: * for example what you'd get by examining obj.constructor michael@0: * @returns Name of the constructor/object if it could be found, else "???" michael@0: */ michael@0: G_Loggifier.prototype.getFunctionName_ = function(constructor) { michael@0: if (G_GDEBUG) { michael@0: return constructor.name || "???"; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Wraps all the methods in an object so that call traces are michael@0: * automatically outputted. michael@0: * michael@0: * @param obj Object to loggify. SHOULD BE THE PROTOTYPE OF A USER-DEFINED michael@0: * object. You can get into trouble if you attempt to michael@0: * loggify something that isn't, for example the Window. michael@0: * michael@0: * Any additional parameters are considered method names which should not be michael@0: * loggified. michael@0: * michael@0: * Usage: michael@0: * G_debugService.loggifier.loggify(MyClass.prototype, michael@0: * "firstMethodNotToLog", michael@0: * "secondMethodNotToLog", michael@0: * ... etc ...); michael@0: */ michael@0: G_Loggifier.prototype.loggify = function(obj) { michael@0: if (G_GDEBUG) { michael@0: if (!G_debugService.callTracingEnabled()) { michael@0: return; michael@0: } michael@0: michael@0: if (typeof window != "undefined" && obj == window || michael@0: this.isLoggified(obj)) // Don't go berserk! michael@0: return; michael@0: michael@0: var zone = G_GetDebugZone(obj); michael@0: if (!zone || !zone.zoneIsEnabled()) { michael@0: return; michael@0: } michael@0: michael@0: this.mark_(obj); michael@0: michael@0: // Helper function returns an instrumented version of michael@0: // objName.meth, with "this" bound properly. (BTW, because we're michael@0: // in a conditional here, functions will only be defined as michael@0: // they're encountered during execution, so declare this helper michael@0: // before using it.) michael@0: michael@0: function wrap(meth, objName, methName) { michael@0: return function() { michael@0: michael@0: // First output the call along with actual parameters michael@0: var args = new Array(arguments.length); michael@0: var argsString = ""; michael@0: for (var i = 0; i < args.length; i++) { michael@0: args[i] = arguments[i]; michael@0: argsString += (i == 0 ? "" : ", "); michael@0: michael@0: if (typeof args[i] == "function") { michael@0: argsString += "[function]"; michael@0: } else { michael@0: argsString += args[i]; michael@0: } michael@0: } michael@0: michael@0: G_TraceCall(this, "> " + objName + "." + methName + "(" + michael@0: argsString + ")"); michael@0: michael@0: // Then run the function, capturing the return value and throws michael@0: try { michael@0: var retVal = meth.apply(this, arguments); michael@0: var reportedRetVal = retVal; michael@0: michael@0: if (typeof reportedRetVal == "undefined") michael@0: reportedRetVal = "void"; michael@0: else if (reportedRetVal === "") michael@0: reportedRetVal = "\"\" (empty string)"; michael@0: } catch (e) { michael@0: if (e && !e.__logged) { michael@0: G_TraceCall(this, "Error: " + e.message + ". " + michael@0: e.fileName + ": " + e.lineNumber); michael@0: try { michael@0: e.__logged = true; michael@0: } catch (e2) { michael@0: // Sometimes we can't add the __logged flag because it's an michael@0: // XPC wrapper michael@0: throw e; michael@0: } michael@0: } michael@0: michael@0: throw e; // Re-throw! michael@0: } michael@0: michael@0: // And spit it out already michael@0: G_TraceCall( michael@0: this, michael@0: "< " + objName + "." + methName + ": " + reportedRetVal); michael@0: michael@0: return retVal; michael@0: }; michael@0: }; michael@0: michael@0: var ignoreLookup = {}; michael@0: michael@0: if (arguments.length > 1) { michael@0: for (var i = 1; i < arguments.length; i++) { michael@0: ignoreLookup[arguments[i]] = true; michael@0: } michael@0: } michael@0: michael@0: // Wrap each method of obj michael@0: for (var p in obj) { michael@0: // Work around bug in Firefox. In ffox typeof RegExp is "function", michael@0: // so make sure this really is a function. Bug as of FFox 1.5b2. michael@0: if (typeof obj[p] == "function" && obj[p].call && !ignoreLookup[p]) { michael@0: var objName = this.getFunctionName_(obj.constructor); michael@0: obj[p] = wrap(obj[p], objName, p); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Simple abstraction around debug settings. The thing with debug settings is michael@0: * that we want to be able to specify a default in the application's startup, michael@0: * but have that default be overridable by the user via their prefs. michael@0: * michael@0: * To generalize this, we package up a dictionary of defaults with the michael@0: * preferences tree. If a setting isn't in the preferences tree, then we grab it michael@0: * from the defaults. michael@0: */ michael@0: function G_DebugSettings() { michael@0: this.defaults_ = {}; michael@0: this.prefs_ = new G_Preferences(); michael@0: } michael@0: michael@0: /** michael@0: * Returns the value of a settings, optionally defaulting to a given value if it michael@0: * doesn't exist. If no default is specified, the default is |undefined|. michael@0: */ michael@0: G_DebugSettings.prototype.getSetting = function(name, opt_default) { michael@0: var override = this.prefs_.getPref(name, null); michael@0: michael@0: if (override !== null) { michael@0: return override; michael@0: } else if (typeof this.defaults_[name] != "undefined") { michael@0: return this.defaults_[name]; michael@0: } else { michael@0: return opt_default; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sets the default value for a setting. If the user doesn't override it with a michael@0: * preference, this is the value which will be returned by getSetting(). michael@0: */ michael@0: G_DebugSettings.prototype.setDefault = function(name, val) { michael@0: this.defaults_[name] = val; michael@0: } michael@0: michael@0: var G_debugService = new G_DebugService(); // Instantiate us! michael@0: michael@0: if (G_GDEBUG) { michael@0: G_debugService.enableAllZones(); michael@0: } michael@0: michael@0: #else michael@0: michael@0: // Stubs for the debugging aids scattered through this component. michael@0: // They will be expanded if you compile yourself a debug build. michael@0: michael@0: function G_Debug(who, msg) { } michael@0: function G_Assert(who, condition, msg) { } michael@0: function G_Error(who, msg) { } michael@0: var G_debugService = { __noSuchMethod__: function() { } }; michael@0: michael@0: #endif