tenfourfox/toolkit/components/url-classifier/content/moz/debug.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

868 lines
24 KiB
JavaScript

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