michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: #ifndef MERGED_COMPARTMENT michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "ProfileCreationTimeAccessor", michael@0: "ProfileMetadataProvider", michael@0: ]; michael@0: michael@0: const {utils: Cu, classes: Cc, interfaces: Ci} = Components; michael@0: michael@0: const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; michael@0: michael@0: Cu.import("resource://gre/modules/Metrics.jsm"); michael@0: michael@0: #endif michael@0: michael@0: const DEFAULT_PROFILE_MEASUREMENT_NAME = "age"; michael@0: const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"}; michael@0: michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/osfile.jsm") michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: michael@0: // Profile creation time access. michael@0: // This is separate from the provider to simplify testing and enable extraction michael@0: // to a shared location in the future. michael@0: this.ProfileCreationTimeAccessor = function(profile, log) { michael@0: this.profilePath = profile || OS.Constants.Path.profileDir; michael@0: if (!this.profilePath) { michael@0: throw new Error("No profile directory."); michael@0: } michael@0: this._log = log || {"debug": function (s) { dump(s + "\n"); }}; michael@0: } michael@0: this.ProfileCreationTimeAccessor.prototype = { michael@0: /** michael@0: * There are three ways we can get our creation time: michael@0: * michael@0: * 1. From our own saved value (to avoid redundant work). michael@0: * 2. From the on-disk JSON file. michael@0: * 3. By calculating it from the filesystem. michael@0: * michael@0: * If we have to calculate, we write out the file; if we have michael@0: * to touch the file, we persist in-memory. michael@0: * michael@0: * @return a promise that resolves to the profile's creation time. michael@0: */ michael@0: get created() { michael@0: if (this._created) { michael@0: return Promise.resolve(this._created); michael@0: } michael@0: michael@0: function onSuccess(times) { michael@0: if (times && times.created) { michael@0: return this._created = times.created; michael@0: } michael@0: return onFailure.call(this, null, times); michael@0: } michael@0: michael@0: function onFailure(err, times) { michael@0: return this.computeAndPersistTimes(times) michael@0: .then(function onSuccess(created) { michael@0: return this._created = created; michael@0: }.bind(this)); michael@0: } michael@0: michael@0: return this.readTimes() michael@0: .then(onSuccess.bind(this), michael@0: onFailure.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Explicitly make `file`, a filename, a full path michael@0: * relative to our profile path. michael@0: */ michael@0: getPath: function (file) { michael@0: return OS.Path.join(this.profilePath, file); michael@0: }, michael@0: michael@0: /** michael@0: * Return a promise which resolves to the JSON contents michael@0: * of the time file in this accessor's profile. michael@0: */ michael@0: readTimes: function (file="times.json") { michael@0: return CommonUtils.readJSON(this.getPath(file)); michael@0: }, michael@0: michael@0: /** michael@0: * Return a promise representing the writing of `contents` michael@0: * to `file` in the specified profile. michael@0: */ michael@0: writeTimes: function (contents, file="times.json") { michael@0: return CommonUtils.writeJSON(contents, this.getPath(file)); michael@0: }, michael@0: michael@0: /** michael@0: * Merge existing contents with a 'created' field, writing them michael@0: * to the specified file. Promise, naturally. michael@0: */ michael@0: computeAndPersistTimes: function (existingContents, file="times.json") { michael@0: let path = this.getPath(file); michael@0: function onOldest(oldest) { michael@0: let contents = existingContents || {}; michael@0: contents.created = oldest; michael@0: return this.writeTimes(contents, path) michael@0: .then(function onSuccess() { michael@0: return oldest; michael@0: }); michael@0: } michael@0: michael@0: return this.getOldestProfileTimestamp() michael@0: .then(onOldest.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Traverse the contents of the profile directory, finding the oldest file michael@0: * and returning its creation timestamp. michael@0: */ michael@0: getOldestProfileTimestamp: function () { michael@0: let self = this; michael@0: let oldest = Date.now() + 1000; michael@0: let iterator = new OS.File.DirectoryIterator(this.profilePath); michael@0: self._log.debug("Iterating over profile " + this.profilePath); michael@0: if (!iterator) { michael@0: throw new Error("Unable to fetch oldest profile entry: no profile iterator."); michael@0: } michael@0: michael@0: function onEntry(entry) { michael@0: function onStatSuccess(info) { michael@0: // OS.File doesn't seem to be behaving. See Bug 827148. michael@0: // Let's do the best we can. This whole function is defensive. michael@0: let date = info.winBirthDate || info.macBirthDate; michael@0: if (!date || !date.getTime()) { michael@0: // OS.File will only return file creation times of any kind on Mac michael@0: // and Windows, where birthTime is defined. michael@0: // That means we're unable to function on Linux, so we use mtime michael@0: // instead. michael@0: self._log.debug("No birth date. Using mtime."); michael@0: date = info.lastModificationDate; michael@0: } michael@0: michael@0: if (date) { michael@0: let timestamp = date.getTime(); michael@0: self._log.debug("Using date: " + entry.path + " = " + date); michael@0: if (timestamp < oldest) { michael@0: oldest = timestamp; michael@0: } michael@0: } michael@0: } michael@0: michael@0: function onStatFailure(e) { michael@0: // Never mind. michael@0: self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e)); michael@0: } michael@0: michael@0: return OS.File.stat(entry.path) michael@0: .then(onStatSuccess, onStatFailure); michael@0: } michael@0: michael@0: let promise = iterator.forEach(onEntry); michael@0: michael@0: function onSuccess() { michael@0: iterator.close(); michael@0: return oldest; michael@0: } michael@0: michael@0: function onFailure(reason) { michael@0: iterator.close(); michael@0: throw new Error("Unable to fetch oldest profile entry: " + reason); michael@0: } michael@0: michael@0: return promise.then(onSuccess, onFailure); michael@0: }, michael@0: } michael@0: michael@0: /** michael@0: * Measurements pertaining to the user's profile. michael@0: */ michael@0: function ProfileMetadataMeasurement() { michael@0: Metrics.Measurement.call(this); michael@0: } michael@0: ProfileMetadataMeasurement.prototype = { michael@0: __proto__: Metrics.Measurement.prototype, michael@0: michael@0: name: DEFAULT_PROFILE_MEASUREMENT_NAME, michael@0: version: 1, michael@0: michael@0: fields: { michael@0: // Profile creation date. Number of days since Unix epoch. michael@0: profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC}, michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Turn a millisecond timestamp into a day timestamp. michael@0: * michael@0: * @param msec a number of milliseconds since epoch. michael@0: * @return the number of whole days denoted by the input. michael@0: */ michael@0: function truncate(msec) { michael@0: return Math.floor(msec / MILLISECONDS_PER_DAY); michael@0: } michael@0: michael@0: /** michael@0: * A Metrics.Provider for profile metadata, such as profile creation time. michael@0: */ michael@0: this.ProfileMetadataProvider = function() { michael@0: Metrics.Provider.call(this); michael@0: } michael@0: this.ProfileMetadataProvider.prototype = { michael@0: __proto__: Metrics.Provider.prototype, michael@0: michael@0: name: "org.mozilla.profile", michael@0: michael@0: measurementTypes: [ProfileMetadataMeasurement], michael@0: michael@0: pullOnly: true, michael@0: michael@0: getProfileCreationDays: function () { michael@0: let accessor = new ProfileCreationTimeAccessor(null, this._log); michael@0: michael@0: return accessor.created michael@0: .then(truncate); michael@0: }, michael@0: michael@0: collectConstantData: function () { michael@0: let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1); michael@0: michael@0: return Task.spawn(function collectConstant() { michael@0: let createdDays = yield this.getProfileCreationDays(); michael@0: michael@0: yield this.enqueueStorageOperation(function storeDays() { michael@0: return m.setLastNumeric("profileCreation", createdDays); michael@0: }); michael@0: }.bind(this)); michael@0: }, michael@0: }; michael@0: