diff --git a/browser/components/migration/FirefoxProfileMigrator.js b/browser/components/migration/FirefoxProfileMigrator.js index c3b0226dfa4..2d644d1fe6b 100644 --- a/browser/components/migration/FirefoxProfileMigrator.js +++ b/browser/components/migration/FirefoxProfileMigrator.js @@ -27,8 +27,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ProfileTimesAccessor", - "resource://gre/modules/services/healthreport/profile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", + "resource://gre/modules/ProfileAge.jsm"); function FirefoxProfileMigrator() { @@ -171,7 +171,7 @@ FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileD file.copyTo(currentProfileDir, ""); } // And record the fact a migration (ie, a reset) happened. - let timesAccessor = new ProfileTimesAccessor(currentProfileDir.path); + let timesAccessor = new ProfileAge(currentProfileDir.path); timesAccessor.recordProfileReset().then( () => aCallback(true), () => aCallback(false) diff --git a/services/healthreport/profile.jsm b/services/healthreport/profile.jsm index 5c6717a1eee..85c887b2c44 100644 --- a/services/healthreport/profile.jsm +++ b/services/healthreport/profile.jsm @@ -6,10 +6,7 @@ "use strict"; -this.EXPORTED_SYMBOLS = [ - "ProfileTimesAccessor", - "ProfileMetadataProvider", -]; +this.EXPORTED_SYMBOLS = ["ProfileMetadataProvider"]; const {utils: Cu, classes: Cc, interfaces: Ci} = Components; @@ -27,195 +24,7 @@ Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/osfile.jsm") Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/utils.js"); - -// Profile access to times.json (eg, creation/reset time). -// This is separate from the provider to simplify testing and enable extraction -// to a shared location in the future. -this.ProfileTimesAccessor = function(profile, log) { - this.profilePath = profile || OS.Constants.Path.profileDir; - if (!this.profilePath) { - throw new Error("No profile directory."); - } - this._log = log || {"debug": function (s) { dump(s + "\n"); }}; -} -this.ProfileTimesAccessor.prototype = { - /** - * There are three ways we can get our creation time: - * - * 1. From our own saved value (to avoid redundant work). - * 2. From the on-disk JSON file. - * 3. By calculating it from the filesystem. - * - * If we have to calculate, we write out the file; if we have - * to touch the file, we persist in-memory. - * - * @return a promise that resolves to the profile's creation time. - */ - get created() { - function onSuccess(times) { - if (times.created) { - return times.created; - } - return onFailure.call(this, null, times); - } - - function onFailure(err, times) { - return this.computeAndPersistCreated(times) - .then(function onSuccess(created) { - return created; - }.bind(this)); - } - - return this.getTimes() - .then(onSuccess.bind(this), - onFailure.bind(this)); - }, - - /** - * Explicitly make `file`, a filename, a full path - * relative to our profile path. - */ - getPath: function (file) { - return OS.Path.join(this.profilePath, file); - }, - - /** - * Return a promise which resolves to the JSON contents - * of the time file, using the already read value if possible. - */ - getTimes: function (file="times.json") { - if (this._times) { - return Promise.resolve(this._times); - } - return this.readTimes(file).then( - times => { - return this.times = times || {}; - } - ); - }, - - /** - * Return a promise which resolves to the JSON contents - * of the time file in this accessor's profile. - */ - readTimes: function (file="times.json") { - return CommonUtils.readJSON(this.getPath(file)); - }, - - /** - * Return a promise representing the writing of `contents` - * to `file` in the specified profile. - */ - writeTimes: function (contents, file="times.json") { - return CommonUtils.writeJSON(contents, this.getPath(file)); - }, - - /** - * Merge existing contents with a 'created' field, writing them - * to the specified file. Promise, naturally. - */ - computeAndPersistCreated: function (existingContents, file="times.json") { - let path = this.getPath(file); - function onOldest(oldest) { - let contents = existingContents || {}; - contents.created = oldest; - this._times = contents; - return this.writeTimes(contents, path) - .then(function onSuccess() { - return oldest; - }); - } - - return this.getOldestProfileTimestamp() - .then(onOldest.bind(this)); - }, - - /** - * Traverse the contents of the profile directory, finding the oldest file - * and returning its creation timestamp. - */ - getOldestProfileTimestamp: function () { - let self = this; - let oldest = Date.now() + 1000; - let iterator = new OS.File.DirectoryIterator(this.profilePath); - self._log.debug("Iterating over profile " + this.profilePath); - if (!iterator) { - throw new Error("Unable to fetch oldest profile entry: no profile iterator."); - } - - function onEntry(entry) { - function onStatSuccess(info) { - // OS.File doesn't seem to be behaving. See Bug 827148. - // Let's do the best we can. This whole function is defensive. - let date = info.winBirthDate || info.macBirthDate; - if (!date || !date.getTime()) { - // OS.File will only return file creation times of any kind on Mac - // and Windows, where birthTime is defined. - // That means we're unable to function on Linux, so we use mtime - // instead. - self._log.debug("No birth date. Using mtime."); - date = info.lastModificationDate; - } - - if (date) { - let timestamp = date.getTime(); - self._log.debug("Using date: " + entry.path + " = " + date); - if (timestamp < oldest) { - oldest = timestamp; - } - } - } - - function onStatFailure(e) { - // Never mind. - self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e)); - } - - return OS.File.stat(entry.path) - .then(onStatSuccess, onStatFailure); - } - - let promise = iterator.forEach(onEntry); - - function onSuccess() { - iterator.close(); - return oldest; - } - - function onFailure(reason) { - iterator.close(); - throw new Error("Unable to fetch oldest profile entry: " + reason); - } - - return promise.then(onSuccess, onFailure); - }, - - /** - * Record (and persist) when a profile reset happened. We just store a - * single value - the timestamp of the most recent reset - but there is scope - * to keep a list of reset times should our health-reporter successor - * be able to make use of that. - * Returns a promise that is resolved once the file has been written. - */ - recordProfileReset: function (time=Date.now(), file="times.json") { - return this.getTimes(file).then( - times => { - times.reset = time; - return this.writeTimes(times, file); - } - ); - }, - - /* Returns a promise that resolves to the time the profile was reset, - * or undefined if not recorded. - */ - get reset() { - return this.getTimes().then( - times => times.reset - ); - }, -} +Cu.import("resource://gre/modules/ProfileAge.jsm"); /** * Measurements pertaining to the user's profile. @@ -283,7 +92,7 @@ this.ProfileMetadataProvider.prototype = { getProfileDays: Task.async(function* () { let result = {}; - let accessor = new ProfileTimesAccessor(null, this._log); + let accessor = new ProfileAge(null, this._log); let created = yield accessor.created; result["profileCreation"] = truncate(created); diff --git a/services/healthreport/tests/xpcshell/test_profile.js b/services/healthreport/tests/xpcshell/test_profile.js index 4458d17a744..215a17bd8ef 100644 --- a/services/healthreport/tests/xpcshell/test_profile.js +++ b/services/healthreport/tests/xpcshell/test_profile.js @@ -15,6 +15,7 @@ do_get_profile(); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Metrics.jsm"); Cu.import("resource://gre/modules/services/healthreport/profile.jsm"); +Cu.import("resource://gre/modules/ProfileAge.jsm"); Cu.import("resource://gre/modules/Task.jsm"); @@ -64,7 +65,7 @@ add_test(function use_os_file() { }); function getAccessor() { - let acc = new ProfileTimesAccessor(); + let acc = new ProfileAge(); print("Profile is " + acc.profilePath); return acc; } diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm index ce0ebbd2e32..7b989005f59 100644 --- a/toolkit/components/telemetry/TelemetryEnvironment.jsm +++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm @@ -24,8 +24,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ctypes", XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); #endif -XPCOMUtils.defineLazyModuleGetter(this, "ProfileTimesAccessor", - "resource://gre/modules/services/healthreport/profile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", + "resource://gre/modules/ProfileAge.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm"); @@ -501,7 +501,7 @@ this.TelemetryEnvironment = { * @return Object containing the profile data. */ _getProfile: Task.async(function* () { - let profileAccessor = new ProfileTimesAccessor(null, this._log); + let profileAccessor = new ProfileAge(null, this._log); let creationDate = yield profileAccessor.created; let resetDate = yield profileAccessor.reset; diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js index 7dffad5946f..31d1fa18fcd 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -15,9 +15,9 @@ Cu.import("resource://testing-common/httpd.js"); XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); -// Lazy load |ProfileTimesAccessor| as it is not available on Android. -XPCOMUtils.defineLazyModuleGetter(this, "ProfileTimesAccessor", - "resource://gre/modules/services/healthreport/profile.jsm"); +// Lazy load |ProfileAge| as it is not available on Android. +XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", + "resource://gre/modules/ProfileAge.jsm"); // The webserver hosting the addons. let gHttpServer = null; @@ -160,11 +160,11 @@ function spoofGfxAdapter() { function spoofProfileReset() { if (gIsAndroid) { - // ProfileTimesAccessor is not available on Android. + // ProfileAge is not available on Android. return true; } - let profileAccessor = new ProfileTimesAccessor(); + let profileAccessor = new ProfileAge(); return profileAccessor.writeTimes({ created: PROFILE_CREATION_DATE_MS, diff --git a/toolkit/modules/ProfileAge.jsm b/toolkit/modules/ProfileAge.jsm new file mode 100644 index 00000000000..27393007068 --- /dev/null +++ b/toolkit/modules/ProfileAge.jsm @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ProfileAge"]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/osfile.jsm") +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://services-common/utils.js"); + +/** + * Profile access to times.json (eg, creation/reset time). + * This is separate from the provider to simplify testing and enable extraction + * to a shared location in the future. + */ +this.ProfileAge = function(profile, log) { + this.profilePath = profile || OS.Constants.Path.profileDir; + if (!this.profilePath) { + throw new Error("No profile directory."); + } + this._log = log || {"debug": function (s) { dump(s + "\n"); }}; +} +this.ProfileAge.prototype = { + /** + * There are three ways we can get our creation time: + * + * 1. From our own saved value (to avoid redundant work). + * 2. From the on-disk JSON file. + * 3. By calculating it from the filesystem. + * + * If we have to calculate, we write out the file; if we have + * to touch the file, we persist in-memory. + * + * @return a promise that resolves to the profile's creation time. + */ + get created() { + function onSuccess(times) { + if (times.created) { + return times.created; + } + return onFailure.call(this, null, times); + } + + function onFailure(err, times) { + return this.computeAndPersistCreated(times) + .then(function onSuccess(created) { + return created; + }.bind(this)); + } + + return this.getTimes() + .then(onSuccess.bind(this), + onFailure.bind(this)); + }, + + /** + * Explicitly make `file`, a filename, a full path + * relative to our profile path. + */ + getPath: function (file) { + return OS.Path.join(this.profilePath, file); + }, + + /** + * Return a promise which resolves to the JSON contents + * of the time file, using the already read value if possible. + */ + getTimes: function (file="times.json") { + if (this._times) { + return Promise.resolve(this._times); + } + return this.readTimes(file).then( + times => { + return this.times = times || {}; + } + ); + }, + + /** + * Return a promise which resolves to the JSON contents + * of the time file in this accessor's profile. + */ + readTimes: function (file="times.json") { + return CommonUtils.readJSON(this.getPath(file)); + }, + + /** + * Return a promise representing the writing of `contents` + * to `file` in the specified profile. + */ + writeTimes: function (contents, file="times.json") { + return CommonUtils.writeJSON(contents, this.getPath(file)); + }, + + /** + * Merge existing contents with a 'created' field, writing them + * to the specified file. Promise, naturally. + */ + computeAndPersistCreated: function (existingContents, file="times.json") { + let path = this.getPath(file); + function onOldest(oldest) { + let contents = existingContents || {}; + contents.created = oldest; + this._times = contents; + return this.writeTimes(contents, path) + .then(function onSuccess() { + return oldest; + }); + } + + return this.getOldestProfileTimestamp() + .then(onOldest.bind(this)); + }, + + /** + * Traverse the contents of the profile directory, finding the oldest file + * and returning its creation timestamp. + */ + getOldestProfileTimestamp: function () { + let self = this; + let oldest = Date.now() + 1000; + let iterator = new OS.File.DirectoryIterator(this.profilePath); + self._log.debug("Iterating over profile " + this.profilePath); + if (!iterator) { + throw new Error("Unable to fetch oldest profile entry: no profile iterator."); + } + + function onEntry(entry) { + function onStatSuccess(info) { + // OS.File doesn't seem to be behaving. See Bug 827148. + // Let's do the best we can. This whole function is defensive. + let date = info.winBirthDate || info.macBirthDate; + if (!date || !date.getTime()) { + // OS.File will only return file creation times of any kind on Mac + // and Windows, where birthTime is defined. + // That means we're unable to function on Linux, so we use mtime + // instead. + self._log.debug("No birth date. Using mtime."); + date = info.lastModificationDate; + } + + if (date) { + let timestamp = date.getTime(); + self._log.debug("Using date: " + entry.path + " = " + date); + if (timestamp < oldest) { + oldest = timestamp; + } + } + } + + function onStatFailure(e) { + // Never mind. + self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e)); + } + + return OS.File.stat(entry.path) + .then(onStatSuccess, onStatFailure); + } + + let promise = iterator.forEach(onEntry); + + function onSuccess() { + iterator.close(); + return oldest; + } + + function onFailure(reason) { + iterator.close(); + throw new Error("Unable to fetch oldest profile entry: " + reason); + } + + return promise.then(onSuccess, onFailure); + }, + + /** + * Record (and persist) when a profile reset happened. We just store a + * single value - the timestamp of the most recent reset - but there is scope + * to keep a list of reset times should our health-reporter successor + * be able to make use of that. + * Returns a promise that is resolved once the file has been written. + */ + recordProfileReset: function (time=Date.now(), file="times.json") { + return this.getTimes(file).then( + times => { + times.reset = time; + return this.writeTimes(times, file); + } + ); + }, + + /* Returns a promise that resolves to the time the profile was reset, + * or undefined if not recorded. + */ + get reset() { + return this.getTimes().then( + times => times.reset + ); + }, +} diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build index 115f5418a01..00f528f2710 100644 --- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -36,6 +36,7 @@ EXTRA_JS_MODULES += [ 'PopupNotifications.jsm', 'Preferences.jsm', 'PrivateBrowsingUtils.jsm', + 'ProfileAge.jsm', 'Promise-backend.js', 'Promise.jsm', 'PromiseUtils.jsm',