gecko-dev/toolkit/components/backgroundtasks/BackgroundTasksUtils.jsm
Nick Alexander 1981f5cc86 Bug 1704146 - Part 2: Return exit codes for default profile states. r=bytesized,application-update-reviewers
When no default profile exists, or there is an issue reading data from
it, we have limited options to surface the issue.  Since we can't read
preferences, we can't rely on driving loglevels up.  Similarly, we
can't use Glean to report metrics since the user's datareporting
preferences won't be respected.

Returning exit codes will allow us to distinguish various situations
in the Windows `taskschd.msc` history at least, as well as allowing
us to test easily.

Differential Revision: https://phabricator.services.mozilla.com/D111528
2021-04-14 18:21:56 +00:00

249 lines
7.8 KiB
JavaScript

/* -*- 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(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {})
.ConsoleAPI;
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;
},
};