Bug 791829 - Add support for resuming a stopped/partial update. r=rstrong

This commit is contained in:
Brian R. Bondy 2012-11-06 20:09:43 -05:00
Родитель 1b5a07f273
Коммит d7a69396d1
1 изменённых файлов: 134 добавлений и 17 удалений

Просмотреть файл

@ -49,6 +49,8 @@ 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";
@ -164,6 +166,13 @@ const UPDATE_WINDOW_NAME = "Update:Wizard";
// 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",
@ -1537,6 +1546,7 @@ const UpdateServiceFactory = {
* @constructor
*/
function UpdateService() {
LOG("Creating UpdateService");
Services.obs.addObserver(this, "xpcom-shutdown", false);
}
@ -1557,6 +1567,16 @@ UpdateService.prototype = {
*/
_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
@ -1578,6 +1598,10 @@ UpdateService.prototype = {
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;
@ -1904,7 +1928,7 @@ UpdateService.prototype = {
"another background check");
// the background checker is contained in notify
this.notify(null);
this._attemptResume();
},
// nsIUpdateCheckListener
@ -1949,6 +1973,32 @@ UpdateService.prototype = {
}
},
/**
* 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
@ -1956,8 +2006,10 @@ UpdateService.prototype = {
*/
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);
},
@ -2365,7 +2417,7 @@ UpdateService.prototype = {
}
// 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);
},
@ -2805,6 +2857,7 @@ Checker.prototype = {
* See nsIUpdateService.idl
*/
checkForUpdates: function UC_checkForUpdates(listener, force) {
LOG("Checker: checkForUpdates, force: " + force);
if (!listener)
throw Cr.NS_ERROR_NULL_POINTER;
@ -3028,10 +3081,14 @@ Checker.prototype = {
* @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 = {
/**
@ -3060,6 +3117,7 @@ Downloader.prototype = {
* Cancels the active download.
*/
cancel: function Downloader_cancel(cancelError) {
LOG("Downloader: cancel");
if (cancelError === undefined) {
cancelError = Cr.NS_BINDING_ABORTED;
}
@ -3085,15 +3143,19 @@ Downloader.prototype = {
* 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);
@ -3119,7 +3181,13 @@ Downloader.prototype = {
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;
},
/**
@ -3232,6 +3300,7 @@ Downloader.prototype = {
* 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;
@ -3373,6 +3442,7 @@ Downloader.prototype = {
if (listener instanceof Ci.nsIProgressEventSink)
listener.onProgress(request, context, progress, maxProgress);
}
this.updateService._consecutiveSocketErrors = 0;
},
/**
@ -3417,7 +3487,16 @@ Downloader.prototype = {
// 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
@ -3451,10 +3530,28 @@ Downloader.prototype = {
// 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;
@ -3484,10 +3581,15 @@ Downloader.prototype = {
}
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;
@ -3552,8 +3654,23 @@ Downloader.prototype = {
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;
}
},
/**