From 2f0f64f3fbc08aefde12ab7e4730b29f43b52c3d Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Tue, 5 Mar 2019 12:51:44 -0800 Subject: [PATCH] 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 --- js/xpconnect/src/xpc.msg | 3 + toolkit/content/aboutProfiles.js | 40 ++- toolkit/content/aboutProfiles.xhtml | 14 +- .../profile/profileSelection.properties | 8 + .../en-US/toolkit/about/aboutProfiles.ftl | 5 + toolkit/profile/content/profileSelection.js | 73 ++++- toolkit/profile/nsIToolkitProfileService.idl | 16 +- toolkit/profile/nsToolkitProfileService.cpp | 264 ++++++++++++++---- toolkit/profile/nsToolkitProfileService.h | 12 +- toolkit/profile/xpcshell/head.js | 6 +- toolkit/profile/xpcshell/test_check_backup.js | 3 + toolkit/profile/xpcshell/test_claim_locked.js | 3 + toolkit/profile/xpcshell/test_clean.js | 3 + .../xpcshell/test_conflict_installs.js | 52 ++++ .../xpcshell/test_conflict_profiles.js | 50 ++++ .../profile/xpcshell/test_create_default.js | 3 + toolkit/profile/xpcshell/test_lock.js | 3 + .../xpcshell/test_missing_profilesini.js | 3 + toolkit/profile/xpcshell/test_new_default.js | 3 + .../xpcshell/test_previous_dedicated.js | 3 + .../profile/xpcshell/test_profile_reset.js | 3 + toolkit/profile/xpcshell/test_remove.js | 3 + .../profile/xpcshell/test_remove_default.js | 3 + .../profile/xpcshell/test_select_default.js | 3 + .../xpcshell/test_select_environment.js | 3 + .../xpcshell/test_select_environment_named.js | 3 + .../profile/xpcshell/test_select_missing.js | 3 + toolkit/profile/xpcshell/test_select_named.js | 3 + .../profile/xpcshell/test_select_noname.js | 3 + .../xpcshell/test_select_profilemanager.js | 3 + .../xpcshell/test_single_profile_selected.js | 3 + .../test_single_profile_unselected.js | 3 + .../xpcshell/test_skip_locked_environment.js | 3 + toolkit/profile/xpcshell/test_snap.js | 3 + toolkit/profile/xpcshell/test_snap_empty.js | 3 + .../xpcshell/test_snatch_environment.js | 3 + .../test_snatch_environment_default.js | 9 +- .../profile/xpcshell/test_startswithlast.js | 3 + toolkit/profile/xpcshell/test_steal_inuse.js | 3 + .../test_update_selected_dedicated.js | 3 + .../xpcshell/test_update_unknown_dedicated.js | 3 + .../test_update_unselected_dedicated.js | 3 + .../profile/xpcshell/test_use_dedicated.js | 3 + toolkit/profile/xpcshell/xpcshell.ini | 2 + toolkit/xre/ProfileReset.h | 11 +- toolkit/xre/nsAppRunner.cpp | 33 ++- xpcom/base/ErrorList.py | 1 + 47 files changed, 580 insertions(+), 109 deletions(-) create mode 100644 toolkit/profile/xpcshell/test_conflict_installs.js create mode 100644 toolkit/profile/xpcshell/test_conflict_profiles.js diff --git a/js/xpconnect/src/xpc.msg b/js/xpconnect/src/xpc.msg index 966dc90c77af..db0bc091904e 100644 --- a/js/xpconnect/src/xpc.msg +++ b/js/xpconnect/src/xpc.msg @@ -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.") diff --git a/toolkit/content/aboutProfiles.js b/toolkit/content/aboutProfiles.js index ae4c25db1201..cc83eff8e0cc 100644 --- a/toolkit/content/aboutProfiles.js +++ b/toolkit/content/aboutProfiles.js @@ -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}); diff --git a/toolkit/content/aboutProfiles.xhtml b/toolkit/content/aboutProfiles.xhtml index ad2010e22c5c..631e5c984a0b 100644 --- a/toolkit/content/aboutProfiles.xhtml +++ b/toolkit/content/aboutProfiles.xhtml @@ -23,12 +23,18 @@

-
-
- +
+

+
+
-
+
+ +
+ +
+
diff --git a/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties b/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties index 030d359ed277..242c8c78b251 100644 --- a/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties +++ b/toolkit/locales/en-US/chrome/mozapps/profile/profileSelection.properties @@ -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 diff --git a/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl b/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl index fbe02f2baaec..4428da0d662d 100644 --- a/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl +++ b/toolkit/locales/en-US/toolkit/about/aboutProfiles.ftl @@ -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 diff --git a/toolkit/profile/content/profileSelection.js b/toolkit/profile/content/profileSelection.js index f8706be25dc2..cfe5bd9064e7 100644 --- a/toolkit/profile/content/profileSelection.js +++ b/toolkit/profile/content/profileSelection.js @@ -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"); diff --git a/toolkit/profile/nsIToolkitProfileService.idl b/toolkit/profile/nsIToolkitProfileService.idl index b554ca4713f7..dbfe3eb44625 100644 --- a/toolkit/profile/nsIToolkitProfileService.idl +++ b/toolkit/profile/nsIToolkitProfileService.idl @@ -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(); }; diff --git a/toolkit/profile/nsToolkitProfileService.cpp b/toolkit/profile/nsToolkitProfileService.cpp index f486f257476e..fa4fc707d938 100644 --- a/toolkit/profile/nsToolkitProfileService.cpp +++ b/toolkit/profile/nsToolkitProfileService.cpp @@ -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> GetSectionStrings(nsINIParser* aParser, return result; } +void RemoveProfileFiles(nsIToolkitProfile* aProfile, bool aInBackground) { + nsCOMPtr rootDir; + aProfile->GetRootDir(getter_AddRefs(rootDir)); + nsCOMPtr 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 lock; + nsresult rv = + NS_LockProfilePath(rootDir, localDir, nullptr, getter_AddRefs(lock)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr 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 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 lock; - nsresult rv = Lock(nullptr, getter_AddRefs(lock)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr rootDir(mRootDir); - nsCOMPtr localDir(mLocalDir); - - nsCOMPtr 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 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 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 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 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; } diff --git a/toolkit/profile/nsToolkitProfileService.h b/toolkit/profile/nsToolkitProfileService.h index d4983377e54f..d785ad0e5a6f 100644 --- a/toolkit/profile/nsToolkitProfileService.h +++ b/toolkit/profile/nsToolkitProfileService.h @@ -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; diff --git a/toolkit/profile/xpcshell/head.js b/toolkit/profile/xpcshell/head.js index fa3c18b5701a..8d96996c5f3f 100644 --- a/toolkit/profile/xpcshell/head.js +++ b/toolkit/profile/xpcshell/head.js @@ -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; } diff --git a/toolkit/profile/xpcshell/test_check_backup.js b/toolkit/profile/xpcshell/test_check_backup.js index a184608552a2..b490c8362244 100644 --- a/toolkit/profile/xpcshell/test_check_backup.js +++ b/toolkit/profile/xpcshell/test_check_backup.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_claim_locked.js b/toolkit/profile/xpcshell/test_claim_locked.js index 751303655884..08957e7f15ea 100644 --- a/toolkit/profile/xpcshell/test_claim_locked.js +++ b/toolkit/profile/xpcshell/test_claim_locked.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_clean.js b/toolkit/profile/xpcshell/test_clean.js index 9d566bf7406b..06029f21b3fe 100644 --- a/toolkit/profile/xpcshell/test_clean.js +++ b/toolkit/profile/xpcshell/test_clean.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_conflict_installs.js b/toolkit/profile/xpcshell/test_conflict_installs.js new file mode 100644 index 000000000000..35970b7ee18f --- /dev/null +++ b/toolkit/profile/xpcshell/test_conflict_installs.js @@ -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 :( +}); diff --git a/toolkit/profile/xpcshell/test_conflict_profiles.js b/toolkit/profile/xpcshell/test_conflict_profiles.js new file mode 100644 index 000000000000..7762832d6f68 --- /dev/null +++ b/toolkit/profile/xpcshell/test_conflict_profiles.js @@ -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 :( +}); diff --git a/toolkit/profile/xpcshell/test_create_default.js b/toolkit/profile/xpcshell/test_create_default.js index 0b544fbd074e..2f7015f654f4 100644 --- a/toolkit/profile/xpcshell/test_create_default.js +++ b/toolkit/profile/xpcshell/test_create_default.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_lock.js b/toolkit/profile/xpcshell/test_lock.js index 065688c0d96f..276caa5af42c 100644 --- a/toolkit/profile/xpcshell/test_lock.js +++ b/toolkit/profile/xpcshell/test_lock.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_missing_profilesini.js b/toolkit/profile/xpcshell/test_missing_profilesini.js index 4d80c5e86c52..edf3f24b2023 100644 --- a/toolkit/profile/xpcshell/test_missing_profilesini.js +++ b/toolkit/profile/xpcshell/test_missing_profilesini.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_new_default.js b/toolkit/profile/xpcshell/test_new_default.js index 713203d5a234..93ea63953d2c 100644 --- a/toolkit/profile/xpcshell/test_new_default.js +++ b/toolkit/profile/xpcshell/test_new_default.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_previous_dedicated.js b/toolkit/profile/xpcshell/test_previous_dedicated.js index 40a39ff345f6..2fe714ecbe21 100644 --- a/toolkit/profile/xpcshell/test_previous_dedicated.js +++ b/toolkit/profile/xpcshell/test_previous_dedicated.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_profile_reset.js b/toolkit/profile/xpcshell/test_profile_reset.js index 5b6ce58f82e8..d98d500c6b0c 100644 --- a/toolkit/profile/xpcshell/test_profile_reset.js +++ b/toolkit/profile/xpcshell/test_profile_reset.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_remove.js b/toolkit/profile/xpcshell/test_remove.js index fad24ba31c1d..0257bbe4bc6d 100644 --- a/toolkit/profile/xpcshell/test_remove.js +++ b/toolkit/profile/xpcshell/test_remove.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_remove_default.js b/toolkit/profile/xpcshell/test_remove_default.js index 813b5f141b3c..ed2dfb3dfade 100644 --- a/toolkit/profile/xpcshell/test_remove_default.js +++ b/toolkit/profile/xpcshell/test_remove_default.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_select_default.js b/toolkit/profile/xpcshell/test_select_default.js index cd0a5644751c..796879e79de5 100644 --- a/toolkit/profile/xpcshell/test_select_default.js +++ b/toolkit/profile/xpcshell/test_select_default.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_select_environment.js b/toolkit/profile/xpcshell/test_select_environment.js index eb80a600d7f7..97813d9263bc 100644 --- a/toolkit/profile/xpcshell/test_select_environment.js +++ b/toolkit/profile/xpcshell/test_select_environment.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_select_environment_named.js b/toolkit/profile/xpcshell/test_select_environment_named.js index c25a5212413f..2e60acd0e0ec 100644 --- a/toolkit/profile/xpcshell/test_select_environment_named.js +++ b/toolkit/profile/xpcshell/test_select_environment_named.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_select_missing.js b/toolkit/profile/xpcshell/test_select_missing.js index 97b1c52b3df8..d03955c6fce3 100644 --- a/toolkit/profile/xpcshell/test_select_missing.js +++ b/toolkit/profile/xpcshell/test_select_missing.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_select_named.js b/toolkit/profile/xpcshell/test_select_named.js index f5ae9484d3fe..f2808bda6f14 100644 --- a/toolkit/profile/xpcshell/test_select_named.js +++ b/toolkit/profile/xpcshell/test_select_named.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_select_noname.js b/toolkit/profile/xpcshell/test_select_noname.js index 3fb0f4c1ded3..88a71237c1e8 100644 --- a/toolkit/profile/xpcshell/test_select_noname.js +++ b/toolkit/profile/xpcshell/test_select_noname.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_select_profilemanager.js b/toolkit/profile/xpcshell/test_select_profilemanager.js index 780244e9ca93..dc3694b07476 100644 --- a/toolkit/profile/xpcshell/test_select_profilemanager.js +++ b/toolkit/profile/xpcshell/test_select_profilemanager.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_single_profile_selected.js b/toolkit/profile/xpcshell/test_single_profile_selected.js index 7e50fab0f69f..7f9883f20e2e 100644 --- a/toolkit/profile/xpcshell/test_single_profile_selected.js +++ b/toolkit/profile/xpcshell/test_single_profile_selected.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_single_profile_unselected.js b/toolkit/profile/xpcshell/test_single_profile_unselected.js index 1c99dcc745c3..5d3675cd243d 100644 --- a/toolkit/profile/xpcshell/test_single_profile_unselected.js +++ b/toolkit/profile/xpcshell/test_single_profile_unselected.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_skip_locked_environment.js b/toolkit/profile/xpcshell/test_skip_locked_environment.js index 995cf267067c..4674baa539aa 100644 --- a/toolkit/profile/xpcshell/test_skip_locked_environment.js +++ b/toolkit/profile/xpcshell/test_skip_locked_environment.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_snap.js b/toolkit/profile/xpcshell/test_snap.js index 3b039970d997..5d8184d1bc7f 100644 --- a/toolkit/profile/xpcshell/test_snap.js +++ b/toolkit/profile/xpcshell/test_snap.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_snap_empty.js b/toolkit/profile/xpcshell/test_snap_empty.js index a95188d8235f..4134c400b337 100644 --- a/toolkit/profile/xpcshell/test_snap_empty.js +++ b/toolkit/profile/xpcshell/test_snap_empty.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/test_snatch_environment.js b/toolkit/profile/xpcshell/test_snatch_environment.js index 8e6a107c6cfa..a6a75d9febfc 100644 --- a/toolkit/profile/xpcshell/test_snatch_environment.js +++ b/toolkit/profile/xpcshell/test_snatch_environment.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_snatch_environment_default.js b/toolkit/profile/xpcshell/test_snatch_environment_default.js index 739fc288fb6e..d3346f42ddde 100644 --- a/toolkit/profile/xpcshell/test_snatch_environment_default.js +++ b/toolkit/profile/xpcshell/test_snatch_environment_default.js @@ -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."); diff --git a/toolkit/profile/xpcshell/test_startswithlast.js b/toolkit/profile/xpcshell/test_startswithlast.js index f606ba026c9e..d33d51624d64 100644 --- a/toolkit/profile/xpcshell/test_startswithlast.js +++ b/toolkit/profile/xpcshell/test_startswithlast.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_steal_inuse.js b/toolkit/profile/xpcshell/test_steal_inuse.js index b784578fec85..e6dc59b3c3c9 100644 --- a/toolkit/profile/xpcshell/test_steal_inuse.js +++ b/toolkit/profile/xpcshell/test_steal_inuse.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_update_selected_dedicated.js b/toolkit/profile/xpcshell/test_update_selected_dedicated.js index 66d6beb01635..63450f0ab044 100644 --- a/toolkit/profile/xpcshell/test_update_selected_dedicated.js +++ b/toolkit/profile/xpcshell/test_update_selected_dedicated.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_update_unknown_dedicated.js b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js index f7732734b021..152a65e6d1fc 100644 --- a/toolkit/profile/xpcshell/test_update_unknown_dedicated.js +++ b/toolkit/profile/xpcshell/test_update_unknown_dedicated.js @@ -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 diff --git a/toolkit/profile/xpcshell/test_update_unselected_dedicated.js b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js index ef979f27eca9..71c0422c22bd 100644 --- a/toolkit/profile/xpcshell/test_update_unselected_dedicated.js +++ b/toolkit/profile/xpcshell/test_update_unselected_dedicated.js @@ -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. diff --git a/toolkit/profile/xpcshell/test_use_dedicated.js b/toolkit/profile/xpcshell/test_use_dedicated.js index 27820031b680..4142d4173721 100644 --- a/toolkit/profile/xpcshell/test_use_dedicated.js +++ b/toolkit/profile/xpcshell/test_use_dedicated.js @@ -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. */ diff --git a/toolkit/profile/xpcshell/xpcshell.ini b/toolkit/profile/xpcshell/xpcshell.ini index 0590002fa6c6..4a1d77dcd1c3 100644 --- a/toolkit/profile/xpcshell/xpcshell.ini +++ b/toolkit/profile/xpcshell/xpcshell.ini @@ -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] diff --git a/toolkit/xre/ProfileReset.h b/toolkit/xre/ProfileReset.h index 5ddad666cf9a..afb76ef17a4e 100644 --- a/toolkit/xre/ProfileReset.h +++ b/toolkit/xre/ProfileReset.h @@ -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; diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index 115627034a18..d797a4c0517f 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -1838,6 +1838,7 @@ static ReturnAbortOnError ShowProfileManager( nsCOMPtr 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"); + } } } diff --git a/xpcom/base/ErrorList.py b/xpcom/base/ErrorList.py index dc6ee8e30dd5..a8359df3d0a2 100755 --- a/xpcom/base/ErrorList.py +++ b/xpcom/base/ErrorList.py @@ -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) # =======================================================================