diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs b/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs new file mode 100644 index 0000000000..d5cf96cbaa --- /dev/null +++ b/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs @@ -0,0 +1,470 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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 { BackgroundUpdate } from "resource://gre/modules/BackgroundUpdate.sys.mjs"; +import { DevToolsSocketStatus } from "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs"; + +const { EXIT_CODE } = BackgroundUpdate; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AppUpdater: "resource://gre/modules/AppUpdater.sys.mjs", + BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs", + ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "UpdateService", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService" +); + +ChromeUtils.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); +}); + +export const backgroundTaskTimeoutSec = Services.prefs.getIntPref( + "app.update.background.timeoutSec", + 10 * 60 +); + +/** + * Verify that pre-conditions to update this installation (both persistent and + * transient) are fulfilled, and if they are all fulfilled, pump the update + * loop. + * + * This means checking for, downloading, and potentially applying updates. + * + * @returns {any} - Returns AppUpdater status upon update loop exit. + */ +async function _attemptBackgroundUpdate() { + let SLUG = "_attemptBackgroundUpdate"; + + // Here's where we do `post-update-processing`. Creating the stub invokes the + // `UpdateServiceStub()` constructor, which handles various migrations (which should not be + // necessary, but we want to run for consistency and any migrations added in the future) and then + // dispatches `post-update-processing` (if appropriate). We want to do this very early, so that + // the real update service is in its fully initialized state before any usage. + lazy.log.debug( + `${SLUG}: creating UpdateServiceStub() for "post-update-processing"` + ); + Cc["@mozilla.org/updates/update-service-stub;1"].createInstance( + Ci.nsISupports + ); + + lazy.log.debug( + `${SLUG}: checking for preconditions necessary to update this installation` + ); + let reasons = await BackgroundUpdate._reasonsToNotUpdateInstallation(); + + if (BackgroundUpdate._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 = []; + } + + reasons.sort(); + for (let reason of reasons) { + Glean.backgroundUpdate.reasons.add(reason); + } + + let enabled = !reasons.length; + if (!enabled) { + lazy.log.info( + `${SLUG}: not running background update task: '${JSON.stringify( + reasons + )}'` + ); + + return lazy.AppUpdater.STATUS.NEVER_CHECKED; + } + + let result = new Promise(resolve => { + let appUpdater = new lazy.AppUpdater(); + + let _appUpdaterListener = (status, progress, progressMax) => { + let stringStatus = lazy.AppUpdater.STATUS.debugStringFor(status); + Glean.backgroundUpdate.states.add(stringStatus); + Glean.backgroundUpdate.finalState.set(stringStatus); + + if (lazy.AppUpdater.STATUS.isTerminalStatus(status)) { + lazy.log.debug( + `${SLUG}: background update transitioned to terminal status ${status}: ${stringStatus}` + ); + appUpdater.removeListener(_appUpdaterListener); + appUpdater.stop(); + resolve(status); + } else if (status == lazy.AppUpdater.STATUS.CHECKING) { + // The usual initial flow for the Background Update Task is to kick off + // the update download and immediately exit. For consistency, we are + // going to enforce this flow. So if we are just now checking for + // updates, we will limit the updater such that it cannot start staging, + // even if we immediately download the entire update. + lazy.log.debug( + `${SLUG}: This session will be limited to downloading updates only.` + ); + lazy.UpdateService.onlyDownloadUpdatesThisSession = true; + } else if ( + status == lazy.AppUpdater.STATUS.DOWNLOADING && + (lazy.UpdateService.onlyDownloadUpdatesThisSession || + (progress !== undefined && progressMax !== undefined)) + ) { + // We get a DOWNLOADING callback with no progress or progressMax values + // when we initially switch to the DOWNLOADING state. But when we get + // onProgress notifications, progress and progressMax will be defined. + // Remember to keep in mind that progressMax is a required value that + // we can count on being meaningful, but it will be set to -1 for BITS + // transfers that haven't begun yet. + if ( + lazy.UpdateService.onlyDownloadUpdatesThisSession || + progressMax < 0 || + progress != progressMax + ) { + lazy.log.debug( + `${SLUG}: Download in progress. Exiting task while download ` + + `transfers` + ); + // If the download is still in progress, we don't want the Background + // Update Task to hang around waiting for it to complete. + lazy.UpdateService.onlyDownloadUpdatesThisSession = true; + + appUpdater.removeListener(_appUpdaterListener); + appUpdater.stop(); + resolve(status); + } else { + lazy.log.debug(`${SLUG}: Download has completed!`); + } + } else { + lazy.log.debug( + `${SLUG}: background update transitioned to status ${status}: ${stringStatus}` + ); + } + }; + appUpdater.addListener(_appUpdaterListener); + + appUpdater.check(); + }); + + return result; +} + +/** + * Maybe submit a "background-update" custom Glean ping. + * + * If data reporting upload in general is enabled Glean will submit a ping. To determine if + * telemetry is enabled, Glean will look at the relevant pref, which was mirrored from the default + * profile. Note that the Firefox policy mechanism will manage this pref, locking it to particular + * values as appropriate. + */ +export async function maybeSubmitBackgroundUpdatePing() { + let SLUG = "maybeSubmitBackgroundUpdatePing"; + + // It should be possible to turn AUSTLMY data into Glean data, but mapping histograms isn't + // trivial, so we don't do it at this time. Bug 1703313. + + // Including a reason allows to differentiate pings sent as part of the task + // and pings queued and sent by Glean on a different schedule. + GleanPings.backgroundUpdate.submit("backgroundupdate_task"); + + lazy.log.info(`${SLUG}: submitted "background-update" ping`); +} + +export async function runBackgroundTask(commandLine) { + let SLUG = "runBackgroundTask"; + lazy.log.error(`${SLUG}: backgroundupdate`); + let automaticRestartFound = + -1 != commandLine.findFlag("automatic-restart", false); + + // Modify Glean metrics for a successful automatic restart. + if (automaticRestartFound) { + Glean.backgroundUpdate.automaticRestartSuccess.set(true); + lazy.log.debug(`${SLUG}: application automatic restart completed`); + } + + // Help debugging. This is a pared down version of + // `dataProviders.application` in `Troubleshoot.sys.mjs`. When adding to this + // debugging data, try to follow the form from that module. + let data = { + name: Services.appinfo.name, + osVersion: + Services.sysinfo.getProperty("name") + + " " + + Services.sysinfo.getProperty("version") + + " " + + Services.sysinfo.getProperty("build"), + version: AppConstants.MOZ_APP_VERSION_DISPLAY, + buildID: Services.appinfo.appBuildID, + distributionID: Services.prefs + .getDefaultBranch("") + .getCharPref("distribution.id", ""), + updateChannel: lazy.UpdateUtils.UpdateChannel, + UpdRootD: Services.dirsvc.get("UpdRootD", Ci.nsIFile).path, + }; + lazy.log.debug(`${SLUG}: current configuration`, data); + + // Other instances running are a transient precondition (during this invocation). We'd prefer to + // check this later, as a reason for not updating, but Glean is not tested in multi-process + // environments and while its storage (backed by rkv) can in theory support multiple processes, it + // is not clear that it in fact does support multiple processes. So we are conservative here. + // There is a potential time-of-check/time-of-use race condition here, but if process B starts + // after we pass this test, that process should exit after it gets to this check, avoiding + // multiple processes using the same Glean storage. If and when more and longer-running + // background tasks become common, we may need to be more fine-grained and share just the Glean + // storage resource. + lazy.log.debug(`${SLUG}: checking if other instance is running`); + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + if (DevToolsSocketStatus.hasSocketOpened()) { + lazy.log.warn( + `${SLUG}: Ignoring the 'multiple instances' check because a DevTools server is listening.` + ); + } else if (syncManager.isOtherInstanceRunning()) { + lazy.log.error(`${SLUG}: another instance is running`); + return EXIT_CODE.OTHER_INSTANCE; + } + + // Here we mirror specific prefs from the default profile into our temporary profile. We want to + // do this early because some of the prefs may impact internals such as log levels. Generally, + // however, we want prefs from the default profile to not impact the mechanics of checking for, + // downloading, and applying updates, since such prefs should be be per-installation prefs, using + // the mechanisms of Bug 1691486. Sadly using this mechanism for many relevant prefs (namely + // `app.update.BITS.enabled` and `app.update.service.enabled`) is difficult: see Bug 1657533. + // + // We also read any Nimbus targeting snapshot from the default profile. + let defaultProfileTargetingSnapshot = {}; + try { + let defaultProfilePrefs; + await lazy.BackgroundTasksUtils.withProfileLock(async lock => { + let predicate = name => { + return ( + name.startsWith("app.update.") || // For obvious reasons. + name.startsWith("datareporting.") || // For Glean. + name.startsWith("logging.") || // For Glean. + name.startsWith("telemetry.fog.") || // For Glean. + name.startsWith("app.partner.") || // For our metrics. + name === "app.shield.optoutstudies.enabled" || // For Nimbus. + name === "services.settings.server" || // For Remote Settings via Nimbus. + name === "services.settings.preview_enabled" || // For Remote Settings via Nimbus. + name === "messaging-system.rsexperimentloader.collection_id" // For Firefox Messaging System. + ); + }; + + defaultProfilePrefs = await lazy.BackgroundTasksUtils.readPreferences( + predicate, + lock + ); + let telemetryClientID = + await lazy.BackgroundTasksUtils.readTelemetryClientID(lock); + Glean.backgroundUpdate.clientId.set(telemetryClientID); + + // Read targeting snapshot, collect background update specific telemetry. Never throws. + defaultProfileTargetingSnapshot = + await BackgroundUpdate.readFirefoxMessagingSystemTargetingSnapshot( + lock + ); + }); + + for (let [name, value] of Object.entries(defaultProfilePrefs)) { + switch (typeof value) { + case "boolean": + Services.prefs.setBoolPref(name, value); + break; + case "number": + Services.prefs.setIntPref(name, value); + break; + case "string": + Services.prefs.setCharPref(name, value); + break; + default: + throw new Error( + `Pref from default profile with name "${name}" has unrecognized type` + ); + } + } + } catch (e) { + if (!lazy.BackgroundTasksUtils.hasDefaultProfile()) { + lazy.log.error(`${SLUG}: caught exception; no default profile exists`, e); + return EXIT_CODE.DEFAULT_PROFILE_DOES_NOT_EXIST; + } + + if (e.name == "CannotLockProfileError") { + lazy.log.error( + `${SLUG}: caught exception; could not lock default profile`, + e + ); + return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_LOCKED; + } + + lazy.log.error( + `${SLUG}: caught exception reading preferences and telemetry client ID from default profile`, + e + ); + return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_READ; + } + + // Now that we have prefs from the default profile, we can configure Firefox-on-Glean. + + // Glean has a preinit queue for metric operations that happen before init, so + // this is safe. We want to have these metrics set before the first possible + // time we might send (built-in) pings. + await BackgroundUpdate.recordUpdateEnvironment(); + + // To help debugging, use the `GLEAN_LOG_PINGS` and `GLEAN_DEBUG_VIEW_TAG` + // environment variables: see + // https://mozilla.github.io/glean/book/user/debugging/index.html. + let gleanRoot = await IOUtils.getDirectory( + Services.dirsvc.get("UpdRootD", Ci.nsIFile).path, + "backgroundupdate", + "datareporting", + "glean" + ); + Services.fog.initializeFOG( + gleanRoot.path, + "firefox.desktop.background.update" + ); + + // For convenience, mirror our loglevel. + let logLevel = Services.prefs.getCharPref( + "app.update.background.loglevel", + "error" + ); + const logLevelPrefs = [ + "browser.newtabpage.activity-stream.asrouter.debugLogLevel", + "messaging-system.log", + "services.settings.loglevel", + "toolkit.backgroundtasks.loglevel", + ]; + for (let logLevelPref of logLevelPrefs) { + lazy.log.info(`${SLUG}: setting ${logLevelPref}=${logLevel}`); + Services.prefs.setCharPref(logLevelPref, logLevel); + } + + // The langpack updating mechanism expects the addons manager, but in background task mode, the + // addons manager is not present. Since we can't update langpacks from the background task + // temporary profile, we disable the langpack updating mechanism entirely. This relies on the + // default profile being the only profile that schedules the OS-level background task and ensuring + // the task is not scheduled when langpacks are present. Non-default profiles that have langpacks + // installed may experience the issues that motivated Bug 1647443. If this turns out to be a + // significant problem in the wild, we could store more information about profiles and their + // active langpacks to disable background updates in more cases, maybe in per-installation prefs. + Services.prefs.setBoolPref("app.update.langpack.enabled", false); + + let result = EXIT_CODE.SUCCESS; + + let stringStatus = lazy.AppUpdater.STATUS.debugStringFor( + lazy.AppUpdater.STATUS.NEVER_CHECKED + ); + Glean.backgroundUpdate.states.add(stringStatus); + Glean.backgroundUpdate.finalState.set(stringStatus); + + let updateStatus = lazy.AppUpdater.STATUS.NEVER_CHECKED; + try { + // Return AppUpdater status from _attemptBackgroundUpdate() to + // check if the status is STATUS.READY_FOR_RESTART. + updateStatus = await _attemptBackgroundUpdate(); + + lazy.log.info(`${SLUG}: attempted background update`); + Glean.backgroundUpdate.exitCodeSuccess.set(true); + + try { + // Now that we've pumped the update loop, we can start Nimbus and the Firefox Messaging System + // and see if we should message the user. This minimizes the risk of messaging impacting the + // function of the background update system. + await lazy.BackgroundTasksUtils.enableNimbus( + commandLine, + defaultProfileTargetingSnapshot.environment + ); + + await lazy.BackgroundTasksUtils.enableFirefoxMessagingSystem( + defaultProfileTargetingSnapshot.environment + ); + } catch (f) { + // Try to make it easy to witness errors in this system. We can pass through any exception + // without disrupting (future) background updates. + // + // Most meaningful issues with the Nimbus/experiments system will be reported via Glean + // events. + lazy.log.warn( + `${SLUG}: exception raised from Nimbus/Firefox Messaging System`, + f + ); + throw f; + } + } catch (e) { + // TODO: in the future, we might want to classify failures into transient and persistent and + // backoff the update task in the face of continuous persistent errors. + lazy.log.error(`${SLUG}: caught exception attempting background update`, e); + + result = EXIT_CODE.EXCEPTION; + Glean.backgroundUpdate.exitCodeException.set(true); + } finally { + // This is the point to report telemetry, assuming that the default profile's data reporting + // configuration allows it. + await maybeSubmitBackgroundUpdatePing(); + } + + // TODO: ensure the update service has persisted its state before we exit. Bug 1700846. + // TODO: ensure that Glean's upload mechanism is aware of Gecko shutdown. Bug 1703572. + await lazy.ExtensionUtils.promiseTimeout(500); + + // If we're in a staged background update, we need to restart Firefox to complete the update. + lazy.log.debug( + `${SLUG}: Checking if staged background update is ready for restart` + ); + // If a restart loop is occurring then automaticRestartFound will be true. + if ( + lazy.NimbusFeatures.backgroundUpdateAutomaticRestart.getVariable( + "enabled" + ) && + updateStatus === lazy.AppUpdater.STATUS.READY_FOR_RESTART && + !automaticRestartFound + ) { + lazy.log.debug( + `${SLUG}: Starting Firefox restart after staged background update` + ); + + // We need to restart Firefox with the same arguments to ensure + // the background update continues from where it was before the restart. + try { + Cc["@mozilla.org/updates/update-processor;1"] + .createInstance(Ci.nsIUpdateProcessor) + .attemptAutomaticApplicationRestartWithLaunchArgs([ + "-automatic-restart", + ]); + // Report an attempted automatic restart. + Glean.backgroundUpdate.automaticRestartAttempted.set(true); + lazy.log.debug(`${SLUG}: automatic application restart queued`); + } catch (e) { + lazy.log.error( + `${SLUG}: caught exception; failed to queue automatic application restart`, + e + ); + } + } + + return result; +} |