Bug 1845134 - Part 4: Update existing ui-icons to use the latest source from acorn...
[gecko.git] / services / common / logmanager.sys.mjs
blob724cfde38b51cadb7d70c7ef5540c6db348b88e0
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 "use strict;";
6 import { Log } from "resource://gre/modules/Log.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
12   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
13 });
15 const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days
17 // "shared" logs (ie, where the same log name is used by multiple LogManager
18 // instances) are a fact of life here - eg, FirefoxAccounts logs are used by
19 // both Sync and Reading List.
20 // However, different instances have different pref branches, so we need to
21 // handle when one pref branch says "Debug" and the other says "Error"
22 // So we (a) keep singleton console and dump appenders and (b) keep track
23 // of the minimum (ie, most verbose) level and use that.
24 // This avoids (a) the most recent setter winning (as that is indeterminate)
25 // and (b) multiple dump/console appenders being added to the same log multiple
26 // times, which would cause messages to appear twice.
28 // Singletons used by each instance.
29 var formatter;
30 var dumpAppender;
31 var consoleAppender;
33 // A set of all preference roots used by all instances.
34 var allBranches = new Set();
36 const STREAM_SEGMENT_SIZE = 4096;
37 const PR_UINT32_MAX = 0xffffffff;
39 /**
40  * Append to an nsIStorageStream
41  *
42  * This writes logging output to an in-memory stream which can later be read
43  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
44  * during logging. Instead, one can periodically consume the input stream and
45  * e.g. write it to disk asynchronously.
46  */
47 class StorageStreamAppender extends Log.Appender {
48   constructor(formatter) {
49     super(formatter);
50     this._name = "StorageStreamAppender";
52     this._converterStream = null; // holds the nsIConverterOutputStream
53     this._outputStream = null; // holds the underlying nsIOutputStream
55     this._ss = null;
56   }
58   get outputStream() {
59     if (!this._outputStream) {
60       // First create a raw stream. We can bail out early if that fails.
61       this._outputStream = this.newOutputStream();
62       if (!this._outputStream) {
63         return null;
64       }
66       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
67       // the instance if we already have one.
68       if (!this._converterStream) {
69         this._converterStream = Cc[
70           "@mozilla.org/intl/converter-output-stream;1"
71         ].createInstance(Ci.nsIConverterOutputStream);
72       }
73       this._converterStream.init(this._outputStream, "UTF-8");
74     }
75     return this._converterStream;
76   }
78   newOutputStream() {
79     let ss = (this._ss = Cc["@mozilla.org/storagestream;1"].createInstance(
80       Ci.nsIStorageStream
81     ));
82     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
83     return ss.getOutputStream(0);
84   }
86   getInputStream() {
87     if (!this._ss) {
88       return null;
89     }
90     return this._ss.newInputStream(0);
91   }
93   reset() {
94     if (!this._outputStream) {
95       return;
96     }
97     this.outputStream.close();
98     this._outputStream = null;
99     this._ss = null;
100   }
102   doAppend(formatted) {
103     if (!formatted) {
104       return;
105     }
106     try {
107       this.outputStream.writeString(formatted + "\n");
108     } catch (ex) {
109       if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
110         // The underlying output stream is closed, so let's open a new one
111         // and try again.
112         this._outputStream = null;
113       }
114       try {
115         this.outputStream.writeString(formatted + "\n");
116       } catch (ex) {
117         // Ah well, we tried, but something seems to be hosed permanently.
118       }
119     }
120   }
123 // A storage appender that is flushable to a file on disk.  Policies for
124 // when to flush, to what file, log rotation etc are up to the consumer
125 // (although it does maintain a .sawError property to help the consumer decide
126 // based on its policies)
127 class FlushableStorageAppender extends StorageStreamAppender {
128   constructor(formatter) {
129     super(formatter);
130     this.sawError = false;
131   }
133   append(message) {
134     if (message.level >= Log.Level.Error) {
135       this.sawError = true;
136     }
137     StorageStreamAppender.prototype.append.call(this, message);
138   }
140   reset() {
141     super.reset();
142     this.sawError = false;
143   }
145   // Flush the current stream to a file. Somewhat counter-intuitively, you
146   // must pass a log which will be written to with details of the operation.
147   async flushToFile(subdirArray, filename, log) {
148     let inStream = this.getInputStream();
149     this.reset();
150     if (!inStream) {
151       log.debug("Failed to flush log to a file - no input stream");
152       return;
153     }
154     log.debug("Flushing file log");
155     log.trace("Beginning stream copy to " + filename + ": " + Date.now());
156     try {
157       await this._copyStreamToFile(inStream, subdirArray, filename, log);
158       log.trace("onCopyComplete", Date.now());
159     } catch (ex) {
160       log.error("Failed to copy log stream to file", ex);
161     }
162   }
164   /**
165    * Copy an input stream to the named file, doing everything off the main
166    * thread.
167    * subDirArray is an array of path components, relative to the profile
168    * directory, where the file will be created.
169    * outputFileName is the filename to create.
170    * Returns a promise that is resolved on completion or rejected with an error.
171    */
172   async _copyStreamToFile(inputStream, subdirArray, outputFileName, log) {
173     let outputDirectory = PathUtils.join(PathUtils.profileDir, ...subdirArray);
174     await IOUtils.makeDirectory(outputDirectory);
175     let fullOutputFileName = PathUtils.join(outputDirectory, outputFileName);
177     let outputStream = Cc[
178       "@mozilla.org/network/file-output-stream;1"
179     ].createInstance(Ci.nsIFileOutputStream);
181     outputStream.init(
182       new lazy.FileUtils.File(fullOutputFileName),
183       -1,
184       -1,
185       Ci.nsIFileOutputStream.DEFER_OPEN
186     );
188     await new Promise(resolve =>
189       lazy.NetUtil.asyncCopy(inputStream, outputStream, () => resolve())
190     );
192     outputStream.close();
193     log.trace("finished copy to", fullOutputFileName);
194   }
197 // The public LogManager object.
198 export function LogManager(prefRoot, logNames, logFilePrefix) {
199   this._prefObservers = [];
200   this.init(prefRoot, logNames, logFilePrefix);
203 LogManager.StorageStreamAppender = StorageStreamAppender;
205 LogManager.prototype = {
206   _cleaningUpFileLogs: false,
208   init(prefRoot, logNames, logFilePrefix) {
209     this._prefs = Services.prefs.getBranch(prefRoot);
210     this._prefsBranch = prefRoot;
212     this.logFilePrefix = logFilePrefix;
213     if (!formatter) {
214       // Create a formatter and various appenders to attach to the logs.
215       formatter = new Log.BasicFormatter();
216       consoleAppender = new Log.ConsoleAppender(formatter);
217       dumpAppender = new Log.DumpAppender(formatter);
218     }
220     allBranches.add(this._prefsBranch);
221     // We create a preference observer for all our prefs so they are magically
222     // reflected if the pref changes after creation.
223     let setupAppender = (
224       appender,
225       prefName,
226       defaultLevel,
227       findSmallest = false
228     ) => {
229       let observer = newVal => {
230         let level = Log.Level[newVal] || defaultLevel;
231         if (findSmallest) {
232           // As some of our appenders have global impact (ie, there is only one
233           // place 'dump' goes to), we need to find the smallest value from all
234           // prefs controlling this appender.
235           // For example, if consumerA has dump=Debug then consumerB sets
236           // dump=Error, we need to keep dump=Debug so consumerA is respected.
237           for (let branch of allBranches) {
238             let lookPrefBranch = Services.prefs.getBranch(branch);
239             let lookVal =
240               Log.Level[lookPrefBranch.getStringPref(prefName, null)];
241             if (lookVal && lookVal < level) {
242               level = lookVal;
243             }
244           }
245         }
246         appender.level = level;
247       };
248       this._prefs.addObserver(prefName, observer);
249       this._prefObservers.push([prefName, observer]);
250       // and call the observer now with the current pref value.
251       observer(this._prefs.getStringPref(prefName, null));
252       return observer;
253     };
255     this._observeConsolePref = setupAppender(
256       consoleAppender,
257       "log.appender.console",
258       Log.Level.Fatal,
259       true
260     );
261     this._observeDumpPref = setupAppender(
262       dumpAppender,
263       "log.appender.dump",
264       Log.Level.Error,
265       true
266     );
268     // The file appender doesn't get the special singleton behaviour.
269     let fapp = (this._fileAppender = new FlushableStorageAppender(formatter));
270     // the stream gets a default of Debug as the user must go out of their way
271     // to see the stuff spewed to it.
272     this._observeStreamPref = setupAppender(
273       fapp,
274       "log.appender.file.level",
275       Log.Level.Debug
276     );
278     // now attach the appenders to all our logs.
279     for (let logName of logNames) {
280       let log = Log.repository.getLogger(logName);
281       for (let appender of [fapp, dumpAppender, consoleAppender]) {
282         log.addAppender(appender);
283       }
284     }
285     // and use the first specified log as a "root" for our log.
286     this._log = Log.repository.getLogger(logNames[0] + ".LogManager");
287   },
289   /**
290    * Cleanup this instance
291    */
292   finalize() {
293     for (let [prefName, observer] of this._prefObservers) {
294       this._prefs.removeObserver(prefName, observer);
295     }
296     this._prefObservers = [];
297     try {
298       allBranches.delete(this._prefsBranch);
299     } catch (e) {}
300     this._prefs = null;
301   },
303   get _logFileSubDirectoryEntries() {
304     // At this point we don't allow a custom directory for the logs, nor allow
305     // it to be outside the profile directory.
306     // This returns an array of the the relative directory entries below the
307     // profile dir, and is the directory about:sync-log uses.
308     return ["weave", "logs"];
309   },
311   get sawError() {
312     return this._fileAppender.sawError;
313   },
315   // Result values for resetFileLog.
316   SUCCESS_LOG_WRITTEN: "success-log-written",
317   ERROR_LOG_WRITTEN: "error-log-written",
319   /**
320    * Possibly generate a log file for all accumulated log messages and refresh
321    * the input & output streams.
322    * Whether a "success" or "error" log is written is determined based on
323    * whether an "Error" log entry was written to any of the logs.
324    * Returns a promise that resolves on completion with either null (for no
325    * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was
326    * written, or ERROR_LOG_WRITTEN if an "error" log was written.
327    */
328   async resetFileLog() {
329     try {
330       let flushToFile;
331       let reasonPrefix;
332       let reason;
333       if (this._fileAppender.sawError) {
334         reason = this.ERROR_LOG_WRITTEN;
335         flushToFile = this._prefs.getBoolPref(
336           "log.appender.file.logOnError",
337           true
338         );
339         reasonPrefix = "error";
340       } else {
341         reason = this.SUCCESS_LOG_WRITTEN;
342         flushToFile = this._prefs.getBoolPref(
343           "log.appender.file.logOnSuccess",
344           false
345         );
346         reasonPrefix = "success";
347       }
349       // might as well avoid creating an input stream if we aren't going to use it.
350       if (!flushToFile) {
351         this._fileAppender.reset();
352         return null;
353       }
355       // We have reasonPrefix at the start of the filename so all "error"
356       // logs are grouped in about:sync-log.
357       let filename =
358         reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt";
359       await this._fileAppender.flushToFile(
360         this._logFileSubDirectoryEntries,
361         filename,
362         this._log
363       );
364       // It's not completely clear to markh why we only do log cleanups
365       // for errors, but for now the Sync semantics have been copied...
366       // (one theory is that only cleaning up on error makes it less
367       // likely old error logs would be removed, but that's not true if
368       // there are occasional errors - let's address this later!)
369       if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) {
370         this._log.trace("Running cleanup.");
371         try {
372           await this.cleanupLogs();
373         } catch (err) {
374           this._log.error("Failed to cleanup logs", err);
375         }
376       }
377       return reason;
378     } catch (ex) {
379       this._log.error("Failed to resetFileLog", ex);
380       return null;
381     }
382   },
384   /**
385    * Finds all logs older than maxErrorAge and deletes them using async I/O.
386    */
387   cleanupLogs() {
388     let maxAge = this._prefs.getIntPref(
389       "log.appender.file.maxErrorAge",
390       DEFAULT_MAX_ERROR_AGE
391     );
392     let threshold = Date.now() - 1000 * maxAge;
393     this._log.debug("Log cleanup threshold time: " + threshold);
395     let shouldDelete = fileInfo => {
396       return fileInfo.lastModified < threshold;
397     };
398     return this._deleteLogFiles(shouldDelete);
399   },
401   /**
402    * Finds all logs and removes them.
403    */
404   removeAllLogs() {
405     return this._deleteLogFiles(() => true);
406   },
408   // Delete some log files. A callback is invoked for each found log file to
409   // determine if that file should be removed.
410   async _deleteLogFiles(cbShouldDelete) {
411     this._cleaningUpFileLogs = true;
412     let logDir = lazy.FileUtils.getDir(
413       "ProfD",
414       this._logFileSubDirectoryEntries
415     );
416     for (const path of await IOUtils.getChildren(logDir.path)) {
417       const name = PathUtils.filename(path);
419       if (!name.startsWith("error-") && !name.startsWith("success-")) {
420         continue;
421       }
423       try {
424         const info = await IOUtils.stat(path);
425         if (!cbShouldDelete(info)) {
426           continue;
427         }
429         this._log.trace(` > Cleanup removing ${name} (${info.lastModified})`);
430         await IOUtils.remove(path);
431         this._log.trace(`Deleted ${name}`);
432       } catch (ex) {
433         this._log.debug(
434           `Encountered error trying to clean up old log file ${name}`,
435           ex
436         );
437       }
438     }
439     this._cleaningUpFileLogs = false;
440     this._log.debug("Done deleting files.");
441     // This notification is used only for tests.
442     Services.obs.notifyObservers(
443       null,
444       "services-tests:common:log-manager:cleanup-logs"
445     );
446   },