|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 #ifndef MERGED_COMPARTMENT |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "ProfileCreationTimeAccessor", |
|
11 "ProfileMetadataProvider", |
|
12 ]; |
|
13 |
|
14 const {utils: Cu, classes: Cc, interfaces: Ci} = Components; |
|
15 |
|
16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; |
|
17 |
|
18 Cu.import("resource://gre/modules/Metrics.jsm"); |
|
19 |
|
20 #endif |
|
21 |
|
22 const DEFAULT_PROFILE_MEASUREMENT_NAME = "age"; |
|
23 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"}; |
|
24 |
|
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"); |
|
30 |
|
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"); }}; |
|
40 } |
|
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 } |
|
58 |
|
59 function onSuccess(times) { |
|
60 if (times && times.created) { |
|
61 return this._created = times.created; |
|
62 } |
|
63 return onFailure.call(this, null, times); |
|
64 } |
|
65 |
|
66 function onFailure(err, times) { |
|
67 return this.computeAndPersistTimes(times) |
|
68 .then(function onSuccess(created) { |
|
69 return this._created = created; |
|
70 }.bind(this)); |
|
71 } |
|
72 |
|
73 return this.readTimes() |
|
74 .then(onSuccess.bind(this), |
|
75 onFailure.bind(this)); |
|
76 }, |
|
77 |
|
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 }, |
|
85 |
|
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 }, |
|
93 |
|
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 }, |
|
101 |
|
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 } |
|
116 |
|
117 return this.getOldestProfileTimestamp() |
|
118 .then(onOldest.bind(this)); |
|
119 }, |
|
120 |
|
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 } |
|
133 |
|
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 } |
|
147 |
|
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 } |
|
156 |
|
157 function onStatFailure(e) { |
|
158 // Never mind. |
|
159 self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e)); |
|
160 } |
|
161 |
|
162 return OS.File.stat(entry.path) |
|
163 .then(onStatSuccess, onStatFailure); |
|
164 } |
|
165 |
|
166 let promise = iterator.forEach(onEntry); |
|
167 |
|
168 function onSuccess() { |
|
169 iterator.close(); |
|
170 return oldest; |
|
171 } |
|
172 |
|
173 function onFailure(reason) { |
|
174 iterator.close(); |
|
175 throw new Error("Unable to fetch oldest profile entry: " + reason); |
|
176 } |
|
177 |
|
178 return promise.then(onSuccess, onFailure); |
|
179 }, |
|
180 } |
|
181 |
|
182 /** |
|
183 * Measurements pertaining to the user's profile. |
|
184 */ |
|
185 function ProfileMetadataMeasurement() { |
|
186 Metrics.Measurement.call(this); |
|
187 } |
|
188 ProfileMetadataMeasurement.prototype = { |
|
189 __proto__: Metrics.Measurement.prototype, |
|
190 |
|
191 name: DEFAULT_PROFILE_MEASUREMENT_NAME, |
|
192 version: 1, |
|
193 |
|
194 fields: { |
|
195 // Profile creation date. Number of days since Unix epoch. |
|
196 profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC}, |
|
197 }, |
|
198 }; |
|
199 |
|
200 /** |
|
201 * Turn a millisecond timestamp into a day timestamp. |
|
202 * |
|
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); |
|
208 } |
|
209 |
|
210 /** |
|
211 * A Metrics.Provider for profile metadata, such as profile creation time. |
|
212 */ |
|
213 this.ProfileMetadataProvider = function() { |
|
214 Metrics.Provider.call(this); |
|
215 } |
|
216 this.ProfileMetadataProvider.prototype = { |
|
217 __proto__: Metrics.Provider.prototype, |
|
218 |
|
219 name: "org.mozilla.profile", |
|
220 |
|
221 measurementTypes: [ProfileMetadataMeasurement], |
|
222 |
|
223 pullOnly: true, |
|
224 |
|
225 getProfileCreationDays: function () { |
|
226 let accessor = new ProfileCreationTimeAccessor(null, this._log); |
|
227 |
|
228 return accessor.created |
|
229 .then(truncate); |
|
230 }, |
|
231 |
|
232 collectConstantData: function () { |
|
233 let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1); |
|
234 |
|
235 return Task.spawn(function collectConstant() { |
|
236 let createdDays = yield this.getProfileCreationDays(); |
|
237 |
|
238 yield this.enqueueStorageOperation(function storeDays() { |
|
239 return m.setLastNumeric("profileCreation", createdDays); |
|
240 }); |
|
241 }.bind(this)); |
|
242 }, |
|
243 }; |
|
244 |