Bug 1528998: Apply profile snatching behaviour when the first run of a dedicated build is after a restart. r=froydnj
authorDave Townsend <dtownsend@oxymoronical.com>
Wed, 27 Feb 2019 19:24:04 +0000
changeset 519373 be0aba20d1d170ceaf5333e3c25b1db23ec42c64
parent 519372 e767399d23cc15bfeae586546f3b9cfb9df573e5
child 519374 86bed3fcf6f50e90e64cbde7b67659a267d6b955
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfroydnj
bugs1528998
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1528998: Apply profile snatching behaviour when the first run of a dedicated build is after a restart. r=froydnj If Firefox was using the default profile before restarting to upgrade to a build supporting dedicated profiles then we should check if we can make the selected profile the default for this build and if not create the user a new profile. Differential Revision: https://phabricator.services.mozilla.com/D20415
toolkit/components/telemetry/Scalars.yaml
toolkit/profile/nsToolkitProfileService.cpp
toolkit/profile/nsToolkitProfileService.h
toolkit/profile/xpcshell/test_select_environment_named.js
toolkit/profile/xpcshell/test_skip_locked_environment.js
toolkit/profile/xpcshell/test_snatch_environment.js
toolkit/profile/xpcshell/test_snatch_environment_default.js
toolkit/profile/xpcshell/xpcshell.ini
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -2460,26 +2460,32 @@ startup:
           occured.
         profile-manager:
           The profile was selected by the profile manager.
         profile-reset:
           The profile was selected for reset, normally this would mean a restart.
         restart:
           The user restarted the application, the same profile as previous will
           be used.
-        argument-profile
+        argument-profile:
           The profile was selected by the --profile command line argument.
-        argument-p
+        argument-p:
           The profile was selected by the -p command line argument.
-        firstrun-claimed-default
+        firstrun-claimed-default:
           A first run of a dedicated profiles build chose the old default
           profile to be the default for this install.
         firstrun-skipped-default:
           A first run of a dedicated profiles build skipped over the old default
           profile and created a new profile.
+        restart-claimed-default:
+          A first run of a dedicated profiles build after a restart chose the
+          old default profile to be the default for this install.
+        restart-skipped-default:
+          A first run of a dedicated profiles build after a restart skipped over
+          the old default profile and created a new profile.
         firstrun-created-default:
           A first run of the application created a new profile to use.
         default:
           The default profile was selected as normal.
     expires: "72"
     keyed: false
     kind: string
     notification_emails:
--- a/toolkit/profile/nsToolkitProfileService.cpp
+++ b/toolkit/profile/nsToolkitProfileService.cpp
@@ -788,16 +788,41 @@ nsresult nsToolkitProfileService::GetPro
   aDescriptor.Assign(profilePath);
   if (aIsRelative) {
     *aIsRelative = isRelative;
   }
 
   return NS_OK;
 }
 
+nsresult nsToolkitProfileService::CreateDefaultProfile(nsIToolkitProfile** aResult) {
+  // Create a new default profile
+  nsAutoCString name;
+  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 = CreateUniqueProfile(nullptr, name, aResult);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (mUseDedicatedProfile) {
+    SetDefaultProfile(mCurrent);
+  } else if (mUseDevEditionProfile) {
+    mDevEditionDefault = mCurrent;
+  } else {
+    mNormalDefault = mCurrent;
+  }
+
+  return NS_OK;
+}
+
 /**
  * An implementation of SelectStartupProfile callable from JavaScript via XPCOM.
  * See nsIToolkitProfileService.idl.
  */
 NS_IMETHODIMP
 nsToolkitProfileService::SelectStartupProfile(
     const nsTArray<nsCString>& aArgv, bool aIsResetting, nsIFile** aRootDir,
     nsIFile** aLocalDir, nsIToolkitProfile** aProfile, bool* aDidCreate) {
@@ -863,34 +888,76 @@ nsresult nsToolkitProfileService::Select
   // app initiated restart).
   nsCOMPtr<nsIFile> lf = GetFileFromEnv("XRE_PROFILE_PATH");
   if (lf) {
     nsCOMPtr<nsIFile> localDir = GetFileFromEnv("XRE_PROFILE_LOCAL_PATH");
     if (!localDir) {
       localDir = lf;
     }
 
+    // Clear out flags that we handled (or should have handled!) last startup.
+    const char* dummy;
+    CheckArg(*aArgc, aArgv, "p", &dummy);
+    CheckArg(*aArgc, aArgv, "profile", &dummy);
+    CheckArg(*aArgc, aArgv, "profilemanager");
+
+    nsCOMPtr<nsIToolkitProfile> profile;
+    GetProfileByDir(lf, localDir, getter_AddRefs(profile));
+
+    if (profile && mIsFirstRun && mUseDedicatedProfile) {
+      if (profile == (mUseDevEditionProfile ? mDevEditionDefault : mNormalDefault)) {
+        // This is the first run of a dedicated profile build where the selected
+        // 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)) {
+          mStartupReason = NS_LITERAL_STRING("restart-claimed-default");
+
+          mCurrent = profile;
+        } else {
+          if (aIsResetting) {
+            // We don't want to create a fresh profile when we're attempting a
+            // profile reset so just bail out here, the calling code will handle it.
+            *aProfile = nullptr;
+            return NS_OK;
+          }
+
+          rv = CreateDefaultProfile(getter_AddRefs(mCurrent));
+          if (NS_FAILED(rv)) {
+            *aProfile = nullptr;
+            return rv;
+          }
+
+          Flush();
+
+          mStartupReason = NS_LITERAL_STRING("restart-skipped-default");
+          *aDidCreate = true;
+          mCreatedAlternateProfile = true;
+        }
+
+        NS_IF_ADDREF(*aProfile = mCurrent);
+        mCurrent->GetRootDir(aRootDir);
+        mCurrent->GetLocalDir(aLocalDir);
+
+        return NS_OK;
+      }
+    }
+
     if (EnvHasValue("XRE_RESTARTED_BY_PROFILE_MANAGER")) {
       mStartupReason = NS_LITERAL_STRING("profile-manager");
     } else if (aIsResetting) {
       mStartupReason = NS_LITERAL_STRING("profile-reset");
     } else {
       mStartupReason = NS_LITERAL_STRING("restart");
     }
 
-    // Clear out flags that we handled (or should have handled!) last startup.
-    const char* dummy;
-    CheckArg(*aArgc, aArgv, "p", &dummy);
-    CheckArg(*aArgc, aArgv, "profile", &dummy);
-    CheckArg(*aArgc, aArgv, "profilemanager");
-
-    GetProfileByDir(lf, localDir, getter_AddRefs(mCurrent));
+    mCurrent = profile;
     lf.forget(aRootDir);
     localDir.forget(aLocalDir);
-    NS_IF_ADDREF(*aProfile = mCurrent);
+    NS_IF_ADDREF(*aProfile = profile);
     return NS_OK;
   }
 
   // Check the -profile command line argument. It accepts a single argument that
   // gives the path to use for the profile.
   ArgResult ar = CheckArg(*aArgc, aArgv, "profile", &arg,
                           CheckArgFlag::CheckOSInt | CheckArgFlag::RemoveArg);
   if (ar == ARG_BAD) {
@@ -1077,36 +1144,18 @@ nsresult nsToolkitProfileService::Select
           // 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 = true;
         }
       }
     }
 
-    // Create a new default profile
-    nsAutoCString name;
-    if (mUseDedicatedProfile) {
-      name.AssignLiteral("default-" NS_STRINGIFY(MOZ_UPDATE_CHANNEL));
-    } else if (mUseDevEditionProfile) {
-      name.AssignLiteral(DEV_EDITION_NAME);
-    } else {
-      name.AssignLiteral(DEFAULT_NAME);
-    }
-
-    rv = CreateUniqueProfile(nullptr, name, getter_AddRefs(mCurrent));
+    rv = CreateDefaultProfile(getter_AddRefs(mCurrent));
     if (NS_SUCCEEDED(rv)) {
-      if (mUseDedicatedProfile) {
-        SetDefaultProfile(mCurrent);
-      } else if (mUseDevEditionProfile) {
-        mDevEditionDefault = mCurrent;
-      } 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));
--- a/toolkit/profile/nsToolkitProfileService.h
+++ b/toolkit/profile/nsToolkitProfileService.h
@@ -96,16 +96,17 @@ class nsToolkitProfileService final : pu
                        nsIToolkitProfile** aResult);
 
   nsresult GetProfileDescriptor(nsIToolkitProfile* aProfile,
                                 nsACString& aDescriptor, bool* aIsRelative);
   bool IsProfileForCurrentInstall(nsIToolkitProfile* aProfile);
   void ClearProfileFromOtherInstalls(nsIToolkitProfile* aProfile);
   bool MaybeMakeDefaultDedicatedProfile(nsIToolkitProfile* aProfile);
   bool IsSnapEnvironment();
+  nsresult CreateDefaultProfile(nsIToolkitProfile** aResult);
 
   // 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.
--- a/toolkit/profile/xpcshell/test_select_environment_named.js
+++ b/toolkit/profile/xpcshell/test_select_environment_named.js
@@ -35,9 +35,12 @@ add_task(async () => {
   let { rootDir, localDir, profile, didCreate } = selectStartupProfile(["-P", "Profile3"]);
   checkStartupReason("restart");
 
   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.equal(profile.name, "Profile1", "The right profile was matched.");
+
+  let service = getProfileService();
+  Assert.notEqual(service.defaultProfile, profile, "Should not be the default profile.");
 });
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_skip_locked_environment.js
@@ -0,0 +1,77 @@
+/*
+ * 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
+ * locked by another install.
+ */
+
+add_task(async () => {
+  let root = makeRandomProfileDir("foo");
+  let local = gDataHomeLocal.clone();
+  local.append("foo");
+
+  writeCompatibilityIni(root);
+
+  let profileData = {
+    options: {
+      startWithLastProfile: true,
+    },
+    profiles: [{
+      name: "Profile1",
+      path: root.leafName,
+      default: true,
+    }, {
+      name: "Profile2",
+      path: "Path2",
+    }, {
+      name: "Profile3",
+      path: "Path3",
+    }],
+  };
+
+  // Another install is using the profile and it is locked.
+  let installData = {
+    installs: {
+      otherinstall: {
+        default: root.leafName,
+        locked: true,
+      },
+    },
+  };
+
+  writeProfilesIni(profileData);
+  writeInstallsIni(installData);
+  checkProfileService(profileData, installData);
+
+  let env = Cc["@mozilla.org/process/environment;1"].
+            getService(Ci.nsIEnvironment);
+  env.set("XRE_PROFILE_PATH", root.path);
+  env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+  let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+  checkStartupReason("restart-skipped-default");
+
+  Assert.ok(didCreate, "Should 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 was returned.");
+  Assert.equal(profile.name, DEDICATED_NAME, "The right profile name was used.");
+
+  let service = getProfileService();
+  Assert.equal(service.defaultProfile, profile, "Should be the default profile.");
+  Assert.equal(service.currentProfile, profile, "Should be the current profile.");
+
+  profileData = readProfilesIni();
+  Assert.equal(profileData.profiles[0].name, DEDICATED_NAME, "Should be the right profile.");
+  Assert.ok(!profileData.profiles[0].default, "Should not be the old default profile.");
+  Assert.equal(profileData.profiles[1].name, "Profile1", "Should be the right profile.");
+  Assert.ok(profileData.profiles[1].default, "Should be the old default profile.");
+  Assert.equal(profileData.profiles[1].path, root.leafName, "Should be the correct path.");
+
+  let hash = xreDirProvider.getInstallHash();
+  installData = readInstallsIni();
+  Assert.equal(Object.keys(installData.installs).length, 2, "Should be one known install.");
+  Assert.notEqual(installData.installs[hash].default, root.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 created the profile for this install.");
+  Assert.equal(installData.installs.otherinstall.default, root.leafName, "Should have left the other profile as the default for the other install.");
+  Assert.ok(installData.installs[hash].locked, "Should still be locked to the other install.");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snatch_environment.js
@@ -0,0 +1,72 @@
+/*
+ * 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
+ * default profile.
+ */
+
+add_task(async () => {
+  let root = makeRandomProfileDir("foo");
+  let local = gDataHomeLocal.clone();
+  local.append("foo");
+
+  writeCompatibilityIni(root);
+
+  let profileData = {
+    options: {
+      startWithLastProfile: true,
+    },
+    profiles: [{
+      name: "Profile1",
+      path: root.leafName,
+      default: true,
+    }, {
+      name: "Profile2",
+      path: "Path2",
+    }, {
+      name: "Profile3",
+      path: "Path3",
+    }],
+  };
+
+  // Another install is using the profile but it isn't locked.
+  let installData = {
+    installs: {
+      otherinstall: {
+        default: root.leafName,
+      },
+    },
+  };
+
+  writeProfilesIni(profileData);
+  writeInstallsIni(installData);
+  checkProfileService(profileData, installData);
+
+  let env = Cc["@mozilla.org/process/environment;1"].
+            getService(Ci.nsIEnvironment);
+  env.set("XRE_PROFILE_PATH", root.path);
+  env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+  let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+  checkStartupReason("restart-claimed-default");
+
+  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.equal(profile.name, "Profile1", "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.");
+
+  profileData = readProfilesIni();
+  Assert.equal(profileData.profiles[0].name, "Profile1", "Should be the right profile.");
+  Assert.ok(profileData.profiles[0].default, "Should still be the old default profile.");
+
+  let hash = xreDirProvider.getInstallHash();
+  installData = readInstallsIni();
+  // The info about the other install will have been removed so it goes through first run on next startup.
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be one known install.");
+  Assert.equal(installData.installs[hash].default, root.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.");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/profile/xpcshell/test_snatch_environment_default.js
@@ -0,0 +1,74 @@
+/*
+ * 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
+ * default profile and lock it when we're the default app.
+ */
+
+add_task(async () => {
+  gIsDefaultApp = true;
+
+  let root = makeRandomProfileDir("foo");
+  let local = gDataHomeLocal.clone();
+  local.append("foo");
+
+  writeCompatibilityIni(root);
+
+  let profileData = {
+    options: {
+      startWithLastProfile: true,
+    },
+    profiles: [{
+      name: "Profile1",
+      path: root.leafName,
+      default: true,
+    }, {
+      name: "Profile2",
+      path: "Path2",
+    }, {
+      name: "Profile3",
+      path: "Path3",
+    }],
+  };
+
+  // Another install is using the profile but it isn't locked.
+  let installData = {
+    installs: {
+      otherinstall: {
+        default: root.leafName,
+      },
+    },
+  };
+
+  writeProfilesIni(profileData);
+  writeInstallsIni(installData);
+  checkProfileService(profileData, installData);
+
+  let env = Cc["@mozilla.org/process/environment;1"].
+            getService(Ci.nsIEnvironment);
+  env.set("XRE_PROFILE_PATH", root.path);
+  env.set("XRE_PROFILE_LOCAL_PATH", local.path);
+
+  let { rootDir, localDir, profile, didCreate } = selectStartupProfile();
+  checkStartupReason("restart-claimed-default");
+
+  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.equal(profile.name, "Profile1", "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.");
+
+  profileData = readProfilesIni();
+  Assert.equal(profileData.profiles[0].name, "Profile1", "Should be the right profile.");
+  Assert.ok(profileData.profiles[0].default, "Should still be the old default profile.");
+
+  let hash = xreDirProvider.getInstallHash();
+  installData = readInstallsIni();
+  // The info about the other install will have been removed so it goes through first run on next startup.
+  Assert.equal(Object.keys(installData.installs).length, 1, "Should be one known install.");
+  Assert.equal(installData.installs[hash].default, root.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.");
+});
--- a/toolkit/profile/xpcshell/xpcshell.ini
+++ b/toolkit/profile/xpcshell/xpcshell.ini
@@ -24,8 +24,11 @@ skip-if = devedition
 [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]
 [test_startswithlast.js]
+[test_snatch_environment.js]
+[test_skip_locked_environment.js]
+[test_snatch_environment_default.js]