toolkit/modules/Log.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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 this.EXPORTED_SYMBOLS = ["Log"];
     9 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
    11 const ONE_BYTE = 1;
    12 const ONE_KILOBYTE = 1024 * ONE_BYTE;
    13 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
    15 const STREAM_SEGMENT_SIZE = 4096;
    16 const PR_UINT32_MAX = 0xffffffff;
    18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    20                                   "resource://gre/modules/osfile.jsm");
    21 XPCOMUtils.defineLazyModuleGetter(this, "Task",
    22                                   "resource://gre/modules/Task.jsm");
    23 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
    26 /*
    27  * Dump a message everywhere we can if we have a failure.
    28  */
    29 function dumpError(text) {
    30   dump(text + "\n");
    31   Cu.reportError(text);
    32 }
    34 this.Log = {
    35   Level: {
    36     Fatal:  70,
    37     Error:  60,
    38     Warn:   50,
    39     Info:   40,
    40     Config: 30,
    41     Debug:  20,
    42     Trace:  10,
    43     All:    0,
    44     Desc: {
    45       70: "FATAL",
    46       60: "ERROR",
    47       50: "WARN",
    48       40: "INFO",
    49       30: "CONFIG",
    50       20: "DEBUG",
    51       10: "TRACE",
    52       0:  "ALL"
    53     },
    54     Numbers: {
    55       "FATAL": 70,
    56       "ERROR": 60,
    57       "WARN": 50,
    58       "INFO": 40,
    59       "CONFIG": 30,
    60       "DEBUG": 20,
    61       "TRACE": 10,
    62       "ALL": 0,
    63     }
    64   },
    66   get repository() {
    67     delete Log.repository;
    68     Log.repository = new LoggerRepository();
    69     return Log.repository;
    70   },
    71   set repository(value) {
    72     delete Log.repository;
    73     Log.repository = value;
    74   },
    76   LogMessage: LogMessage,
    77   Logger: Logger,
    78   LoggerRepository: LoggerRepository,
    80   Formatter: Formatter,
    81   BasicFormatter: BasicFormatter,
    82   MessageOnlyFormatter: MessageOnlyFormatter,
    83   StructuredFormatter: StructuredFormatter,
    85   Appender: Appender,
    86   DumpAppender: DumpAppender,
    87   ConsoleAppender: ConsoleAppender,
    88   StorageStreamAppender: StorageStreamAppender,
    90   FileAppender: FileAppender,
    91   BoundedFileAppender: BoundedFileAppender,
    93   ParameterFormatter: ParameterFormatter,
    94   // Logging helper:
    95   // let logger = Log.repository.getLogger("foo");
    96   // logger.info(Log.enumerateInterfaces(someObject).join(","));
    97   enumerateInterfaces: function Log_enumerateInterfaces(aObject) {
    98     let interfaces = [];
   100     for (i in Ci) {
   101       try {
   102         aObject.QueryInterface(Ci[i]);
   103         interfaces.push(i);
   104       }
   105       catch(ex) {}
   106     }
   108     return interfaces;
   109   },
   111   // Logging helper:
   112   // let logger = Log.repository.getLogger("foo");
   113   // logger.info(Log.enumerateProperties(someObject).join(","));
   114   enumerateProperties: function (aObject, aExcludeComplexTypes) {
   115     let properties = [];
   117     for (p in aObject) {
   118       try {
   119         if (aExcludeComplexTypes &&
   120             (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function"))
   121           continue;
   122         properties.push(p + " = " + aObject[p]);
   123       }
   124       catch(ex) {
   125         properties.push(p + " = " + ex);
   126       }
   127     }
   129     return properties;
   130   },
   132   _formatError: function _formatError(e) {
   133     let result = e.toString();
   134     if (e.fileName) {
   135       result +=  " (" + e.fileName;
   136       if (e.lineNumber) {
   137         result += ":" + e.lineNumber;
   138       }
   139       if (e.columnNumber) {
   140         result += ":" + e.columnNumber;
   141       }
   142       result += ")";
   143     }
   144     return result + " " + Log.stackTrace(e);
   145   },
   147   // This is for back compatibility with services/common/utils.js; we duplicate
   148   // some of the logic in ParameterFormatter
   149   exceptionStr: function exceptionStr(e) {
   150     if (!e) {
   151       return "" + e;
   152     }
   153     if (e instanceof Ci.nsIException) {
   154       return e.toString() + " " + Log.stackTrace(e);
   155     }
   156     else if (isError(e)) {
   157       return Log._formatError(e);
   158     }
   159     // else
   160     let message = e.message ? e.message : e;
   161     return message + " " + Log.stackTrace(e);
   162   },
   164   stackTrace: function stackTrace(e) {
   165     // Wrapped nsIException
   166     if (e.location) {
   167       let frame = e.location;
   168       let output = [];
   169       while (frame) {
   170         // Works on frames or exceptions, munges file:// URIs to shorten the paths
   171         // FIXME: filename munging is sort of hackish, might be confusing if
   172         // there are multiple extensions with similar filenames
   173         let str = "<file:unknown>";
   175         let file = frame.filename || frame.fileName;
   176         if (file) {
   177           str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
   178         }
   180         if (frame.lineNumber) {
   181           str += ":" + frame.lineNumber;
   182         }
   184         if (frame.name) {
   185           str = frame.name + "()@" + str;
   186         }
   188         if (str) {
   189           output.push(str);
   190         }
   191         frame = frame.caller;
   192       }
   193       return "Stack trace: " + output.join(" < ");
   194     }
   195     // Standard JS exception
   196     if (e.stack) {
   197       return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
   198         replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
   199     }
   201     return "No traceback available";
   202   }
   203 };
   205 /*
   206  * LogMessage
   207  * Encapsulates a single log event's data
   208  */
   209 function LogMessage(loggerName, level, message, params) {
   210   this.loggerName = loggerName;
   211   this.level = level;
   212   /*
   213    * Special case to handle "log./level/(object)", for example logging a caught exception
   214    * without providing text or params like: catch(e) { logger.warn(e) }
   215    * Treating this as an empty text with the object in the 'params' field causes the
   216    * object to be formatted properly by BasicFormatter.
   217    */
   218   if (!params && message && (typeof(message) == "object") &&
   219       (typeof(message.valueOf()) != "string")) {
   220     this.message = null;
   221     this.params = message;
   222   } else {
   223     // If the message text is empty, or a string, or a String object, normal handling
   224     this.message = message;
   225     this.params = params;
   226   }
   228   // The _structured field will correspond to whether this message is to
   229   // be interpreted as a structured message.
   230   this._structured = this.params && this.params.action;
   231   this.time = Date.now();
   232 }
   233 LogMessage.prototype = {
   234   get levelDesc() {
   235     if (this.level in Log.Level.Desc)
   236       return Log.Level.Desc[this.level];
   237     return "UNKNOWN";
   238   },
   240   toString: function LogMsg_toString() {
   241     let msg = "LogMessage [" + this.time + " " + this.level + " " +
   242       this.message;
   243     if (this.params) {
   244       msg += " " + JSON.stringify(this.params);
   245     }
   246     return msg + "]"
   247   }
   248 };
   250 /*
   251  * Logger
   252  * Hierarchical version.  Logs to all appenders, assigned or inherited
   253  */
   255 function Logger(name, repository) {
   256   if (!repository)
   257     repository = Log.repository;
   258   this._name = name;
   259   this.children = [];
   260   this.ownAppenders = [];
   261   this.appenders = [];
   262   this._repository = repository;
   263 }
   264 Logger.prototype = {
   265   get name() {
   266     return this._name;
   267   },
   269   _level: null,
   270   get level() {
   271     if (this._level != null)
   272       return this._level;
   273     if (this.parent)
   274       return this.parent.level;
   275     dumpError("Log warning: root logger configuration error: no level defined");
   276     return Log.Level.All;
   277   },
   278   set level(level) {
   279     this._level = level;
   280   },
   282   _parent: null,
   283   get parent() this._parent,
   284   set parent(parent) {
   285     if (this._parent == parent) {
   286       return;
   287     }
   288     // Remove ourselves from parent's children
   289     if (this._parent) {
   290       let index = this._parent.children.indexOf(this);
   291       if (index != -1) {
   292         this._parent.children.splice(index, 1);
   293       }
   294     }
   295     this._parent = parent;
   296     parent.children.push(this);
   297     this.updateAppenders();
   298   },
   300   updateAppenders: function updateAppenders() {
   301     if (this._parent) {
   302       let notOwnAppenders = this._parent.appenders.filter(function(appender) {
   303         return this.ownAppenders.indexOf(appender) == -1;
   304       }, this);
   305       this.appenders = notOwnAppenders.concat(this.ownAppenders);
   306     } else {
   307       this.appenders = this.ownAppenders.slice();
   308     }
   310     // Update children's appenders.
   311     for (let i = 0; i < this.children.length; i++) {
   312       this.children[i].updateAppenders();
   313     }
   314   },
   316   addAppender: function Logger_addAppender(appender) {
   317     if (this.ownAppenders.indexOf(appender) != -1) {
   318       return;
   319     }
   320     this.ownAppenders.push(appender);
   321     this.updateAppenders();
   322   },
   324   removeAppender: function Logger_removeAppender(appender) {
   325     let index = this.ownAppenders.indexOf(appender);
   326     if (index == -1) {
   327       return;
   328     }
   329     this.ownAppenders.splice(index, 1);
   330     this.updateAppenders();
   331   },
   333   /**
   334    * Logs a structured message object.
   335    *
   336    * @param action
   337    *        (string) A message action, one of a set of actions known to the
   338    *          log consumer.
   339    * @param params
   340    *        (object) Parameters to be included in the message.
   341    *          If _level is included as a key and the corresponding value
   342    *          is a number or known level name, the message will be logged
   343    *          at the indicated level. If _message is included as a key, the
   344    *          value is used as the descriptive text for the message.
   345    */
   346   logStructured: function (action, params) {
   347     if (!action) {
   348       throw "An action is required when logging a structured message.";
   349     }
   350     if (!params) {
   351       return this.log(this.level, undefined, {"action": action});
   352     }
   353     if (typeof(params) != "object") {
   354       throw "The params argument is required to be an object.";
   355     }
   357     let level = params._level;
   358     if (level) {
   359       let ulevel = level.toUpperCase();
   360       if (ulevel in Log.Level.Numbers) {
   361         level = Log.Level.Numbers[ulevel];
   362       }
   363     } else {
   364       level = this.level;
   365     }
   367     params.action = action;
   368     this.log(level, params._message, params);
   369   },
   371   log: function (level, string, params) {
   372     if (this.level > level)
   373       return;
   375     // Hold off on creating the message object until we actually have
   376     // an appender that's responsible.
   377     let message;
   378     let appenders = this.appenders;
   379     for (let appender of appenders) {
   380       if (appender.level > level) {
   381         continue;
   382       }
   383       if (!message) {
   384         message = new LogMessage(this._name, level, string, params);
   385       }
   386       appender.append(message);
   387     }
   388   },
   390   fatal: function (string, params) {
   391     this.log(Log.Level.Fatal, string, params);
   392   },
   393   error: function (string, params) {
   394     this.log(Log.Level.Error, string, params);
   395   },
   396   warn: function (string, params) {
   397     this.log(Log.Level.Warn, string, params);
   398   },
   399   info: function (string, params) {
   400     this.log(Log.Level.Info, string, params);
   401   },
   402   config: function (string, params) {
   403     this.log(Log.Level.Config, string, params);
   404   },
   405   debug: function (string, params) {
   406     this.log(Log.Level.Debug, string, params);
   407   },
   408   trace: function (string, params) {
   409     this.log(Log.Level.Trace, string, params);
   410   }
   411 };
   413 /*
   414  * LoggerRepository
   415  * Implements a hierarchy of Loggers
   416  */
   418 function LoggerRepository() {}
   419 LoggerRepository.prototype = {
   420   _loggers: {},
   422   _rootLogger: null,
   423   get rootLogger() {
   424     if (!this._rootLogger) {
   425       this._rootLogger = new Logger("root", this);
   426       this._rootLogger.level = Log.Level.All;
   427     }
   428     return this._rootLogger;
   429   },
   430   set rootLogger(logger) {
   431     throw "Cannot change the root logger";
   432   },
   434   _updateParents: function LogRep__updateParents(name) {
   435     let pieces = name.split('.');
   436     let cur, parent;
   438     // find the closest parent
   439     // don't test for the logger name itself, as there's a chance it's already
   440     // there in this._loggers
   441     for (let i = 0; i < pieces.length - 1; i++) {
   442       if (cur)
   443         cur += '.' + pieces[i];
   444       else
   445         cur = pieces[i];
   446       if (cur in this._loggers)
   447         parent = cur;
   448     }
   450     // if we didn't assign a parent above, there is no parent
   451     if (!parent)
   452       this._loggers[name].parent = this.rootLogger;
   453     else
   454       this._loggers[name].parent = this._loggers[parent];
   456     // trigger updates for any possible descendants of this logger
   457     for (let logger in this._loggers) {
   458       if (logger != name && logger.indexOf(name) == 0)
   459         this._updateParents(logger);
   460     }
   461   },
   463   /**
   464    * Obtain a named Logger.
   465    *
   466    * The returned Logger instance for a particular name is shared among
   467    * all callers. In other words, if two consumers call getLogger("foo"),
   468    * they will both have a reference to the same object.
   469    *
   470    * @return Logger
   471    */
   472   getLogger: function (name) {
   473     if (name in this._loggers)
   474       return this._loggers[name];
   475     this._loggers[name] = new Logger(name, this);
   476     this._updateParents(name);
   477     return this._loggers[name];
   478   },
   480   /**
   481    * Obtain a Logger that logs all string messages with a prefix.
   482    *
   483    * A common pattern is to have separate Logger instances for each instance
   484    * of an object. But, you still want to distinguish between each instance.
   485    * Since Log.repository.getLogger() returns shared Logger objects,
   486    * monkeypatching one Logger modifies them all.
   487    *
   488    * This function returns a new object with a prototype chain that chains
   489    * up to the original Logger instance. The new prototype has log functions
   490    * that prefix content to each message.
   491    *
   492    * @param name
   493    *        (string) The Logger to retrieve.
   494    * @param prefix
   495    *        (string) The string to prefix each logged message with.
   496    */
   497   getLoggerWithMessagePrefix: function (name, prefix) {
   498     let log = this.getLogger(name);
   500     let proxy = {__proto__: log};
   502     for (let level in Log.Level) {
   503       if (level == "Desc") {
   504         continue;
   505       }
   507       let lc = level.toLowerCase();
   508       proxy[lc] = function (msg, ...args) {
   509         return log[lc].apply(log, [prefix + msg, ...args]);
   510       };
   511     }
   513     return proxy;
   514   },
   515 };
   517 /*
   518  * Formatters
   519  * These massage a LogMessage into whatever output is desired.
   520  * BasicFormatter and StructuredFormatter are implemented here.
   521  */
   523 // Abstract formatter
   524 function Formatter() {}
   525 Formatter.prototype = {
   526   format: function Formatter_format(message) {}
   527 };
   529 // Basic formatter that doesn't do anything fancy.
   530 function BasicFormatter(dateFormat) {
   531   if (dateFormat) {
   532     this.dateFormat = dateFormat;
   533   }
   534   this.parameterFormatter = new ParameterFormatter();
   535 }
   536 BasicFormatter.prototype = {
   537   __proto__: Formatter.prototype,
   539   /**
   540    * Format the text of a message with optional parameters.
   541    * If the text contains ${identifier}, replace that with
   542    * the value of params[identifier]; if ${}, replace that with
   543    * the entire params object. If no params have been substituted
   544    * into the text, format the entire object and append that
   545    * to the message.
   546    */
   547   formatText: function (message) {
   548     let params = message.params;
   549     if (!params) {
   550       return message.message || "";
   551     }
   552     // Defensive handling of non-object params
   553     // We could add a special case for NSRESULT values here...
   554     let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function');
   556     // if we have params, try and find substitutions.
   557     if (message.params && this.parameterFormatter) {
   558       // have we successfully substituted any parameters into the message?
   559       // in the log message
   560       let subDone = false;
   561       let regex = /\$\{(\S*)\}/g;
   562       let textParts = [];
   563       if (message.message) {
   564         textParts.push(message.message.replace(regex, (_, sub) => {
   565           // ${foo} means use the params['foo']
   566           if (sub) {
   567             if (pIsObject && sub in message.params) {
   568               subDone = true;
   569               return this.parameterFormatter.format(message.params[sub]);
   570             }
   571             return '${' + sub + '}';
   572           }
   573           // ${} means use the entire params object.
   574           subDone = true;
   575           return this.parameterFormatter.format(message.params);
   576         }));
   577       }
   578       if (!subDone) {
   579         // There were no substitutions in the text, so format the entire params object
   580         let rest = this.parameterFormatter.format(message.params);
   581         if (rest !== null && rest != "{}") {
   582           textParts.push(rest);
   583         }
   584       }
   585       return textParts.join(': ');
   586     }
   587   },
   589   format: function BF_format(message) {
   590     return message.time + "\t" +
   591       message.loggerName + "\t" +
   592       message.levelDesc + "\t" +
   593       this.formatText(message);
   594   }
   595 };
   597 /**
   598  * A formatter that only formats the string message component.
   599  */
   600 function MessageOnlyFormatter() {
   601 }
   602 MessageOnlyFormatter.prototype = Object.freeze({
   603   __proto__: Formatter.prototype,
   605   format: function (message) {
   606     return message.message;
   607   },
   608 });
   610 // Structured formatter that outputs JSON based on message data.
   611 // This formatter will format unstructured messages by supplying
   612 // default values.
   613 function StructuredFormatter() { }
   614 StructuredFormatter.prototype = {
   615   __proto__: Formatter.prototype,
   617   format: function (logMessage) {
   618     let output = {
   619       _time: logMessage.time,
   620       _namespace: logMessage.loggerName,
   621       _level: logMessage.levelDesc
   622     };
   624     for (let key in logMessage.params) {
   625       output[key] = logMessage.params[key];
   626     }
   628     if (!output.action) {
   629       output.action = "UNKNOWN";
   630     }
   632     if (!output._message && logMessage.message) {
   633       output._message = logMessage.message;
   634     }
   636     return JSON.stringify(output);
   637   }
   638 }
   640 /**
   641  * Test an object to see if it is a Mozilla JS Error.
   642  */
   643 function isError(aObj) {
   644   return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj &&
   645           "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj);
   646 };
   648 /*
   649  * Parameter Formatters
   650  * These massage an object used as a parameter for a LogMessage into
   651  * a string representation of the object.
   652  */
   654 function ParameterFormatter() {
   655   this._name = "ParameterFormatter"
   656 }
   657 ParameterFormatter.prototype = {
   658   format: function(ob) {
   659     try {
   660       if (ob === undefined) {
   661         return "undefined";
   662       }
   663       if (ob === null) {
   664         return "null";
   665       }
   666       // Pass through primitive types and objects that unbox to primitive types.
   667       if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") &&
   668           typeof(ob) != "function") {
   669         return ob;
   670       }
   671       if (ob instanceof Ci.nsIException) {
   672         return ob.toString() + " " + Log.stackTrace(ob);
   673       }
   674       else if (isError(ob)) {
   675         return Log._formatError(ob);
   676       }
   677       // Just JSONify it. Filter out our internal fields and those the caller has
   678       // already handled.
   679       return JSON.stringify(ob, (key, val) => {
   680         if (INTERNAL_FIELDS.has(key)) {
   681           return undefined;
   682         }
   683         return val;
   684       });
   685     }
   686     catch (e) {
   687       dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e));
   688     }
   689     // Fancy formatting failed. Just toSource() it - but even this may fail!
   690     try {
   691       return ob.toSource();
   692     } catch (_) { }
   693     try {
   694       return "" + ob;
   695     } catch (_) {
   696       return "[object]"
   697     }
   698   }
   699 }
   701 /*
   702  * Appenders
   703  * These can be attached to Loggers to log to different places
   704  * Simply subclass and override doAppend to implement a new one
   705  */
   707 function Appender(formatter) {
   708   this._name = "Appender";
   709   this._formatter = formatter? formatter : new BasicFormatter();
   710 }
   711 Appender.prototype = {
   712   level: Log.Level.All,
   714   append: function App_append(message) {
   715     if (message) {
   716       this.doAppend(this._formatter.format(message));
   717     }
   718   },
   719   toString: function App_toString() {
   720     return this._name + " [level=" + this.level +
   721       ", formatter=" + this._formatter + "]";
   722   },
   723   doAppend: function App_doAppend(formatted) {}
   724 };
   726 /*
   727  * DumpAppender
   728  * Logs to standard out
   729  */
   731 function DumpAppender(formatter) {
   732   Appender.call(this, formatter);
   733   this._name = "DumpAppender";
   734 }
   735 DumpAppender.prototype = {
   736   __proto__: Appender.prototype,
   738   doAppend: function DApp_doAppend(formatted) {
   739     dump(formatted + "\n");
   740   }
   741 };
   743 /*
   744  * ConsoleAppender
   745  * Logs to the javascript console
   746  */
   748 function ConsoleAppender(formatter) {
   749   Appender.call(this, formatter);
   750   this._name = "ConsoleAppender";
   751 }
   752 ConsoleAppender.prototype = {
   753   __proto__: Appender.prototype,
   755   // XXX this should be replaced with calls to the Browser Console
   756   append: function App_append(message) {
   757     if (message) {
   758       let m = this._formatter.format(message);
   759       if (message.level > Log.Level.Warn) {
   760         Cu.reportError(m);
   761         return;
   762       }
   763       this.doAppend(m);
   764     }
   765   },
   767   doAppend: function CApp_doAppend(formatted) {
   768     Cc["@mozilla.org/consoleservice;1"].
   769       getService(Ci.nsIConsoleService).logStringMessage(formatted);
   770   }
   771 };
   773 /**
   774  * Append to an nsIStorageStream
   775  *
   776  * This writes logging output to an in-memory stream which can later be read
   777  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
   778  * during logging. Instead, one can periodically consume the input stream and
   779  * e.g. write it to disk asynchronously.
   780  */
   781 function StorageStreamAppender(formatter) {
   782   Appender.call(this, formatter);
   783   this._name = "StorageStreamAppender";
   784 }
   786 StorageStreamAppender.prototype = {
   787   __proto__: Appender.prototype,
   789   _converterStream: null, // holds the nsIConverterOutputStream
   790   _outputStream: null,    // holds the underlying nsIOutputStream
   792   _ss: null,
   794   get outputStream() {
   795     if (!this._outputStream) {
   796       // First create a raw stream. We can bail out early if that fails.
   797       this._outputStream = this.newOutputStream();
   798       if (!this._outputStream) {
   799         return null;
   800       }
   802       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
   803       // the instance if we already have one.
   804       if (!this._converterStream) {
   805         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
   806                                   .createInstance(Ci.nsIConverterOutputStream);
   807       }
   808       this._converterStream.init(
   809         this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
   810         Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
   811     }
   812     return this._converterStream;
   813   },
   815   newOutputStream: function newOutputStream() {
   816     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
   817                           .createInstance(Ci.nsIStorageStream);
   818     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
   819     return ss.getOutputStream(0);
   820   },
   822   getInputStream: function getInputStream() {
   823     if (!this._ss) {
   824       return null;
   825     }
   826     return this._ss.newInputStream(0);
   827   },
   829   reset: function reset() {
   830     if (!this._outputStream) {
   831       return;
   832     }
   833     this.outputStream.close();
   834     this._outputStream = null;
   835     this._ss = null;
   836   },
   838   doAppend: function (formatted) {
   839     if (!formatted) {
   840       return;
   841     }
   842     try {
   843       this.outputStream.writeString(formatted + "\n");
   844     } catch(ex) {
   845       if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
   846         // The underlying output stream is closed, so let's open a new one
   847         // and try again.
   848         this._outputStream = null;
   849       } try {
   850           this.outputStream.writeString(formatted + "\n");
   851       } catch (ex) {
   852         // Ah well, we tried, but something seems to be hosed permanently.
   853       }
   854     }
   855   }
   856 };
   858 /**
   859  * File appender
   860  *
   861  * Writes output to file using OS.File.
   862  */
   863 function FileAppender(path, formatter) {
   864   Appender.call(this, formatter);
   865   this._name = "FileAppender";
   866   this._encoder = new TextEncoder();
   867   this._path = path;
   868   this._file = null;
   869   this._fileReadyPromise = null;
   871   // This is a promise exposed for testing/debugging the logger itself.
   872   this._lastWritePromise = null;
   873 }
   875 FileAppender.prototype = {
   876   __proto__: Appender.prototype,
   878   _openFile: function () {
   879     return Task.spawn(function _openFile() {
   880       try {
   881         this._file = yield OS.File.open(this._path,
   882                                         {truncate: true});
   883       } catch (err) {
   884         if (err instanceof OS.File.Error) {
   885           this._file = null;
   886         } else {
   887           throw err;
   888         }
   889       }
   890     }.bind(this));
   891   },
   893   _getFile: function() {
   894     if (!this._fileReadyPromise) {
   895       this._fileReadyPromise = this._openFile();
   896       return this._fileReadyPromise;
   897     }
   899     return this._fileReadyPromise.then(_ => {
   900       if (!this._file) {
   901         return this._openFile();
   902       }
   903     });
   904   },
   906   doAppend: function (formatted) {
   907     let array = this._encoder.encode(formatted + "\n");
   908     if (this._file) {
   909       this._lastWritePromise = this._file.write(array);
   910     } else {
   911       this._lastWritePromise = this._getFile().then(_ => {
   912         this._fileReadyPromise = null;
   913         if (this._file) {
   914           return this._file.write(array);
   915         }
   916       });
   917     }
   918   },
   920   reset: function () {
   921     let fileClosePromise = this._file.close();
   922     return fileClosePromise.then(_ => {
   923       this._file = null;
   924       return OS.File.remove(this._path);
   925     });
   926   }
   927 };
   929 /**
   930  * Bounded File appender
   931  *
   932  * Writes output to file using OS.File. After the total message size
   933  * (as defined by formatted.length) exceeds maxSize, existing messages
   934  * will be discarded, and subsequent writes will be appended to a new log file.
   935  */
   936 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
   937   FileAppender.call(this, path, formatter);
   938   this._name = "BoundedFileAppender";
   939   this._size = 0;
   940   this._maxSize = maxSize;
   941   this._closeFilePromise = null;
   942 }
   944 BoundedFileAppender.prototype = {
   945   __proto__: FileAppender.prototype,
   947   doAppend: function (formatted) {
   948     if (!this._removeFilePromise) {
   949       if (this._size < this._maxSize) {
   950         this._size += formatted.length;
   951         return FileAppender.prototype.doAppend.call(this, formatted);
   952       }
   953       this._removeFilePromise = this.reset();
   954     }
   955     this._removeFilePromise.then(_ => {
   956       this._removeFilePromise = null;
   957       this.doAppend(formatted);
   958     });
   959   },
   961   reset: function () {
   962     let fileClosePromise;
   963     if (this._fileReadyPromise) {
   964       // An attempt to open the file may still be in progress.
   965       fileClosePromise = this._fileReadyPromise.then(_ => {
   966         return this._file.close();
   967       });
   968     } else {
   969       fileClosePromise = this._file.close();
   970     }
   972     return fileClosePromise.then(_ => {
   973       this._size = 0;
   974       this._file = null;
   975       return OS.File.remove(this._path);
   976     });
   977   }
   978 };

mercurial