summaryrefslogtreecommitdiffstats
path: root/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs')
-rw-r--r--toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs407
1 files changed, 407 insertions, 0 deletions
diff --git a/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs
new file mode 100644
index 0000000000..9521af5a56
--- /dev/null
+++ b/toolkit/components/backgroundtasks/BackgroundTasksUtils.sys.mjs
@@ -0,0 +1,407 @@
+/* -*- 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+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: "toolkit.backgroundtasks.loglevel",
+ prefix: "BackgroundTasksUtils",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "ProfileService",
+ "@mozilla.org/toolkit/profile-service;1",
+ "nsIToolkitProfileService"
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ASRouter:
+ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+ "resource:///modules/asrouter/ASRouter.sys.mjs",
+ ASRouterDefaultConfig:
+ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
+ "resource:///modules/asrouter/ASRouterDefaultConfig.sys.mjs",
+
+ ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
+
+ RemoteSettingsExperimentLoader:
+ "resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs",
+});
+
+class CannotLockProfileError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "CannotLockProfileError";
+ }
+}
+
+export var BackgroundTasksUtils = {
+ // Manage our own default profile that can be overridden for testing. It's
+ // easier to do this here rather than using the profile service itself.
+ _defaultProfileInitialized: false,
+ _defaultProfile: null,
+
+ getDefaultProfile() {
+ if (!this._defaultProfileInitialized) {
+ this._defaultProfileInitialized = true;
+ // This is all test-only.
+ let defaultProfilePath = Services.env.get(
+ "MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH"
+ );
+ let noDefaultProfile = Services.env.get(
+ "MOZ_BACKGROUNDTASKS_NO_DEFAULT_PROFILE"
+ );
+ if (defaultProfilePath) {
+ lazy.log.info(
+ `getDefaultProfile: using default profile path ${defaultProfilePath}`
+ );
+ var tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ tmpd.initWithPath(defaultProfilePath);
+ // Sadly this writes to `profiles.ini`, but there's little to be done.
+ this._defaultProfile = lazy.ProfileService.createProfile(
+ tmpd,
+ `MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH-${Date.now()}`
+ );
+ } else if (noDefaultProfile) {
+ lazy.log.info(`getDefaultProfile: setting default profile to null`);
+ this._defaultProfile = null;
+ } else {
+ try {
+ lazy.log.info(
+ `getDefaultProfile: using ProfileService.defaultProfile`
+ );
+ this._defaultProfile = lazy.ProfileService.defaultProfile;
+ } catch (e) {}
+ }
+ }
+ return this._defaultProfile;
+ },
+
+ hasDefaultProfile() {
+ return this.getDefaultProfile() != null;
+ },
+
+ currentProfileIsDefaultProfile() {
+ let defaultProfile = this.getDefaultProfile();
+ let currentProfile = lazy.ProfileService.currentProfile;
+ // This comparison needs to accommodate null on both sides.
+ let isDefaultProfile = defaultProfile && currentProfile == defaultProfile;
+ return isDefaultProfile;
+ },
+
+ _throwIfNotLocked(lock) {
+ if (!(lock instanceof Ci.nsIProfileLock)) {
+ throw new Error("Passed lock was not an instance of nsIProfileLock");
+ }
+
+ try {
+ // In release builds, `.directory` throws NS_ERROR_NOT_INITIALIZED when
+ // unlocked. In debug builds, `.directory` when the profile is not locked
+ // will crash via `NS_ERROR`.
+ if (lock.directory) {
+ return;
+ }
+ } catch (e) {
+ if (
+ !(
+ e instanceof Ci.nsIException &&
+ e.result == Cr.NS_ERROR_NOT_INITIALIZED
+ )
+ ) {
+ throw e;
+ }
+ }
+ throw new Error("Profile is not locked");
+ },
+
+ /**
+ * Locks the given profile and provides the path to it to the callback.
+ * The callback should return a promise and once settled the profile is
+ * unlocked and then the promise returned back to the caller of this function.
+ *
+ * @template T
+ * @param {(lock: nsIProfileLock) => Promise<T>} callback
+ * @param {nsIToolkitProfile} [profile] defaults to default profile
+ * @return {Promise<T>}
+ */
+ async withProfileLock(callback, profile = this.getDefaultProfile()) {
+ if (!profile) {
+ throw new Error("No default profile exists");
+ }
+
+ let lock;
+ try {
+ lock = profile.lock({});
+ lazy.log.info(
+ `withProfileLock: locked profile at ${lock.directory.path}`
+ );
+ } catch (e) {
+ throw new CannotLockProfileError(`Cannot lock profile: ${e}`);
+ }
+
+ try {
+ // We must await to ensure any logging is displayed after the callback resolves.
+ return await callback(lock);
+ } finally {
+ try {
+ lazy.log.info(
+ `withProfileLock: unlocking profile at ${lock.directory.path}`
+ );
+ lock.unlock();
+ lazy.log.info(`withProfileLock: unlocked profile`);
+ } catch (e) {
+ lazy.log.warn(`withProfileLock: error unlocking profile`, e);
+ }
+ }
+ },
+
+ /**
+ * Reads the preferences from "prefs.js" out of a profile, optionally
+ * returning only names satisfying a given predicate.
+ *
+ * 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 {(name: string) => boolean} [predicate] a predicate to filter
+ * preferences by; if not given, all preferences are accepted.
+ * @param {nsIProfileLock} [lock] optional lock to use
+ * @returns {object} with keys that are string preference names and values
+ * that are string|number|boolean preference values.
+ */
+ async readPreferences(predicate = null, lock = null) {
+ if (!lock) {
+ return this.withProfileLock(profileLock =>
+ this.readPreferences(predicate, profileLock)
+ );
+ }
+
+ this._throwIfNotLocked(lock);
+ lazy.log.info(`readPreferences: profile is locked`);
+
+ let prefs = {};
+ let addPref = (kind, name, value, sticky, locked) => {
+ if (predicate && !predicate(name)) {
+ return;
+ }
+ prefs[name] = value;
+ };
+
+ // We ignore any "user.js" file, since usage is low and doing otherwise
+ // requires implementing a bit more of `nsIPrefsService` than feels safe.
+ let prefsFile = lock.directory.clone();
+ prefsFile.append("prefs.js");
+ lazy.log.info(`readPreferences: will parse prefs ${prefsFile.path}`);
+
+ let data = await IOUtils.read(prefsFile.path);
+ lazy.log.debug(
+ `readPreferences: parsing prefs from buffer of length ${data.length}`
+ );
+
+ Services.prefs.parsePrefsFromBuffer(
+ data,
+ {
+ onStringPref: addPref,
+ onIntPref: addPref,
+ onBoolPref: addPref,
+ onError(message) {
+ // Firefox itself manages "prefs.js", so errors should be infrequent.
+ lazy.log.error(message);
+ },
+ },
+ prefsFile.path
+ );
+
+ lazy.log.debug(`readPreferences: parsed prefs from buffer`, prefs);
+ return prefs;
+ },
+
+ /**
+ * Reads the snapshotted Firefox Messaging System targeting out of a profile.
+ *
+ * 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}
+ */
+ async readFirefoxMessagingSystemTargetingSnapshot(lock = null) {
+ if (!lock) {
+ return this.withProfileLock(profileLock =>
+ this.readFirefoxMessagingSystemTargetingSnapshot(profileLock)
+ );
+ }
+
+ this._throwIfNotLocked(lock);
+
+ let snapshotFile = lock.directory.clone();
+ snapshotFile.append("targeting.snapshot.json");
+
+ lazy.log.info(
+ `readFirefoxMessagingSystemTargetingSnapshot: will read Firefox Messaging ` +
+ `System targeting snapshot from ${snapshotFile.path}`
+ );
+
+ return IOUtils.readJSON(snapshotFile.path);
+ },
+
+ /**
+ * Reads the Telemetry Client ID out of a profile.
+ *
+ * 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 {string}
+ */
+ async readTelemetryClientID(lock = null) {
+ if (!lock) {
+ return this.withProfileLock(profileLock =>
+ this.readTelemetryClientID(profileLock)
+ );
+ }
+
+ this._throwIfNotLocked(lock);
+
+ let stateFile = lock.directory.clone();
+ stateFile.append("datareporting");
+ stateFile.append("state.json");
+
+ lazy.log.info(
+ `readPreferences: will read Telemetry client ID from ${stateFile.path}`
+ );
+
+ let state = await IOUtils.readJSON(stateFile.path);
+
+ return state.clientID;
+ },
+
+ /**
+ * Enable the Nimbus experimentation framework.
+ *
+ * @param {nsICommandLine} commandLine if given, accept command line parameters
+ * like `--url about:studies?...` or
+ * `--url file:path/to.json` to explicitly
+ * opt-on to experiment branches.
+ * @param {object} defaultProfile snapshot of Firefox Messaging System
+ * targeting from default browsing profile.
+ */
+ async enableNimbus(commandLine, defaultProfile = {}) {
+ try {
+ await lazy.ExperimentManager.onStartup({ defaultProfile });
+ } catch (err) {
+ lazy.log.error("Failed to initialize ExperimentManager:", err);
+ throw err;
+ }
+
+ try {
+ await lazy.RemoteSettingsExperimentLoader.init({ forceSync: true });
+ } catch (err) {
+ lazy.log.error(
+ "Failed to initialize RemoteSettingsExperimentLoader:",
+ err
+ );
+ throw err;
+ }
+
+ // Allow manual explicit opt-in to experiment branches to facilitate testing.
+ //
+ // Process command line arguments, like
+ // `--url about:studies?optin_slug=nalexander-ms-test1&optin_branch=treatment-a&optin_collection=nimbus-preview`
+ // or
+ // `--url file:///Users/nalexander/Mozilla/gecko/experiment.json?optin_branch=treatment-a`.
+ let ar;
+ while ((ar = commandLine?.handleFlagWithParam("url", false))) {
+ let uri = commandLine.resolveURI(ar);
+ const params = new URLSearchParams(uri.query);
+
+ if (uri.schemeIs("about") && uri.filePath == "studies") {
+ // Allow explicit opt-in. In the future, we might take this pref from
+ // the default browsing profile.
+ Services.prefs.setBoolPref("nimbus.debug", true);
+
+ const data = {
+ slug: params.get("optin_slug"),
+ branch: params.get("optin_branch"),
+ collection: params.get("optin_collection"),
+ };
+ await lazy.RemoteSettingsExperimentLoader.optInToExperiment(data);
+ lazy.log.info(`Opted in to experiment: ${JSON.stringify(data)}`);
+ }
+
+ if (uri.schemeIs("file")) {
+ let branchSlug = params.get("optin_branch");
+ let path = decodeURIComponent(uri.filePath);
+ let response = await fetch(uri.spec);
+ let recipe = await response.json();
+ if (recipe.permissions) {
+ // Saved directly from Experimenter, there's a top-level `data`. Hand
+ // written, that's not the norm.
+ recipe = recipe.data;
+ }
+ let branch = recipe.branches.find(b => b.slug == branchSlug);
+
+ lazy.ExperimentManager.forceEnroll(recipe, branch);
+ lazy.log.info(`Forced enrollment into: ${path}, branch: ${branchSlug}`);
+ }
+ }
+ },
+
+ /**
+ * Enable the Firefox Messaging System and, when successfully initialized,
+ * trigger a message with trigger id `backgroundTask`.
+ *
+ * @param {object} defaultProfile - snapshot of Firefox Messaging System
+ * targeting from default browsing profile.
+ */
+ async enableFirefoxMessagingSystem(defaultProfile = {}) {
+ function logArgs(tag, ...args) {
+ lazy.log.debug(`FxMS invoked ${tag}: ${JSON.stringify(args)}`);
+ }
+
+ let { messageHandler, router, createStorage } =
+ lazy.ASRouterDefaultConfig();
+
+ if (!router.initialized) {
+ const storage = await createStorage();
+ await router.init({
+ storage,
+ // Background tasks never send legacy telemetry.
+ sendTelemetry: logArgs.bind(null, "sendTelemetry"),
+ dispatchCFRAction: messageHandler.handleCFRAction.bind(messageHandler),
+ // There's no child process involved in background tasks, so swallow all
+ // of these messages.
+ clearChildMessages: logArgs.bind(null, "clearChildMessages"),
+ clearChildProviders: logArgs.bind(null, "clearChildProviders"),
+ updateAdminState: () => {},
+ });
+ }
+
+ await lazy.ASRouter.waitForInitialized;
+
+ const triggerId = "backgroundTask";
+ await lazy.ASRouter.sendTriggerMessage({
+ browser: null,
+ id: triggerId,
+ context: {
+ defaultProfile,
+ },
+ });
+ lazy.log.info(
+ "Triggered Firefox Messaging System with trigger id 'backgroundTask'"
+ );
+ },
+};