Bug 1893722: Recover StoreID from prefs in the event of a profiles.ini reset. r=jhirsch

Differential Revision: https://phabricator.services.mozilla.com/D226806
This commit is contained in:
Dave Townsend
2024-10-28 09:56:36 +00:00
parent 56aa5f8451
commit 38e9faea9d
9 changed files with 311 additions and 41 deletions

View File

@@ -91,6 +91,7 @@ class SelectableProfileServiceClass {
#asyncShutdownBlocker = null;
#initialized = false;
#groupToolkitProfile = null;
#storeID = null;
#currentProfile = null;
#everyWindowCallbackId = "SelectableProfileService";
#defaultAvatars = [
@@ -101,6 +102,7 @@ class SelectableProfileServiceClass {
"shopping",
"star",
];
#initPromise = null;
static #dirSvc = null;
constructor() {
@@ -111,6 +113,23 @@ class SelectableProfileServiceClass {
this.#groupToolkitProfile =
this.#profileService.currentProfile ?? this.#profileService.groupProfile;
this.#storeID = this.#groupToolkitProfile?.storeID;
if (!this.#storeID) {
this.#storeID = Services.prefs.getCharPref(
"toolkit.profiles.storeID",
""
);
if (this.#storeID) {
// This can happen if profiles.ini has been reset by a version of Firefox prior to 67 and
// the current profile is not the current default for the group. We can recover by
// attempting to find the group profile from the database.
this.#initPromise = this.restoreStoreID()
.catch(console.error)
.finally(() => (this.#initPromise = null));
}
}
}
/**
@@ -167,6 +186,10 @@ class SelectableProfileServiceClass {
}
}
get storeID() {
return this.#storeID;
}
get groupToolkitProfile() {
return this.#groupToolkitProfile;
}
@@ -188,7 +211,7 @@ class SelectableProfileServiceClass {
}
async maybeCreateProfilesStorePath() {
if (!this.#groupToolkitProfile || this.#groupToolkitProfile.storeID) {
if (this.storeID) {
return;
}
@@ -202,6 +225,7 @@ class SelectableProfileServiceClass {
.replace("{", "")
.split("-")[0];
this.#groupToolkitProfile.storeID = storageID;
this.#storeID = storageID;
await this.#attemptFlushProfileService();
}
@@ -210,22 +234,34 @@ class SelectableProfileServiceClass {
return PathUtils.join(
SelectableProfileServiceClass.PROFILE_GROUPS_DIR,
`${this.#groupToolkitProfile.storeID}.sqlite`
`${this.storeID}.sqlite`
);
}
/**
* At startup, store the nsToolkitProfile for the group.
* Get the groupDBPath from the nsToolkitProfile, and connect to it.
*
* @returns {Promise}
*/
async init() {
init() {
if (!this.#initPromise) {
this.#initPromise = this.#init().finally(
() => (this.#initPromise = null)
);
}
return this.#initPromise;
}
async #init() {
if (this.#initialized || !this.groupToolkitProfile) {
return;
}
// If the storeID doesn't exist, we don't want to create the db until we
// need to so we early return.
if (!this.groupToolkitProfile.storeID) {
if (!this.storeID) {
return;
}
@@ -347,6 +383,33 @@ class SelectableProfileServiceClass {
}
}
async restoreStoreID() {
try {
await this.#init();
for (let profile of await this.getAllProfiles()) {
let groupProfile = this.#profileService.getProfileByDir(
await profile.rootDir
);
if (groupProfile) {
this.#groupToolkitProfile = groupProfile;
this.#groupToolkitProfile.storeID = this.storeID;
await this.#profileService.asyncFlush();
return;
}
}
} catch (e) {
console.error(e);
}
// If we were unable to find a matching toolkit profile then assume the
// store ID is bogus so clear it and uninit.
this.#storeID = null;
await this.uninit();
Services.prefs.clearUserPref("toolkit.profiles.storeID");
}
async handleEvent(event) {
switch (event.type) {
case "activate": {
@@ -422,11 +485,12 @@ class SelectableProfileServiceClass {
* and vacuum the group DB.
*/
async deleteProfileGroup() {
if (this.getAllProfiles().length) {
if ((await this.getAllProfiles()).length) {
return;
}
this.#groupToolkitProfile.storeID = null;
this.#storeID = null;
await this.#attemptFlushProfileService();
await this.vacuumAndCloseGroupDB();
}

View File

@@ -42,6 +42,18 @@ async function initSelectableProfileService() {
await SelectableProfileService.maybeSetupDataStore();
}
function getRelativeProfilePath(path) {
let relativePath = path.getRelativePath(
Services.dirsvc.get("UAppData", Ci.nsIFile)
);
if (AppConstants.platform === "win") {
relativePath = relativePath.replace("/", "\\");
}
return relativePath;
}
async function createTestProfile(profileData = {}) {
const SelectableProfileService = getSelectableProfileService();

View File

@@ -0,0 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(
{
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
},
async function test_recover_storeID() {
startProfileService();
Services.prefs.setCharPref("toolkit.profiles.storeID", "foobar");
const SelectableProfileService = getSelectableProfileService();
await SelectableProfileService.init();
Assert.ok(
!SelectableProfileService.initialized,
"Didn't initialize the service"
);
let profile = SelectableProfileService.currentProfile;
Assert.ok(!profile, "Should not have a current profile");
Assert.equal(
getProfileService().currentProfile.storeID,
null,
"Should not have updated the store ID on the profile"
);
Assert.ok(
!Services.prefs.prefHasUserValue("toolkit.profiles.storeID"),
"Should have cleared the storeID pref"
);
}
);

View File

@@ -0,0 +1,76 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { Sqlite } = ChromeUtils.importESModule(
"resource://gre/modules/Sqlite.sys.mjs"
);
add_task(
{
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
},
async function test_recover_storeID() {
startProfileService();
Services.prefs.setCharPref("toolkit.profiles.storeID", "foobar");
// The database needs to exist already
let groupsPath = PathUtils.join(
Services.dirsvc.get("UAppData", Ci.nsIFile).path,
"Profile Groups"
);
await IOUtils.makeDirectory(groupsPath);
let dbFile = PathUtils.join(groupsPath, "foobar.sqlite");
let db = await Sqlite.openConnection({
path: dbFile,
openNotExclusive: true,
});
let path = getRelativeProfilePath(
getProfileService().currentProfile.rootDir
);
// Slightly annoying we have to replicate this...
await db.execute(`CREATE TABLE IF NOT EXISTS "Profiles" (
id INTEGER NOT NULL,
path TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar TEXT NOT NULL,
themeL10nId TEXT NOT NULL,
themeFg TEXT NOT NULL,
themeBg TEXT NOT NULL,
PRIMARY KEY(id)
);`);
await db.execute(
`INSERT INTO Profiles VALUES (NULL, :path, :name, :avatar, :themeL10nId, :themeFg, :themeBg);`,
{
path,
name: "Fake Profile",
avatar: "book",
themeL10nId: "default",
themeFg: "",
themeBg: "",
}
);
await db.close();
const SelectableProfileService = getSelectableProfileService();
await SelectableProfileService.init();
Assert.ok(
SelectableProfileService.initialized,
"Did initialize the service"
);
let profile = SelectableProfileService.currentProfile;
Assert.ok(profile, "Should have a current profile");
Assert.equal(profile.name, "Fake Profile");
Assert.equal(
getProfileService().currentProfile.storeID,
"foobar",
"Should have updated the store ID on the profile"
);
}
);

View File

@@ -2,6 +2,10 @@
head = "../../../../../toolkit/profile/test/xpcshell/head.js head.js"
firefox-appdir = "browser"
["test_fail_recover_storeID.js"]
["test_recover_storeID.js"]
["test_selectable_profile_launch.js"]
["test_selectable_profile_service_exists.js"]

View File

@@ -103,6 +103,14 @@ interface nsIToolkitProfileService : nsISupports
*/
nsIToolkitProfile getProfileByName(in AUTF8String aName);
/**
* Get a profile by directory. Finds a profile with the matching root directory
*
* @param aRootDir The root directory to match against.
* @param aLocalDir And optional local directory to also match against.
*/
nsIToolkitProfile getProfileByDir(in nsIFile aRootDir, [optional] in nsIFile aLocalDir);
/**
* Create a new profile.
*

View File

@@ -80,6 +80,7 @@ using namespace mozilla;
#define PROFILE_DB_VERSION "2"
#define INSTALL_PREFIX "Install"
#define INSTALL_PREFIX_LENGTH 7
#define STORE_ID_PREF "toolkit.profiles.storeID"
struct KeyValue {
KeyValue(const char* aKey, const char* aValue) : key(aKey), value(aValue) {}
@@ -357,32 +358,35 @@ nsToolkitProfile::SetStoreID(const nsACString& aStoreID) {
mSection.get(), "StoreID", PromiseFlatCString(aStoreID).get());
NS_ENSURE_SUCCESS(rv, rv);
rv = prefs->SetCharPref("toolkit.profiles.storeID", aStoreID);
rv = nsToolkitProfileService::gService->mProfileDB.SetString(
mSection.get(), "ShowSelector", mShowProfileSelector ? "1" : "0");
NS_ENSURE_SUCCESS(rv, rv);
nsToolkitProfileService::gService->mGroupProfile = this;
} else {
rv = nsToolkitProfileService::gService->mProfileDB.DeleteString(
mSection.get(), "StoreID");
if (nsToolkitProfileService::gService->mCurrent == this) {
rv = prefs->SetCharPref(STORE_ID_PREF, aStoreID);
NS_ENSURE_SUCCESS(rv, rv);
// If the string was not present in the ini file, just ignore the error.
if (rv == NS_ERROR_FAILURE) {
rv = NS_OK;
nsToolkitProfileService::gService->mGroupProfile = this;
}
} else {
// If the string was not present in the ini file, just ignore the error.
nsToolkitProfileService::gService->mProfileDB.DeleteString(mSection.get(),
"StoreID");
// We need a StoreID to show the profile selector, so if StoreID has been
// removed, then remove ShowSelector also.
mShowProfileSelector = false;
rv = nsToolkitProfileService::gService->mProfileDB.DeleteString(
mSection.get(), "ShowSelector");
if (rv == NS_ERROR_FAILURE) {
rv = NS_OK;
// If the string was not present in the ini file, just ignore the error.
nsToolkitProfileService::gService->mProfileDB.DeleteString(mSection.get(),
"ShowSelector");
if (nsToolkitProfileService::gService->mCurrent == this) {
rv = prefs->ClearUserPref(STORE_ID_PREF);
NS_ENSURE_SUCCESS(rv, rv);
nsToolkitProfileService::gService->mGroupProfile = nullptr;
}
rv = prefs->ClearUserPref("toolkit.profiles.storeID");
NS_ENSURE_SUCCESS(rv, rv);
nsToolkitProfileService::gService->mGroupProfile = nullptr;
}
mStoreID = aStoreID;
@@ -670,13 +674,15 @@ void nsToolkitProfileService::CompleteStartup() {
glean::startup::profile_database_version.Set(mStartupFileVersion);
glean::startup::profile_count.Set(static_cast<uint32_t>(mProfiles.length()));
nsresult rv;
bool needsFlush = false;
// If we started into an unmanaged profile in a profile group, set the group
// profile to be the managed profile belonging to the group.
nsresult rv;
nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
if (!mCurrent) {
nsCString storeID;
rv = prefs->GetCharPref("toolkit.profiles.storeID", storeID);
rv = prefs->GetCharPref(STORE_ID_PREF, storeID);
if (NS_SUCCEEDED(rv) && !storeID.IsEmpty()) {
mGroupProfile = GetProfileByStoreID(storeID);
}
@@ -685,32 +691,44 @@ void nsToolkitProfileService::CompleteStartup() {
// profile for some group.
if (!mCurrent->mStoreID.IsVoid()) {
mGroupProfile = mCurrent;
rv = prefs->SetCharPref("toolkit.profiles.storeID", mCurrent->mStoreID);
rv = prefs->SetCharPref(STORE_ID_PREF, mCurrent->mStoreID);
NS_ENSURE_SUCCESS_VOID(rv);
} else {
// Otherwise if the current profile has a store ID set in prefs but not in
// the database then restore it. This can happen if a version of Firefox
// prior to 67 has overwritten the database.
nsCString storeID;
rv = prefs->GetCharPref(STORE_ID_PREF, storeID);
if (NS_SUCCEEDED(rv) && !storeID.IsEmpty()) {
rv = mCurrent->SetStoreID(storeID);
if (NS_SUCCEEDED(rv)) {
needsFlush = true;
}
}
}
}
if (mMaybeLockProfile) {
nsCOMPtr<nsIToolkitShellService> shell =
do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID);
if (!shell) {
return;
if (shell) {
bool isDefaultApp;
rv = shell->IsDefaultApplication(&isDefaultApp);
if (NS_SUCCEEDED(rv) && isDefaultApp) {
mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
needsFlush = true;
}
}
}
bool isDefaultApp;
nsresult rv = shell->IsDefaultApplication(&isDefaultApp);
NS_ENSURE_SUCCESS_VOID(rv);
if (isDefaultApp) {
mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
// There is a very small chance that this could fail if something else
// overwrote the profiles database since we started up, probably less than
// a second ago. There isn't really a sane response here, all the other
// profile changes are already flushed so whether we fail to flush here or
// force quit the app makes no difference.
NS_ENSURE_SUCCESS_VOID(Flush());
}
if (needsFlush) {
// There is a very small chance that this could fail if something else
// overwrote the profiles database since we started up, probably less than
// a second ago. There isn't really a sane response here, all the other
// profile changes are already flushed so whether we fail to flush here or
// force quit the app makes no difference.
NS_ENSURE_SUCCESS_VOID(Flush());
}
}
@@ -2077,6 +2095,16 @@ void nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir,
}
}
NS_IMETHODIMP
nsToolkitProfileService::GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
nsIToolkitProfile** aResult) {
RefPtr<nsToolkitProfile> result;
GetProfileByDir(aRootDir, aLocalDir, getter_AddRefs(result));
result.forget(aResult);
return NS_OK;
}
nsresult NS_LockProfilePath(nsIFile* aPath, nsIFile* aTempPath,
nsIProfileUnlocker** aUnlocker,
nsIProfileLock** aResult) {

View File

@@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the StoreID is restored if available in prefs.
*/
add_task(
{
skip_if: () => !AppConstants.MOZ_SELECTABLE_PROFILES,
},
async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
let profilesIni = {
profiles: [
{
name: "default",
path: defaultProfile.leafName,
default: true,
},
],
installs: {
[hash]: {
default: defaultProfile.leafName,
},
},
};
writeProfilesIni(profilesIni);
Services.prefs.setCharPref("toolkit.profiles.storeID", "bishbashbosh");
let service = getProfileService();
let { profile } = selectStartupProfile();
Assert.equal(profile.rootDir.path, defaultProfile.path);
Assert.equal(service.currentProfile, profile);
Assert.equal(service.groupProfile, profile);
Assert.equal(profile.storeID, "bishbashbosh");
checkProfileService();
}
);

View File

@@ -40,6 +40,8 @@ skip-if = ["os == 'android'"]
["test_remove_default.js"]
["test_restore_storeID.js"]
["test_select_backgroundtasks_ephemeral.js"]
["test_select_backgroundtasks_not_ephemeral_create.js"]