Bug 791829 - Add support for resuming a stopped/partial update. r=rstrong
authorBrian R. Bondy <netzen@gmail.com>
Tue, 06 Nov 2012 20:09:43 -0500
changeset 112974 5f17f8908bd480a56ca96f890cb5558c37728df5
parent 112973 073c95d208a8b54fec010bca2f9a052132043eb6
child 112975 2886c065d702143d686eb1a0f7fcb5194487f522
push id23844
push userryanvm@gmail.com
push dateSun, 11 Nov 2012 19:29:10 +0000
treeherdermozilla-central@b2bdbfe06b10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrstrong
bugs791829
milestone19.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 791829 - Add support for resuming a stopped/partial update. r=rstrong
toolkit/mozapps/update/nsUpdateService.js
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -44,16 +44,18 @@ const PREF_APP_UPDATE_SHOW_INSTALLED_UI 
 const PREF_APP_UPDATE_SILENT              = "app.update.silent";
 const PREF_APP_UPDATE_STAGE_ENABLED       = "app.update.staging.enabled";
 const PREF_APP_UPDATE_URL                 = "app.update.url";
 const PREF_APP_UPDATE_URL_DETAILS         = "app.update.url.details";
 const PREF_APP_UPDATE_URL_OVERRIDE        = "app.update.url.override";
 const PREF_APP_UPDATE_SERVICE_ENABLED     = "app.update.service.enabled";
 const PREF_APP_UPDATE_SERVICE_ERRORS      = "app.update.service.errors";
 const PREF_APP_UPDATE_SERVICE_MAX_ERRORS  = "app.update.service.maxErrors";
+const PREF_APP_UPDATE_SOCKET_ERRORS       = "app.update.socket.maxErrors";
+const PREF_APP_UPDATE_RETRY_TIMEOUT       = "app.update.socket.retryTimeout";
 
 const PREF_APP_DISTRIBUTION               = "distribution.id";
 const PREF_APP_DISTRIBUTION_VERSION       = "distribution.version";
 
 const PREF_EM_HOTFIX_ID                   = "extensions.hotfix.id";
 
 const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";
 const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul";
@@ -159,16 +161,23 @@ const DOWNLOAD_BACKGROUND_INTERVAL  = 60
 const DOWNLOAD_FOREGROUND_INTERVAL  = 0;
 
 const UPDATE_WINDOW_NAME      = "Update:Wizard";
 
 // The number of consecutive failures when updating using the service before
 // setting the app.update.service.enabled preference to false.
 const DEFAULT_SERVICE_MAX_ERRORS = 10;
 
+// The number of consecutive socket errors to allow before falling back to
+// downloading a different MAR file or failing if already downloading the full.
+const DEFAULT_SOCKET_MAX_ERRORS = 10;
+
+// The number of milliseconds to wait before retrying a connection error.
+const DEFAULT_UPDATE_RETRY_TIMEOUT = 2000;
+
 var gLocale     = null;
 
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
   return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
 });
@@ -1532,16 +1541,17 @@ const UpdateServiceFactory = {
 };
 
 /**
  * UpdateService
  * A Service for managing the discovery and installation of software updates.
  * @constructor
  */
 function UpdateService() {
+  LOG("Creating UpdateService");
   Services.obs.addObserver(this, "xpcom-shutdown", false);
 }
 
 UpdateService.prototype = {
   /**
    * The downloader we are using to download updates. There is only ever one of
    * these.
    */
@@ -1553,16 +1563,26 @@ UpdateService.prototype = {
   _incompatAddonsCount: 0,
 
   /**
    * Whether or not the service registered the "online" observer.
    */
   _registeredOnlineObserver: false,
 
   /**
+   * The current number of consecutive socket errors
+   */
+  _consecutiveSocketErrors: 0,
+
+  /**
+   * A timer used to retry socket errors
+   */
+  _retryTimer: null,
+
+  /**
    * Handle Observer Service notifications
    * @param   subject
    *          The subject of the notification
    * @param   topic
    *          The notification name
    * @param   data
    *          Additional data
    */
@@ -1573,16 +1593,20 @@ UpdateService.prototype = {
       this._postUpdateProcessing();
       break;
     case "network:offline-status-changed":
       this._offlineStatusChanged(data);
       break;
     case "xpcom-shutdown":
       Services.obs.removeObserver(this, "xpcom-shutdown");
 
+      if (this._retryTimer) {
+        this._retryTimer.cancel();
+      }
+
       this.pauseDownload();
       // Prevent leaking the downloader (bug 454964)
       this._downloader = null;
       break;
     }
   },
 
   /**
@@ -1899,17 +1923,17 @@ UpdateService.prototype = {
 
     Services.obs.removeObserver(this, "network:offline-status-changed");
     this._registeredOnlineObserver = false;
 
     LOG("UpdateService:_offlineStatusChanged - network is online, forcing " +
         "another background check");
 
     // the background checker is contained in notify
-    this.notify(null);
+    this._attemptResume();
   },
 
   // nsIUpdateCheckListener
   onProgress: function AUS_onProgress(request, position, totalSize) {
   },
 
   onCheckComplete: function AUS_onCheckComplete(request, updates, updateCount) {
     this._selectAndInstallUpdate(updates);
@@ -1944,25 +1968,53 @@ UpdateService.prototype = {
 
     if (errCount >= maxErrors) {
       var prompter = Cc["@mozilla.org/updates/update-prompt;1"].
                      createInstance(Ci.nsIUpdatePrompt);
       prompter.showUpdateError(update);
     }
   },
 
+
+  /**
+   * Called when a connection should be resumed
+   */
+  _attemptResume: function AUS_attemptResume() {
+    LOG("UpdateService:_attemptResume")
+    // If a download is in progress, then resume it.
+    if (this._downloader && this._downloader._patch &&
+        this._downloader._patch.state == STATE_DOWNLOADING &&
+        this._downloader._update) {
+      LOG("UpdateService:_attemptResume - _patch.state: " +
+          this._downloader._patch.state);
+      // Make sure downloading is the state for selectPatch to work correctly
+      writeStatusFile(getUpdatesDir(), STATE_DOWNLOADING);
+      var status = this.downloadUpdate(this._downloader._update,
+                                       this._downloader.background);
+      LOG("UpdateService:_attemptResume - downloadUpdate status: " + status);
+      if (status == STATE_NONE) {
+        cleanupActiveUpdate();
+      }
+      return; 
+    }
+
+    this.backgroundChecker.checkForUpdates(this, false);
+  },
+
   /**
    * Notified when a timer fires
    * @param   timer
    *          The timer that fired
    */
   notify: function AUS_notify(timer) {
     // If a download is in progress or the patch has been staged do nothing.
-    if (this.isDownloading || this._downloader && this._downloader.patchIsStaged)
-      return;
+    if (this.isDownloading ||
+        this._downloader && this._downloader.patchIsStaged) {
+      return; 
+    }
 
     this.backgroundChecker.checkForUpdates(this, false);
   },
 
   /**
    * Determine the update from the specified updates that should be offered.
    * If both valid major and minor updates are available the minor update will
    * be offered.
@@ -2360,17 +2412,17 @@ UpdateService.prototype = {
         LOG("UpdateService:downloadUpdate - no support for downloading more " +
             "than one update at a time");
         return readStatusFile(getUpdatesDir());
       }
       this._downloader.cancel();
     }
     // Set the previous application version prior to downloading the update.
     update.previousAppVersion = Services.appinfo.version;
-    this._downloader = new Downloader(background);
+    this._downloader = new Downloader(background, this);
     return this._downloader.downloadUpdate(update);
   },
 
   applyUpdateInBackground: function AUS_applyUpdateInBackground(update) {
     // If we can't stage an update, then just bail out!
     if (!gCanStageUpdates) {
       return;
     }
@@ -2800,16 +2852,17 @@ Checker.prototype = {
     LOG("Checker:getUpdateURL - update URL: " + url);
     return url;
   },
 
   /**
    * See nsIUpdateService.idl
    */
   checkForUpdates: function UC_checkForUpdates(listener, force) {
+    LOG("Checker: checkForUpdates, force: " + force);
     if (!listener)
       throw Cr.NS_ERROR_NULL_POINTER;
 
     var url = this.getUpdateURL(force);
     if (!url || (!this.enabled && !force))
       return;
 
     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
@@ -3023,20 +3076,24 @@ Checker.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateChecker])
 };
 
 /**
  * Manages the download of updates
  * @param   background
  *          Whether or not this downloader is operating in background
  *          update mode.
+ * @param   updateService
+ *          The update service that created this downloader.
  * @constructor
  */
-function Downloader(background) {
+function Downloader(background, updateService) {
+  LOG("Creating Downloader");
   this.background = background;
+  this.updateService = updateService;
 }
 Downloader.prototype = {
   /**
    * The nsIUpdatePatch that we are downloading
    */
   _patch: null,
 
   /**
@@ -3055,16 +3112,17 @@ Downloader.prototype = {
    * between the new version and the previous version.
    */
   isCompleteUpdate: null,
 
   /**
    * Cancels the active download.
    */
   cancel: function Downloader_cancel(cancelError) {
+    LOG("Downloader: cancel");
     if (cancelError === undefined) {
       cancelError = Cr.NS_BINDING_ABORTED;
     }
     if (this._request && this._request instanceof Ci.nsIRequest) {
       this._request.cancel(cancelError);
     }
   },
 
@@ -3080,25 +3138,29 @@ Downloader.prototype = {
            readState == STATE_APPLIED || readState == STATE_APPLIED_SVC;
   },
 
   /**
    * Verify the downloaded file.  We assume that the download is complete at
    * this point.
    */
   _verifyDownload: function Downloader__verifyDownload() {
+    LOG("Downloader:_verifyDownload called");
     if (!this._request)
       return false;
 
     var destination = this._request.destination;
 
     // Ensure that the file size matches the expected file size.
-    if (destination.fileSize != this._patch.size)
+    if (destination.fileSize != this._patch.size) {
+      LOG("Downloader:_verifyDownload downloaded size != expected size.");
       return false;
-
+    }
+
+    LOG("Downloader:_verifyDownload downloaded size == expected size.");
     var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
                      createInstance(Ci.nsIFileInputStream);
     fileStream.init(destination, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
 
     try {
       var hash = Cc["@mozilla.org/security/hash;1"].
                  createInstance(Ci.nsICryptoHash);
       var hashFunction = Ci.nsICryptoHash[this._patch.hashFunction.toUpperCase()];
@@ -3114,17 +3176,23 @@ Downloader.prototype = {
     } catch (e) {
       LOG("Downloader:_verifyDownload - failed to compute hash of the " +
           "downloaded update archive");
       digest = "";
     }
 
     fileStream.close();
 
-    return digest == this._patch.hashValue.toLowerCase();
+    if (digest == this._patch.hashValue.toLowerCase()) {
+      LOG("Downloader:_verifyDownload hashes match.");
+      return true;
+    }
+
+    LOG("Downloader:_verifyDownload hashes do not match. ");
+    return false;
   },
 
   /**
    * Select the patch to use given the current state of updateDir and the given
    * set of update patches.
    * @param   update
    *          A nsIUpdate object to select a patch from
    * @param   updateDir
@@ -3227,16 +3295,17 @@ Downloader.prototype = {
   },
 
   /**
    * Download and stage the given update.
    * @param   update
    *          A nsIUpdate object to download a patch for. Cannot be null.
    */
   downloadUpdate: function Downloader_downloadUpdate(update) {
+    LOG("UpdateService:_downloadUpdate");
     if (!update)
       throw Cr.NS_ERROR_NULL_POINTER;
 
     var updateDir = getUpdatesDir();
 
     this._update = update;
 
     // This function may return null, which indicates that there are no patches
@@ -3368,16 +3437,17 @@ Downloader.prototype = {
 
     var listeners = this._listeners.concat();
     var listenerCount = listeners.length;
     for (var i = 0; i < listenerCount; ++i) {
       var listener = listeners[i];
       if (listener instanceof Ci.nsIProgressEventSink)
         listener.onProgress(request, context, progress, maxProgress);
     }
+    this.updateService._consecutiveSocketErrors = 0;
   },
 
   /**
    * When we have new status text
    * @param   request
    *          The nsIRequest object for the transfer
    * @param   context
    *          Additional data
@@ -3412,17 +3482,26 @@ Downloader.prototype = {
     if (request instanceof Ci.nsIIncrementalDownload)
       LOG("Downloader:onStopRequest - original URI spec: " + request.URI.spec +
           ", final URI spec: " + request.finalURI.spec + ", status: " + status);
 
     // XXX ehsan shouldShowPrompt should always be false here.
     // But what happens when there is already a UI showing?
     var state = this._patch.state;
     var shouldShowPrompt = false;
+    var shouldRegisterOnlineObserver = false;
+    var shouldRetrySoon = false;
     var deleteActiveUpdate = false;
+    var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_RETRY_TIMEOUT,
+                               DEFAULT_UPDATE_RETRY_TIMEOUT);
+    var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_ERRORS,
+                          DEFAULT_SOCKET_MAX_ERRORS);
+    LOG("Downloader:onStopRequest - status: " + status + ", " +
+        "current fail: " + this.updateService._consecutiveSocketErrors + ", " +
+        "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout);
     if (Components.isSuccessCode(status)) {
       if (this._verifyDownload()) {
         state = shouldUseService() ? STATE_PENDING_SVC : STATE_PENDING
 
         // We only need to explicitly show the prompt if this is a background
         // download, since otherwise some kind of UI is already visible and
         // that UI will notify.
         if (this.background)
@@ -3446,20 +3525,38 @@ Downloader.prototype = {
         this._update.statusText = message;
 
         if (this._update.isCompleteUpdate || this._update.patchCount != 2)
           deleteActiveUpdate = true;
 
         // Destroy the updates directory, since we're done with it.
         cleanUpUpdatesDir();
       }
-    }
-    else if (status != Cr.NS_BINDING_ABORTED &&
-             status != Cr.NS_ERROR_ABORT &&
-             status != Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
+    } else if (status == Cr.NS_ERROR_OFFLINE) {
+      // Register an online observer to try again.
+      // The online observer will continue the incremental download by
+      // calling downloadUpdate on the active update which continues
+      // downloading the file from where it was.
+      LOG("Downloader:onStopRequest - offline, register online observer: true");
+      shouldRegisterOnlineObserver = true;
+      deleteActiveUpdate = false;
+    // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED, and
+    // NS_ERROR_NET_RESET can be returned when disconnecting the internet while
+    // a download of a MAR is in progress.  There may be others but I have not
+    // encountered them during testing.
+    } else if ((status == Cr.NS_ERROR_NET_TIMEOUT ||
+                status == Cr.NS_ERROR_CONNECTION_REFUSED ||
+                status == Cr.NS_ERROR_NET_RESET) &&
+               this.updateService._consecutiveSocketErrors < maxFail) {
+      LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
+      shouldRetrySoon = true;
+      deleteActiveUpdate = false;
+    } else if (status != Cr.NS_BINDING_ABORTED &&
+               status != Cr.NS_ERROR_ABORT &&
+               status != Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
       LOG("Downloader:onStopRequest - non-verification failure");
       // Some sort of other failure, log this in the |statusText| property
       state = STATE_DOWNLOAD_FAILED;
 
       // XXXben - if |request| (The Incremental Download) provided a means
       // for accessing the http channel we could do more here.
 
       this._update.statusText = getStatusTextFromCode(status,
@@ -3479,20 +3576,25 @@ Downloader.prototype = {
       um.activeUpdate = null;
     }
     else {
       if (um.activeUpdate)
         um.activeUpdate.state = state;
     }
     um.saveUpdates();
 
-    var listeners = this._listeners.concat();
-    var listenerCount = listeners.length;
-    for (var i = 0; i < listenerCount; ++i)
-      listeners[i].onStopRequest(request, context, status);
+    // Only notify listeners about the stopped state if we
+    // aren't handling an internal retry.
+    if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
+      var listeners = this._listeners.concat();
+      var listenerCount = listeners.length;
+      for (var i = 0; i < listenerCount; ++i) {
+        listeners[i].onStopRequest(request, context, status);
+      }
+    }
 
     this._request = null;
 
     if (state == STATE_DOWNLOAD_FAILED) {
       var allFailed = true;
       // Check if there is a complete update patch that can be downloaded.
       if (!this._update.isCompleteUpdate && this._update.patchCount == 2) {
         LOG("Downloader:onStopRequest - verification of patch failed, " +
@@ -3547,18 +3649,33 @@ Downloader.prototype = {
 
     if (state == STATE_PENDING || state == STATE_PENDING_SVC) {
       // Initiate the background update job.
       Cc["@mozilla.org/updates/update-service;1"].
         getService(Ci.nsIApplicationUpdateService).
         applyUpdateInBackground(this._update);
     }
 
-    // Prevent leaking the update object (bug 454964)
-    this._update = null;
+    if (shouldRegisterOnlineObserver) {
+      LOG("Downloader:onStopRequest - Registering online observer");
+      this.updateService._registerOnlineObserver();
+    } else if (shouldRetrySoon) {
+      LOG("Downloader:onStopRequest - Retrying soon");
+      this.updateService._consecutiveSocketErrors++;
+      if (this.updateService._retryTimer) {
+        this.updateService._retryTimer.cancel();
+      }
+      this.updateService._retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this.updateService._retryTimer.initWithCallback(function() {
+        this._attemptResume();
+      }.bind(this.updateService), retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
+    } else {
+      // Prevent leaking the update object (bug 454964)
+      this._update = null;
+    }
   },
 
   /**
    * See nsIInterfaceRequestor.idl
    */
   getInterface: function Downloader_getInterface(iid) {
     // The network request may require proxy authentication, so provide the
     // default nsIAuthPrompt if requested.