1. main patch - Bug 642765 - Add ability to channel change to the client. r=mossop
authorRobert Strong <robert.bugzilla@gmail.com>
Mon, 11 Apr 2011 21:24:16 -0700
changeset 67961 30d29e1da9e3017021290954bee6776146ce17b7
parent 67960 35e27b34c7c515f8f3b76962ca5521fa5ab64bee
child 67962 b4dcf0feefd18f8f94c28f8dbaa1e02d7aecf7fe
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmossop
bugs1, 642765
milestone2.2a1pre
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
1. main patch - Bug 642765 - Add ability to channel change to the client. r=mossop
toolkit/mozapps/update/nsUpdateService.js
toolkit/mozapps/update/test/shared.js
toolkit/mozapps/update/test/unit/test_0040_general.js
toolkit/xre/nsUpdateDriver.cpp
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -57,16 +57,17 @@ const PREF_APP_UPDATE_BACKGROUND_INTERVA
 const PREF_APP_UPDATE_BACKGROUNDERRORS    = "app.update.backgroundErrors";
 const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
 const PREF_APP_UPDATE_CERTS_BRANCH        = "app.update.certs.";
 const PREF_APP_UPDATE_CERT_CHECKATTRS     = "app.update.cert.checkAttributes";
 const PREF_APP_UPDATE_CERT_ERRORS         = "app.update.cert.errors";
 const PREF_APP_UPDATE_CERT_MAXERRORS      = "app.update.cert.maxErrors";
 const PREF_APP_UPDATE_CERT_REQUIREBUILTIN = "app.update.cert.requireBuiltIn";
 const PREF_APP_UPDATE_CHANNEL             = "app.update.channel";
+const PREF_APP_UPDATE_DESIREDCHANNEL      = "app.update.desiredChannel";
 const PREF_APP_UPDATE_ENABLED             = "app.update.enabled";
 const PREF_APP_UPDATE_IDLETIME            = "app.update.idletime";
 const PREF_APP_UPDATE_INCOMPATIBLE_MODE   = "app.update.incompatible.mode";
 const PREF_APP_UPDATE_INTERVAL            = "app.update.interval";
 const PREF_APP_UPDATE_LOG                 = "app.update.log";
 const PREF_APP_UPDATE_MODE                = "app.update.mode";
 const PREF_APP_UPDATE_NEVER_BRANCH        = "app.update.never.";
 const PREF_APP_UPDATE_POSTUPDATE          = "app.update.postupdate";
@@ -100,16 +101,17 @@ const KEY_GRED            = "GreD";
 #define USE_UPDROOT
 #endif
 
 #ifdef USE_UPDROOT
 const KEY_UPDROOT         = "UpdRootD";
 #endif
 
 const DIR_UPDATES         = "updates";
+const FILE_CHANNELCHANGE  = "channelchange";
 const FILE_UPDATE_STATUS  = "update.status";
 const FILE_UPDATE_VERSION = "update.version";
 #ifdef ANDROID
 const FILE_UPDATE_ARCHIVE = "update.apk";
 #else
 const FILE_UPDATE_ARCHIVE = "update.mar";
 #endif
 const FILE_UPDATE_LOG     = "update.log"
@@ -489,16 +491,22 @@ function writeStatusFile(dir, state) {
 #           update doesn't provide the appVersion attribute in the update xml.
  */
 function writeVersionFile(dir, version) {
   var versionFile = dir.clone();
   versionFile.append(FILE_UPDATE_VERSION);
   writeStringToFile(versionFile, version);
 }
 
+function createChannelChangeFile(dir) {
+  var channelChangeFile = dir.clone();
+  channelChangeFile.append(FILE_CHANNELCHANGE);
+  channelChangeFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+}
+
 /**
  * Removes the contents of the Updates Directory
  */
 function cleanUpUpdatesDir() {
   // Bail out if we don't have appropriate permissions
   try {
     var updateDir = getUpdatesDir();
   }
@@ -614,16 +622,29 @@ function getUpdateChannel() {
   }
   catch (e) {
     Components.utils.reportError(e);
   }
 
   return channel;
 }
 
+function getDesiredChannel() {
+  let desiredChannel = getPref("getCharPref", PREF_APP_UPDATE_DESIREDCHANNEL, null);
+  if (!desiredChannel)
+    return null;
+  
+  if (desiredChannel == getUpdateChannel()) {
+    Services.prefs.clearUserPref(PREF_APP_UPDATE_DESIREDCHANNEL);
+    return null;
+  }
+  LOG("getDesiredChannel - channel set to: " + desiredChannel);
+  return desiredChannel;
+}
+
 /* Get the distribution pref values, from defaults only */
 function getDistributionPrefValue(aPrefName) {
   var prefValue = "default";
 
   try {
     prefValue = Services.prefs.getDefaultBranch(null).getCharPref(aPrefName);
   } catch (e) {
     // use default when pref not found
@@ -1118,16 +1139,19 @@ const UpdateServiceFactory = {
 
 /**
  * UpdateService
  * A Service for managing the discovery and installation of software updates.
  * @constructor
  */
 function UpdateService() {
   Services.obs.addObserver(this, "xpcom-shutdown", false);
+  // This will clear the preference if the channel is the same as the
+  // application's channel.
+  getDesiredChannel();
 }
 
 UpdateService.prototype = {
   /**
    * The downloader we are using to download updates. There is only ever one of
    * these.
    */
   _downloader: null,
@@ -1220,16 +1244,19 @@ UpdateService.prototype = {
 
       // Update the patch's metadata.
       um.activeUpdate = update;
       Services.prefs.setBoolPref(PREF_APP_UPDATE_POSTUPDATE, true);
       prompter.showUpdateInstalled();
 
       // Done with this update. Clean it up.
       cleanupActiveUpdate();
+
+      if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DESIREDCHANNEL))      
+        Services.prefs.clearUserPref(PREF_APP_UPDATE_DESIREDCHANNEL);
     }
     else {
       // If we hit an error, then the error code will be included in the status
       // string following a colon and a space. If we had an I/O error, then we
       // assume that the patch is not invalid, and we re-stage the patch so that
       // it can be attempted again the next time we restart. This will leave a
       // space at the beginning of the error code when there is a failure which
       // will be removed by using parseInt below. This prevents panic which has
@@ -1237,22 +1264,20 @@ UpdateService.prototype = {
       // example) when testing releases due to forgetting to include the space.
       var ary = status.split(":");
       update.state = ary[0];
       if (update.state == STATE_FAILED && ary[1]) {
         update.errorCode = parseInt(ary[1]);
         if (update.errorCode == WRITE_ERROR) {
           prompter.showUpdateError(update);
           writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
-          writeVersionFile(getUpdatesDir(), update.appVersion);
           return;
         }
         else if (update.errorCode == ELEVATION_CANCELED) {
           writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
-          writeVersionFile(getUpdatesDir(), update.appVersion);
           return;
         }
       }
 
       // Something went wrong with the patch application process.
       cleanupActiveUpdate();
 
       update.statusText = gUpdateBundle.GetStringFromName("patchApplyFailure");
@@ -1345,16 +1370,22 @@ UpdateService.prototype = {
    * @param   updates
    *          An array of available nsIUpdate items
    * @returns The nsIUpdate to offer.
    */
   selectUpdate: function AUS_selectUpdate(updates) {
     if (updates.length == 0)
       return null;
 
+    if (getDesiredChannel()) {
+      LOG("Checker:selectUpdate - skipping version checks for change change " +
+          "request");
+      return updates[0];
+    }
+
     // Choose the newest of the available minor and major updates.
     var majorUpdate = null;
     var minorUpdate = null;
     var vc = Services.vc;
 
     updates.forEach(function(aUpdate) {
       // Ignore updates for older versions of the application and updates for
       // the same version of the application with the same build ID.
@@ -1696,17 +1727,17 @@ UpdateService.prototype = {
   downloadUpdate: function AUS_downloadUpdate(update, background) {
     if (!update)
       throw Cr.NS_ERROR_NULL_POINTER;
 
     // Don't download the update if the update's version is less than the
     // current application's version or the update's version is the same as the
     // application's version and the build ID is the same as the application's
     // build ID.
-    if (update.appVersion &&
+    if (!getDesiredChannel() && update.appVersion &&
         (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 ||
          update.buildID && update.buildID == Services.appinfo.appBuildID &&
          update.appVersion == Services.appinfo.version)) {
       LOG("UpdateService:downloadUpdate - canceling download of update since " +
           "it is for an earlier or same application version and build ID.\n" +
           "current application version: " + Services.appinfo.version + "\n" +
           "update application version : " + update.appVersion + "\n" +
           "current build ID: " + Services.appinfo.appBuildID + "\n" +
@@ -1897,18 +1928,19 @@ UpdateManager.prototype = {
     this._ensureUpdates();
     return this._updates.length;
   },
 
   /**
    * See nsIUpdateService.idl
    */
   get activeUpdate() {
+    let currentChannel = getDesiredChannel() || getUpdateChannel();
     if (this._activeUpdate &&
-        this._activeUpdate.channel != getUpdateChannel()) {
+        this._activeUpdate.channel != currentChannel) {
       // User switched channels, clear out any old active updates and remove
       // partial downloads
       this._activeUpdate = null;
       this.saveUpdates();
 
       // Destroy the updates directory, since we're done with it.
       cleanUpUpdatesDir();
     }
@@ -2073,16 +2105,20 @@ Checker.prototype = {
     url = url.replace(/%CHANNEL%/g, getUpdateChannel());
     url = url.replace(/%PLATFORM_VERSION%/g, Services.appinfo.platformVersion);
     url = url.replace(/%DISTRIBUTION%/g,
                       getDistributionPrefValue(PREF_APP_DISTRIBUTION));
     url = url.replace(/%DISTRIBUTION_VERSION%/g,
                       getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
     url = url.replace(/\+/g, "%2B");
 
+    let desiredChannel = getDesiredChannel();
+    if (desiredChannel)
+      url += (url.indexOf("?") != -1 ? "&" : "?") + "newchannel=" + desiredChannel;
+
     if (force)
       url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1";
 
     LOG("Checker:getUpdateURL - update URL: " + url);
     return url;
   },
 
   /**
@@ -2154,17 +2190,17 @@ Checker.prototype = {
       updateElement.QueryInterface(Ci.nsIDOMElement);
       try {
         var update = new Update(updateElement);
       } catch (e) {
         LOG("Checker:updates get - invalid <update/>, ignoring...");
         continue;
       }
       update.serviceURL = this.getUpdateURL(this._forced);
-      update.channel = getUpdateChannel();
+      update.channel = getDesiredChannel() || getUpdateChannel();
       updates.push(update);
     }
 
     return updates;
   },
 
   /**
    * Returns the status code for the XMLHttpRequest
@@ -2668,16 +2704,18 @@ Downloader.prototype = {
         // download, since otherwise some kind of UI is already visible and
         // that UI will notify.
         if (this.background)
           shouldShowPrompt = true;
 
         // Tell the updater.exe we're ready to apply.
         writeStatusFile(getUpdatesDir(), state);
         writeVersionFile(getUpdatesDir(), this._update.appVersion);
+        if (getDesiredChannel())
+          createChannelChangeFile(getUpdatesDir());
         this._update.installDate = (new Date()).getTime();
         this._update.statusText = gUpdateBundle.GetStringFromName("installPending");
       }
       else {
         LOG("Downloader:onStopRequest - download verification failed");
         state = STATE_DOWNLOAD_FAILED;
 
         // TODO: use more informative error code here
--- a/toolkit/mozapps/update/test/shared.js
+++ b/toolkit/mozapps/update/test/shared.js
@@ -48,16 +48,17 @@ const PREF_APP_UPDATE_AUTO              
 const PREF_APP_UPDATE_BACKGROUNDERRORS    = "app.update.backgroundErrors";
 const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
 const PREF_APP_UPDATE_CERTS_BRANCH        = "app.update.certs.";
 const PREF_APP_UPDATE_CERT_CHECKATTRS     = "app.update.cert.checkAttributes";
 const PREF_APP_UPDATE_CERT_ERRORS         = "app.update.cert.errors";
 const PREF_APP_UPDATE_CERT_MAXERRORS      = "app.update.cert.maxErrors";
 const PREF_APP_UPDATE_CERT_REQUIREBUILTIN = "app.update.cert.requireBuiltIn";
 const PREF_APP_UPDATE_CHANNEL             = "app.update.channel";
+const PREF_APP_UPDATE_DESIREDCHANNEL      = "app.update.desiredChannel";
 const PREF_APP_UPDATE_ENABLED             = "app.update.enabled";
 const PREF_APP_UPDATE_IDLETIME            = "app.update.idletime";
 const PREF_APP_UPDATE_LOG                 = "app.update.log";
 const PREF_APP_UPDATE_NEVER_BRANCH        = "app.update.never.";
 const PREF_APP_UPDATE_PROMPTWAITTIME      = "app.update.promptWaitTime";
 const PREF_APP_UPDATE_SHOW_INSTALLED_UI   = "app.update.showInstalledUI";
 const PREF_APP_UPDATE_SILENT              = "app.update.silent";
 const PREF_APP_UPDATE_URL                 = "app.update.url";
--- a/toolkit/mozapps/update/test/unit/test_0040_general.js
+++ b/toolkit/mozapps/update/test/unit/test_0040_general.js
@@ -269,35 +269,84 @@ function run_test_pt11() {
   gUpdateChecker.checkForUpdates(updateCheckListener, true);
 }
 
 function check_test_pt11() {
   do_check_eq(getResult(gRequestURL), "test_distro_version");
   run_test_pt12();
 }
 
-// url constructed that doesn't have a parameter - bug 454357
+// url with force param that doesn't already have a param - bug 454357
 function run_test_pt12() {
   gCheckFunc = check_test_pt12;
   var url = URL_PREFIX;
-  logTestInfo("testing url constructed that doesn't have a parameter - " + url);
+  logTestInfo("testing url with force param that doesn't already have a " +
+              "param - " + url);
   setUpdateURLOverride(url);
   gUpdateChecker.checkForUpdates(updateCheckListener, true);
 }
 
 function check_test_pt12() {
   do_check_eq(getResult(gRequestURL), "?force=1");
   run_test_pt13();
 }
 
-// url constructed that has a parameter - bug 454357
+// url with force param that already has a param - bug 454357
 function run_test_pt13() {
   gCheckFunc = check_test_pt13;
   var url = URL_PREFIX + "?extra=param";
+  logTestInfo("testing url with force param that already has a param - " + url);
   logTestInfo("testing url constructed that has a parameter - " + url);
   setUpdateURLOverride(url);
   gUpdateChecker.checkForUpdates(updateCheckListener, true);
 }
 
 function check_test_pt13() {
   do_check_eq(getResult(gRequestURL), "?extra=param&force=1");
+  run_test_pt14();
+}
+
+// url with newchannel param that doesn't already have a param
+function run_test_pt14() {
+  gCheckFunc = check_test_pt14;
+  Services.prefs.setCharPref(PREF_APP_UPDATE_DESIREDCHANNEL, "testchannel");
+  var url = URL_PREFIX;
+  logTestInfo("testing url with newchannel param that doesn't already have a " +
+              "param - " + url);
+  setUpdateURLOverride(url);
+  gUpdateChecker.checkForUpdates(updateCheckListener, false);
+}
+
+function check_test_pt14() {
+  do_check_eq(getResult(gRequestURL), "?newchannel=testchannel");
+  run_test_pt15();
+}
+
+// url with newchannel param that already has a param
+function run_test_pt15() {
+  gCheckFunc = check_test_pt15;
+  Services.prefs.setCharPref(PREF_APP_UPDATE_DESIREDCHANNEL, "testchannel");
+  var url = URL_PREFIX + "?extra=param";
+  logTestInfo("testing url with newchannel param that already has a " +
+              "param - " + url);
+  setUpdateURLOverride(url);
+  gUpdateChecker.checkForUpdates(updateCheckListener, false);
+}
+
+function check_test_pt15() {
+  do_check_eq(getResult(gRequestURL), "?extra=param&newchannel=testchannel");
+  run_test_pt16();
+}
+
+// url with force and newchannel params
+function run_test_pt16() {
+  gCheckFunc = check_test_pt16;
+  Services.prefs.setCharPref(PREF_APP_UPDATE_DESIREDCHANNEL, "testchannel");
+  var url = URL_PREFIX;
+  logTestInfo("testing url with force and newchannel params - " + url);
+  setUpdateURLOverride(url);
+  gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_pt16() {
+  do_check_eq(getResult(gRequestURL), "?newchannel=testchannel&force=1");
   do_test_finished();
 }
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -225,16 +225,22 @@ SetStatusApplying(nsILocalFile *statusFi
 }
 
 static PRBool
 GetVersionFile(nsIFile *dir, nsCOMPtr<nsILocalFile> &result)
 {
   return GetFile(dir, NS_LITERAL_CSTRING("update.version"), result);
 }
 
+static PRBool
+GetChannelChangeFile(nsIFile *dir, nsCOMPtr<nsILocalFile> &result)
+{
+  return GetFile(dir, NS_LITERAL_CSTRING("channelchange"), result);
+}
+
 // Compares the current application version with the update's application
 // version.
 static PRBool
 IsOlderVersion(nsILocalFile *versionFile, const char *&appVersion)
 {
   PRFileDesc *fd = nsnull;
   nsresult rv = versionFile->OpenNSPRFileDesc(PR_RDONLY, 0660, &fd);
   if (NS_FAILED(rv))
@@ -508,21 +514,23 @@ ProcessUpdates(nsIFile *greDir, nsIFile 
 
   rv = updatesDir->AppendNative(NS_LITERAL_CSTRING("0"));
   if (NS_FAILED(rv))
     return rv;
 
   nsCOMPtr<nsILocalFile> statusFile;
   if (GetStatusFile(updatesDir, statusFile) && IsPending(statusFile)) {
     nsCOMPtr<nsILocalFile> versionFile;
+    nsCOMPtr<nsILocalFile> channelChangeFile;
     // Remove the update if the update application version file doesn't exist
     // or if the update's application version is less than the current
     // application version.
-    if (!GetVersionFile(updatesDir, versionFile) ||
-        IsOlderVersion(versionFile, appVersion)) {
+    if (!GetChannelChangeFile(updatesDir, channelChangeFile) &&
+        (!GetVersionFile(updatesDir, versionFile) ||
+         IsOlderVersion(versionFile, appVersion))) {
       updatesDir->Remove(PR_TRUE);
     } else {
       ApplyUpdate(greDir, updatesDir, statusFile, appDir, argc, argv);
     }
   }
 
   return NS_OK;
 }