Bug 1874684 - Part 17: Fix uninitialised variable warnings from clang-tidy. r=allstarschh
[gecko.git] / toolkit / mozapps / update / BackgroundTask_backgroundupdate.sys.mjs
blobe892c762d2fec8e4dde6c458ec220bc0d69ef270
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { BackgroundUpdate } from "resource://gre/modules/BackgroundUpdate.sys.mjs";
7 import { DevToolsSocketStatus } from "resource://devtools/shared/security/DevToolsSocketStatus.sys.mjs";
9 const { EXIT_CODE } = BackgroundUpdate;
11 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
12 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
14 const lazy = {};
16 ChromeUtils.defineESModuleGetters(lazy, {
17   AppUpdater: "resource://gre/modules/AppUpdater.sys.mjs",
18   BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
19   ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
20   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
21   UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
22 });
24 XPCOMUtils.defineLazyServiceGetter(
25   lazy,
26   "UpdateService",
27   "@mozilla.org/updates/update-service;1",
28   "nsIApplicationUpdateService"
31 ChromeUtils.defineLazyGetter(lazy, "log", () => {
32   let { ConsoleAPI } = ChromeUtils.importESModule(
33     "resource://gre/modules/Console.sys.mjs"
34   );
35   let consoleOptions = {
36     // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
37     // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
38     maxLogLevel: "error",
39     maxLogLevelPref: "app.update.background.loglevel",
40     prefix: "BackgroundUpdate",
41   };
42   return new ConsoleAPI(consoleOptions);
43 });
45 export const backgroundTaskTimeoutSec = Services.prefs.getIntPref(
46   "app.update.background.timeoutSec",
47   10 * 60
50 /**
51  * Verify that pre-conditions to update this installation (both persistent and
52  * transient) are fulfilled, and if they are all fulfilled, pump the update
53  * loop.
54  *
55  * This means checking for, downloading, and potentially applying updates.
56  *
57  * @returns {any} - Returns AppUpdater status upon update loop exit.
58  */
59 async function _attemptBackgroundUpdate() {
60   let SLUG = "_attemptBackgroundUpdate";
62   // Here's where we do `post-update-processing`.  Creating the stub invokes the
63   // `UpdateServiceStub()` constructor, which handles various migrations (which should not be
64   // necessary, but we want to run for consistency and any migrations added in the future) and then
65   // dispatches `post-update-processing` (if appropriate).  We want to do this very early, so that
66   // the real update service is in its fully initialized state before any usage.
67   lazy.log.debug(
68     `${SLUG}: creating UpdateServiceStub() for "post-update-processing"`
69   );
70   Cc["@mozilla.org/updates/update-service-stub;1"].createInstance(
71     Ci.nsISupports
72   );
74   lazy.log.debug(
75     `${SLUG}: checking for preconditions necessary to update this installation`
76   );
77   let reasons = await BackgroundUpdate._reasonsToNotUpdateInstallation();
79   if (BackgroundUpdate._force()) {
80     // We want to allow developers and testers to monkey with the system.
81     lazy.log.debug(
82       `${SLUG}: app.update.background.force=true, ignoring reasons: ${JSON.stringify(
83         reasons
84       )}`
85     );
86     reasons = [];
87   }
89   reasons.sort();
90   for (let reason of reasons) {
91     Glean.backgroundUpdate.reasons.add(reason);
92   }
94   let enabled = !reasons.length;
95   if (!enabled) {
96     lazy.log.info(
97       `${SLUG}: not running background update task: '${JSON.stringify(
98         reasons
99       )}'`
100     );
102     return lazy.AppUpdater.STATUS.NEVER_CHECKED;
103   }
105   let result = new Promise(resolve => {
106     let appUpdater = new lazy.AppUpdater();
108     let _appUpdaterListener = (status, progress, progressMax) => {
109       let stringStatus = lazy.AppUpdater.STATUS.debugStringFor(status);
110       Glean.backgroundUpdate.states.add(stringStatus);
111       Glean.backgroundUpdate.finalState.set(stringStatus);
113       if (lazy.AppUpdater.STATUS.isTerminalStatus(status)) {
114         lazy.log.debug(
115           `${SLUG}: background update transitioned to terminal status ${status}: ${stringStatus}`
116         );
117         appUpdater.removeListener(_appUpdaterListener);
118         appUpdater.stop();
119         resolve(status);
120       } else if (status == lazy.AppUpdater.STATUS.CHECKING) {
121         // The usual initial flow for the Background Update Task is to kick off
122         // the update download and immediately exit. For consistency, we are
123         // going to enforce this flow. So if we are just now checking for
124         // updates, we will limit the updater such that it cannot start staging,
125         // even if we immediately download the entire update.
126         lazy.log.debug(
127           `${SLUG}: This session will be limited to downloading updates only.`
128         );
129         lazy.UpdateService.onlyDownloadUpdatesThisSession = true;
130       } else if (
131         status == lazy.AppUpdater.STATUS.DOWNLOADING &&
132         (lazy.UpdateService.onlyDownloadUpdatesThisSession ||
133           (progress !== undefined && progressMax !== undefined))
134       ) {
135         // We get a DOWNLOADING callback with no progress or progressMax values
136         // when we initially switch to the DOWNLOADING state. But when we get
137         // onProgress notifications, progress and progressMax will be defined.
138         // Remember to keep in mind that progressMax is a required value that
139         // we can count on being meaningful, but it will be set to -1 for BITS
140         // transfers that haven't begun yet.
141         if (
142           lazy.UpdateService.onlyDownloadUpdatesThisSession ||
143           progressMax < 0 ||
144           progress != progressMax
145         ) {
146           lazy.log.debug(
147             `${SLUG}: Download in progress. Exiting task while download ` +
148               `transfers`
149           );
150           // If the download is still in progress, we don't want the Background
151           // Update Task to hang around waiting for it to complete.
152           lazy.UpdateService.onlyDownloadUpdatesThisSession = true;
154           appUpdater.removeListener(_appUpdaterListener);
155           appUpdater.stop();
156           resolve(status);
157         } else {
158           lazy.log.debug(`${SLUG}: Download has completed!`);
159         }
160       } else {
161         lazy.log.debug(
162           `${SLUG}: background update transitioned to status ${status}: ${stringStatus}`
163         );
164       }
165     };
166     appUpdater.addListener(_appUpdaterListener);
168     appUpdater.check();
169   });
171   return result;
175  * Maybe submit a "background-update" custom Glean ping.
177  * If data reporting upload in general is enabled Glean will submit a ping.  To determine if
178  * telemetry is enabled, Glean will look at the relevant pref, which was mirrored from the default
179  * profile.  Note that the Firefox policy mechanism will manage this pref, locking it to particular
180  * values as appropriate.
181  */
182 export async function maybeSubmitBackgroundUpdatePing() {
183   let SLUG = "maybeSubmitBackgroundUpdatePing";
185   // It should be possible to turn AUSTLMY data into Glean data, but mapping histograms isn't
186   // trivial, so we don't do it at this time.  Bug 1703313.
188   // Including a reason allows to differentiate pings sent as part of the task
189   // and pings queued and sent by Glean on a different schedule.
190   GleanPings.backgroundUpdate.submit("backgroundupdate_task");
192   lazy.log.info(`${SLUG}: submitted "background-update" ping`);
195 export async function runBackgroundTask(commandLine) {
196   let SLUG = "runBackgroundTask";
197   lazy.log.error(`${SLUG}: backgroundupdate`);
198   let automaticRestartFound =
199     -1 != commandLine.findFlag("automatic-restart", false);
201   // Modify Glean metrics for a successful automatic restart.
202   if (automaticRestartFound) {
203     Glean.backgroundUpdate.automaticRestartSuccess.set(true);
204     lazy.log.debug(`${SLUG}: application automatic restart completed`);
205   }
207   // Help debugging.  This is a pared down version of
208   // `dataProviders.application` in `Troubleshoot.sys.mjs`.  When adding to this
209   // debugging data, try to follow the form from that module.
210   let data = {
211     name: Services.appinfo.name,
212     osVersion:
213       Services.sysinfo.getProperty("name") +
214       " " +
215       Services.sysinfo.getProperty("version") +
216       " " +
217       Services.sysinfo.getProperty("build"),
218     version: AppConstants.MOZ_APP_VERSION_DISPLAY,
219     buildID: Services.appinfo.appBuildID,
220     distributionID: Services.prefs
221       .getDefaultBranch("")
222       .getCharPref("distribution.id", ""),
223     updateChannel: lazy.UpdateUtils.UpdateChannel,
224     UpdRootD: Services.dirsvc.get("UpdRootD", Ci.nsIFile).path,
225   };
226   lazy.log.debug(`${SLUG}: current configuration`, data);
228   // Other instances running are a transient precondition (during this invocation).  We'd prefer to
229   // check this later, as a reason for not updating, but Glean is not tested in multi-process
230   // environments and while its storage (backed by rkv) can in theory support multiple processes, it
231   // is not clear that it in fact does support multiple processes.  So we are conservative here.
232   // There is a potential time-of-check/time-of-use race condition here, but if process B starts
233   // after we pass this test, that process should exit after it gets to this check, avoiding
234   // multiple processes using the same Glean storage.  If and when more and longer-running
235   // background tasks become common, we may need to be more fine-grained and share just the Glean
236   // storage resource.
237   lazy.log.debug(`${SLUG}: checking if other instance is running`);
238   let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
239     Ci.nsIUpdateSyncManager
240   );
241   if (DevToolsSocketStatus.hasSocketOpened()) {
242     lazy.log.warn(
243       `${SLUG}: Ignoring the 'multiple instances' check because a DevTools server is listening.`
244     );
245   } else if (syncManager.isOtherInstanceRunning()) {
246     lazy.log.error(`${SLUG}: another instance is running`);
247     return EXIT_CODE.OTHER_INSTANCE;
248   }
250   // Here we mirror specific prefs from the default profile into our temporary profile.  We want to
251   // do this early because some of the prefs may impact internals such as log levels.  Generally,
252   // however, we want prefs from the default profile to not impact the mechanics of checking for,
253   // downloading, and applying updates, since such prefs should be be per-installation prefs, using
254   // the mechanisms of Bug 1691486.  Sadly using this mechanism for many relevant prefs (namely
255   // `app.update.BITS.enabled` and `app.update.service.enabled`) is difficult: see Bug 1657533.
256   //
257   // We also read any Nimbus targeting snapshot from the default profile.
258   let defaultProfileTargetingSnapshot = {};
259   try {
260     let defaultProfilePrefs;
261     await lazy.BackgroundTasksUtils.withProfileLock(async lock => {
262       let predicate = name => {
263         return (
264           name.startsWith("app.update.") || // For obvious reasons.
265           name.startsWith("datareporting.") || // For Glean.
266           name.startsWith("logging.") || // For Glean.
267           name.startsWith("telemetry.fog.") || // For Glean.
268           name.startsWith("app.partner.") || // For our metrics.
269           name === "app.shield.optoutstudies.enabled" || // For Nimbus.
270           name === "services.settings.server" || // For Remote Settings via Nimbus.
271           name === "services.settings.preview_enabled" || // For Remote Settings via Nimbus.
272           name === "messaging-system.rsexperimentloader.collection_id" // For Firefox Messaging System.
273         );
274       };
276       defaultProfilePrefs = await lazy.BackgroundTasksUtils.readPreferences(
277         predicate,
278         lock
279       );
280       let telemetryClientID =
281         await lazy.BackgroundTasksUtils.readTelemetryClientID(lock);
282       Glean.backgroundUpdate.clientId.set(telemetryClientID);
284       // Read targeting snapshot, collect background update specific telemetry.  Never throws.
285       defaultProfileTargetingSnapshot =
286         await BackgroundUpdate.readFirefoxMessagingSystemTargetingSnapshot(
287           lock
288         );
289     });
291     for (let [name, value] of Object.entries(defaultProfilePrefs)) {
292       switch (typeof value) {
293         case "boolean":
294           Services.prefs.setBoolPref(name, value);
295           break;
296         case "number":
297           Services.prefs.setIntPref(name, value);
298           break;
299         case "string":
300           Services.prefs.setCharPref(name, value);
301           break;
302         default:
303           throw new Error(
304             `Pref from default profile with name "${name}" has unrecognized type`
305           );
306       }
307     }
308   } catch (e) {
309     if (!lazy.BackgroundTasksUtils.hasDefaultProfile()) {
310       lazy.log.error(`${SLUG}: caught exception; no default profile exists`, e);
311       return EXIT_CODE.DEFAULT_PROFILE_DOES_NOT_EXIST;
312     }
314     if (e.name == "CannotLockProfileError") {
315       lazy.log.error(
316         `${SLUG}: caught exception; could not lock default profile`,
317         e
318       );
319       return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_LOCKED;
320     }
322     lazy.log.error(
323       `${SLUG}: caught exception reading preferences and telemetry client ID from default profile`,
324       e
325     );
326     return EXIT_CODE.DEFAULT_PROFILE_CANNOT_BE_READ;
327   }
329   // Now that we have prefs from the default profile, we can configure Firefox-on-Glean.
331   // Glean has a preinit queue for metric operations that happen before init, so
332   // this is safe.  We want to have these metrics set before the first possible
333   // time we might send (built-in) pings.
334   await BackgroundUpdate.recordUpdateEnvironment();
336   // To help debugging, use the `GLEAN_LOG_PINGS` and `GLEAN_DEBUG_VIEW_TAG`
337   // environment variables: see
338   // https://mozilla.github.io/glean/book/user/debugging/index.html.
339   let gleanRoot = await IOUtils.getDirectory(
340     Services.dirsvc.get("UpdRootD", Ci.nsIFile).path,
341     "backgroundupdate",
342     "datareporting",
343     "glean"
344   );
345   Services.fog.initializeFOG(
346     gleanRoot.path,
347     "firefox.desktop.background.update",
348     /* disableInternalPings */ true
349   );
351   // For convenience, mirror our loglevel.
352   let logLevel = Services.prefs.getCharPref(
353     "app.update.background.loglevel",
354     "error"
355   );
356   const logLevelPrefs = [
357     "browser.newtabpage.activity-stream.asrouter.debugLogLevel",
358     "messaging-system.log",
359     "services.settings.loglevel",
360     "toolkit.backgroundtasks.loglevel",
361   ];
362   for (let logLevelPref of logLevelPrefs) {
363     lazy.log.info(`${SLUG}: setting ${logLevelPref}=${logLevel}`);
364     Services.prefs.setCharPref(logLevelPref, logLevel);
365   }
367   // The langpack updating mechanism expects the addons manager, but in background task mode, the
368   // addons manager is not present.  Since we can't update langpacks from the background task
369   // temporary profile, we disable the langpack updating mechanism entirely.  This relies on the
370   // default profile being the only profile that schedules the OS-level background task and ensuring
371   // the task is not scheduled when langpacks are present.  Non-default profiles that have langpacks
372   // installed may experience the issues that motivated Bug 1647443.  If this turns out to be a
373   // significant problem in the wild, we could store more information about profiles and their
374   // active langpacks to disable background updates in more cases, maybe in per-installation prefs.
375   Services.prefs.setBoolPref("app.update.langpack.enabled", false);
377   let result = EXIT_CODE.SUCCESS;
379   let stringStatus = lazy.AppUpdater.STATUS.debugStringFor(
380     lazy.AppUpdater.STATUS.NEVER_CHECKED
381   );
382   Glean.backgroundUpdate.states.add(stringStatus);
383   Glean.backgroundUpdate.finalState.set(stringStatus);
385   let updateStatus = lazy.AppUpdater.STATUS.NEVER_CHECKED;
386   try {
387     // Return AppUpdater status from _attemptBackgroundUpdate() to
388     // check if the status is STATUS.READY_FOR_RESTART.
389     updateStatus = await _attemptBackgroundUpdate();
391     lazy.log.info(`${SLUG}: attempted background update`);
392     Glean.backgroundUpdate.exitCodeSuccess.set(true);
394     try {
395       // Now that we've pumped the update loop, we can start Nimbus and the Firefox Messaging System
396       // and see if we should message the user.  This minimizes the risk of messaging impacting the
397       // function of the background update system.
398       await lazy.BackgroundTasksUtils.enableNimbus(
399         commandLine,
400         defaultProfileTargetingSnapshot.environment
401       );
403       await lazy.BackgroundTasksUtils.enableFirefoxMessagingSystem(
404         defaultProfileTargetingSnapshot.environment
405       );
406     } catch (f) {
407       // Try to make it easy to witness errors in this system.  We can pass through any exception
408       // without disrupting (future) background updates.
409       //
410       // Most meaningful issues with the Nimbus/experiments system will be reported via Glean
411       // events.
412       lazy.log.warn(
413         `${SLUG}: exception raised from Nimbus/Firefox Messaging System`,
414         f
415       );
416       throw f;
417     }
418   } catch (e) {
419     // TODO: in the future, we might want to classify failures into transient and persistent and
420     // backoff the update task in the face of continuous persistent errors.
421     lazy.log.error(`${SLUG}: caught exception attempting background update`, e);
423     result = EXIT_CODE.EXCEPTION;
424     Glean.backgroundUpdate.exitCodeException.set(true);
425   } finally {
426     // This is the point to report telemetry, assuming that the default profile's data reporting
427     // configuration allows it.
428     await maybeSubmitBackgroundUpdatePing();
429   }
431   // TODO: ensure the update service has persisted its state before we exit.  Bug 1700846.
432   // TODO: ensure that Glean's upload mechanism is aware of Gecko shutdown.  Bug 1703572.
433   await lazy.ExtensionUtils.promiseTimeout(500);
435   // If we're in a staged background update, we need to restart Firefox to complete the update.
436   lazy.log.debug(
437     `${SLUG}: Checking if staged background update is ready for restart`
438   );
439   // If a restart loop is occurring then automaticRestartFound will be true.
440   if (
441     lazy.NimbusFeatures.backgroundUpdateAutomaticRestart.getVariable(
442       "enabled"
443     ) &&
444     updateStatus === lazy.AppUpdater.STATUS.READY_FOR_RESTART &&
445     !automaticRestartFound
446   ) {
447     lazy.log.debug(
448       `${SLUG}: Starting Firefox restart after staged background update`
449     );
451     // We need to restart Firefox with the same arguments to ensure
452     // the background update continues from where it was before the restart.
453     try {
454       Cc["@mozilla.org/updates/update-processor;1"]
455         .createInstance(Ci.nsIUpdateProcessor)
456         .attemptAutomaticApplicationRestartWithLaunchArgs([
457           "-automatic-restart",
458         ]);
459       // Report an attempted automatic restart.
460       Glean.backgroundUpdate.automaticRestartAttempted.set(true);
461       lazy.log.debug(`${SLUG}: automatic application restart queued`);
462     } catch (e) {
463       lazy.log.error(
464         `${SLUG}: caught exception; failed to queue automatic application restart`,
465         e
466       );
467     }
468   }
470   return result;