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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
10 ChromeUtils.defineLazyGetter(lazy, "log", () => {
11 let { ConsoleAPI } = ChromeUtils.importESModule(
12 "resource://gre/modules/Console.sys.mjs"
14 let consoleOptions = {
15 // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
16 // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
18 maxLogLevelPref: "toolkit.backgroundtasks.loglevel",
19 prefix: "BackgroundTasksUtils",
21 return new ConsoleAPI(consoleOptions);
24 XPCOMUtils.defineLazyServiceGetter(
27 "@mozilla.org/toolkit/profile-service;1",
28 "nsIToolkitProfileService"
31 ChromeUtils.defineESModuleGetters(lazy, {
33 // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
34 "resource:///modules/asrouter/ASRouter.sys.mjs",
35 ASRouterDefaultConfig:
36 // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
37 "resource:///modules/asrouter/ASRouterDefaultConfig.sys.mjs",
39 ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
41 RemoteSettingsExperimentLoader:
42 "resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs",
45 class CannotLockProfileError extends Error {
46 constructor(message) {
48 this.name = "CannotLockProfileError";
52 export var BackgroundTasksUtils = {
53 // Manage our own default profile that can be overridden for testing. It's
54 // easier to do this here rather than using the profile service itself.
55 _defaultProfileInitialized: false,
56 _defaultProfile: null,
59 if (!this._defaultProfileInitialized) {
60 this._defaultProfileInitialized = true;
61 // This is all test-only.
62 let defaultProfilePath = Services.env.get(
63 "MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH"
65 let noDefaultProfile = Services.env.get(
66 "MOZ_BACKGROUNDTASKS_NO_DEFAULT_PROFILE"
68 if (defaultProfilePath) {
70 `getDefaultProfile: using default profile path ${defaultProfilePath}`
72 var tmpd = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
73 tmpd.initWithPath(defaultProfilePath);
74 // Sadly this writes to `profiles.ini`, but there's little to be done.
75 this._defaultProfile = lazy.ProfileService.createProfile(
77 `MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH-${Date.now()}`
79 } else if (noDefaultProfile) {
80 lazy.log.info(`getDefaultProfile: setting default profile to null`);
81 this._defaultProfile = null;
85 `getDefaultProfile: using ProfileService.defaultProfile`
87 this._defaultProfile = lazy.ProfileService.defaultProfile;
91 return this._defaultProfile;
95 return this.getDefaultProfile() != null;
98 currentProfileIsDefaultProfile() {
99 let defaultProfile = this.getDefaultProfile();
100 let currentProfile = lazy.ProfileService.currentProfile;
101 // This comparison needs to accommodate null on both sides.
102 let isDefaultProfile = defaultProfile && currentProfile == defaultProfile;
103 return isDefaultProfile;
106 _throwIfNotLocked(lock) {
107 if (!(lock instanceof Ci.nsIProfileLock)) {
108 throw new Error("Passed lock was not an instance of nsIProfileLock");
112 // In release builds, `.directory` throws NS_ERROR_NOT_INITIALIZED when
113 // unlocked. In debug builds, `.directory` when the profile is not locked
114 // will crash via `NS_ERROR`.
115 if (lock.directory) {
121 e instanceof Ci.nsIException &&
122 e.result == Cr.NS_ERROR_NOT_INITIALIZED
128 throw new Error("Profile is not locked");
132 * Locks the given profile and provides the path to it to the callback.
133 * The callback should return a promise and once settled the profile is
134 * unlocked and then the promise returned back to the caller of this function.
137 * @param {(lock: nsIProfileLock) => Promise<T>} callback
138 * @param {nsIToolkitProfile} [profile] defaults to default profile
139 * @return {Promise<T>}
141 async withProfileLock(callback, profile = this.getDefaultProfile()) {
143 throw new Error("No default profile exists");
148 lock = profile.lock({});
150 `withProfileLock: locked profile at ${lock.directory.path}`
153 throw new CannotLockProfileError(`Cannot lock profile: ${e}`);
157 // We must await to ensure any logging is displayed after the callback resolves.
158 return await callback(lock);
162 `withProfileLock: unlocking profile at ${lock.directory.path}`
165 lazy.log.info(`withProfileLock: unlocked profile`);
167 lazy.log.warn(`withProfileLock: error unlocking profile`, e);
173 * Reads the preferences from "prefs.js" out of a profile, optionally
174 * returning only names satisfying a given predicate.
176 * If no `lock` is given, the default profile is locked and the preferences
177 * read from it. If `lock` is given, read from the given lock's directory.
179 * @param {(name: string) => boolean} [predicate] a predicate to filter
180 * preferences by; if not given, all preferences are accepted.
181 * @param {nsIProfileLock} [lock] optional lock to use
182 * @returns {object} with keys that are string preference names and values
183 * that are string|number|boolean preference values.
185 async readPreferences(predicate = null, lock = null) {
187 return this.withProfileLock(profileLock =>
188 this.readPreferences(predicate, profileLock)
192 this._throwIfNotLocked(lock);
193 lazy.log.info(`readPreferences: profile is locked`);
196 let addPref = (kind, name, value) => {
197 if (predicate && !predicate(name)) {
203 // We ignore any "user.js" file, since usage is low and doing otherwise
204 // requires implementing a bit more of `nsIPrefsService` than feels safe.
205 let prefsFile = lock.directory.clone();
206 prefsFile.append("prefs.js");
207 lazy.log.info(`readPreferences: will parse prefs ${prefsFile.path}`);
209 let data = await IOUtils.read(prefsFile.path);
211 `readPreferences: parsing prefs from buffer of length ${data.length}`
214 Services.prefs.parsePrefsFromBuffer(
217 onStringPref: addPref,
221 // Firefox itself manages "prefs.js", so errors should be infrequent.
222 lazy.log.error(message);
228 lazy.log.debug(`readPreferences: parsed prefs from buffer`, prefs);
233 * Reads the snapshotted Firefox Messaging System targeting out of a profile.
235 * If no `lock` is given, the default profile is locked and the preferences
236 * read from it. If `lock` is given, read from the given lock's directory.
238 * @param {nsIProfileLock} [lock] optional lock to use
241 async readFirefoxMessagingSystemTargetingSnapshot(lock = null) {
243 return this.withProfileLock(profileLock =>
244 this.readFirefoxMessagingSystemTargetingSnapshot(profileLock)
248 this._throwIfNotLocked(lock);
250 let snapshotFile = lock.directory.clone();
251 snapshotFile.append("targeting.snapshot.json");
254 `readFirefoxMessagingSystemTargetingSnapshot: will read Firefox Messaging ` +
255 `System targeting snapshot from ${snapshotFile.path}`
258 return IOUtils.readJSON(snapshotFile.path);
262 * Reads the Telemetry Client ID out of a profile.
264 * If no `lock` is given, the default profile is locked and the preferences
265 * read from it. If `lock` is given, read from the given lock's directory.
267 * @param {nsIProfileLock} [lock] optional lock to use
270 async readTelemetryClientID(lock = null) {
272 return this.withProfileLock(profileLock =>
273 this.readTelemetryClientID(profileLock)
277 this._throwIfNotLocked(lock);
279 let stateFile = lock.directory.clone();
280 stateFile.append("datareporting");
281 stateFile.append("state.json");
284 `readPreferences: will read Telemetry client ID from ${stateFile.path}`
287 let state = await IOUtils.readJSON(stateFile.path);
289 return state.clientID;
293 * Enable the Nimbus experimentation framework.
295 * @param {nsICommandLine} commandLine if given, accept command line parameters
296 * like `--url about:studies?...` or
297 * `--url file:path/to.json` to explicitly
298 * opt-on to experiment branches.
299 * @param {object} defaultProfile snapshot of Firefox Messaging System
300 * targeting from default browsing profile.
302 async enableNimbus(commandLine, defaultProfile = {}) {
304 await lazy.ExperimentManager.onStartup({ defaultProfile });
306 lazy.log.error("Failed to initialize ExperimentManager:", err);
311 await lazy.RemoteSettingsExperimentLoader.init({ forceSync: true });
314 "Failed to initialize RemoteSettingsExperimentLoader:",
320 // Allow manual explicit opt-in to experiment branches to facilitate testing.
322 // Process command line arguments, like
323 // `--url about:studies?optin_slug=nalexander-ms-test1&optin_branch=treatment-a&optin_collection=nimbus-preview`
325 // `--url file:///Users/nalexander/Mozilla/gecko/experiment.json?optin_branch=treatment-a`.
327 while ((ar = commandLine?.handleFlagWithParam("url", false))) {
328 let uri = commandLine.resolveURI(ar);
329 const params = new URLSearchParams(uri.query);
331 if (uri.schemeIs("about") && uri.filePath == "studies") {
332 // Allow explicit opt-in. In the future, we might take this pref from
333 // the default browsing profile.
334 Services.prefs.setBoolPref("nimbus.debug", true);
337 slug: params.get("optin_slug"),
338 branch: params.get("optin_branch"),
339 collection: params.get("optin_collection"),
341 await lazy.RemoteSettingsExperimentLoader.optInToExperiment(data);
342 lazy.log.info(`Opted in to experiment: ${JSON.stringify(data)}`);
345 if (uri.schemeIs("file")) {
346 let branchSlug = params.get("optin_branch");
347 let path = decodeURIComponent(uri.filePath);
348 let response = await fetch(uri.spec);
349 let recipe = await response.json();
350 if (recipe.permissions) {
351 // Saved directly from Experimenter, there's a top-level `data`. Hand
352 // written, that's not the norm.
353 recipe = recipe.data;
355 let branch = recipe.branches.find(b => b.slug == branchSlug);
357 lazy.ExperimentManager.forceEnroll(recipe, branch);
358 lazy.log.info(`Forced enrollment into: ${path}, branch: ${branchSlug}`);
364 * Enable the Firefox Messaging System and, when successfully initialized,
365 * trigger a message with trigger id `backgroundTask`.
367 * @param {object} defaultProfile - snapshot of Firefox Messaging System
368 * targeting from default browsing profile.
370 async enableFirefoxMessagingSystem(defaultProfile = {}) {
371 function logArgs(tag, ...args) {
372 lazy.log.debug(`FxMS invoked ${tag}: ${JSON.stringify(args)}`);
375 let { messageHandler, router, createStorage } =
376 lazy.ASRouterDefaultConfig();
378 if (!router.initialized) {
379 const storage = await createStorage();
382 // Background tasks never send legacy telemetry.
383 sendTelemetry: logArgs.bind(null, "sendTelemetry"),
384 dispatchCFRAction: messageHandler.handleCFRAction.bind(messageHandler),
385 // There's no child process involved in background tasks, so swallow all
386 // of these messages.
387 clearChildMessages: logArgs.bind(null, "clearChildMessages"),
388 clearChildProviders: logArgs.bind(null, "clearChildProviders"),
389 updateAdminState: () => {},
393 await lazy.ASRouter.waitForInitialized;
395 const triggerId = "backgroundTask";
396 await lazy.ASRouter.sendTriggerMessage({
404 "Triggered Firefox Messaging System with trigger id 'backgroundTask'"