Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/. */
var EXPORTED_SYMBOLS = ["BackgroundTasksUtils"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
let consoleOptions = {
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.jsm for details.
maxLogLevel: "error",
maxLogLevelPref: "toolkit.backgroundtasks.loglevel",
prefix: "BackgroundTasksUtils",
};
return new ConsoleAPI(consoleOptions);
});
XPCOMUtils.defineLazyServiceGetter(
this,
"ProfileService",
"@mozilla.org/toolkit/profile-service;1",
"nsIToolkitProfileService"
);
class CannotLockProfileError extends Error {
constructor(message) {
super(message);
this.name = "CannotLockProfileError";
}
}
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.
const env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
let defaultProfilePath = env.get(
"MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH"
);
let noDefaultProfile = env.get("MOZ_BACKGROUNDTASKS_NO_DEFAULT_PROFILE");
if (defaultProfilePath) {
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 = ProfileService.createProfile(
tmpd,
`MOZ_BACKGROUNDTASKS_DEFAULT_PROFILE_PATH-${Date.now()}`
);
} else if (noDefaultProfile) {
log.info(`getDefaultProfile: setting default profile to null`);
this._defaultProfile = null;
} else {
try {
log.info(`getDefaultProfile: using ProfileService.defaultProfile`);
this._defaultProfile = ProfileService.defaultProfile;
} catch (e) {}
}
}
return this._defaultProfile;
},
hasDefaultProfile() {
return this.getDefaultProfile() != null;
},
currentProfileIsDefaultProfile() {
let defaultProfile = this.getDefaultProfile();
let currentProfile = 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({});
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 {
log.info(
`withProfileLock: unlocking profile at ${lock.directory.path}`
);
lock.unlock();
log.info(`withProfileLock: unlocked profile`);
} catch (e) {
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);
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");
log.info(`readPreferences: will parse prefs ${prefsFile.path}`);
let data = await IOUtils.read(prefsFile.path);
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.
log.error(message);
},
},
prefsFile.path
);
log.debug(`readPreferences: parsed prefs from buffer`, prefs);
return prefs;
},
/**
* 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");
log.info(
`readPreferences: will read Telemetry client ID from ${stateFile.path}`
);
// This JSON is always UTF-8.
let data = await IOUtils.readUTF8(stateFile.path);
let state = JSON.parse(data);
return state.clientID;
},
};