1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/healthreport/profile.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,244 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +#ifndef MERGED_COMPARTMENT 1.11 + 1.12 +this.EXPORTED_SYMBOLS = [ 1.13 + "ProfileCreationTimeAccessor", 1.14 + "ProfileMetadataProvider", 1.15 +]; 1.16 + 1.17 +const {utils: Cu, classes: Cc, interfaces: Ci} = Components; 1.18 + 1.19 +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; 1.20 + 1.21 +Cu.import("resource://gre/modules/Metrics.jsm"); 1.22 + 1.23 +#endif 1.24 + 1.25 +const DEFAULT_PROFILE_MEASUREMENT_NAME = "age"; 1.26 +const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"}; 1.27 + 1.28 +Cu.import("resource://gre/modules/Promise.jsm"); 1.29 +Cu.import("resource://gre/modules/osfile.jsm") 1.30 +Cu.import("resource://gre/modules/Task.jsm"); 1.31 +Cu.import("resource://gre/modules/Log.jsm"); 1.32 +Cu.import("resource://services-common/utils.js"); 1.33 + 1.34 +// Profile creation time access. 1.35 +// This is separate from the provider to simplify testing and enable extraction 1.36 +// to a shared location in the future. 1.37 +this.ProfileCreationTimeAccessor = function(profile, log) { 1.38 + this.profilePath = profile || OS.Constants.Path.profileDir; 1.39 + if (!this.profilePath) { 1.40 + throw new Error("No profile directory."); 1.41 + } 1.42 + this._log = log || {"debug": function (s) { dump(s + "\n"); }}; 1.43 +} 1.44 +this.ProfileCreationTimeAccessor.prototype = { 1.45 + /** 1.46 + * There are three ways we can get our creation time: 1.47 + * 1.48 + * 1. From our own saved value (to avoid redundant work). 1.49 + * 2. From the on-disk JSON file. 1.50 + * 3. By calculating it from the filesystem. 1.51 + * 1.52 + * If we have to calculate, we write out the file; if we have 1.53 + * to touch the file, we persist in-memory. 1.54 + * 1.55 + * @return a promise that resolves to the profile's creation time. 1.56 + */ 1.57 + get created() { 1.58 + if (this._created) { 1.59 + return Promise.resolve(this._created); 1.60 + } 1.61 + 1.62 + function onSuccess(times) { 1.63 + if (times && times.created) { 1.64 + return this._created = times.created; 1.65 + } 1.66 + return onFailure.call(this, null, times); 1.67 + } 1.68 + 1.69 + function onFailure(err, times) { 1.70 + return this.computeAndPersistTimes(times) 1.71 + .then(function onSuccess(created) { 1.72 + return this._created = created; 1.73 + }.bind(this)); 1.74 + } 1.75 + 1.76 + return this.readTimes() 1.77 + .then(onSuccess.bind(this), 1.78 + onFailure.bind(this)); 1.79 + }, 1.80 + 1.81 + /** 1.82 + * Explicitly make `file`, a filename, a full path 1.83 + * relative to our profile path. 1.84 + */ 1.85 + getPath: function (file) { 1.86 + return OS.Path.join(this.profilePath, file); 1.87 + }, 1.88 + 1.89 + /** 1.90 + * Return a promise which resolves to the JSON contents 1.91 + * of the time file in this accessor's profile. 1.92 + */ 1.93 + readTimes: function (file="times.json") { 1.94 + return CommonUtils.readJSON(this.getPath(file)); 1.95 + }, 1.96 + 1.97 + /** 1.98 + * Return a promise representing the writing of `contents` 1.99 + * to `file` in the specified profile. 1.100 + */ 1.101 + writeTimes: function (contents, file="times.json") { 1.102 + return CommonUtils.writeJSON(contents, this.getPath(file)); 1.103 + }, 1.104 + 1.105 + /** 1.106 + * Merge existing contents with a 'created' field, writing them 1.107 + * to the specified file. Promise, naturally. 1.108 + */ 1.109 + computeAndPersistTimes: function (existingContents, file="times.json") { 1.110 + let path = this.getPath(file); 1.111 + function onOldest(oldest) { 1.112 + let contents = existingContents || {}; 1.113 + contents.created = oldest; 1.114 + return this.writeTimes(contents, path) 1.115 + .then(function onSuccess() { 1.116 + return oldest; 1.117 + }); 1.118 + } 1.119 + 1.120 + return this.getOldestProfileTimestamp() 1.121 + .then(onOldest.bind(this)); 1.122 + }, 1.123 + 1.124 + /** 1.125 + * Traverse the contents of the profile directory, finding the oldest file 1.126 + * and returning its creation timestamp. 1.127 + */ 1.128 + getOldestProfileTimestamp: function () { 1.129 + let self = this; 1.130 + let oldest = Date.now() + 1000; 1.131 + let iterator = new OS.File.DirectoryIterator(this.profilePath); 1.132 + self._log.debug("Iterating over profile " + this.profilePath); 1.133 + if (!iterator) { 1.134 + throw new Error("Unable to fetch oldest profile entry: no profile iterator."); 1.135 + } 1.136 + 1.137 + function onEntry(entry) { 1.138 + function onStatSuccess(info) { 1.139 + // OS.File doesn't seem to be behaving. See Bug 827148. 1.140 + // Let's do the best we can. This whole function is defensive. 1.141 + let date = info.winBirthDate || info.macBirthDate; 1.142 + if (!date || !date.getTime()) { 1.143 + // OS.File will only return file creation times of any kind on Mac 1.144 + // and Windows, where birthTime is defined. 1.145 + // That means we're unable to function on Linux, so we use mtime 1.146 + // instead. 1.147 + self._log.debug("No birth date. Using mtime."); 1.148 + date = info.lastModificationDate; 1.149 + } 1.150 + 1.151 + if (date) { 1.152 + let timestamp = date.getTime(); 1.153 + self._log.debug("Using date: " + entry.path + " = " + date); 1.154 + if (timestamp < oldest) { 1.155 + oldest = timestamp; 1.156 + } 1.157 + } 1.158 + } 1.159 + 1.160 + function onStatFailure(e) { 1.161 + // Never mind. 1.162 + self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e)); 1.163 + } 1.164 + 1.165 + return OS.File.stat(entry.path) 1.166 + .then(onStatSuccess, onStatFailure); 1.167 + } 1.168 + 1.169 + let promise = iterator.forEach(onEntry); 1.170 + 1.171 + function onSuccess() { 1.172 + iterator.close(); 1.173 + return oldest; 1.174 + } 1.175 + 1.176 + function onFailure(reason) { 1.177 + iterator.close(); 1.178 + throw new Error("Unable to fetch oldest profile entry: " + reason); 1.179 + } 1.180 + 1.181 + return promise.then(onSuccess, onFailure); 1.182 + }, 1.183 +} 1.184 + 1.185 +/** 1.186 + * Measurements pertaining to the user's profile. 1.187 + */ 1.188 +function ProfileMetadataMeasurement() { 1.189 + Metrics.Measurement.call(this); 1.190 +} 1.191 +ProfileMetadataMeasurement.prototype = { 1.192 + __proto__: Metrics.Measurement.prototype, 1.193 + 1.194 + name: DEFAULT_PROFILE_MEASUREMENT_NAME, 1.195 + version: 1, 1.196 + 1.197 + fields: { 1.198 + // Profile creation date. Number of days since Unix epoch. 1.199 + profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC}, 1.200 + }, 1.201 +}; 1.202 + 1.203 +/** 1.204 + * Turn a millisecond timestamp into a day timestamp. 1.205 + * 1.206 + * @param msec a number of milliseconds since epoch. 1.207 + * @return the number of whole days denoted by the input. 1.208 + */ 1.209 +function truncate(msec) { 1.210 + return Math.floor(msec / MILLISECONDS_PER_DAY); 1.211 +} 1.212 + 1.213 +/** 1.214 + * A Metrics.Provider for profile metadata, such as profile creation time. 1.215 + */ 1.216 +this.ProfileMetadataProvider = function() { 1.217 + Metrics.Provider.call(this); 1.218 +} 1.219 +this.ProfileMetadataProvider.prototype = { 1.220 + __proto__: Metrics.Provider.prototype, 1.221 + 1.222 + name: "org.mozilla.profile", 1.223 + 1.224 + measurementTypes: [ProfileMetadataMeasurement], 1.225 + 1.226 + pullOnly: true, 1.227 + 1.228 + getProfileCreationDays: function () { 1.229 + let accessor = new ProfileCreationTimeAccessor(null, this._log); 1.230 + 1.231 + return accessor.created 1.232 + .then(truncate); 1.233 + }, 1.234 + 1.235 + collectConstantData: function () { 1.236 + let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1); 1.237 + 1.238 + return Task.spawn(function collectConstant() { 1.239 + let createdDays = yield this.getProfileCreationDays(); 1.240 + 1.241 + yield this.enqueueStorageOperation(function storeDays() { 1.242 + return m.setLastNumeric("profileCreation", createdDays); 1.243 + }); 1.244 + }.bind(this)); 1.245 + }, 1.246 +}; 1.247 +