Bumping manifests a=b2g-bump
[gecko.git] / services / healthreport / profile.jsm
blob5f75e8b83faa82676bd67dab5c5ded2d187aeff9
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 #ifndef MERGED_COMPARTMENT
7 "use strict";
9 this.EXPORTED_SYMBOLS = [
10   "ProfileCreationTimeAccessor",
11   "ProfileMetadataProvider",
14 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
18 Cu.import("resource://gre/modules/Metrics.jsm");
20 #endif
22 const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
23 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
25 Cu.import("resource://gre/modules/Promise.jsm");
26 Cu.import("resource://gre/modules/osfile.jsm")
27 Cu.import("resource://gre/modules/Task.jsm");
28 Cu.import("resource://gre/modules/Log.jsm");
29 Cu.import("resource://services-common/utils.js");
31 // Profile creation time access.
32 // This is separate from the provider to simplify testing and enable extraction
33 // to a shared location in the future.
34 this.ProfileCreationTimeAccessor = function(profile, log) {
35   this.profilePath = profile || OS.Constants.Path.profileDir;
36   if (!this.profilePath) {
37     throw new Error("No profile directory.");
38   }
39   this._log = log || {"debug": function (s) { dump(s + "\n"); }};
41 this.ProfileCreationTimeAccessor.prototype = {
42   /**
43    * There are three ways we can get our creation time:
44    *
45    * 1. From our own saved value (to avoid redundant work).
46    * 2. From the on-disk JSON file.
47    * 3. By calculating it from the filesystem.
48    *
49    * If we have to calculate, we write out the file; if we have
50    * to touch the file, we persist in-memory.
51    *
52    * @return a promise that resolves to the profile's creation time.
53    */
54   get created() {
55     if (this._created) {
56       return Promise.resolve(this._created);
57     }
59     function onSuccess(times) {
60       if (times && times.created) {
61         return this._created = times.created;
62       }
63       return onFailure.call(this, null, times);
64     }
66     function onFailure(err, times) {
67       return this.computeAndPersistTimes(times)
68                  .then(function onSuccess(created) {
69                          return this._created = created;
70                        }.bind(this));
71     }
73     return this.readTimes()
74                .then(onSuccess.bind(this),
75                      onFailure.bind(this));
76   },
78   /**
79    * Explicitly make `file`, a filename, a full path
80    * relative to our profile path.
81    */
82   getPath: function (file) {
83     return OS.Path.join(this.profilePath, file);
84   },
86   /**
87    * Return a promise which resolves to the JSON contents
88    * of the time file in this accessor's profile.
89    */
90   readTimes: function (file="times.json") {
91     return CommonUtils.readJSON(this.getPath(file));
92   },
94   /**
95    * Return a promise representing the writing of `contents`
96    * to `file` in the specified profile.
97    */
98   writeTimes: function (contents, file="times.json") {
99     return CommonUtils.writeJSON(contents, this.getPath(file));
100   },
102   /**
103    * Merge existing contents with a 'created' field, writing them
104    * to the specified file. Promise, naturally.
105    */
106   computeAndPersistTimes: function (existingContents, file="times.json") {
107     let path = this.getPath(file);
108     function onOldest(oldest) {
109       let contents = existingContents || {};
110       contents.created = oldest;
111       return this.writeTimes(contents, path)
112                  .then(function onSuccess() {
113                    return oldest;
114                  });
115     }
117     return this.getOldestProfileTimestamp()
118                .then(onOldest.bind(this));
119   },
121   /**
122    * Traverse the contents of the profile directory, finding the oldest file
123    * and returning its creation timestamp.
124    */
125   getOldestProfileTimestamp: function () {
126     let self = this;
127     let oldest = Date.now() + 1000;
128     let iterator = new OS.File.DirectoryIterator(this.profilePath);
129     self._log.debug("Iterating over profile " + this.profilePath);
130     if (!iterator) {
131       throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
132     }
134     function onEntry(entry) {
135       function onStatSuccess(info) {
136         // OS.File doesn't seem to be behaving. See Bug 827148.
137         // Let's do the best we can. This whole function is defensive.
138         let date = info.winBirthDate || info.macBirthDate;
139         if (!date || !date.getTime()) {
140           // OS.File will only return file creation times of any kind on Mac
141           // and Windows, where birthTime is defined.
142           // That means we're unable to function on Linux, so we use mtime
143           // instead.
144           self._log.debug("No birth date. Using mtime.");
145           date = info.lastModificationDate;
146         }
148         if (date) {
149           let timestamp = date.getTime();
150           self._log.debug("Using date: " + entry.path + " = " + date);
151           if (timestamp < oldest) {
152             oldest = timestamp;
153           }
154         }
155       }
157       function onStatFailure(e) {
158         // Never mind.
159         self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e));
160       }
162       return OS.File.stat(entry.path)
163                     .then(onStatSuccess, onStatFailure);
164     }
166     let promise = iterator.forEach(onEntry);
168     function onSuccess() {
169       iterator.close();
170       return oldest;
171     }
173     function onFailure(reason) {
174       iterator.close();
175       throw new Error("Unable to fetch oldest profile entry: " + reason);
176     }
178     return promise.then(onSuccess, onFailure);
179   },
183  * Measurements pertaining to the user's profile.
184  */
185 function ProfileMetadataMeasurement() {
186   Metrics.Measurement.call(this);
188 ProfileMetadataMeasurement.prototype = {
189   __proto__: Metrics.Measurement.prototype,
191   name: DEFAULT_PROFILE_MEASUREMENT_NAME,
192   version: 1,
194   fields: {
195     // Profile creation date. Number of days since Unix epoch.
196     profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
197   },
201  * Turn a millisecond timestamp into a day timestamp.
203  * @param msec a number of milliseconds since epoch.
204  * @return the number of whole days denoted by the input.
205  */
206 function truncate(msec) {
207   return Math.floor(msec / MILLISECONDS_PER_DAY);
211  * A Metrics.Provider for profile metadata, such as profile creation time.
212  */
213 this.ProfileMetadataProvider = function() {
214   Metrics.Provider.call(this);
216 this.ProfileMetadataProvider.prototype = {
217   __proto__: Metrics.Provider.prototype,
219   name: "org.mozilla.profile",
221   measurementTypes: [ProfileMetadataMeasurement],
223   pullOnly: true,
225   getProfileCreationDays: function () {
226     let accessor = new ProfileCreationTimeAccessor(null, this._log);
228     return accessor.created
229                    .then(truncate);
230   },
232   collectConstantData: function () {
233     let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
235     return Task.spawn(function collectConstant() {
236       let createdDays = yield this.getProfileCreationDays();
238       yield this.enqueueStorageOperation(function storeDays() {
239         return m.setLastNumeric("profileCreation", createdDays);
240       });
241     }.bind(this));
242   },