summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs
blob: e94039c54635de4dd6c87847f6a1e022392a7dde (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
/* -*- 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/. */

const { BackgroundUpdate } = ChromeUtils.import(
  "resource://gre/modules/BackgroundUpdate.jsm"
);
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, {
  BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
});

XPCOMUtils.defineLazyModuleGetters(lazy, {
  AppUpdater: "resource://gre/modules/AppUpdater.jsm",
  ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UpdateService",
  "@mozilla.org/updates/update-service;1",
  "nsIApplicationUpdateService"
);

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);
});

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 {boolean} - `true` if an update loop was completed.
 */
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 false;
  }

  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(true);
      } 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(true);
        } 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`);

  // 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 (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();

  // The final leaf is for the benefit of `FileUtils`.  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 = lazy.FileUtils.getFile("UpdRootD", [
    "backgroundupdate",
    "datareporting",
    "glean",
    "__dummy__",
  ]).parent.path;
  Services.fog.initializeFOG(gleanRoot, "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);

  try {
    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);

  return result;
}