Bug 1529879: Block changing the profile list when another process has changed it. r=froydnj,Gijs,flod

On startup we record the size and modified time of the profile lists. If
changed we refuse to flush any new changes to disk. Also adds a getter to check
if they've changed so the UI can do something sensible.

All attempts to flush are now checked for success. In some cases in early
startup the failure mode isn't great, we just quit startup. The assumption
though is that it's extremely unlikely that the files will have changed on disk
in the time between when they are read and when profile selection occurs, likely
less than a second later.

The profile reset flow is changed to only delete the old profile and flush once
all the migration has completed, so if something fails the user gets back to
their old profile.

In testing I ended up having to fix bug 1522584 so background file deletions on
a background thread are safer.

I haven't implemented any UI tests right now since making modifications to the
profiles means modifying the actual user's profiles which I'm not keen to do.
See bug 1539868.

Differential Revision: https://phabricator.services.mozilla.com/D25278

--HG--
extra : rebase_source : b9fb01c5f2faaf7d534800b700bb02b8c88af023
extra : source : ad5ac4d5c8f7240809a205be2960924813f1e705
This commit is contained in:
Dave Townsend 2019-03-05 12:51:44 -08:00
Родитель 7c8109a2b6
Коммит 2f0f64f3fb
47 изменённых файлов: 580 добавлений и 109 удалений

Просмотреть файл

@ -240,3 +240,6 @@ XPC_MSG_DEF(NS_ERROR_BLOCKED_URI , "The URI is blocked")
XPC_MSG_DEF(NS_ERROR_HARMFUL_URI , "The URI is harmful")
XPC_MSG_DEF(NS_ERROR_FINGERPRINTING_URI , "The URI is fingerprinting")
XPC_MSG_DEF(NS_ERROR_CRYPTOMINING_URI , "The URI is cryptomining")
/* Profile manager error codes */
XPC_MSG_DEF(NS_ERROR_DATABASE_CHANGED , "Flushing the profiles to disk would have overwritten changes made elsewhere.")

Просмотреть файл

@ -14,6 +14,30 @@ XPCOMUtils.defineLazyServiceGetter(
"nsIToolkitProfileService"
);
async function flush() {
try {
ProfileService.flush();
refreshUI();
} catch (e) {
let [title, msg, button] = await document.l10n.formatValues([
{ id: "profiles-flush-fail-title" },
{ id: e.result == Cr.NS_ERROR_DATABASE_CHANGED ?
"profiles-flush-conflict" :
"profiles-flush-failed" },
{ id: "profiles-flush-restart-button" },
]);
const PS = Ci.nsIPromptService;
let result = Services.prompt.confirmEx(window, title, msg,
(PS.BUTTON_POS_0 * PS.BUTTON_TITLE_CANCEL) +
(PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING),
null, button, null, null, {});
if (result == 1) {
restart(false);
}
}
}
function refreshUI() {
let parent = document.getElementById("profiles");
while (parent.firstChild) {
@ -210,8 +234,7 @@ async function renameProfile(profile) {
return;
}
ProfileService.flush();
refreshUI();
flush();
}
}
@ -280,14 +303,13 @@ async function removeProfile(profile) {
return;
}
ProfileService.flush();
refreshUI();
flush();
}
async function defaultProfile(profile) {
try {
ProfileService.defaultProfile = profile;
ProfileService.flush();
flush();
} catch (e) {
// This can happen on dev-edition.
let [title, msg] = await document.l10n.formatValues([
@ -297,7 +319,6 @@ async function defaultProfile(profile) {
Services.prompt.alert(window, title, msg);
}
refreshUI();
}
function openProfile(profile) {
@ -331,5 +352,10 @@ function restart(safeMode) {
}
window.addEventListener("DOMContentLoaded", function() {
refreshUI();
if (ProfileService.isListOutdated) {
document.getElementById("owned").hidden = true;
} else {
document.getElementById("conflict").hidden = true;
refreshUI();
}
}, {once: true});

Просмотреть файл

@ -23,12 +23,18 @@
</div>
<h1 data-l10n-id="profiles-title"></h1>
<div data-l10n-id="profiles-subtitle"></div>
<div>
<button id="create-button" data-l10n-id="profiles-create"></button>
<div id="conflict">
<p data-l10n-id="profiles-conflict" />
</div>
<div id="owned">
<div data-l10n-id="profiles-subtitle"></div>
<div id="profiles" class="tab"></div>
<div>
<button id="create-button" data-l10n-id="profiles-create"></button>
</div>
<div id="profiles" class="tab"></div>
</div>
</body>
</html>

Просмотреть файл

@ -51,3 +51,11 @@ profileDeletionFailedTitle=Deletion Failed
# Profile reset
# LOCALIZATION NOTE (resetBackupDirectory): Directory name for the profile directory backup created during reset. This directory is placed in a location users will see it (ie. their desktop). %S is the application name.
resetBackupDirectory=Old %S Data
flushFailTitle=Changes not saved
# LOCALIZATION NOTE (conflictMessage): %1$S is brandProductName, %2$S is brandShortName.
conflictMessage=Another copy of %1$S has made changes to profiles. You must restart %2$S before making more changes.
flushFailMessage=An unexpected error has prevented your changes from being saved.
# LOCALIZATION NOTE (flushFailRestartButton): $S is brandShortName.
flushFailRestartButton=Restart %S
flushFailExitButton=Exit

Просмотреть файл

@ -9,6 +9,11 @@ profiles-create = Create a New Profile
profiles-restart-title = Restart
profiles-restart-in-safe-mode = Restart with Add-ons Disabled…
profiles-restart-normal = Restart normally…
profiles-conflict = Another copy of { -brand-product-name } has made changes to profiles. You must restart { -brand-short-name } before making more changes.
profiles-flush-fail-title = Changes not saved
profiles-flush-conflict = { profiles-conflict }
profiles-flush-failed = An unexpected error has prevented your changes from being saved.
profiles-flush-restart-button = Restart { -brand-short-name }
# Variables:
# $name (String) - Name of the profile

Просмотреть файл

@ -16,6 +16,7 @@ var gDialogParams;
var gProfileManagerBundle;
var gBrandBundle;
var gProfileService;
var gNeedsFlush = false;
function startup() {
try {
@ -59,6 +60,48 @@ function startup() {
document.addEventListener("dialogcancel", exitDialog);
}
function flush(cancelled) {
updateStartupPrefs();
gDialogParams.SetInt(1, document.getElementById("offlineState").checked ? 1 : 0);
if (gNeedsFlush) {
try {
gProfileService.flush();
} catch (e) {
let productName = gBrandBundle.getString("brandProductName");
let appName = gBrandBundle.getString("brandShortName");
let title = gProfileManagerBundle.getString("flushFailTitle");
let restartButton = gProfileManagerBundle.getFormattedString("flushFailRestartButton",
[appName]);
let exitButton = gProfileManagerBundle.getString("flushFailExitButton");
let message;
if (e.result == undefined) {
message = gProfileManagerBundle.getFormattedString("conflictMessage",
[productName, appName]);
} else {
message = gProfileManagerBundle.getString("flushFailMessage");
}
const PS = Ci.nsIPromptService;
let result = Services.prompt.confirmEx(window, title, message,
(PS.BUTTON_POS_0 * PS.BUTTON_TITLE_IS_STRING) +
(PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING),
restartButton, exitButton, null, null, {});
gDialogParams.SetInt(0, result == 0 ? Ci.nsIToolkitProfileService.restart
: Ci.nsIToolkitProfileService.exit);
return;
}
gNeedsFlush = false;
}
gDialogParams.SetInt(0, cancelled ? Ci.nsIToolkitProfileService.exit
: Ci.nsIToolkitProfileService.launchWithProfile);
}
function acceptDialog(event) {
var appName = gBrandBundle.getString("brandShortName");
@ -76,26 +119,28 @@ function acceptDialog(event) {
gDialogParams.objects.insertElementAt(selectedProfile.profile.rootDir, 0);
gDialogParams.objects.insertElementAt(selectedProfile.profile.localDir, 1);
try {
gProfileService.defaultProfile = selectedProfile.profile;
} catch (e) {
// This can happen on dev-edition. We'll still restart with the selected
// profile based on the lock's directories.
if (gProfileService.defaultProfile != selectedProfile.profile) {
try {
gProfileService.defaultProfile = selectedProfile.profile;
gNeedsFlush = true;
} catch (e) {
// This can happen on dev-edition. We'll still restart with the selected
// profile based on the lock's directories.
}
}
updateStartupPrefs();
gDialogParams.SetInt(0, 1);
/* Bug 257777 */
gDialogParams.SetInt(1, document.getElementById("offlineState").checked ? 1 : 0);
flush(false);
}
function exitDialog() {
updateStartupPrefs();
flush(true);
}
function updateStartupPrefs() {
var autoSelectLastProfile = document.getElementById("autoSelectLastProfile");
gProfileService.startWithLastProfile = autoSelectLastProfile.checked;
if (gProfileService.startWithLastProfile != autoSelectLastProfile.checked) {
gProfileService.startWithLastProfile = autoSelectLastProfile.checked;
gNeedsFlush = true;
}
}
// handle key event on listboxes
@ -139,6 +184,8 @@ function CreateProfile(aProfile) {
profilesElement.ensureElementIsVisible(listitem);
profilesElement.selectItem(listitem);
gNeedsFlush = true;
}
// rename the selected profile
@ -167,6 +214,7 @@ function RenameProfile() {
try {
selectedProfile.name = newName;
gNeedsFlush = true;
} catch (e) {
var alTitle = gProfileManagerBundle.getString("profileNameInvalidTitle");
var alMsg = gProfileManagerBundle.getFormattedString("profileNameInvalid", [newName]);
@ -220,6 +268,7 @@ function ConfirmDelete() {
try {
selectedProfile.remove(deleteFiles);
gNeedsFlush = true;
} catch (e) {
let title = gProfileManagerBundle.getString("profileDeletionFailedTitle");
let msg = gProfileManagerBundle.getString("profileDeletionFailed");

Просмотреть файл

@ -13,6 +13,12 @@ interface nsIProfileLock;
[scriptable, builtinclass, uuid(1947899b-f369-48fa-89da-f7c37bb1e6bc)]
interface nsIToolkitProfileService : nsISupports
{
/**
* Tests whether the profile lists on disk have changed since they were
* loaded. When this is true attempts to flush changes to disk will fail.
*/
[infallible] readonly attribute boolean isListOutdated;
/**
* When a downgrade is detected UI is presented to the user to ask how to
* proceed. These flags are used to pass some information to the UI.
@ -30,6 +36,12 @@ interface nsIToolkitProfileService : nsISupports
createNewProfile = 1,
};
cenum profileManagerResult: 8 {
exit = 0,
launchWithProfile = 1,
restart = 2,
};
attribute boolean startWithLastProfile;
readonly attribute nsISimpleEnumerator /*nsIToolkitProfile*/ profiles;
@ -128,7 +140,9 @@ interface nsIToolkitProfileService : nsISupports
readonly attribute unsigned long profileCount;
/**
* Flush the profiles list file.
* Flush the profiles list file. This will fail with
* NS_ERROR_DATABASE_CHANGED if the files on disk have changed since the
* profiles were loaded.
*/
void flush();
};

Просмотреть файл

@ -46,6 +46,7 @@
#include "mozilla/UniquePtr.h"
#include "nsIToolkitShellService.h"
#include "mozilla/Telemetry.h"
#include "nsProxyRelease.h"
using namespace mozilla;
@ -82,6 +83,52 @@ nsTArray<UniquePtr<KeyValue>> GetSectionStrings(nsINIParser* aParser,
return result;
}
void RemoveProfileFiles(nsIToolkitProfile* aProfile, bool aInBackground) {
nsCOMPtr<nsIFile> rootDir;
aProfile->GetRootDir(getter_AddRefs(rootDir));
nsCOMPtr<nsIFile> localDir;
aProfile->GetLocalDir(getter_AddRefs(localDir));
// Just lock the directories, don't mark the profile as locked or the lock
// will attempt to release its reference to the profile on the background
// thread which will assert.
nsCOMPtr<nsIProfileLock> lock;
nsresult rv =
NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock));
NS_ENSURE_SUCCESS_VOID(rv);
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"nsToolkitProfile::RemoveProfileFiles",
[rootDir, localDir, lock]() mutable {
bool equals;
nsresult rv = rootDir->Equals(localDir, &equals);
// The root dir might contain the temp dir, so remove
// the temp dir first.
if (NS_SUCCEEDED(rv) && !equals) {
localDir->Remove(true);
}
// Ideally we'd unlock after deleting but since the lock is a file
// in the profile we must unlock before removing.
lock->Unlock();
// nsIProfileLock is not threadsafe so release our reference to it on
// the main thread.
NS_ReleaseOnMainThreadSystemGroup(
"nsToolkitProfile::RemoveProfileFiles::Unlock", lock.forget());
rv = rootDir->Remove(true);
NS_ENSURE_SUCCESS_VOID(rv);
});
if (aInBackground) {
nsCOMPtr<nsIEventTarget> target =
do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
target->Dispatch(runnable, NS_DISPATCH_NORMAL);
} else {
runnable->Run();
}
}
nsToolkitProfile::nsToolkitProfile(const nsACString& aName, nsIFile* aRootDir,
nsIFile* aLocalDir, bool aFromDB)
: mName(aName),
@ -178,38 +225,7 @@ nsresult nsToolkitProfile::RemoveInternal(bool aRemoveFiles,
}
if (aRemoveFiles) {
// Check if another instance is using this profile.
nsCOMPtr<nsIProfileLock> lock;
nsresult rv = Lock(nullptr, getter_AddRefs(lock));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> rootDir(mRootDir);
nsCOMPtr<nsIFile> localDir(mLocalDir);
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
"nsToolkitProfile::RemoveInternal", [rootDir, localDir, lock]() {
bool equals;
nsresult rv = rootDir->Equals(localDir, &equals);
// The root dir might contain the temp dir, so remove
// the temp dir first.
if (NS_SUCCEEDED(rv) && !equals) {
localDir->Remove(true);
}
// Ideally we'd unlock after deleting but since the lock is a file
// in the profile we must unlock before removing.
lock->Unlock();
rootDir->Remove(true);
});
if (aInBackground) {
nsCOMPtr<nsIEventTarget> target =
do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
target->Dispatch(runnable, NS_DISPATCH_NORMAL);
} else {
runnable->Run();
}
RemoveProfileFiles(this, aInBackground);
}
nsINIParser* db = &nsToolkitProfileService::gService->mProfileDB;
@ -373,7 +389,13 @@ nsToolkitProfileService::nsToolkitProfileService()
mCreatedAlternateProfile(false),
mStartupReason(NS_LITERAL_STRING("unknown")),
mMaybeLockProfile(false),
mUpdateChannel(NS_STRINGIFY(MOZ_UPDATE_CHANNEL)) {
mUpdateChannel(NS_STRINGIFY(MOZ_UPDATE_CHANNEL)),
mProfileDBExists(false),
mProfileDBFileSize(0),
mProfileDBModifiedTime(0),
mInstallDBExists(false),
mInstallDBFileSize(0),
mInstallDBModifiedTime(0) {
#ifdef MOZ_DEV_EDITION
mUseDevEditionProfile = true;
#endif
@ -406,7 +428,13 @@ void nsToolkitProfileService::CompleteStartup() {
if (isDefaultApp) {
mProfileDB.SetString(mInstallSection.get(), "Locked", "1");
Flush();
// 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());
}
}
}
@ -484,20 +512,22 @@ bool nsToolkitProfileService::IsProfileForCurrentInstall(
* default install or the profile has been explicitely chosen by some other
* means then we won't use it.
*
* Returns true if we chose to make the profile the new dedicated default.
* aResult will be set to true if we chose to make the profile the new dedicated
* default.
*/
bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
nsIToolkitProfile* aProfile) {
nsresult nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
nsIToolkitProfile* aProfile, bool* aResult) {
nsresult rv;
*aResult = false;
// If the profile was last used by a different install then we won't use it.
if (!IsProfileForCurrentInstall(aProfile)) {
return false;
return NS_OK;
}
nsCString descriptor;
rv = GetProfileDescriptor(aProfile, descriptor, nullptr);
NS_ENSURE_SUCCESS(rv, false);
NS_ENSURE_SUCCESS(rv, rv);
// Get a list of all the installs.
nsTArray<nsCString> installs = GetKnownInstalls();
@ -524,7 +554,7 @@ bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
nsCString isLocked;
rv = mProfileDB.GetString(install.get(), "Locked", isLocked);
if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) {
return false;
return NS_OK;
}
inUseInstalls.AppendElement(install);
@ -547,13 +577,92 @@ bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
mProfileDB.DeleteString(mInstallSection.get(), "Locked");
// Persist the changes.
Flush();
rv = Flush();
NS_ENSURE_SUCCESS(rv, rv);
// Once XPCOM is available check if this is the default application and if so
// lock the profile again.
mMaybeLockProfile = true;
*aResult = true;
return true;
return NS_OK;
}
bool
IsFileOutdated(nsIFile* aFile, bool aExists, PRTime aLastModified,
int64_t aLastSize) {
nsCOMPtr<nsIFile> file;
nsresult rv = aFile->Clone(getter_AddRefs(file));
if (NS_FAILED(rv)) {
return false;
}
bool exists;
rv = aFile->Exists(&exists);
if (NS_FAILED(rv) || exists != aExists) {
return true;
}
if (!exists) {
return false;
}
int64_t size;
rv = aFile->GetFileSize(&size);
if (NS_FAILED(rv) || size != aLastSize) {
return true;
}
PRTime time;
rv = aFile->GetLastModifiedTime(&time);
if (NS_FAILED(rv) || time != aLastModified) {
return true;
}
return false;
}
nsresult
UpdateFileStats(nsIFile* aFile, bool* aExists, PRTime* aLastModified,
int64_t* aLastSize) {
nsCOMPtr<nsIFile> file;
nsresult rv = aFile->Clone(getter_AddRefs(file));
NS_ENSURE_SUCCESS(rv, rv);
rv = file->Exists(aExists);
NS_ENSURE_SUCCESS(rv, rv);
if (!(*aExists)) {
*aLastModified = 0;
*aLastSize = 0;
return NS_OK;
}
rv = file->GetFileSize(aLastSize);
NS_ENSURE_SUCCESS(rv, rv);
rv = file->GetLastModifiedTime(aLastModified);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMETHODIMP
nsToolkitProfileService::GetIsListOutdated(bool *aResult) {
if (IsFileOutdated(mProfileDBFile, mProfileDBExists, mProfileDBModifiedTime,
mProfileDBFileSize)) {
*aResult = true;
return NS_OK;
}
if (IsFileOutdated(mInstallDBFile, mInstallDBExists, mInstallDBModifiedTime,
mInstallDBFileSize)) {
*aResult = true;
return NS_OK;
}
*aResult = false;
return NS_OK;
}
struct ImportInstallsClosure {
@ -605,11 +714,18 @@ nsresult nsToolkitProfileService::Init() {
rv = mInstallDBFile->AppendNative(NS_LITERAL_CSTRING("installs.ini"));
NS_ENSURE_SUCCESS(rv, rv);
rv = UpdateFileStats(mInstallDBFile, &mInstallDBExists,
&mInstallDBModifiedTime, &mInstallDBFileSize);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString buffer;
bool exists;
rv = mProfileDBFile->IsFile(&exists);
if (NS_SUCCEEDED(rv) && exists) {
rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
&mProfileDBModifiedTime, &mProfileDBFileSize);
if (NS_SUCCEEDED(rv) && mProfileDBExists) {
mProfileDBFile->GetFileSize(&mProfileDBFileSize);
mProfileDBFile->GetLastModifiedTime(&mProfileDBModifiedTime);
rv = mProfileDB.Init(mProfileDBFile);
// Init does not fail on parsing errors, only on OOM/really unexpected
// conditions.
@ -628,9 +744,7 @@ nsresult nsToolkitProfileService::Init() {
// any install data from the backup.
nsINIParser installDB;
rv = mInstallDBFile->IsFile(&exists);
if (NS_SUCCEEDED(rv) && exists &&
NS_SUCCEEDED(installDB.Init(mInstallDBFile))) {
if (mInstallDBExists && NS_SUCCEEDED(installDB.Init(mInstallDBFile))) {
// There is install data to import.
ImportInstallsClosure closure = {&installDB, &mProfileDB};
installDB.GetSections(&ImportInstalls, &closure);
@ -1059,7 +1173,10 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
// profile is the previous default so we should either make it the
// default profile for this install or push the user to a new profile.
if (MaybeMakeDefaultDedicatedProfile(profile)) {
bool result;
rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
NS_ENSURE_SUCCESS(rv, rv);
if (result) {
mStartupReason = NS_LITERAL_STRING("restart-claimed-default");
mCurrent = profile;
@ -1078,7 +1195,8 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
return rv;
}
Flush();
rv = Flush();
NS_ENSURE_SUCCESS(rv, rv);
mStartupReason = NS_LITERAL_STRING("restart-skipped-default");
*aDidCreate = true;
@ -1182,14 +1300,10 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
getter_AddRefs(profile));
}
// Some pathological arguments can make it this far
if (NS_FAILED(rv)) {
if (NS_FAILED(rv) || NS_FAILED(Flush())) {
PR_fprintf(PR_STDERR, "Error creating profile.\n");
return rv;
}
rv = NS_ERROR_ABORT;
Flush();
return rv;
return NS_ERROR_ABORT;
}
// Check the -p command line argument. It either accepts a profile name and
@ -1283,7 +1397,10 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
// generated by bug 1518591) or it is from an ancient version. We'll opt
// to leave it for older versions in this case.
if (exists) {
if (MaybeMakeDefaultDedicatedProfile(profile)) {
bool result;
rv = MaybeMakeDefaultDedicatedProfile(profile, &result);
NS_ENSURE_SUCCESS(rv, rv);
if (result) {
mStartupReason = NS_LITERAL_STRING("firstrun-claimed-default");
mCurrent = profile;
@ -1316,7 +1433,8 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
SetNormalDefault(newProfile);
}
Flush();
rv = Flush();
NS_ENSURE_SUCCESS(rv, rv);
if (mCreatedAlternateProfile) {
mStartupReason = NS_LITERAL_STRING("firstrun-skipped-default");
@ -1377,12 +1495,11 @@ nsresult nsToolkitProfileService::CreateResetProfile(
newProfileName, getter_AddRefs(newProfile));
if (NS_FAILED(rv)) return rv;
rv = Flush();
if (NS_FAILED(rv)) return rv;
mCurrent = newProfile;
newProfile.forget(aNewProfile);
// Don't flush the changes yet. That will happen once the migration
// successfully completes.
return NS_OK;
}
@ -1419,6 +1536,8 @@ nsresult nsToolkitProfileService::ApplyResetProfile(
nsresult rv = aOldProfile->GetName(name);
NS_ENSURE_SUCCESS(rv, rv);
// Don't remove the old profile's files until after we've successfully flushed
// the profile changes to disk.
rv = aOldProfile->Remove(false);
NS_ENSURE_SUCCESS(rv, rv);
@ -1427,7 +1546,15 @@ nsresult nsToolkitProfileService::ApplyResetProfile(
rv = mCurrent->SetName(name);
NS_ENSURE_SUCCESS(rv, rv);
return Flush();
rv = Flush();
NS_ENSURE_SUCCESS(rv, rv);
// Now that the profile changes are flushed, try to remove the old profile's
// files. If we fail the worst that will happen is that an orphan directory is
// left. Let this run in the background while we start up.
RemoveProfileFiles(aOldProfile, true);
return NS_OK;
}
NS_IMETHODIMP
@ -1696,6 +1823,10 @@ nsToolkitProfileService::GetProfileCount(uint32_t* aResult) {
NS_IMETHODIMP
nsToolkitProfileService::Flush() {
if (GetIsListOutdated()) {
return NS_ERROR_DATABASE_CHANGED;
}
nsresult rv;
// If we aren't using dedicated profiles then nothing about the list of
@ -1739,18 +1870,27 @@ nsToolkitProfileService::Flush() {
}
fclose(writeFile);
rv = UpdateFileStats(mInstallDBFile, &mInstallDBExists,
&mInstallDBModifiedTime, &mInstallDBFileSize);
NS_ENSURE_SUCCESS(rv, rv);
} else {
rv = mInstallDBFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
mInstallDBExists = false;
}
}
rv = mProfileDB.WriteToFile(mProfileDBFile);
NS_ENSURE_SUCCESS(rv, rv);
rv = UpdateFileStats(mProfileDBFile, &mProfileDBExists,
&mProfileDBModifiedTime, &mProfileDBFileSize);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}

Просмотреть файл

@ -102,7 +102,8 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
nsACString& aDescriptor, bool* aIsRelative);
bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
bool MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile);
nsresult MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile,
bool* aResult);
bool IsSnapEnvironment();
nsresult CreateDefaultProfile(nsIToolkitProfile** aResult);
void SetNormalDefault(nsIToolkitProfile* aProfile);
@ -149,10 +150,17 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
bool mCreatedAlternateProfile;
nsString mStartupReason;
bool mMaybeLockProfile;
// Holds the current application update channel. This is only really held
// so it can be overriden in tests.
nsCString mUpdateChannel;
// Keep track of some attributes of the databases so we can tell if another
// process has changed them.
bool mProfileDBExists;
int64_t mProfileDBFileSize;
PRTime mProfileDBModifiedTime;
bool mInstallDBExists;
int64_t mInstallDBFileSize;
PRTime mInstallDBModifiedTime;
static nsToolkitProfileService* gService;

Просмотреть файл

@ -1,6 +1,5 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
http://creativecommons.org/publicdomain/zero/1.0/ */
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
@ -103,7 +102,7 @@ function selectStartupProfile(args = [], isResetting = false) {
if (profile.value) {
Assert.ok(rootDir.value.equals(profile.value.rootDir), "Should have matched the root dir.");
Assert.ok(localDir.value.equals(profile.value.localDir), "Should have matched the local dir.");
Assert.equal(service.currentProfile, profile.value, "Should have marked the profile as the current profile.");
Assert.ok(service.currentProfile === profile.value, "Should have marked the profile as the current profile.");
} else {
Assert.ok(!service.currentProfile, "Should be no current profile.");
}
@ -333,7 +332,6 @@ function readInstallsIni() {
};
if (!target.exists()) {
dump("Missing installs.ini\n");
return installData;
}

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that when the profiles DB is missing the install data we reload it.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile already locked to a different install
* isn't claimed by this install.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests from a clean state.
* Then does some testing that creating new profiles and marking them as

Просмотреть файл

@ -0,0 +1,52 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the profile service refuses to flush when the install.ini file
* has been modified.
*/
function check_unchanged(service) {
Assert.ok(!service.isListOutdated, "Should not have detected a modification.");
try {
service.flush();
Assert.ok(true, "Should have flushed.");
} catch (e) {
Assert.ok(false, "Should have succeeded flushing.");
}
}
function check_outdated(service) {
Assert.ok(service.isListOutdated, "Should have detected a modification.");
try {
service.flush();
Assert.ok(false, "Should have failed to flush.");
} catch (e) {
Assert.equal(e.result, Cr.NS_ERROR_DATABASE_CHANGED, "Should have refused to flush.");
}
}
add_task(async () => {
let service = getProfileService();
Assert.ok(!service.isListOutdated, "Should not be modified yet.");
let installsini = gDataHome.clone();
installsini.append("installs.ini");
Assert.ok(!installsini.exists(), "File should not exist yet.");
installsini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
check_outdated(service);
installsini.remove(false);
// We have to do profile selection to actually have any install data.
selectStartupProfile();
check_unchanged(service);
let oldTime = installsini.lastModifiedTime;
installsini.lastModifiedTime = oldTime - 10000;
check_outdated(service);
// We can't reset the modification time back to exactly what it was, so I
// guess we can't do much more here :(
});

Просмотреть файл

@ -0,0 +1,50 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the profile service refuses to flush when the profiles.ini file
* has been modified.
*/
function check_unchanged(service) {
Assert.ok(!service.isListOutdated, "Should not have detected a modification.");
try {
service.flush();
Assert.ok(true, "Should have flushed.");
} catch (e) {
Assert.ok(false, "Should have succeeded flushing.");
}
}
function check_outdated(service) {
Assert.ok(service.isListOutdated, "Should have detected a modification.");
try {
service.flush();
Assert.ok(false, "Should have failed to flush.");
} catch (e) {
Assert.equal(e.result, Cr.NS_ERROR_DATABASE_CHANGED, "Should have refused to flush.");
}
}
add_task(async () => {
let service = getProfileService();
Assert.ok(!service.isListOutdated, "Should not be modified yet.");
let profilesini = gDataHome.clone();
profilesini.append("profiles.ini");
Assert.ok(!profilesini.exists(), "File should not exist yet.");
profilesini.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
check_outdated(service);
profilesini.remove(false);
check_unchanged(service);
let oldTime = profilesini.lastModifiedTime;
profilesini.lastModifiedTime = oldTime - 10000;
check_outdated(service);
// We can't reset the modification time back to exactly what it was, so I
// guess we can't do much more here :(
});

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that from an empty database a default profile is created.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that when the default application claims the old-style default profile
* it locks it to itself.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* When profiles.ini is missing there isn't any point in restoring from any
* installs.ini, the profiles it refers to are gone anyway.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile previously used by this build gets
* updated to a dedicated profile for this build.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* If install.ini lists a default profile for this build but that profile no
* longer exists don't try to steal the old-style default even if it was used

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that from an empty database profile reset doesn't create a new profile.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests adding and removing functions correctly.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that calling nsIToolkitProfile.remove on the default profile correctly
* removes the profile.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that from a database of profiles the default profile is selected.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the environment variables are used to select a profile.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the environment variables are used to select a profile.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that when choosing an unknown profile the profile manager is shown.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that from a database of profiles the correct profile is selected.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that when passing the -P command line argument and not passing a
* profile name the profile manager is opened.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that when requested the profile manager is shown.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Previous versions of Firefox automatically used a single profile even if it
* wasn't marked as the default. So we should try to upgrade that one if it was

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Previous versions of Firefox automatically used a single profile even if it
* wasn't marked as the default. So we should try to upgrade that one if it was

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the environment variables are used to select a profile and that
* on the first run of a dedicated profile build we don't snatch it if it is

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile not previously used by this build gets
* used in a snap environment.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that from a clean slate snap builds create an appropriate profile.
*/

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the environment variables are used to select a profile and that
* on the first run of a dedicated profile build we snatch it if it was the

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that the environment variables are used to select a profile and that
* on the first run of a dedicated profile build we snatch it if it was the
@ -50,12 +53,12 @@ add_task(async () => {
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(rootDir.equals(root), "Should have selected the right root dir.");
Assert.ok(localDir.equals(local), "Should have selected the right local dir.");
Assert.ok(profile, "A named profile matches this.");
Assert.ok(!!profile, "A named profile matches this.");
Assert.equal(profile.name, PROFILE_DEFAULT, "The right profile was matched.");
let service = getProfileService();
Assert.equal(service.defaultProfile, profile, "Should be the default profile.");
Assert.equal(service.currentProfile, profile, "Should be the current profile.");
Assert.ok(service.defaultProfile === profile, "Should be the default profile.");
Assert.ok(service.currentProfile === profile, "Should be the current profile.");
profileData = readProfilesIni();
Assert.equal(profileData.profiles[0].name, PROFILE_DEFAULT, "Should be the right profile.");

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that if profiles.ini is set to not start with the last profile then
* we show the profile manager in preference to assigning the old default.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile previously used by this build but
* that has already been claimed by a different build gets stolen by this build.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile previously used by this build gets
* updated to a dedicated profile for this build.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile not previously used by any build
* doesn't get updated to a dedicated profile for this build and we don't set

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Tests that an old-style default profile not previously used by this build gets
* ignored.

Просмотреть файл

@ -1,3 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that if installs.ini lists a profile we use it as the default.
*/

Просмотреть файл

@ -35,3 +35,5 @@ skip-if = devedition
[test_check_backup.js]
[test_missing_profilesini.js]
[test_remove.js]
[test_conflict_profiles.js]
[test_conflict_installs.js]

Просмотреть файл

@ -47,10 +47,12 @@ class ProfileResetCleanupAsyncTask : public mozilla::Runnable {
* local profile dir.
*/
NS_IMETHOD Run() override {
// Copy to the destination then delete the profile. A move doesn't follow
// links.
// Copy profile's files to the destination. The profile folder will be
// removed after the changes to the known profiles have been flushed to disk
// in nsToolkitProfileService::ApplyResetProfile which isn't called until
// after this thread finishes copying the files.
nsresult rv = mProfileDir->CopyToFollowingLinks(mTargetDir, mLeafName);
if (NS_SUCCEEDED(rv)) rv = mProfileDir->Remove(true);
// I guess we just warn if we fail to make the backup?
if (NS_WARN_IF(NS_FAILED(rv))) {
NS_WARNING("Could not backup the root profile directory");
}
@ -62,8 +64,9 @@ class ProfileResetCleanupAsyncTask : public mozilla::Runnable {
nsresult rvLocal = mProfileDir->Equals(mProfileLocalDir, &sameDir);
if (NS_SUCCEEDED(rvLocal) && !sameDir) {
rvLocal = mProfileLocalDir->Remove(true);
if (NS_FAILED(rvLocal))
if (NS_FAILED(rvLocal)) {
NS_WARNING("Could not remove the old local profile directory (cache)");
}
}
gProfileResetCleanupCompleted = true;

Просмотреть файл

@ -1838,6 +1838,7 @@ static ReturnAbortOnError ShowProfileManager(
nsCOMPtr<nsIFile> profD, profLD;
bool offline = false;
int32_t dialogReturn;
{
ScopedXPCOMStartup xpcom;
@ -1879,11 +1880,10 @@ static ReturnAbortOnError ShowProfileManager(
NS_ENSURE_SUCCESS_LOG(rv, rv);
aProfileSvc->Flush();
int32_t dialogConfirmed;
rv = ioParamBlock->GetInt(0, &dialogConfirmed);
if (NS_FAILED(rv) || dialogConfirmed == 0) return NS_ERROR_ABORT;
rv = ioParamBlock->GetInt(0, &dialogReturn);
if (NS_FAILED(rv) || dialogReturn == nsIToolkitProfileService::exit) {
return NS_ERROR_ABORT;
}
int32_t startOffline;
rv = ioParamBlock->GetInt(1, &startOffline);
@ -1899,13 +1899,21 @@ static ReturnAbortOnError ShowProfileManager(
}
}
SaveFileToEnv("XRE_PROFILE_PATH", profD);
SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD);
SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
if (offline) {
SaveToEnv("XRE_START_OFFLINE=1");
}
// User requested that we restart back into the profile manager.
if (dialogReturn == nsIToolkitProfileService::restart) {
SaveToEnv("XRE_RESTART_TO_PROFILE_MANAGER=1");
SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
} else {
MOZ_ASSERT(dialogReturn == nsIToolkitProfileService::launchWithProfile);
SaveFileToEnv("XRE_PROFILE_PATH", profD);
SaveFileToEnv("XRE_PROFILE_LOCAL_PATH", profLD);
SaveToEnv("XRE_RESTARTED_BY_PROFILE_MANAGER=1");
}
if (gRestartedByOS) {
// Re-add this argument when actually starting the application.
char** newArgv =
@ -2011,6 +2019,10 @@ static nsresult SelectProfile(nsToolkitProfileService* aProfileSvc,
gDoMigration = true;
}
if (EnvHasValue("XRE_RESTART_TO_PROFILE_MANAGER")) {
return ShowProfileManager(aProfileSvc, aNative);
}
// Ask the profile manager to select the profile directories to use.
bool didCreate = false;
rv = aProfileSvc->SelectStartupProfile(&gArgc, gArgv, gDoProfileReset,
@ -4361,8 +4373,9 @@ nsresult XREMain::XRE_mainRun() {
if (gDoProfileReset) {
nsresult backupCreated =
ProfileResetCleanup(mProfileSvc, gResetOldProfile);
if (NS_FAILED(backupCreated))
if (NS_FAILED(backupCreated)) {
NS_WARNING("Could not cleanup the profile that was reset");
}
}
}

Просмотреть файл

@ -756,6 +756,7 @@ with modules["XPCONNECT"]:
with modules["PROFILE"]:
errors["NS_ERROR_LAUNCHED_CHILD_PROCESS"] = FAILURE(200)
errors["NS_ERROR_SHOW_PROFILE_MANAGER"] = FAILURE(201)
errors["NS_ERROR_DATABASE_CHANGED"] = FAILURE(202)
# =======================================================================