Bug 1474285: Implement dedicated profiles per install. r=froydnj, r=Gijs

Uses a different profile depending on the install directory of the application.
installs.ini is used to map a hash of the install directory to a profile
directory.

If no profile is marked as default for the current install we use a heuristic
explained in the code to decide whether to use the profile that would have
been used before this feature.

The feature is disabled in snap builds where the install directory changes for
every version of the app, but multiple instances cannot share profiles anyway.
A boolean flag is used to turn on the feature because in a later patch we need
to be able to turn off the behaviour at runtime.

Includes code folded in from bug 1518634, bug 1522751, bug 1518632 and bug 1523024.

--HG--
extra : rebase_source : b4608f6e8800af4f154daf0e0262f521c8ebd9fd
extra : intermediate-source : ba34b021c8e995ec7fc7c7fbb3dcc5dcf268278c
extra : source : e406bf0bcd665bd0e54ddb13d9ae880004badef1
This commit is contained in:
Dave Townsend 2019-01-25 16:02:28 -08:00
parent dd2eee0019
commit 007be95c70
29 changed files with 1504 additions and 191 deletions

View File

@ -410,26 +410,6 @@ var gMainPane = {
// of the pane hiding/showing code potentially interfering:
document.getElementById("drmGroup").setAttribute("style", "display: none !important");
}
if (AppConstants.MOZ_DEV_EDITION) {
let uAppData = OS.Constants.Path.userApplicationDataDir;
let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
setEventListener("getStarted", "click", gMainPane.onGetStarted);
OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
() => separateProfileModeCheckbox.checked = true);
if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
document.getElementById("sync-dev-edition-root").hidden = false;
fxAccounts.getSignedInUser().then(data => {
document.getElementById("getStarted").selectedIndex = data ? 1 : 0;
}).catch(Cu.reportError);
}
}
// Initialize the Firefox Updates section.
let version = AppConstants.MOZ_APP_VERSION_DISPLAY;

View File

@ -30,20 +30,6 @@
hidden="true">
<label><html:h2 data-l10n-id="startup-header"/></label>
#ifdef MOZ_DEV_EDITION
<vbox id="separateProfileBox">
<checkbox id="separateProfileMode"
data-l10n-id="separate-profile-mode"/>
<hbox id="sync-dev-edition-root" align="center" class="indent" hidden="true">
<label id="useFirefoxSync" data-l10n-id="use-firefox-sync"/>
<deck id="getStarted">
<label class="text-link" data-l10n-id="get-started-not-logged-in"/>
<label class="text-link" data-l10n-id="get-started-configured"/>
</deck>
</hbox>
</vbox>
#endif
<vbox id="startupPageBox">
<checkbox id="browserRestoreSession"
data-l10n-id="startup-restore-previous-session"/>

View File

@ -33,6 +33,12 @@ interface nsIToolkitProfileService : nsISupports
*/
attribute nsIToolkitProfile defaultProfile;
/**
* True if during startup a new profile was created for this install instead
* of using the profile that was the default for older versions.
*/
readonly attribute boolean createdAlternateProfile;
/**
* Selects or creates a profile to use based on the profiles database, any
* environment variables and any command line arguments. Will not create

View File

@ -29,12 +29,12 @@
#endif
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceDefs.h"
#include "nsNetCID.h"
#include "nsXULAppAPI.h"
#include "nsThreadUtils.h"
#include "nsIRunnable.h"
#include "nsINIParser.h"
#include "nsXREDirProvider.h"
#include "nsAppRunner.h"
#include "nsString.h"
@ -43,6 +43,8 @@
#include "mozilla/Attributes.h"
#include "mozilla/Sprintf.h"
#include "nsPrintfCString.h"
#include "mozilla/UniquePtr.h"
#include "nsIToolkitShellService.h"
using namespace mozilla;
@ -172,6 +174,9 @@ nsresult nsToolkitProfile::RemoveInternal(bool aRemoveFiles,
if (nsToolkitProfileService::gService->mDevEditionDefault == this) {
nsToolkitProfileService::gService->mDevEditionDefault = nullptr;
}
if (nsToolkitProfileService::gService->mDedicatedProfile == this) {
nsToolkitProfileService::gService->SetDefaultProfile(nullptr);
}
return NS_OK;
}
@ -291,7 +296,13 @@ nsToolkitProfileService::nsToolkitProfileService()
: mStartupProfileSelected(false),
mStartWithLast(true),
mIsFirstRun(true),
mUseDevEditionProfile(false) {
mUseDevEditionProfile(false),
#ifdef MOZ_DEDICATED_PROFILES
mUseDedicatedProfile(!IsSnapEnvironment()),
#else
mUseDedicatedProfile(false),
#endif
mCreatedAlternateProfile(false) {
#ifdef MOZ_DEV_EDITION
mUseDevEditionProfile = true;
#endif
@ -300,6 +311,167 @@ nsToolkitProfileService::nsToolkitProfileService()
nsToolkitProfileService::~nsToolkitProfileService() { gService = nullptr; }
// Tests whether the passed profile was last used by this install.
bool nsToolkitProfileService::IsProfileForCurrentInstall(
nsIToolkitProfile* aProfile) {
nsCOMPtr<nsIFile> profileDir;
nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
NS_ENSURE_SUCCESS(rv, false);
nsCOMPtr<nsIFile> compatFile;
rv = profileDir->Clone(getter_AddRefs(compatFile));
NS_ENSURE_SUCCESS(rv, false);
rv = compatFile->Append(NS_LITERAL_STRING("compatibility.ini"));
NS_ENSURE_SUCCESS(rv, false);
nsINIParser compatData;
rv = compatData.Init(compatFile);
// If the file is missing then either this is an empty profile (likely
// generated by bug 1518591) or it is from an ancient version. We'll opt to
// use it in this case.
if (NS_FAILED(rv)) {
return true;
}
/**
* In xpcshell gDirServiceProvider doesn't have all the correct directories
* set so using NS_GetSpecialDirectory works better there. But in a normal
* app launch the component registry isn't initialized so
* NS_GetSpecialDirectory doesn't work. So we have to use two different
* paths to support testing.
*/
nsCOMPtr<nsIFile> currentGreDir;
rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(currentGreDir));
if (rv == NS_ERROR_NOT_INITIALIZED) {
currentGreDir = gDirServiceProvider->GetGREDir();
MOZ_ASSERT(currentGreDir, "No GRE dir found.");
} else if (NS_FAILED(rv)) {
return false;
}
nsCString greDirPath;
rv = compatData.GetString("Compatibility", "LastPlatformDir", greDirPath);
// If this string is missing then this profile is from an ancient version.
// We'll opt to use it in this case.
if (NS_FAILED(rv)) {
return true;
}
nsCOMPtr<nsIFile> greDir;
rv = NS_NewNativeLocalFile(EmptyCString(), false, getter_AddRefs(greDir));
NS_ENSURE_SUCCESS(rv, false);
rv = greDir->SetPersistentDescriptor(greDirPath);
NS_ENSURE_SUCCESS(rv, false);
bool equal;
rv = greDir->Equals(currentGreDir, &equal);
NS_ENSURE_SUCCESS(rv, false);
return equal;
}
/**
* Used the first time an install with dedicated profile support runs. Decides
* whether to mark the passed profile as the default for this install.
*
* The goal is to reduce disruption but ideally end up with the OS default
* install using the old default profile.
*
* If the decision is to use the profile then it will be unassigned as the
* dedicated default for other installs.
*
* We won't attempt to use the profile if it was last used by a different
* install.
*
* If the profile is currently in use by an install that was either the OS
* 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.
*/
bool nsToolkitProfileService::MaybeMakeDefaultDedicatedProfile(
nsIToolkitProfile* aProfile) {
nsresult rv;
// If the profile was last used by a different install then we won't use it.
if (!IsProfileForCurrentInstall(aProfile)) {
return false;
}
nsCString descriptor;
rv = GetProfileDescriptor(aProfile, descriptor, nullptr);
NS_ENSURE_SUCCESS(rv, false);
// Get a list of all the installs.
nsTArray<nsCString> installs = GetKnownInstalls();
// Cache the installs that use the profile.
nsTArray<nsCString> inUseInstalls;
// See if the profile is already in use by an install that hasn't locked it.
for (uint32_t i = 0; i < installs.Length(); i++) {
const nsCString& install = installs[i];
nsCString path;
rv = mInstallData.GetString(install.get(), "Default", path);
if (NS_FAILED(rv)) {
continue;
}
// Is this install using the profile we care about?
if (!descriptor.Equals(path)) {
continue;
}
// Is this profile locked to this other install?
nsCString isLocked;
rv = mInstallData.GetString(install.get(), "Locked", isLocked);
if (NS_SUCCEEDED(rv) && isLocked.Equals("1")) {
return false;
}
inUseInstalls.AppendElement(install);
}
// At this point we've decided to take the profile. Strip it from other
// installs.
for (uint32_t i = 0; i < inUseInstalls.Length(); i++) {
// Removing the default setting entirely will make the install go through
// the first run process again at startup and create itself a new profile.
mInstallData.DeleteString(inUseInstalls[i].get(), "Default");
}
// Set this as the default profile for this install.
SetDefaultProfile(aProfile);
bool isDefaultApp = false;
nsCOMPtr<nsIToolkitShellService> shell =
do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID);
if (shell) {
rv = shell->IsDefaultApplication(&isDefaultApp);
// If the shell component is following XPCOM rules then this shouldn't be
// needed, but let's be safe.
if (NS_FAILED(rv)) {
isDefaultApp = false;
}
}
if (!isDefaultApp) {
// SetDefaultProfile will have locked this profile to this install so no
// other installs will steal it, but this was auto-selected so we want to
// unlock it so that the OS default install can take it at a later time.
mInstallData.DeleteString(mInstallHash.get(), "Locked");
}
// Persist the changes.
Flush();
return true;
}
nsresult nsToolkitProfileService::Init() {
NS_ASSERTION(gDirServiceProvider, "No dirserviceprovider!");
nsresult rv;
@ -310,30 +482,51 @@ nsresult nsToolkitProfileService::Init() {
rv = nsXREDirProvider::GetUserLocalDataDirectory(getter_AddRefs(mTempData));
NS_ENSURE_SUCCESS(rv, rv);
nsCString installProfilePath;
if (mUseDedicatedProfile) {
// Load the dedicated profiles database.
rv = mAppData->Clone(getter_AddRefs(mInstallFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = mInstallFile->AppendNative(NS_LITERAL_CSTRING("installs.ini"));
NS_ENSURE_SUCCESS(rv, rv);
nsString installHash;
rv = gDirServiceProvider->GetInstallHash(installHash);
NS_ENSURE_SUCCESS(rv, rv);
CopyUTF16toUTF8(installHash, mInstallHash);
rv = mInstallData.Init(mInstallFile);
if (NS_SUCCEEDED(rv)) {
// Try to find the descriptor for the default profile for this install.
rv = mInstallData.GetString(mInstallHash.get(), "Default",
installProfilePath);
// Not having a value means this install doesn't appear in installs.ini so
// this is the first run for this install.
mIsFirstRun = NS_FAILED(rv);
}
}
rv = mAppData->Clone(getter_AddRefs(mListFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = mListFile->AppendNative(NS_LITERAL_CSTRING("profiles.ini"));
NS_ENSURE_SUCCESS(rv, rv);
nsINIParser parser;
bool exists;
rv = mListFile->IsFile(&exists);
if (NS_FAILED(rv) || !exists) {
return NS_OK;
if (NS_SUCCEEDED(rv) && exists) {
rv = parser.Init(mListFile);
// Init does not fail on parsing errors, only on OOM/really unexpected
// conditions.
if (NS_FAILED(rv)) {
return rv;
}
}
int64_t size;
rv = mListFile->GetFileSize(&size);
if (NS_FAILED(rv) || !size) {
return NS_OK;
}
nsINIParser parser;
rv = parser.Init(mListFile);
// Init does not fail on parsing errors, only on OOM/really unexpected
// conditions.
if (NS_FAILED(rv)) return rv;
nsAutoCString buffer;
rv = parser.GetString("General", "StartWithLastProfile", buffer);
if (NS_SUCCEEDED(rv) && buffer.EqualsLiteral("0")) mStartWithLast = false;
@ -341,16 +534,20 @@ nsresult nsToolkitProfileService::Init() {
nsToolkitProfile* currentProfile = nullptr;
#ifdef MOZ_DEV_EDITION
nsCOMPtr<nsIFile> ignoreSeparateProfile;
rv = mAppData->Clone(getter_AddRefs(ignoreSeparateProfile));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIFile> ignoreDevEditionProfile;
rv = mAppData->Clone(getter_AddRefs(ignoreDevEditionProfile));
if (NS_FAILED(rv)) {
return rv;
}
rv = ignoreSeparateProfile->AppendNative(
rv = ignoreDevEditionProfile->AppendNative(
NS_LITERAL_CSTRING("ignore-dev-edition-profile"));
if (NS_FAILED(rv)) return rv;
if (NS_FAILED(rv)) {
return rv;
}
bool shouldIgnoreSeparateProfile;
rv = ignoreSeparateProfile->Exists(&shouldIgnoreSeparateProfile);
rv = ignoreDevEditionProfile->Exists(&shouldIgnoreSeparateProfile);
if (NS_FAILED(rv)) return rv;
mUseDevEditionProfile = !shouldIgnoreSeparateProfile;
@ -416,6 +613,13 @@ nsresult nsToolkitProfileService::Init() {
mNormalDefault = currentProfile;
}
// Is this the default profile for this install?
if (mUseDedicatedProfile && !mDedicatedProfile &&
installProfilePath.Equals(filePath)) {
// Found a profile for this install.
mDedicatedProfile = currentProfile;
}
if (name.EqualsLiteral(DEV_EDITION_NAME)) {
mDevEditionDefault = currentProfile;
} else {
@ -429,13 +633,15 @@ nsresult nsToolkitProfileService::Init() {
mNormalDefault = autoSelectProfile;
}
if (mUseDevEditionProfile) {
// When using the separate dev-edition profile not finding it means this is
// a first run.
mIsFirstRun = !mDevEditionDefault;
} else {
// If there are no normal profiles then this is a first run.
mIsFirstRun = nonDevEditionProfiles == 0;
if (!mUseDedicatedProfile) {
if (mUseDevEditionProfile) {
// When using the separate dev-edition profile not finding it means this
// is a first run.
mIsFirstRun = !mDevEditionDefault;
} else {
// If there are no normal profiles then this is a first run.
mIsFirstRun = nonDevEditionProfiles == 0;
}
}
return NS_OK;
@ -488,6 +694,11 @@ nsToolkitProfileService::GetCurrentProfile(nsIToolkitProfile** aResult) {
NS_IMETHODIMP
nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile** aResult) {
if (mUseDedicatedProfile) {
NS_IF_ADDREF(*aResult = mDedicatedProfile);
return NS_OK;
}
if (mUseDevEditionProfile) {
NS_IF_ADDREF(*aResult = mDevEditionDefault);
return NS_OK;
@ -499,6 +710,30 @@ nsToolkitProfileService::GetDefaultProfile(nsIToolkitProfile** aResult) {
NS_IMETHODIMP
nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) {
if (mUseDedicatedProfile) {
if (mDedicatedProfile != aProfile) {
if (!aProfile) {
// Setting this to the empty string means no profile will be found on
// startup but we'll recognise that this install has been used
// previously.
mInstallData.SetString(mInstallHash.get(), "Default", "");
} else {
nsCString profilePath;
nsresult rv = GetProfileDescriptor(aProfile, profilePath, nullptr);
NS_ENSURE_SUCCESS(rv, rv);
mInstallData.SetString(mInstallHash.get(), "Default",
profilePath.get());
}
mDedicatedProfile = aProfile;
// Some kind of choice has happened here, lock this profile to this
// install.
mInstallData.SetString(mInstallHash.get(), "Locked", "1");
}
return NS_OK;
}
if (mUseDevEditionProfile && aProfile != mDevEditionDefault) {
// The separate profile is hardcoded.
return NS_ERROR_FAILURE;
@ -508,6 +743,42 @@ nsToolkitProfileService::SetDefaultProfile(nsIToolkitProfile* aProfile) {
return NS_OK;
}
NS_IMETHODIMP
nsToolkitProfileService::GetCreatedAlternateProfile(bool* aResult) {
*aResult = mCreatedAlternateProfile;
return NS_OK;
}
// Gets the profile root directory descriptor for storing in profiles.ini or
// installs.ini.
nsresult nsToolkitProfileService::GetProfileDescriptor(
nsIToolkitProfile* aProfile, nsACString& aDescriptor, bool* aIsRelative) {
nsCOMPtr<nsIFile> profileDir;
nsresult rv = aProfile->GetRootDir(getter_AddRefs(profileDir));
NS_ENSURE_SUCCESS(rv, rv);
// if the profile dir is relative to appdir...
bool isRelative;
rv = mAppData->Contains(profileDir, &isRelative);
nsCString profilePath;
if (NS_SUCCEEDED(rv) && isRelative) {
// we use a relative descriptor
rv = profileDir->GetRelativeDescriptor(mAppData, profilePath);
} else {
// otherwise, a persistent descriptor
rv = profileDir->GetPersistentDescriptor(profilePath);
}
NS_ENSURE_SUCCESS(rv, rv);
aDescriptor.Assign(profilePath);
if (aIsRelative) {
*aIsRelative = isRelative;
}
return NS_OK;
}
/**
* An implementation of SelectStartupProfile callable from JavaScript via XPCOM.
* See nsIToolkitProfileService.idl.
@ -728,31 +999,65 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
return NS_OK;
}
// create a default profile
if (mUseDedicatedProfile) {
// This is the first run of a dedicated profile install. We have to decide
// whether to use the default profile used by non-dedicated-profile
// installs or to create a new profile.
// Find what would have been the default profile for old installs.
nsCOMPtr<nsIToolkitProfile> profile = mNormalDefault;
if (mUseDevEditionProfile) {
profile = mDevEditionDefault;
}
if (profile && MaybeMakeDefaultDedicatedProfile(profile)) {
mCurrent = profile;
profile->GetRootDir(aRootDir);
profile->GetLocalDir(aLocalDir);
profile.forget(aProfile);
return NS_OK;
}
// We're going to create a new profile for this install. If there was a
// potential previous default to use then the user may be confused over
// why we're not using that anymore so set a flag for the front-end to use
// to notify the user about what has happened.
mCreatedAlternateProfile = !!profile;
}
// Create a new default profile
nsAutoCString name;
if (mUseDevEditionProfile) {
if (mUseDedicatedProfile) {
name.AssignLiteral("default-" NS_STRINGIFY(MOZ_UPDATE_CHANNEL));
} else if (mUseDevEditionProfile) {
name.AssignLiteral(DEV_EDITION_NAME);
} else {
name.AssignLiteral(DEFAULT_NAME);
}
nsresult rv = CreateProfile(nullptr, name, getter_AddRefs(mCurrent));
rv = CreateProfile(nullptr, name, getter_AddRefs(mCurrent));
if (NS_SUCCEEDED(rv)) {
if (mUseDevEditionProfile) {
if (mUseDedicatedProfile) {
SetDefaultProfile(mCurrent);
} else if (mUseDevEditionProfile) {
mDevEditionDefault = mCurrent;
// If the only profile is the new dev-edition-profile then older
// versions may try to auto-select it. Create a default profile for them
// to use instead.
if (mFirst && !mFirst->mNext) {
CreateProfile(nullptr, NS_LITERAL_CSTRING(DEFAULT_NAME),
getter_AddRefs(mNormalDefault));
}
} else {
mNormalDefault = mCurrent;
}
// If there is only one profile and it isn't meant to be the profile that
// older versions of Firefox use then we must create a default profile
// for older versions of Firefox to avoid the existing profile being
// auto-selected.
if ((mUseDedicatedProfile || mUseDevEditionProfile) && mFirst &&
!mFirst->mNext) {
CreateProfile(nullptr, NS_LITERAL_CSTRING(DEFAULT_NAME),
getter_AddRefs(mNormalDefault));
}
Flush();
// Use the new profile.
mCurrent->GetRootDir(aRootDir);
mCurrent->GetLocalDir(aLocalDir);
NS_ADDREF(*aProfile = mCurrent);
@ -762,7 +1067,7 @@ nsresult nsToolkitProfileService::SelectStartupProfile(
}
}
// There are multiple profiles available.
// We've been told not to use the selected profile automatically.
if (!mStartWithLast) {
return NS_ERROR_SHOW_PROFILE_MANAGER;
}
@ -815,6 +1120,50 @@ nsresult nsToolkitProfileService::CreateResetProfile(
return NS_OK;
}
/**
* This is responsible for deleting the old profile, copying its name to the
* current profile and if the old profile was default making the new profile
* default as well.
*/
nsresult nsToolkitProfileService::ApplyResetProfile(
nsIToolkitProfile* aOldProfile) {
// If the old profile would have been the default for old installs then mark
// the new profile as such.
if (mNormalDefault == aOldProfile) {
mNormalDefault = mCurrent;
}
if (mUseDedicatedProfile && mDedicatedProfile == aOldProfile) {
bool wasLocked = false;
nsCString val;
if (NS_SUCCEEDED(
mInstallData.GetString(mInstallHash.get(), "Locked", val))) {
wasLocked = val.Equals("1");
}
SetDefaultProfile(mCurrent);
// Make the locked state match if necessary.
if (!wasLocked) {
mInstallData.DeleteString(mInstallHash.get(), "Locked");
}
}
nsCString name;
nsresult rv = aOldProfile->GetName(name);
NS_ENSURE_SUCCESS(rv, rv);
rv = aOldProfile->Remove(false);
NS_ENSURE_SUCCESS(rv, rv);
// Switching the name will make this the default for dev-edition if
// appropriate.
rv = mCurrent->SetName(name);
NS_ENSURE_SUCCESS(rv, rv);
return Flush();
}
NS_IMETHODIMP
nsToolkitProfileService::GetProfileByName(const nsACString& aName,
nsIToolkitProfile** aResult) {
@ -978,6 +1327,49 @@ nsToolkitProfileService::CreateProfile(nsIFile* aRootDir,
return NS_OK;
}
/**
* Snaps (https://snapcraft.io/) use a different installation directory for
* every version of an application. Since dedicated profiles uses the
* installation directory to determine which profile to use this would lead
* snap users getting a new profile on every application update.
*
* However the only way to have multiple installation of a snap is to install
* a new snap instance. Different snap instances have different user data
* directories and so already will not share profiles, in fact one instance
* will not even be able to see the other instance's profiles since
* profiles.ini will be stored in different places.
*
* So we can just disable dedicated profile support in this case and revert
* back to the old method of just having a single default profile and still
* get essentially the same benefits as dedicated profiles provides.
*/
bool nsToolkitProfileService::IsSnapEnvironment() {
return !!PR_GetEnv("SNAP_NAME");
}
struct FindInstallsClosure {
nsINIParser* installData;
nsTArray<nsCString>* installs;
};
static bool FindInstalls(const char* aSection, void* aClosure) {
FindInstallsClosure* closure = static_cast<FindInstallsClosure*>(aClosure);
nsCString install(aSection);
closure->installs->AppendElement(install);
return true;
}
nsTArray<nsCString> nsToolkitProfileService::GetKnownInstalls() {
nsTArray<nsCString> result;
FindInstallsClosure closure = {&mInstallData, &result};
mInstallData.GetSections(&FindInstalls, &closure);
return result;
}
nsresult nsToolkitProfileService::CreateTimesInternal(nsIFile* aProfileDir) {
nsresult rv = NS_ERROR_FAILURE;
nsCOMPtr<nsIFile> creationLog;
@ -1023,11 +1415,17 @@ nsToolkitProfileService::GetProfileCount(uint32_t* aResult) {
NS_IMETHODIMP
nsToolkitProfileService::Flush() {
nsresult rv;
if (mUseDedicatedProfile) {
rv = mInstallData.WriteToFile(mInstallFile);
NS_ENSURE_SUCCESS(rv, rv);
}
// Errors during writing might cause unhappy semi-written files.
// To avoid this, write the entire thing to a buffer, then write
// that buffer to disk.
nsresult rv;
uint32_t pCount = 0;
nsToolkitProfile* cur;
@ -1050,17 +1448,9 @@ nsToolkitProfileService::Flush() {
pCount = 0;
while (cur) {
// if the profile dir is relative to appdir...
bool isRelative;
rv = mAppData->Contains(cur->mRootDir, &isRelative);
if (NS_SUCCEEDED(rv) && isRelative) {
// we use a relative descriptor
rv = cur->mRootDir->GetRelativeDescriptor(mAppData, path);
} else {
// otherwise, a persistent descriptor
rv = cur->mRootDir->GetPersistentDescriptor(path);
NS_ENSURE_SUCCESS(rv, rv);
}
nsresult rv = GetProfileDescriptor(cur, path, &isRelative);
NS_ENSURE_SUCCESS(rv, rv);
pos +=
snprintf(pos, end - pos,

View File

@ -13,6 +13,7 @@
#include "nsIFactory.h"
#include "nsSimpleEnumerator.h"
#include "nsProfileLock.h"
#include "nsINIParser.h"
class nsToolkitProfile final : public nsIToolkitProfile {
public:
@ -77,6 +78,7 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
nsIFile** aRootDir, nsIFile** aLocalDir,
nsIToolkitProfile** aProfile, bool* aDidCreate);
nsresult CreateResetProfile(nsIToolkitProfile** aNewProfile);
nsresult ApplyResetProfile(nsIToolkitProfile* aOldProfile);
private:
friend class nsToolkitProfile;
@ -92,12 +94,25 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
void GetProfileByDir(nsIFile* aRootDir, nsIFile* aLocalDir,
nsIToolkitProfile** aResult);
nsresult GetProfileDescriptor(nsIToolkitProfile* aProfile,
nsACString& aDescriptor, bool* aIsRelative);
bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
bool MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile);
bool IsSnapEnvironment();
// Returns the known install hashes from the installs database. Modifying the
// installs database is safe while iterating the returned array.
nsTArray<nsCString> GetKnownInstalls();
// Tracks whether SelectStartupProfile has been called.
bool mStartupProfileSelected;
// The first profile in a linked list of profiles loaded from profiles.ini.
RefPtr<nsToolkitProfile> mFirst;
// The profile selected for use at startup, if it exists in profiles.ini.
nsCOMPtr<nsIToolkitProfile> mCurrent;
// The profile selected for this install in installs.ini.
nsCOMPtr<nsIToolkitProfile> mDedicatedProfile;
// The default profile used by non-dev-edition builds.
nsCOMPtr<nsIToolkitProfile> mNormalDefault;
// The profile used if mUseDevEditionProfile is true (the default on
@ -109,12 +124,23 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
nsCOMPtr<nsIFile> mTempData;
// The location of profiles.ini.
nsCOMPtr<nsIFile> mListFile;
// The location of installs.ini.
nsCOMPtr<nsIFile> mInstallFile;
// The data loaded from installs.ini.
nsINIParser mInstallData;
// The install hash for the currently running install.
nsCString mInstallHash;
// Whether to start with the selected profile by default.
bool mStartWithLast;
// True if during startup it appeared that this is the first run.
bool mIsFirstRun;
// True if the default profile is the separate dev-edition-profile.
bool mUseDevEditionProfile;
// True if this install should use a dedicated default profile.
const bool mUseDedicatedProfile;
// True if during startup no dedicated profile was already selected, an old
// default profile existed but was rejected so a new profile was created.
bool mCreatedAlternateProfile;
static nsToolkitProfileService* gService;

View File

@ -23,6 +23,46 @@ let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].
xreDirProvider.setUserDataDirectory(gDataHome, false);
xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
let gIsDefaultApp = false;
const ShellService = {
register() {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
let factory = {
createInstance(outer, iid) {
if (outer != null) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
return ShellService.QueryInterface(iid);
},
};
registrar.registerFactory(this.ID, "ToolkitShellService", this.CONTRACT, factory);
},
isDefaultApplication() {
return gIsDefaultApp;
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIToolkitShellService]),
ID: Components.ID("{ce724e0c-ed70-41c9-ab31-1033b0b591be}"),
CONTRACT: "@mozilla.org/toolkit/shell-service;1",
};
ShellService.register();
let gIsSnap = false;
function simulateSnapEnvironment() {
let env = Cc["@mozilla.org/process/environment;1"].
getService(Ci.nsIEnvironment);
env.set("SNAP_NAME", "foo");
gIsSnap = true;
}
function getProfileService() {
return Cc["@mozilla.org/toolkit/profile-service;1"].
getService(Ci.nsIToolkitProfileService);
@ -33,6 +73,8 @@ if (AppConstants.MOZ_DEV_EDITION) {
PROFILE_DEFAULT = "dev-edition-default";
}
let DEDICATED_NAME = `default-${AppConstants.MOZ_UPDATE_CHANNEL}`;
/**
* Creates a random profile path for use.
*/
@ -88,6 +130,29 @@ function safeGet(ini, section, key) {
}
}
/**
* Writes a compatibility.ini file that marks the give profile directory as last
* used by the given install path.
*/
function writeCompatibilityIni(dir, appDir = FileUtils.getDir("CurProcD", []),
greDir = FileUtils.getDir("GreD", [])) {
let target = dir.clone();
target.append("compatibility.ini");
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
getService(Ci.nsIINIParserFactory);
let ini = factory.createINIParser().QueryInterface(Ci.nsIINIParserWriter);
// The profile service doesn't care about these so just use fixed values
ini.setString("Compatibility", "LastVersion", "64.0a1_20180919123806/20180919123806");
ini.setString("Compatibility", "LastOSABI", "Darwin_x86_64-gcc3");
ini.setString("Compatibility", "LastPlatformDir", greDir.persistentDescriptor);
ini.setString("Compatibility", "LastAppDir", appDir.persistentDescriptor);
ini.writeFile(target);
}
/**
* Writes a profiles.ini based on the passed profile data.
* profileData should contain two properties, options and profiles.
@ -135,7 +200,9 @@ function readProfilesIni() {
target.append("profiles.ini");
let profileData = {
options: {},
options: {
startWithLastProfile: true,
},
profiles: [],
};
@ -178,11 +245,69 @@ function readProfilesIni() {
return profileData;
}
/**
* Writes an installs.ini based on the supplied data. Should be an object with
* keys for every installation hash each mapping to an object. Each object
* should have a default property for the relative path to the profile.
*/
function writeInstallsIni(installData) {
let target = gDataHome.clone();
target.append("installs.ini");
const { installs = {} } = installData;
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
getService(Ci.nsIINIParserFactory);
let ini = factory.createINIParser(null).QueryInterface(Ci.nsIINIParserWriter);
for (let hash of Object.keys(installs)) {
ini.setString(hash, "Default", installs[hash].default);
if ("locked" in installs[hash]) {
ini.setString(hash, "Locked", installs[hash].locked ? "1" : "0");
}
}
ini.writeFile(target);
}
/**
* Reads installs.ini into a structure like that used in the above function.
*/
function readInstallsIni() {
let target = gDataHome.clone();
target.append("installs.ini");
let installData = {
installs: {},
};
if (!target.exists()) {
return installData;
}
let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
getService(Ci.nsIINIParserFactory);
let ini = factory.createINIParser(target);
let sections = ini.getSections();
while (sections.hasMore()) {
let hash = sections.getNext();
if (hash != "General") {
installData.installs[hash] = {
default: safeGet(ini, hash, "Default"),
locked: safeGet(ini, hash, "Locked") == 1,
};
}
}
return installData;
}
/**
* Checks that the profile service seems to have the right data in it compared
* to profile and install data structured as in the above functions.
*/
function checkProfileService(profileData = readProfilesIni()) {
function checkProfileService(profileData = readProfilesIni(), installData = readInstallsIni()) {
let service = getProfileService();
let serviceProfiles = Array.from(service.profiles);
@ -193,7 +318,10 @@ function checkProfileService(profileData = readProfilesIni()) {
serviceProfiles.sort((a, b) => a.name.localeCompare(b.name));
profileData.profiles.sort((a, b) => a.name.localeCompare(b.name));
let defaultProfile = null;
let hash = xreDirProvider.getInstallHash();
let defaultPath = hash in installData.installs ? installData.installs[hash].default : null;
let dedicatedProfile = null;
let snapProfile = null;
for (let i = 0; i < serviceProfiles.length; i++) {
let serviceProfile = serviceProfiles[i];
@ -205,14 +333,31 @@ function checkProfileService(profileData = readProfilesIni()) {
expectedPath.setRelativeDescriptor(gDataHome, expectedProfile.path);
Assert.equal(serviceProfile.rootDir.path, expectedPath.path, "Should have the same path.");
if (expectedProfile.path == defaultPath) {
dedicatedProfile = serviceProfile;
}
if (AppConstants.MOZ_DEV_EDITION) {
if (expectedProfile.name == PROFILE_DEFAULT) {
defaultProfile = serviceProfile;
snapProfile = serviceProfile;
}
} else if (expectedProfile.default) {
defaultProfile = serviceProfile;
snapProfile = serviceProfile;
}
}
Assert.equal(service.defaultProfile, defaultProfile, "Should have seen the right profile as default.");
if (gIsSnap) {
Assert.equal(service.defaultProfile, snapProfile, "Should have seen the right profile selected.");
} else {
Assert.equal(service.defaultProfile, dedicatedProfile, "Should have seen the right profile selected.");
}
}
/**
* Asynchronously reads an nsIFile from disk.
*/
async function readFile(file) {
let decoder = new TextDecoder();
let data = await OS.File.read(file.path);
return decoder.decode(data);
}

View File

@ -0,0 +1,46 @@
/*
* Tests that an old-style default profile already locked to a different install
* isn't claimed by this install.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
let hash = xreDirProvider.getInstallHash();
writeProfilesIni({
installs: {
other: {
default: defaultProfile.leafName,
locked: true,
},
},
});
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be two known installs.");
Assert.notEqual(installData.installs[hash].default, defaultProfile.leafName, "Should not have marked the original default profile as the default for this install.");
Assert.ok(installData.installs[hash].locked, "Should have locked as we created this profile for this install.");
checkProfileService(profileData, installData);
Assert.ok(didCreate, "Should have created a new profile.");
Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using a different directory.");
Assert.equal(selectedProfile.name, DEDICATED_NAME);
});

View File

@ -0,0 +1,110 @@
/*
* Tests from a clean state.
* Then does some testing that creating new profiles and marking them as
* selected works.
*/
add_task(async () => {
let service = getProfileService();
let target = gDataHome.clone();
target.append("profiles.ini");
Assert.ok(!target.exists(), "profiles.ini should not exist yet.");
target.leafName = "installs.ini";
Assert.ok(!target.exists(), "installs.ini should not exist yet.");
// Create a new profile to use.
let newProfile = service.createProfile(null, "dedicated");
service.flush();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "dedicated", "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
// The new profile hasn't been marked as the default yet!
Assert.equal(Object.keys(installData.installs).length, 0, "Should be no defaults for installs yet.");
checkProfileService(profileData, installData);
service.defaultProfile = newProfile;
service.flush();
profileData = readProfilesIni();
installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
profile = profileData.profiles[0];
Assert.equal(profile.name, "dedicated", "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
let hash = xreDirProvider.getInstallHash();
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
checkProfileService(profileData, installData);
let otherProfile = service.createProfile(null, "another");
service.defaultProfile = otherProfile;
service.flush();
profileData = readProfilesIni();
installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
profile = profileData.profiles[0];
Assert.equal(profile.name, "another", "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
profile = profileData.profiles[1];
Assert.equal(profile.name, "dedicated", "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
checkProfileService(profileData, installData);
newProfile.remove(true);
service.flush();
profileData = readProfilesIni();
installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
profile = profileData.profiles[0];
Assert.equal(profile.name, "another", "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, profileData.profiles[0].path, "Should have marked the new profile as the default for this install.");
checkProfileService(profileData, installData);
otherProfile.remove(true);
service.flush();
profileData = readProfilesIni();
installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 0, "Should have the right number of profiles.");
// We leave a reference to the missing profile to stop us trying to steal the
// old-style default profile on next startup.
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
checkProfileService(profileData, installData);
});

View File

@ -5,14 +5,27 @@
add_task(async () => {
let service = getProfileService();
let { profile, didCreate } = selectStartupProfile();
checkProfileService();
let profileData = readProfilesIni();
let installData = readInstallsIni();
checkProfileService(profileData, installData);
Assert.ok(didCreate, "Should have created a new profile.");
if (AppConstants.MOZ_DEV_EDITION) {
Assert.equal(service.profileCount, 2, "Should be two profiles.");
} else {
Assert.equal(service.profileCount, 1, "Should be only one profile.");
Assert.equal(profile, service.defaultProfile, "Should now be the default profile.");
}
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have created a new profile with the right name.");
Assert.equal(profile, service.defaultProfile, "Should now be the default profile.");
Assert.equal(profile.name, DEDICATED_NAME, "Should have created a new profile with the right name.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
profile = profileData.profiles[1];
Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
let hash = xreDirProvider.getInstallHash();
Assert.ok(installData.installs[hash].locked, "Should have locked the profile");
});

View File

@ -0,0 +1,43 @@
/*
* Tests that when the default application claims the old-style default profile
* it locks it to itself.
*/
add_task(async () => {
gIsDefaultApp = true;
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: true,
}],
});
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let hash = xreDirProvider.getInstallHash();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
Assert.ok(installData.installs[hash].locked, "Should have locked as we're the default app.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, "default");
});

View File

@ -0,0 +1,73 @@
/*
* Tests that an old-style default profile previously used by this build gets
* updated to a dedicated profile for this build.
*/
add_task(async () => {
let mydefaultProfile = makeRandomProfileDir("mydefault");
let defaultProfile = makeRandomProfileDir("default");
let devDefaultProfile = makeRandomProfileDir("devedition");
writeCompatibilityIni(mydefaultProfile);
writeCompatibilityIni(devDefaultProfile);
writeProfilesIni({
profiles: [{
name: "mydefault",
path: mydefaultProfile.leafName,
default: true,
}, {
name: "default",
path: defaultProfile.leafName,
}, {
name: "dev-edition-default",
path: devDefaultProfile.leafName,
}],
});
let service = getProfileService();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let hash = xreDirProvider.getInstallHash();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 3, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original non-default profile.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
profile = profileData.profiles[1];
Assert.equal(profile.name, "dev-edition-default", "Should have the right name.");
Assert.equal(profile.path, devDefaultProfile.leafName, "Should be the original dev default profile.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
profile = profileData.profiles[2];
Assert.equal(profile.name, "mydefault", "Should have the right name.");
Assert.equal(profile.path, mydefaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
if (AppConstants.MOZ_DEV_EDITION) {
Assert.equal(installData.installs[hash].default, devDefaultProfile.leafName, "Should have marked the original dev default profile as the default for this install.");
} else {
Assert.equal(installData.installs[hash].default, mydefaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
}
Assert.ok(!installData.installs[hash].locked, "Should not be locked as we're not the default app.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
if (AppConstants.MOZ_DEV_EDITION) {
Assert.ok(selectedProfile.rootDir.equals(devDefaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, "dev-edition-default");
} else {
Assert.ok(selectedProfile.rootDir.equals(mydefaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, "mydefault");
}
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
});

View File

@ -0,0 +1,48 @@
/**
* 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
* by this build. It means this install has previously used dedicated profiles.
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: true,
}],
});
writeInstallsIni({
installs: {
[hash]: {
default: "foobar",
},
},
});
let service = getProfileService();
testStartsProfileManager();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
// We keep the data here so we don't steal on the next reboot...
Assert.equal(Object.keys(installData.installs).length, 1, "Still list the broken reference.");
checkProfileService(profileData, installData);
});

View File

@ -11,4 +11,5 @@ add_task(async () => {
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(!profile, "Should not be a returned profile.");
Assert.equal(service.profileCount, 0, "Still should be no profiles.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
});

View File

@ -0,0 +1,61 @@
/**
* Tests that calling nsIToolkitProfile.remove on the default profile correctly
* removes the profile.
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
let profilesIni = {
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: true,
}],
};
writeProfilesIni(profilesIni);
let installsIni = {
installs: {
[hash]: {
default: defaultProfile.leafName,
},
},
};
writeInstallsIni(installsIni);
let service = getProfileService();
checkProfileService(profilesIni, installsIni);
let { profile, didCreate } = selectStartupProfile();
Assert.ok(!didCreate, "Should have not created a new profile.");
Assert.equal(profile.name, "default", "Should have selected the default profile.");
Assert.equal(profile, service.defaultProfile, "Should have selected the default profile.");
checkProfileService(profilesIni, installsIni);
// In an actual run of Firefox we wouldn't be able to delete the profile in
// use because it would be locked. But we don't actually lock the profile in
// tests.
profile.remove(false);
Assert.ok(!service.defaultProfile, "Should no longer be a default profile.");
Assert.equal(profile, service.currentProfile, "Should still be the profile in use.");
// These are the modifications that should have been made.
profilesIni.profiles.pop();
installsIni.installs[hash].default = "";
checkProfileService(profilesIni, installsIni);
service.flush();
// And that should have flushed to disk correctly.
checkProfileService();
// checkProfileService doesn't differentiate between a blank default profile
// for the install and a missing install.
let installs = readInstallsIni();
Assert.equal(installs.installs[hash].default, "", "Should be a blank default profile.");
});

View File

@ -3,6 +3,8 @@
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let profileData = {
options: {
startWithLastProfile: true,
@ -15,6 +17,13 @@ add_task(async () => {
path: "Path3",
}],
};
let installData = {
installs: {
[hash]: {
default: "Path2",
},
},
};
if (AppConstants.MOZ_DEV_EDITION) {
profileData.profiles.push({
@ -34,13 +43,15 @@ add_task(async () => {
}
writeProfilesIni(profileData);
writeInstallsIni(installData);
let { profile, didCreate } = selectStartupProfile();
let service = getProfileService();
checkProfileService(profileData);
let { profile, didCreate } = selectStartupProfile();
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.equal(profile, service.defaultProfile, "Should have returned the default profile.");
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have selected the right profile");
Assert.equal(profile.name, "default", "Should have selected the right profile");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
});

View File

@ -0,0 +1,45 @@
/*
* 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
* last used by this build. This test checks the case where it was.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: false,
}],
});
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let hash = xreDirProvider.getInstallHash();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
let service = getProfileService();
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, "default");
});

View File

@ -0,0 +1,54 @@
/*
* 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
* last used by this build. This test checks the case where it wasn't.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
// Just pretend this profile was last used by something in the profile dir.
let greDir = gProfD.clone();
greDir.append("app");
writeCompatibilityIni(defaultProfile, greDir, greDir);
writeProfilesIni({
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: false,
}],
});
let service = getProfileService();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 0, "Should be no defaults for installs yet.");
checkProfileService(profileData, installData);
let { profile: selectedProfile, didCreate } = selectStartupProfile();
Assert.ok(didCreate, "Should have created a new profile.");
Assert.ok(service.createdAlternateProfile, "Should have created an alternate profile.");
Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, DEDICATED_NAME);
profileData = readProfilesIni();
profile = profileData.profiles[0];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should now be marked as the old-style default.");
checkProfileService(profileData);
});

View File

@ -0,0 +1,44 @@
/*
* Tests that an old-style default profile not previously used by this build gets
* used in a snap environment.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
// Just pretend this profile was last used by something in the profile dir.
let greDir = gProfD.clone();
greDir.append("app");
writeCompatibilityIni(defaultProfile, greDir, greDir);
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
simulateSnapEnvironment();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let profileData = readProfilesIni();
let installsINI = gDataHome.clone();
installsINI.append("installs.ini");
Assert.ok(!installsINI.exists(), "Installs database should not have been created.");
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
checkProfileService(profileData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
});

View File

@ -0,0 +1,20 @@
/*
* Tests that from a clean slate snap builds create an appropriate profile.
*/
add_task(async () => {
simulateSnapEnvironment();
let service = getProfileService();
let { profile, didCreate } = selectStartupProfile();
Assert.ok(didCreate, "Should have created a new profile.");
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have used the normal name.");
if (AppConstants.MOZ_DEV_EDITION) {
Assert.equal(service.profileCount, 2, "Should be two profiles.");
} else {
Assert.equal(service.profileCount, 1, "Should be only one profile.");
}
checkProfileService();
});

View File

@ -0,0 +1,51 @@
/*
* 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.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
writeInstallsIni({
installs: {
otherhash: {
default: defaultProfile.leafName,
},
},
});
let service = getProfileService();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let hash = xreDirProvider.getInstallHash();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should only be one known installs.");
Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have taken the original default profile as the default for the current install.");
Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
});

View File

@ -0,0 +1,44 @@
/*
* Tests that an old-style default profile previously used by this build gets
* updated to a dedicated profile for this build.
*/
add_task(async () => {
let defaultProfile = makeRandomProfileDir("default");
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
let service = getProfileService();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let hash = xreDirProvider.getInstallHash();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be only one known install.");
Assert.equal(installData.installs[hash].default, defaultProfile.leafName, "Should have marked the original default profile as the default for this install.");
Assert.ok(!installData.installs[hash].locked, "Should not have locked as we're not the default app.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
});

View File

@ -0,0 +1,42 @@
/*
* Tests that an old-style default profile not previously used by any build gets
* updated to a dedicated profile for this build.
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
let service = getProfileService();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 1, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be a default for installs.");
Assert.equal(installData.installs[hash].default, profile.path, "Should have the right default profile.");
Assert.ok(!installData.installs[hash].locked, "Should not have locked as we didn't create this profile for this install.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(!service.createdAlternateProfile, "Should not have created an alternate profile.");
Assert.ok(selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, PROFILE_DEFAULT);
});

View File

@ -0,0 +1,56 @@
/*
* Tests that an old-style default profile not previously used by this build gets
* ignored.
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
// Just pretend this profile was last used by something in the profile dir.
let greDir = gProfD.clone();
greDir.append("app");
writeCompatibilityIni(defaultProfile, greDir, greDir);
writeProfilesIni({
profiles: [{
name: PROFILE_DEFAULT,
path: defaultProfile.leafName,
default: true,
}],
});
let service = getProfileService();
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 2, "Should have the right number of profiles.");
// The name ordering is different for dev edition.
if (AppConstants.MOZ_DEV_EDITION) {
profileData.profiles.reverse();
}
let profile = profileData.profiles[0];
Assert.equal(profile.name, PROFILE_DEFAULT, "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
profile = profileData.profiles[1];
Assert.equal(profile.name, DEDICATED_NAME, "Should have the right name.");
Assert.notEqual(profile.path, defaultProfile.leafName, "Should not be the original default profile.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 1, "Should be a default for this install.");
Assert.equal(installData.installs[hash].default, profile.path, "Should have marked the new profile as the default for this install.");
Assert.ok(installData.installs[hash].locked, "Should have locked as we created this profile for this install.");
checkProfileService(profileData, installData);
Assert.ok(didCreate, "Should have created a new profile.");
Assert.ok(service.createdAlternateProfile, "Should have created an alternate profile.");
Assert.ok(!selectedProfile.rootDir.equals(defaultProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, DEDICATED_NAME);
});

View File

@ -0,0 +1,66 @@
/**
* Tests that if installs.ini lists a profile we use it as the default.
*/
add_task(async () => {
let hash = xreDirProvider.getInstallHash();
let defaultProfile = makeRandomProfileDir("default");
let dedicatedProfile = makeRandomProfileDir("dedicated");
let devProfile = makeRandomProfileDir("devedition");
// Make sure we don't steal the old-style default.
writeCompatibilityIni(defaultProfile);
writeProfilesIni({
profiles: [{
name: "default",
path: defaultProfile.leafName,
default: true,
}, {
name: "dedicated",
path: dedicatedProfile.leafName,
}, {
name: "dev-edition-default",
path: devProfile.leafName,
}],
});
writeInstallsIni({
installs: {
[hash]: {
default: dedicatedProfile.leafName,
},
"otherhash": {
default: "foobar",
},
},
});
let { profile: selectedProfile, didCreate } = selectStartupProfile();
let profileData = readProfilesIni();
let installData = readInstallsIni();
Assert.ok(profileData.options.startWithLastProfile, "Should be set to start with the last profile.");
Assert.equal(profileData.profiles.length, 3, "Should have the right number of profiles.");
let profile = profileData.profiles[0];
Assert.equal(profile.name, `dedicated`, "Should have the right name.");
Assert.equal(profile.path, dedicatedProfile.leafName, "Should be the expected dedicated profile.");
Assert.ok(!profile.default, "Should not be marked as the old-style default.");
profile = profileData.profiles[1];
Assert.equal(profile.name, "default", "Should have the right name.");
Assert.equal(profile.path, defaultProfile.leafName, "Should be the original default profile.");
Assert.ok(profile.default, "Should be marked as the old-style default.");
Assert.equal(Object.keys(installData.installs).length, 2, "Should be two known installs.");
Assert.equal(installData.installs[hash].default, dedicatedProfile.leafName, "Should have kept the default for this install.");
Assert.equal(installData.installs.otherhash.default, "foobar", "Should have kept the default for the other install.");
checkProfileService(profileData, installData);
Assert.ok(!didCreate, "Should not have created a new profile.");
Assert.ok(selectedProfile.rootDir.equals(dedicatedProfile), "Should be using the right directory.");
Assert.equal(selectedProfile.name, "dedicated");
});

View File

@ -11,3 +11,20 @@ skip-if = toolkit == 'android'
[test_select_environment.js]
[test_select_environment_named.js]
[test_profile_reset.js]
[test_clean.js]
[test_previous_dedicated.js]
[test_single_profile_selected.js]
skip-if = devedition
[test_single_profile_unselected.js]
skip-if = devedition
[test_update_selected_dedicated.js]
[test_update_unknown_dedicated.js]
[test_update_unselected_dedicated.js]
[test_use_dedicated.js]
[test_new_default.js]
[test_steal_inuse.js]
[test_snap.js]
[test_snap_empty.js]
[test_remove_default.js]
[test_claim_locked.js]
[test_lock.js]

View File

@ -31,10 +31,12 @@ static const char kProfileProperties[] =
"chrome://mozapps/locale/profile/profileSelection.properties";
/**
* Delete the profile directory being reset after a backup and delete the local
* profile directory.
* Spin up a thread to backup the old profile's main directory and delete the
* profile's local directory. Once complete have the profile service remove the
* old profile and if necessary make the new profile the default.
*/
nsresult ProfileResetCleanup(nsIToolkitProfile* aOldProfile) {
nsresult ProfileResetCleanup(nsToolkitProfileService* aService,
nsIToolkitProfile* aOldProfile) {
nsresult rv;
nsCOMPtr<nsIFile> profileDir;
rv = aOldProfile->GetRootDir(getter_AddRefs(profileDir));
@ -142,10 +144,5 @@ nsresult ProfileResetCleanup(nsIToolkitProfile* aOldProfile) {
auto* piWindow = nsPIDOMWindowOuter::From(progressWindow);
piWindow->Close();
// Delete the old profile from profiles.ini. The folder was already deleted by
// the thread above.
rv = aOldProfile->Remove(false);
if (NS_FAILED(rv)) NS_WARNING("Could not remove the profile");
return rv;
return aService->ApplyResetProfile(aOldProfile);
}

View File

@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsIToolkitProfileService.h"
#include "nsToolkitProfileService.h"
#include "nsIFile.h"
#include "nsThreadUtils.h"
@ -11,7 +11,8 @@ static bool gProfileResetCleanupCompleted = false;
static const char kResetProgressURL[] =
"chrome://global/content/resetProfileProgress.xul";
nsresult ProfileResetCleanup(nsIToolkitProfile* aOldProfile);
nsresult ProfileResetCleanup(nsToolkitProfileService* aService,
nsIToolkitProfile* aOldProfile);
class ProfileResetCleanupResultTask : public mozilla::Runnable {
public:

View File

@ -2050,41 +2050,6 @@ static ReturnAbortOnError ShowProfileManager(
return LaunchChild(aNative);
}
/**
* Get the currently running profile using its root directory.
*
* @param aProfileSvc The profile service
* @param aCurrentProfileRoot The root directory of the current profile.
* @param aProfile Out-param that returns the profile object.
* @return an error if aCurrentProfileRoot is not found
*/
static nsresult GetCurrentProfile(nsIToolkitProfileService* aProfileSvc,
nsIFile* aCurrentProfileRoot,
nsIToolkitProfile** aProfile) {
NS_ENSURE_ARG_POINTER(aProfileSvc);
NS_ENSURE_ARG_POINTER(aProfile);
nsCOMPtr<nsISimpleEnumerator> profiles;
nsresult rv = aProfileSvc->GetProfiles(getter_AddRefs(profiles));
if (NS_FAILED(rv)) return rv;
bool foundMatchingProfile = false;
nsCOMPtr<nsISupports> supports;
rv = profiles->GetNext(getter_AddRefs(supports));
while (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIToolkitProfile> profile = do_QueryInterface(supports);
nsCOMPtr<nsIFile> profileRoot;
profile->GetRootDir(getter_AddRefs(profileRoot));
profileRoot->Equals(aCurrentProfileRoot, &foundMatchingProfile);
if (foundMatchingProfile) {
profile.forget(aProfile);
return NS_OK;
}
rv = profiles->GetNext(getter_AddRefs(supports));
}
return rv;
}
static bool gDoMigration = false;
static bool gDoProfileReset = false;
static nsCOMPtr<nsIToolkitProfile> gResetOldProfile;
@ -4130,18 +4095,6 @@ nsresult XREMain::XRE_mainRun() {
}
{
bool profileWasDefault = false;
if (gDoProfileReset) {
nsCOMPtr<nsIToolkitProfile> defaultProfile;
// This can fail if there is no default profile.
// That shouldn't stop reset from proceeding.
nsresult gotDefault =
mProfileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile));
if (NS_SUCCEEDED(gotDefault)) {
profileWasDefault = defaultProfile == gResetOldProfile;
}
}
// Profile Migration
if (mAppData->flags & NS_XRE_ENABLE_PROFILE_MIGRATOR && gDoMigration) {
gDoMigration = false;
@ -4161,33 +4114,11 @@ nsresult XREMain::XRE_mainRun() {
}
if (gDoProfileReset) {
nsresult backupCreated = ProfileResetCleanup(gResetOldProfile);
nsresult backupCreated = ProfileResetCleanup(
static_cast<nsToolkitProfileService*>(mProfileSvc.get()),
gResetOldProfile);
if (NS_FAILED(backupCreated))
NS_WARNING("Could not cleanup the profile that was reset");
nsCOMPtr<nsIToolkitProfile> newProfile;
rv = GetCurrentProfile(mProfileSvc, mProfD, getter_AddRefs(newProfile));
if (NS_SUCCEEDED(rv)) {
nsAutoCString name;
gResetOldProfile->GetName(name);
newProfile->SetName(name);
mProfileName.Assign(name);
// Set the new profile as the default after we're done cleaning up the
// old profile, iff that profile was already the default
if (profileWasDefault) {
rv = mProfileSvc->SetDefaultProfile(newProfile);
if (NS_FAILED(rv))
NS_WARNING("Could not set current profile as the default");
}
} else {
NS_WARNING(
"Could not find current profile to set as default / change name.");
}
// Need to write out the fact that the profile has been removed, the new
// profile renamed, and potentially that the selected/default profile
// changed.
mProfileSvc->Flush();
}
}

View File

@ -5,6 +5,9 @@
#include "nsAppRunner.h"
#include "nsXREDirProvider.h"
#ifndef ANDROID
# include "commonupdatedir.h"
#endif
#include "jsapi.h"
#include "xpcpublic.h"
@ -1621,7 +1624,7 @@ nsresult nsXREDirProvider::AppendProfilePath(nsIFile* aFile, bool aLocal) {
vendor = gAppData->vendor;
}
nsresult rv;
nsresult rv = NS_OK;
#if defined(XP_MACOSX)
if (!profile.IsEmpty()) {
@ -1683,10 +1686,13 @@ nsresult nsXREDirProvider::AppendProfilePath(nsIFile* aFile, bool aLocal) {
folder.Truncate();
}
folder.Append(appName);
ToLowerCase(folder);
// This can be the case in tests.
if (!appName.IsEmpty()) {
folder.Append(appName);
ToLowerCase(folder);
rv = aFile->AppendNative(folder);
rv = aFile->AppendNative(folder);
}
}
NS_ENSURE_SUCCESS(rv, rv);