summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/update/BackgroundUpdate.sys.mjs')
-rw-r--r--toolkit/mozapps/update/BackgroundUpdate.sys.mjs891
1 files changed, 891 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/BackgroundUpdate.sys.mjs b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
new file mode 100644
index 0000000000..fd88a33dfc
--- /dev/null
+++ b/toolkit/mozapps/update/BackgroundUpdate.sys.mjs
@@ -0,0 +1,891 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { EXIT_CODE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+ TaskScheduler: "resource://gre/modules/TaskScheduler.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: "app.update.background.loglevel",
+ prefix: "BackgroundUpdate",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "localization", () => {
+ return new Localization(
+ ["branding/brand.ftl", "toolkit/updates/backgroundupdate.ftl"],
+ true
+ );
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ idleService: ["@mozilla.org/widget/useridleservice;1", "nsIUserIdleService"],
+ UpdateService: [
+ "@mozilla.org/updates/update-service;1",
+ "nsIApplicationUpdateService",
+ ],
+});
+
+// We may want to change the definition of the task over time. When we do this,
+// we need to remove and re-register the task. We will make sure this happens
+// by storing the installed version number of the task to a pref and comparing
+// that version number to the current version. If they aren't equal, we know
+// that we have to re-register the task.
+const TASK_DEF_CURRENT_VERSION = 3;
+const TASK_INSTALLED_VERSION_PREF =
+ "app.update.background.lastInstalledTaskVersion";
+
+export var BackgroundUpdate = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsINamed",
+ "nsIObserver",
+ "nsITimerCallback",
+ ]),
+ name: "BackgroundUpdate",
+
+ _initialized: false,
+
+ get taskId() {
+ let taskId = "backgroundupdate";
+ if (AppConstants.platform == "win") {
+ // In the future, we might lift this to TaskScheduler Win impl, so that
+ // all tasks associated with this installation look consistent in the
+ // Windows Task Scheduler UI.
+ taskId = `${AppConstants.MOZ_APP_DISPLAYNAME_DO_NOT_USE} Background Update`;
+ }
+ return taskId;
+ },
+
+ /**
+ * Whether this installation has an App and a GRE omnijar.
+ *
+ * Installations without an omnijar are generally developer builds and should not be updated.
+ *
+ * @returns {boolean} - true if this installation has an App and a GRE omnijar.
+ */
+ async _hasOmnijar() {
+ const appOmniJar = lazy.FileUtils.getFile("XCurProcD", [
+ AppConstants.OMNIJAR_NAME,
+ ]);
+ const greOmniJar = lazy.FileUtils.getFile("GreD", [
+ AppConstants.OMNIJAR_NAME,
+ ]);
+
+ let bothExist =
+ (await IOUtils.exists(appOmniJar.path)) &&
+ (await IOUtils.exists(greOmniJar.path));
+
+ return bothExist;
+ },
+
+ _force() {
+ // We want to allow developers and testers to monkey with the system.
+ return Services.prefs.getBoolPref("app.update.background.force", false);
+ },
+
+ /**
+ * Check eligibility criteria determining if this installation should be updated using the
+ * background updater.
+ *
+ * These reasons should not factor in transient reasons, for example if there are currently multiple
+ * Firefox instances running.
+ *
+ * Both the browser proper and the backgroundupdate background task invoke this function, so avoid
+ * using profile specifics here. Profile specifics that the background task specifically sources
+ * from the default profile are available here.
+ *
+ * @returns [string] - descriptions of failed criteria; empty if all criteria were met.
+ */
+ async _reasonsToNotUpdateInstallation() {
+ let SLUG = "_reasonsToNotUpdateInstallation";
+ let reasons = [];
+
+ lazy.log.debug(`${SLUG}: checking app.update.auto`);
+ let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled();
+ if (!updateAuto) {
+ reasons.push(this.REASON.NO_APP_UPDATE_AUTO);
+ }
+
+ lazy.log.debug(`${SLUG}: checking app.update.background.enabled`);
+ let updateBackground = await lazy.UpdateUtils.readUpdateConfigSetting(
+ "app.update.background.enabled"
+ );
+ if (!updateBackground) {
+ reasons.push(this.REASON.NO_APP_UPDATE_BACKGROUND_ENABLED);
+ }
+
+ const bts =
+ "@mozilla.org/backgroundtasks;1" in Cc &&
+ Cc["@mozilla.org/backgroundtasks;1"].getService(Ci.nsIBackgroundTasks);
+
+ lazy.log.debug(`${SLUG}: checking for MOZ_BACKGROUNDTASKS`);
+ if (!AppConstants.MOZ_BACKGROUNDTASKS || !bts) {
+ reasons.push(this.REASON.NO_MOZ_BACKGROUNDTASKS);
+ }
+
+ // The methods exposed by the update service named like `canX` answer the
+ // question "can I do action X RIGHT NOW", where-as we want the variants
+ // named like `canUsuallyX` to answer the question "can I usually do X, now
+ // and in the future".
+ let updateService = Cc["@mozilla.org/updates/update-service;1"].getService(
+ Ci.nsIApplicationUpdateService
+ );
+
+ lazy.log.debug(
+ `${SLUG}: checking that updates are not disabled by policy, testing ` +
+ `configuration, or abnormal runtime environment`
+ );
+ if (!updateService.canUsuallyCheckForUpdates) {
+ reasons.push(this.REASON.CANNOT_USUALLY_CHECK);
+ }
+
+ lazy.log.debug(
+ `${SLUG}: checking that we can make progress: updates can stage and/or apply`
+ );
+ if (
+ !updateService.canUsuallyStageUpdates &&
+ !updateService.canUsuallyApplyUpdates
+ ) {
+ reasons.push(this.REASON.CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY);
+ }
+
+ if (AppConstants.platform == "win") {
+ if (!updateService.canUsuallyUseBits) {
+ // There's no technical reason to require BITS, but the experience
+ // without BITS will be worse because the background tasks will run
+ // while downloading, consuming valuable resources.
+ reasons.push(this.REASON.WINDOWS_CANNOT_USUALLY_USE_BITS);
+ }
+ }
+
+ lazy.log.debug(`${SLUG}: checking that this installation has an omnijar`);
+ if (!(await this._hasOmnijar())) {
+ reasons.push(this.REASON.NO_OMNIJAR);
+ }
+
+ if (updateService.manualUpdateOnly) {
+ reasons.push(this.REASON.MANUAL_UPDATE_ONLY);
+ }
+
+ this._recordGleanMetrics(reasons);
+
+ return reasons;
+ },
+
+ /**
+ * Check if this particular profile should schedule tasks to update this installation using the
+ * background updater.
+ *
+ * Only the browser proper should invoke this function, not background tasks, so this is the place
+ * to use profile specifics.
+ *
+ * @returns [string] - descriptions of failed criteria; empty if all criteria were met.
+ */
+ async _reasonsToNotScheduleUpdates() {
+ let SLUG = "_reasonsToNotScheduleUpdates";
+ let reasons = [];
+
+ const bts =
+ "@mozilla.org/backgroundtasks;1" in Cc &&
+ Cc["@mozilla.org/backgroundtasks;1"].getService(Ci.nsIBackgroundTasks);
+
+ if (bts && bts.isBackgroundTaskMode) {
+ throw new Components.Exception(
+ `Not available in --backgroundtask mode`,
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+
+ // No default profile happens under xpcshell but also when running local
+ // builds. It's unexpected in the wild so we track it separately.
+ if (!lazy.BackgroundTasksUtils.hasDefaultProfile()) {
+ reasons.push(this.REASON.NO_DEFAULT_PROFILE_EXISTS);
+ }
+
+ if (!lazy.BackgroundTasksUtils.currentProfileIsDefaultProfile()) {
+ reasons.push(this.REASON.NOT_DEFAULT_PROFILE);
+ }
+
+ lazy.log.debug(`${SLUG}: checking app.update.langpack.enabled`);
+ let updateLangpack = Services.prefs.getBoolPref(
+ "app.update.langpack.enabled",
+ true
+ );
+ if (updateLangpack) {
+ lazy.log.debug(
+ `${SLUG}: app.update.langpack.enabled=true, checking that no langpacks are installed`
+ );
+
+ let langpacks = await lazy.AddonManager.getAddonsByTypes(["locale"]);
+ lazy.log.debug(`${langpacks.length} langpacks installed`);
+ if (langpacks.length) {
+ reasons.push(this.REASON.LANGPACK_INSTALLED);
+ }
+ }
+
+ if (AppConstants.platform == "win") {
+ let serviceRegKeyExists;
+ try {
+ serviceRegKeyExists = Cc["@mozilla.org/updates/update-processor;1"]
+ .createInstance(Ci.nsIUpdateProcessor)
+ .getServiceRegKeyExists();
+ } catch (ex) {
+ lazy.log.error(
+ `${SLUG}: Failed to check for Maintenance Service Registry Key: ${ex}`
+ );
+ serviceRegKeyExists = false;
+ }
+ if (!serviceRegKeyExists) {
+ reasons.push(this.REASON.SERVICE_REGISTRY_KEY_MISSING);
+ }
+ } else {
+ reasons.push(this.REASON.UNSUPPORTED_OS);
+ }
+
+ this._recordGleanMetrics(reasons);
+
+ return reasons;
+ },
+
+ /**
+ * Register a background update task.
+ *
+ * @param {string} [taskId]
+ * The task identifier; defaults to the platform-specific background update task ID.
+ * @return {object} non-null if the background task was registered.
+ */
+ async _registerBackgroundUpdateTask(taskId = this.taskId) {
+ let binary = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ let args = [
+ "--MOZ_LOG",
+ // Note: `maxsize:1` means 1Mb total size, trimmed to 512kb on overflow.
+ "sync,prependheader,timestamp,append,maxsize:1,Dump:5",
+ "--MOZ_LOG_FILE",
+ // The full path might hit command line length limits, but also makes it
+ // much easier to find the relevant log file when starting from the
+ // Windows Task Scheduler UI.
+ lazy.FileUtils.getFile("UpdRootD", ["backgroundupdate.moz_log"]).path,
+ "--backgroundtask",
+ "backgroundupdate",
+ ];
+
+ let workingDirectory = lazy.FileUtils.getDir("UpdRootD", [], true).path;
+
+ let description = await lazy.localization.formatValue(
+ "backgroundupdate-task-description"
+ );
+
+ // Let the task run for a maximum of 20 minutes before the task scheduler
+ // stops it.
+ let executionTimeoutSec = 20 * 60;
+
+ let result = await lazy.TaskScheduler.registerTask(
+ taskId,
+ binary.path,
+ // Keep this default in sync with the preference in firefox.js.
+ Services.prefs.getIntPref("app.update.background.interval", 60 * 60 * 7),
+ {
+ workingDirectory,
+ args,
+ description,
+ executionTimeoutSec,
+ }
+ );
+
+ Services.prefs.setIntPref(
+ TASK_INSTALLED_VERSION_PREF,
+ TASK_DEF_CURRENT_VERSION
+ );
+
+ return result;
+ },
+
+ /**
+ * Background Update is controlled by the per-installation pref
+ * "app.update.background.enabled". When Background Update was still in the
+ * experimental phase, the default value of this pref may have been changed.
+ * Now that the feature has been rolled out, we need to make sure that the
+ * desired default value is restored.
+ */
+ async ensureExperimentToRolloutTransitionPerformed() {
+ if (!lazy.UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED) {
+ return;
+ }
+ const transitionPerformedPref = "app.update.background.rolledout";
+ if (Services.prefs.getBoolPref(transitionPerformedPref, false)) {
+ // writeUpdateConfigSetting serializes access to the config file. Because
+ // of this, we can safely return here without worrying about another call
+ // to this function that might still be in progress.
+ return;
+ }
+ Services.prefs.setBoolPref(transitionPerformedPref, true);
+
+ const defaultValue =
+ lazy.UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
+ .defaultValue;
+ await lazy.UpdateUtils.writeUpdateConfigSetting(
+ "app.update.background.enabled",
+ defaultValue,
+ { setDefaultOnly: true }
+ );
+
+ // To be thorough, remove any traces of the pref that used to control the
+ // default value that we just set. We don't want any users to have the
+ // impression that that pref is still useful.
+ Services.prefs.clearUserPref("app.update.background.scheduling.enabled");
+ },
+
+ observe(subject, topic, data) {
+ let whatChanged;
+ switch (topic) {
+ case "idle-daily":
+ this._snapshot.saveSoon();
+ return;
+
+ case "user-interaction-active":
+ this._startTargetingSnapshottingTimer();
+ Services.obs.removeObserver(this, "idle-daily");
+ Services.obs.removeObserver(this, "user-interaction-active");
+ lazy.log.debug(
+ `observe: ${topic}; started targeting snapshotting timer`
+ );
+ return;
+
+ case "nsPref:changed":
+ whatChanged = `per-profile pref ${data}`;
+ break;
+
+ case "auto-update-config-change":
+ whatChanged = `per-installation pref app.update.auto`;
+ break;
+
+ case "background-update-config-change":
+ whatChanged = `per-installation pref app.update.background.enabled`;
+ break;
+ }
+
+ lazy.log.debug(
+ `observe: ${whatChanged} may have changed; invoking maybeScheduleBackgroundUpdateTask`
+ );
+ this.maybeScheduleBackgroundUpdateTask();
+ },
+
+ /**
+ * Maybe schedule (or unschedule) background tasks using OS-level task scheduling mechanisms.
+ *
+ * @return {boolean} true if a task is now scheduled, false otherwise.
+ */
+ async maybeScheduleBackgroundUpdateTask() {
+ let SLUG = "maybeScheduleBackgroundUpdateTask";
+
+ await this.ensureExperimentToRolloutTransitionPerformed();
+
+ lazy.log.info(
+ `${SLUG}: checking eligibility before scheduling background update task`
+ );
+
+ // datetime with an empty parameter records 'now'
+ Glean.backgroundUpdate.timeLastUpdateScheduled.set();
+
+ let previousEnabled;
+ let successfullyReadPrevious;
+ try {
+ previousEnabled = await lazy.TaskScheduler.taskExists(this.taskId);
+ successfullyReadPrevious = true;
+ } catch (ex) {
+ successfullyReadPrevious = false;
+ }
+
+ const previousReasons = Services.prefs.getCharPref(
+ "app.update.background.previous.reasons",
+ null
+ );
+
+ if (!this._initialized) {
+ Services.obs.addObserver(this, "auto-update-config-change");
+ Services.obs.addObserver(this, "background-update-config-change");
+
+ // Witness when our own prefs change.
+ Services.prefs.addObserver("app.update.background.force", this);
+ Services.prefs.addObserver("app.update.background.interval", this);
+
+ // Witness when the langpack updating feature is changed.
+ Services.prefs.addObserver("app.update.langpack.enabled", this);
+
+ // Witness when langpacks come and go.
+ const onAddonEvent = async addon => {
+ if (addon.type != "locale") {
+ return;
+ }
+ lazy.log.debug(
+ `${SLUG}: langpacks may have changed; invoking maybeScheduleBackgroundUpdateTask`
+ );
+ // No need to await this promise.
+ this.maybeScheduleBackgroundUpdateTask();
+ };
+ const addonsListener = {
+ onEnabled: onAddonEvent,
+ onDisabled: onAddonEvent,
+ onInstalled: onAddonEvent,
+ onUninstalled: onAddonEvent,
+ };
+ lazy.AddonManager.addAddonListener(addonsListener);
+
+ this._initialized = true;
+ }
+
+ lazy.log.debug(
+ `${SLUG}: checking for reasons to not update this installation`
+ );
+ let reasons = await this._reasonsToNotUpdateInstallation();
+
+ lazy.log.debug(
+ `${SLUG}: checking for reasons to not schedule background updates with this profile`
+ );
+ let moreReasons = await this._reasonsToNotScheduleUpdates();
+ reasons.push(...moreReasons);
+
+ let enabled = !reasons.length;
+
+ if (this._force()) {
+ // We want to allow developers and testers to monkey with the system.
+ lazy.log.debug(
+ `${SLUG}: app.update.background.force=true, ignoring reasons: ${JSON.stringify(
+ reasons
+ )}`
+ );
+ reasons = [];
+ enabled = true;
+ }
+
+ let updatePreviousPrefs = () => {
+ if (reasons.length) {
+ Services.prefs.setCharPref(
+ "app.update.background.previous.reasons",
+ JSON.stringify(reasons)
+ );
+ } else {
+ Services.prefs.clearUserPref("app.update.background.previous.reasons");
+ }
+ };
+
+ try {
+ // Interacting with `TaskScheduler.jsm` can throw, so we'll catch.
+ if (!enabled) {
+ lazy.log.info(
+ `${SLUG}: not scheduling background update: '${JSON.stringify(
+ reasons
+ )}'`
+ );
+
+ if (!successfullyReadPrevious || previousEnabled) {
+ await lazy.TaskScheduler.deleteTask(this.taskId);
+ lazy.log.debug(
+ `${SLUG}: witnessed falling (enabled -> disabled) edge; deleted task ${this.taskId}.`
+ );
+ }
+
+ updatePreviousPrefs();
+
+ return false;
+ }
+
+ if (successfullyReadPrevious && previousEnabled) {
+ let taskInstalledVersion = Services.prefs.getIntPref(
+ TASK_INSTALLED_VERSION_PREF,
+ 1
+ );
+ if (taskInstalledVersion == TASK_DEF_CURRENT_VERSION) {
+ lazy.log.info(
+ `${SLUG}: background update was previously enabled; not registering task.`
+ );
+
+ return true;
+ }
+ lazy.log.info(
+ `${SLUG}: Detected task version change from ` +
+ `${taskInstalledVersion} to ${TASK_DEF_CURRENT_VERSION}. ` +
+ `Removing task so the new version can be registered`
+ );
+ try {
+ await lazy.TaskScheduler.deleteTask(this.taskId);
+ } catch (e) {
+ lazy.log.error(`${SLUG}: Error removing old task: ${e}`);
+ }
+ try {
+ // When the update directory was moved, we migrated the old contents
+ // to the new location. This can potentially happen in a background
+ // task. However, we also need to re-register the background task
+ // with the task scheduler in order to update the MOZ_LOG_FILE value
+ // to point to the new location. If the task runs before Firefox has
+ // a chance to re-register the task, the log file may be recreated in
+ // the old location. In practice, this would be unusual, because
+ // MOZ_LOG_FILE will not create the parent directories necessary to
+ // put a log file in the specified location. But just to be safe,
+ // we'll do some cleanup when we re-register the task to make sure
+ // that no log file is hanging around in the old location.
+ let oldUpdateDir = lazy.FileUtils.getDir("OldUpdRootD", [], false);
+ let oldLog = oldUpdateDir.clone();
+ oldLog.append("backgroundupdate.moz_log");
+
+ if (oldLog.exists()) {
+ oldLog.remove(false);
+ // We may have created some directories in order to put this log
+ // file in this location. Clean them up if they are empty.
+ // (If we pass false for the recurse parameter, directories with
+ // contents will not be removed)
+ //
+ // Potentially removes "C:\ProgramData\Mozilla\updates\<hash>"
+ oldUpdateDir.remove(false);
+ // Potentially removes "C:\ProgramData\Mozilla\updates"
+ oldUpdateDir.parent.remove(false);
+ // Potentially removes "C:\ProgramData\Mozilla"
+ oldUpdateDir.parent.parent.remove(false);
+ }
+ } catch (ex) {
+ lazy.log.warn(
+ `${SLUG}: Ignoring error encountered attempting to remove stale log file: ${ex}`
+ );
+ }
+ }
+
+ lazy.log.info(
+ `${SLUG}: background update was previously disabled for reasons: '${previousReasons}'`
+ );
+
+ await this._registerBackgroundUpdateTask(this.taskId);
+ lazy.log.info(
+ `${SLUG}: witnessed rising (disabled -> enabled) edge; registered task ${this.taskId}`
+ );
+
+ updatePreviousPrefs();
+
+ return true;
+ } catch (e) {
+ lazy.log.error(
+ `${SLUG}: exiting after uncaught exception in maybeScheduleBackgroundUpdateTask!`,
+ e
+ );
+
+ return false;
+ }
+ },
+
+ /**
+ * Record parts of the update environment for our custom Glean ping.
+ *
+ * This is just like the Telemetry Environment, but pared down to what we're
+ * likely to use in background update-specific analyses.
+ *
+ * Right now this is only for use in the background update task, but after Bug
+ * 1703313 (migrating AUS telemetry to be Glean-aware) we might use it more
+ * generally.
+ */
+ async recordUpdateEnvironment() {
+ try {
+ Glean.update.serviceEnabled.set(
+ Services.prefs.getBoolPref("app.update.service.enabled", false)
+ );
+ } catch (e) {
+ // It's fine if some or all of these are missing.
+ }
+
+ // In the background update task, this should always be enabled, but let's
+ // find out if there's an error in the system.
+ Glean.update.autoDownload.set(
+ await lazy.UpdateUtils.getAppUpdateAutoEnabled()
+ );
+ Glean.update.backgroundUpdate.set(
+ await lazy.UpdateUtils.readUpdateConfigSetting(
+ "app.update.background.enabled"
+ )
+ );
+
+ Glean.update.channel.set(lazy.UpdateUtils.UpdateChannel);
+ Glean.update.enabled.set(
+ !Services.policies || Services.policies.isAllowed("appUpdate")
+ );
+
+ Glean.update.canUsuallyApplyUpdates.set(
+ lazy.UpdateService.canUsuallyApplyUpdates
+ );
+ Glean.update.canUsuallyCheckForUpdates.set(
+ lazy.UpdateService.canUsuallyCheckForUpdates
+ );
+ Glean.update.canUsuallyStageUpdates.set(
+ lazy.UpdateService.canUsuallyStageUpdates
+ );
+ Glean.update.canUsuallyUseBits.set(lazy.UpdateService.canUsuallyUseBits);
+ },
+
+ /**
+ * Schedule periodic snapshotting of the Firefox Messaging System
+ * targeting configuration.
+ *
+ * The background update task will target messages based on the
+ * latest snapshot of the default profile's targeting configuration.
+ */
+ async scheduleFirefoxMessagingSystemTargetingSnapshotting() {
+ let SLUG = "scheduleFirefoxMessagingSystemTargetingSnapshotting";
+ let path = PathUtils.join(PathUtils.profileDir, "targeting.snapshot.json");
+
+ let snapshot = new lazy.JSONFile({
+ beforeSave: async () => {
+ if (Services.startup.shuttingDown) {
+ // Collecting targeting information can be slow and cause shutdown
+ // crashes. Just write what we have in that case. During shutdown,
+ // the regular log apparatus is not available, so use `dump`.
+ if (lazy.log.shouldLog("debug")) {
+ dump(
+ `${SLUG}: shutting down, so not updating Firefox Messaging System targeting information from beforeSave\n`
+ );
+ }
+ return;
+ }
+
+ lazy.log.debug(
+ `${SLUG}: preparing to write Firefox Messaging System targeting information to ${path}`
+ );
+
+ // Merge latest data into existing data. This data may be partial, due
+ // to runtime errors and abbreviated collections, especially when
+ // shutting down. We accept the risk of incomplete or even internally
+ // inconsistent data: it's generally better to have stale data (and
+ // potentially target a user as they appeared in the past) than to block
+ // shutdown for more accurate results. An alternate approach would be
+ // to restrict the targeting data collected, but it's hard to
+ // distinguish expensive collection operations and the system loses
+ // flexibility when restrictions of this type are added.
+ let latestData = await lazy.ASRouterTargeting.getEnvironmentSnapshot();
+ // We expect to always have data, but: belt-and-braces.
+ if (snapshot?.data?.environment) {
+ Object.assign(snapshot.data.environment, latestData.environment);
+ } else {
+ snapshot.data = latestData;
+ }
+ },
+ path,
+ });
+
+ // We don't `load`, since we don't care about reading existing (now stale)
+ // data.
+ snapshot.data = await lazy.ASRouterTargeting.getEnvironmentSnapshot();
+
+ // Persist.
+ snapshot.saveSoon();
+
+ this._snapshot = snapshot;
+
+ // Continue persisting periodically. `JSONFile.sys.mjs` will also persist one
+ // last time before shutdown.
+ // Hold a reference to prevent GC.
+ this._targetingSnapshottingTimer = Cc[
+ "@mozilla.org/timer;1"
+ ].createInstance(Ci.nsITimer);
+ // By default, snapshot Firefox Messaging System targeting for use by the
+ // background update task every 60 minutes.
+ this._targetingSnapshottingTimerIntervalSec = Services.prefs.getIntPref(
+ "app.update.background.messaging.targeting.snapshot.intervalSec",
+ 3600
+ );
+ this._startTargetingSnapshottingTimer();
+ },
+
+ // nsITimerCallback
+ notify() {
+ const SLUG = "_targetingSnapshottingTimerCallback";
+
+ if (Services.startup.shuttingDown) {
+ // Collecting targeting information can be slow and cause shutdown
+ // crashes, so if we're shutting down, don't try to collect. During
+ // shutdown, the regular log apparatus is not available, so use `dump`.
+ if (lazy.log.shouldLog("debug")) {
+ dump(
+ `${SLUG}: shutting down, so not updating Firefox Messaging System targeting information from timer\n`
+ );
+ }
+ return;
+ }
+
+ this._snapshot.saveSoon();
+
+ if (
+ lazy.idleService.idleTime >
+ this._targetingSnapshottingTimerIntervalSec * 1000
+ ) {
+ lazy.log.debug(
+ `${SLUG}: idle time longer than interval, adding observers`
+ );
+ Services.obs.addObserver(this, "idle-daily");
+ Services.obs.addObserver(this, "user-interaction-active");
+ } else {
+ lazy.log.debug(`${SLUG}: restarting timer`);
+ this._startTargetingSnapshottingTimer();
+ }
+ },
+
+ _startTargetingSnapshottingTimer() {
+ this._targetingSnapshottingTimer.initWithCallback(
+ this,
+ this._targetingSnapshottingTimerIntervalSec * 1000,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Reads the snapshotted Firefox Messaging System targeting out of a profile.
+ * Collects background update specific telemetry. Never throws.
+ *
+ * If no `lock` is given, the default profile is locked and the preferences
+ * read from it. If `lock` is given, read from the given lock's directory.
+ *
+ * @param {nsIProfileLock} [lock] optional lock to use
+ * @returns {object} possibly empty targeting snapshot.
+ */
+ async readFirefoxMessagingSystemTargetingSnapshot(lock = null) {
+ let SLUG = "readFirefoxMessagingSystemTargetingSnapshot";
+
+ let defaultProfileTargetingSnapshot = {};
+
+ Glean.backgroundUpdate.targetingExists.set(false);
+ Glean.backgroundUpdate.targetingException.set(true);
+ try {
+ defaultProfileTargetingSnapshot =
+ await lazy.BackgroundTasksUtils.readFirefoxMessagingSystemTargetingSnapshot(
+ lock
+ );
+ Glean.backgroundUpdate.targetingExists.set(true);
+ Glean.backgroundUpdate.targetingException.set(false);
+
+ if (defaultProfileTargetingSnapshot?.version) {
+ Glean.backgroundUpdate.targetingVersion.set(
+ defaultProfileTargetingSnapshot.version
+ );
+ }
+ if (defaultProfileTargetingSnapshot?.environment?.firefoxVersion) {
+ Glean.backgroundUpdate.targetingEnvFirefoxVersion.set(
+ defaultProfileTargetingSnapshot.environment.firefoxVersion
+ );
+ }
+ if (defaultProfileTargetingSnapshot?.environment?.currentDate) {
+ Glean.backgroundUpdate.targetingEnvCurrentDate.set(
+ // Glean date times are provided in nanoseconds, `getTime()` yields
+ // milliseconds (after the Unix epoch).
+ new Date(
+ defaultProfileTargetingSnapshot.environment.currentDate
+ ).getTime() * 1000
+ );
+ }
+ if (defaultProfileTargetingSnapshot?.environment?.profileAgeCreated) {
+ Glean.backgroundUpdate.targetingEnvProfileAge.set(
+ // Glean date times are provided in nanoseconds, `profileAgeCreated`
+ // is in milliseconds (after the Unix epoch).
+ defaultProfileTargetingSnapshot.environment.profileAgeCreated * 1000
+ );
+ }
+ } catch (f) {
+ if (DOMException.isInstance(f) && f.name === "NotFoundError") {
+ Glean.backgroundUpdate.targetingException.set(false);
+ lazy.log.info(`${SLUG}: no default profile targeting snapshot exists`);
+ } else {
+ lazy.log.warn(
+ `${SLUG}: ignoring exception reading default profile targeting snapshot`,
+ f
+ );
+ }
+ }
+
+ return defaultProfileTargetingSnapshot;
+ },
+
+ /**
+ * Local helper function to record all reasons why the background updater is
+ * not used with Glean. This function will only track the first 20 reasons.
+ * It is also fault tolerant and will only display debug messages if the
+ * metric cannot be recorded for any reason.
+ *
+ * @param {array of strings} [reasons]
+ * a list of BackgroundUpdate.REASON values (=> string)
+ */
+ async _recordGleanMetrics(reasons) {
+ // Record Glean metrics with all the reasons why the update was impossible.
+ for (const [key, value] of Object.entries(this.REASON)) {
+ if (reasons.includes(value)) {
+ try {
+ // `testGetValue` throws `NS_ERROR_LOSS_OF_SIGNIFICANT_DATA` in case
+ // of `InvalidOverflow` and other outstanding errors.
+ Glean.backgroundUpdate.reasonsToNotUpdate.testGetValue();
+ Glean.backgroundUpdate.reasonsToNotUpdate.add(key);
+ } catch (e) {
+ // Debug print an error message and break the loop to avoid Glean
+ // messages on the console would otherwise be caused by the add().
+ lazy.log.debug("Error recording reasonsToNotUpdate");
+ console.log("Error recording reasonsToNotUpdate");
+ break;
+ }
+ }
+ }
+ },
+};
+
+BackgroundUpdate.REASON = {
+ CANNOT_USUALLY_CHECK:
+ "cannot usually check for updates due to policy, testing configuration, or runtime environment",
+ CANNOT_USUALLY_STAGE_AND_CANNOT_USUALLY_APPLY:
+ "updates cannot usually stage and cannot usually apply",
+ LANGPACK_INSTALLED:
+ "app.update.langpack.enabled=true and at least one langpack is installed",
+ MANUAL_UPDATE_ONLY: "the ManualAppUpdateOnly policy is enabled",
+ NO_DEFAULT_PROFILE_EXISTS: "no default profile exists",
+ NOT_DEFAULT_PROFILE: "not default profile",
+ NO_APP_UPDATE_AUTO: "app.update.auto=false",
+ NO_APP_UPDATE_BACKGROUND_ENABLED: "app.update.background.enabled=false",
+ NO_MOZ_BACKGROUNDTASKS: "MOZ_BACKGROUNDTASKS=0",
+ NO_OMNIJAR: "no omnijar",
+ SERVICE_REGISTRY_KEY_MISSING:
+ "the maintenance service registry key is not present",
+ UNSUPPORTED_OS: "unsupported OS",
+ WINDOWS_CANNOT_USUALLY_USE_BITS: "on Windows but cannot usually use BITS",
+};
+
+/**
+ * Specific exit codes for `--backgroundtask backgroundupdate`.
+ *
+ * These help distinguish common failure cases. In particular, they distinguish
+ * "default profile does not exist" from "default profile cannot be locked" from
+ * more general errors reading from the default profile.
+ */
+BackgroundUpdate.EXIT_CODE = {
+ ...EXIT_CODE,
+ // We clone the other exit codes simply so we can use one object for all the codes.
+ DEFAULT_PROFILE_DOES_NOT_EXIST: 11,
+ DEFAULT_PROFILE_CANNOT_BE_LOCKED: 12,
+ DEFAULT_PROFILE_CANNOT_BE_READ: 13,
+ // Another instance is running.
+ OTHER_INSTANCE: 21,
+};