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
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");
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.");
39 this._log = log || {"debug": function (s) { dump(s + "\n"); }};
41 this.ProfileCreationTimeAccessor.prototype = {
43 * There are three ways we can get our creation time:
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.
49 * If we have to calculate, we write out the file; if we have
50 * to touch the file, we persist in-memory.
52 * @return a promise that resolves to the profile's creation time.
56 return Promise.resolve(this._created);
59 function onSuccess(times) {
60 if (times && times.created) {
61 return this._created = times.created;
63 return onFailure.call(this, null, times);
66 function onFailure(err, times) {
67 return this.computeAndPersistTimes(times)
68 .then(function onSuccess(created) {
69 return this._created = created;
73 return this.readTimes()
74 .then(onSuccess.bind(this),
75 onFailure.bind(this));
79 * Explicitly make `file`, a filename, a full path
80 * relative to our profile path.
82 getPath: function (file) {
83 return OS.Path.join(this.profilePath, file);
87 * Return a promise which resolves to the JSON contents
88 * of the time file in this accessor's profile.
90 readTimes: function (file="times.json") {
91 return CommonUtils.readJSON(this.getPath(file));
95 * Return a promise representing the writing of `contents`
96 * to `file` in the specified profile.
98 writeTimes: function (contents, file="times.json") {
99 return CommonUtils.writeJSON(contents, this.getPath(file));
103 * Merge existing contents with a 'created' field, writing them
104 * to the specified file. Promise, naturally.
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() {
117 return this.getOldestProfileTimestamp()
118 .then(onOldest.bind(this));
122 * Traverse the contents of the profile directory, finding the oldest file
123 * and returning its creation timestamp.
125 getOldestProfileTimestamp: function () {
127 let oldest = Date.now() + 1000;
128 let iterator = new OS.File.DirectoryIterator(this.profilePath);
129 self._log.debug("Iterating over profile " + this.profilePath);
131 throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
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
144 self._log.debug("No birth date. Using mtime.");
145 date = info.lastModificationDate;
149 let timestamp = date.getTime();
150 self._log.debug("Using date: " + entry.path + " = " + date);
151 if (timestamp < oldest) {
157 function onStatFailure(e) {
159 self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e));
162 return OS.File.stat(entry.path)
163 .then(onStatSuccess, onStatFailure);
166 let promise = iterator.forEach(onEntry);
168 function onSuccess() {
173 function onFailure(reason) {
175 throw new Error("Unable to fetch oldest profile entry: " + reason);
178 return promise.then(onSuccess, onFailure);
183 * Measurements pertaining to the user's profile.
185 function ProfileMetadataMeasurement() {
186 Metrics.Measurement.call(this);
188 ProfileMetadataMeasurement.prototype = {
189 __proto__: Metrics.Measurement.prototype,
191 name: DEFAULT_PROFILE_MEASUREMENT_NAME,
195 // Profile creation date. Number of days since Unix epoch.
196 profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
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.
206 function truncate(msec) {
207 return Math.floor(msec / MILLISECONDS_PER_DAY);
211 * A Metrics.Provider for profile metadata, such as profile creation time.
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],
225 getProfileCreationDays: function () {
226 let accessor = new ProfileCreationTimeAccessor(null, this._log);
228 return accessor.created
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);