Bug 1885602 - Part 5: Implement navigating to the SUMO help topic from the menu heade...
[gecko.git] / toolkit / modules / ProfileAge.sys.mjs
blobea824f5a9187c936d846f8e5f780dfeafe908cdd
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 import { Log } from "resource://gre/modules/Log.sys.mjs";
7 const FILE_TIMES = "times.json";
9 /**
10  * Traverse the contents of the profile directory, finding the oldest file
11  * and returning its creation timestamp.
12  */
13 async function getOldestProfileTimestamp(profilePath, log) {
14   let start = Date.now();
15   let oldest = start + 1000;
16   log.debug("Iterating over profile " + profilePath);
18   try {
19     for (const childPath of await IOUtils.getChildren(profilePath)) {
20       try {
21         let info = await IOUtils.stat(childPath);
22         let timestamp;
23         if (info.creationTime !== undefined) {
24           timestamp = info.creationTime;
25         } else {
26           // We only support file creation times on Mac and Windows. We have to
27           // settle for mtime on Linux.
28           log.debug("No birth date. Using mtime.");
29           timestamp = info.lastModified;
30         }
32         log.debug(`Using date: ${childPath} = ${timestamp}`);
33         if (timestamp < oldest) {
34           oldest = timestamp;
35         }
36       } catch (e) {
37         // Never mind.
38         log.debug("Stat failure", e);
39       }
40     }
41   } catch (reason) {
42     throw new Error("Unable to fetch oldest profile entry: " + reason);
43   }
45   return oldest;
48 /**
49  * Profile access to times.json (eg, creation/reset time).
50  * This is separate from the provider to simplify testing and enable extraction
51  * to a shared location in the future.
52  */
53 class ProfileAgeImpl {
54   constructor(profile, times) {
55     this._profilePath = profile;
56     this._times = times;
57     this._log = Log.repository.getLogger("Toolkit.ProfileAge");
59     if ("firstUse" in this._times && this._times.firstUse === null) {
60       // Indicates that this is a new profile that needs a first use timestamp.
61       this._times.firstUse = Date.now();
62       this.writeTimes();
63     }
64   }
66   get profilePath() {
67     if (!this._profilePath) {
68       this._profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
69     }
71     return this._profilePath;
72   }
74   /**
75    * There are two ways we can get our creation time:
76    *
77    * 1. From the on-disk JSON file.
78    * 2. By calculating it from the filesystem.
79    *
80    * If we have to calculate, we write out the file; if we have
81    * to touch the file, we persist in-memory.
82    *
83    * @return a promise that resolves to the profile's creation time.
84    */
85   get created() {
86     // This can be an expensive operation so make sure we only do it once.
87     if (this._created) {
88       return this._created;
89     }
91     if (!this._times.created) {
92       this._created = this.computeAndPersistCreated();
93     } else {
94       this._created = Promise.resolve(this._times.created);
95     }
97     return this._created;
98   }
100   /**
101    * Returns a promise to the time of first use of the profile. This may be
102    * undefined if the first use time is unknown.
103    */
104   get firstUse() {
105     if ("firstUse" in this._times) {
106       return Promise.resolve(this._times.firstUse);
107     }
108     return Promise.resolve(undefined);
109   }
111   /**
112    * Return a promise representing the writing the current times to the profile.
113    */
114   async writeTimes() {
115     try {
116       await IOUtils.writeJSON(
117         PathUtils.join(this.profilePath, FILE_TIMES),
118         this._times
119       );
120     } catch (e) {
121       if (
122         !DOMException.isInstance(e) ||
123         e.name !== "AbortError" ||
124         e.message !== "IOUtils: Shutting down and refusing additional I/O tasks"
125       ) {
126         throw e;
127       }
128     }
129   }
131   /**
132    * Calculates the created time by scanning the profile directory, sets it in
133    * the current set of times and persists it to the profile. Returns a promise
134    * that resolves when all of that is complete.
135    */
136   async computeAndPersistCreated() {
137     let oldest = await getOldestProfileTimestamp(this.profilePath, this._log);
138     this._times.created = oldest;
139     await this.writeTimes();
140     return oldest;
141   }
143   /**
144    * Record (and persist) when a profile reset happened.  We just store a
145    * single value - the timestamp of the most recent reset - but there is scope
146    * to keep a list of reset times should our health-reporter successor
147    * be able to make use of that.
148    * Returns a promise that is resolved once the file has been written.
149    */
150   recordProfileReset(time = Date.now()) {
151     this._times.reset = time;
152     return this.writeTimes();
153   }
155   /* Returns a promise that resolves to the time the profile was reset,
156    * or undefined if not recorded.
157    */
158   get reset() {
159     if ("reset" in this._times) {
160       return Promise.resolve(this._times.reset);
161     }
162     return Promise.resolve(undefined);
163   }
166 // A Map from profile directory to a promise that resolves to the ProfileAgeImpl.
167 const PROFILES = new Map();
169 async function initProfileAge(profile) {
170   let timesPath = PathUtils.join(profile, FILE_TIMES);
172   try {
173     let times = await IOUtils.readJSON(timesPath);
174     return new ProfileAgeImpl(profile, times || {});
175   } catch (e) {
176     // Indicates that the file was missing or broken. In this case we want to
177     // record the first use time as now. The constructor will set this and write
178     // times.json
179     return new ProfileAgeImpl(profile, { firstUse: null });
180   }
184  * Returns a promise that resolves to an instance of ProfileAgeImpl. Will always
185  * return the same instance for every call for the same profile.
187  * @param {string} profile The path to the profile directory.
188  * @return {Promise<ProfileAgeImpl>} Resolves to the ProfileAgeImpl.
189  */
190 export function ProfileAge(profile) {
191   if (!profile) {
192     profile = PathUtils.profileDir;
193   }
195   if (PROFILES.has(profile)) {
196     return PROFILES.get(profile);
197   }
199   let promise = initProfileAge(profile);
200   PROFILES.set(profile, promise);
201   return promise;