Bug 1694515 - Part 2: Add BackgroundTasksUtils module for locking profiles and reading prefs. r=bytesized,mossop

Differential Revision: https://phabricator.services.mozilla.com/D107711
This commit is contained in:
Nick Alexander 2021-03-12 04:08:42 +00:00
parent 0b77649773
commit d60f45d4c0
5 changed files with 265 additions and 0 deletions

View File

@ -263,6 +263,13 @@ if (AppConstants.platform == "android") {
});
}
if (AppConstants.MOZ_BACKGROUNDTASKS) {
// These utilities are for background tasks, not regular headed browsing.
whitelist.push({
file: "resource://gre/modules/BackgroundTasksUtils.jsm",
});
}
whitelist = new Set(
whitelist
.filter(

View File

@ -0,0 +1,157 @@
/* -*- 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"
);
var BackgroundTasksUtils = {
_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 = ProfileService.defaultProfile) {
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 Error(`Unable to 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;
},
};

View File

@ -34,6 +34,7 @@ XPIDL_MODULE = "toolkit_backgroundtasks"
EXTRA_JS_MODULES += [
"BackgroundTasksManager.jsm",
"BackgroundTasksUtils.jsm",
]
EXTRA_JS_MODULES.backgroundtasks += [

View File

@ -0,0 +1,99 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim: sw=4 ts=4 sts=4 et
* 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 { BackgroundTasksUtils } = ChromeUtils.import(
"resource://gre/modules/BackgroundTasksUtils.jsm"
);
setupProfileService();
add_task(async function test_withProfileLock() {
let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
let profilePath = do_get_profile();
profilePath.append(`test_withProfileLock`);
let profile = profileService.createUniqueProfile(
profilePath,
"test_withProfileLock"
);
await BackgroundTasksUtils.withProfileLock(async lock => {
BackgroundTasksUtils._throwIfNotLocked(lock);
}, profile);
// In debug builds, an unlocked lock crashes via `NS_ERROR` when
// `.directory` is invoked. There's no way to check that the lock
// is unlocked, so we can't realistically test this scenario in
// debug builds.
if (!AppConstants.DEBUG) {
await Assert.rejects(
BackgroundTasksUtils.withProfileLock(async lock => {
lock.unlock();
BackgroundTasksUtils._throwIfNotLocked(lock);
}, profile),
/Profile is not locked/
);
}
});
add_task(async function test_readPreferences() {
let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
let profilePath = do_get_profile();
profilePath.append(`test_readPreferences`);
let profile = profileService.createUniqueProfile(
profilePath,
"test_readPreferences"
);
// Before we write any preferences, we fail to read.
await Assert.rejects(
BackgroundTasksUtils.withProfileLock(
lock => BackgroundTasksUtils.readPreferences(null, lock),
profile
),
/NotFoundError/
);
let s = `user_pref("testPref.bool1", true);
user_pref("testPref.bool2", false);
user_pref("testPref.int1", 23);
user_pref("testPref.int2", -1236);
`;
let prefsFile = profile.rootDir.clone();
prefsFile.append("prefs.js");
await IOUtils.writeUTF8(prefsFile.path, s);
// Now we can read all the prefs.
let prefs = await BackgroundTasksUtils.withProfileLock(
lock => BackgroundTasksUtils.readPreferences(null, lock),
profile
);
let expected = {
"testPref.bool1": true,
"testPref.bool2": false,
"testPref.int1": 23,
"testPref.int2": -1236,
};
Assert.deepEqual(prefs, expected, "Prefs read are correct");
// And we can filter the read as well.
prefs = await BackgroundTasksUtils.withProfileLock(
lock =>
BackgroundTasksUtils.readPreferences(name => name.endsWith("1"), lock),
profile
);
expected = {
"testPref.bool1": true,
"testPref.int1": 23,
};
Assert.deepEqual(prefs, expected, "Filtered prefs read are correct");
});

View File

@ -15,5 +15,6 @@ support-files =
[test_backgroundtask_shouldprocessupdates.js]
[test_backgroundtask_specific_pref.js]
[test_backgroundtask_update_sync_manager.js]
[test_backgroundtasksutils.js]
[test_manifest_with_backgroundtask.js]
[test_manifest_without_backgroundtask.js]