Bug 772484 - Allow user to cancel add-on update check and clean up correctly when they do. r=Unfocused
authorIrving Reid <irving@mozilla.com>
Fri, 14 Mar 2014 08:52:55 -0400
changeset 191895 b7761b7514f73fac04de664f8db546f400f2bd6e
parent 191894 b753b98bcf5509947b2d6f63d597a9a934dd6992
child 191896 4832153571bc9547a70cfbeb4cf288e21b1f9e03
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused
bugs772484
milestone30.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 772484 - Allow user to cancel add-on update check and clean up correctly when they do. r=Unfocused
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/content/selectAddons.js
toolkit/mozapps/extensions/content/update.js
toolkit/mozapps/extensions/internal/AddonRepository.jsm
toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_bug557956.js
toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js
toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs
toolkit/mozapps/extensions/test/browser/head.js
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1069,44 +1069,40 @@ var AddonManagerInternal = {
       this.getAllAddons(function getAddonsCallback(aAddons) {
         // If there is a known hotfix then exclude it from the list of add-ons to update.
         var ids = [a.id for each (a in aAddons) if (a.id != hotfixID)];
 
         // Repopulate repository cache first, to ensure compatibility overrides
         // are up to date before checking for addon updates.
         AddonRepository.backgroundUpdateCheck(
                      ids, function BUC_backgroundUpdateCheckCallback() {
-          AddonManagerInternal.updateAddonRepositoryData(
-                                    function BUC_updateAddonCallback() {
-
-            pendingUpdates += aAddons.length;
-            aAddons.forEach(function BUC_forEachCallback(aAddon) {
-              if (aAddon.id == hotfixID) {
-                notifyComplete();
-                return;
-              }
+          pendingUpdates += aAddons.length;
+          aAddons.forEach(function BUC_forEachCallback(aAddon) {
+            if (aAddon.id == hotfixID) {
+              notifyComplete();
+              return;
+            }
 
-              // Check all add-ons for updates so that any compatibility updates will
-              // be applied
-              aAddon.findUpdates({
-                onUpdateAvailable: function BUC_onUpdateAvailable(aAddon, aInstall) {
-                  // Start installing updates when the add-on can be updated and
-                  // background updates should be applied.
-                  if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
-                      AddonManager.shouldAutoUpdate(aAddon)) {
-                    aInstall.install();
-                  }
-                },
+            // Check all add-ons for updates so that any compatibility updates will
+            // be applied
+            aAddon.findUpdates({
+              onUpdateAvailable: function BUC_onUpdateAvailable(aAddon, aInstall) {
+                // Start installing updates when the add-on can be updated and
+                // background updates should be applied.
+                if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
+                    AddonManager.shouldAutoUpdate(aAddon)) {
+                  aInstall.install();
+                }
+              },
 
-                onUpdateFinished: notifyComplete
-              }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
-            });
+              onUpdateFinished: notifyComplete
+            }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+          });
 
-            notifyComplete();
-          });
+          notifyComplete();
         });
       });
     }
 
     if (checkHotfix) {
       var hotfixVersion = "";
       try {
         hotfixVersion = Services.prefs.getCharPref(PREF_EM_HOTFIX_LASTVERSION);
@@ -1420,16 +1416,18 @@ var AddonManagerInternal = {
       nextObject: function updateAddonRepositoryData_nextObject(aCaller, aProvider) {
         callProvider(aProvider,
                      "updateAddonRepositoryData",
                      null,
                      aCaller.callNext.bind(aCaller));
       },
       noMoreObjects: function updateAddonRepositoryData_noMoreObjects(aCaller) {
         safeCall(aCallback);
+        // only tests should care about this
+        Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated", null);
       }
     });
   },
 
   /**
    * Asynchronously gets an AddonInstall for a URL.
    *
    * @param  aUrl
--- a/toolkit/mozapps/extensions/content/selectAddons.js
+++ b/toolkit/mozapps/extensions/content/selectAddons.js
@@ -93,31 +93,28 @@ var gChecking = {
       self._progress.value = 0;
       self._progress.max = aAddons.length;
       self._progress.mode = "determined";
 
       // Ensure compatibility overrides are up to date before checking for
       // individual addon updates.
       let ids = [addon.id for each (addon in aAddons)];
       AddonRepository.repopulateCache(ids, function gChecking_repopulateCache() {
-        AddonManagerPrivate.updateAddonRepositoryData(function gChecking_updateAddonRepositoryData() {
+        for (let addonItem of aAddons) {
+          // Ignore disabled themes
+          if (addonItem.type != "theme" || !addonItem.userDisabled) {
+            gAddons[addonItem.id] = {
+              addon: addonItem,
+              install: null,
+              wasActive: addonItem.isActive
+            }
+          }
 
-          for (let addonItem of aAddons) {
-            // Ignore disabled themes
-            if (addonItem.type != "theme" || !addonItem.userDisabled) {
-              gAddons[addonItem.id] = {
-                addon: addonItem,
-                install: null,
-                wasActive: addonItem.isActive
-              }
-            }
-
-            addonItem.findUpdates(self, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
-          }
-        });
+          addonItem.findUpdates(self, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+        }
       });
     });
   },
 
   onUpdateAvailable: function gChecking_onUpdateAvailable(aAddon, aInstall) {
     // If the add-on can be upgraded then remember the new version
     if (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE)
       gAddons[aAddon.id].install = aInstall;
--- a/toolkit/mozapps/extensions/content/update.js
+++ b/toolkit/mozapps/extensions/content/update.js
@@ -14,35 +14,37 @@ const PREF_EM_HOTFIX_ID                 
 
 // timeout (in milliseconds) to wait for response to the metadata ping
 const METADATA_TIMEOUT    = 30000;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
 
-
-var gInteruptable = true;
-var gPendingClose = false;
-
+Components.utils.import("resource://gre/modules/Log.jsm");
+let logger = Log.repository.getLogger("addons.update-dialog");
 
 var gUpdateWizard = {
   // When synchronizing app compatibility info this contains all installed
   // add-ons. When checking for compatible versions this contains only
   // incompatible add-ons.
   addons: [],
   // Contains a list of add-ons that were disabled prior to the application
   // upgrade.
   inactiveAddonIDs: [],
   // The add-ons that we found updates available for
   addonsToUpdate: [],
   shouldSuggestAutoChecking: false,
   shouldAutoCheck: false,
   xpinstallEnabled: true,
   xpinstallLocked: false,
+  // cached AddonInstall entries for add-ons we might want to update,
+  // keyed by add-on ID
+  addonInstalls: new Map(),
+  shuttingDown: false,
 
   init: function gUpdateWizard_init()
   {
     this.inactiveAddonIDs = window.arguments[0];
 
     try {
       this.shouldSuggestAutoChecking =
         !Services.prefs.getBoolPref(PREF_UPDATE_EXTENSIONS_ENABLED);
@@ -105,27 +107,36 @@ var gUpdateWizard = {
 
   onWizardClose: function gUpdateWizard_onWizardClose(aEvent)
   {
     return this.onWizardCancel();
   },
 
   onWizardCancel: function gUpdateWizard_onWizardCancel()
   {
-    if (!gInteruptable) {
-      gPendingClose = true;
-      this._setUpButton("back", null, true);
-      this._setUpButton("next", null, true);
-      this._setUpButton("cancel", null, true);
-      return false;
+    gUpdateWizard.shuttingDown = true;
+    // Allow add-ons to continue downloading and installing
+    // in the background, though some may require a later restart
+    // Pages that are waiting for user input go into the background
+    // on cancel
+    if (gMismatchPage.waiting) {
+      logger.info("Dialog closed in mismatch page");
+      if (gUpdateWizard.addonInstalls.size > 0) {
+        gInstallingPage.startInstalls([i for ([, i] of gUpdateWizard.addonInstalls)]);
+      }
+      return true;
     }
 
-    if (gInstallingPage.installing) {
-      gInstallingPage.cancelInstalls();
-      return false;
+    // Pages that do asynchronous things will just keep running and check
+    // gUpdateWizard.shuttingDown to trigger background behaviour
+    if (!gInstallingPage.installing) {
+      logger.info("Dialog closed while waiting for updated compatibility information");
+    }
+    else {
+      logger.info("Dialog closed while downloading and installing updates");
     }
     return true;
   }
 };
 
 var gOfflinePage = {
   onPageAdvanced: function gOfflinePage_onPageAdvanced()
   {
@@ -138,106 +149,152 @@ var gOfflinePage = {
     var nextbtn = document.documentElement.getButton("next");
     nextbtn.disabled = !nextbtn.disabled;
   }
 }
 
 var gVersionInfoPage = {
   _completeCount: 0,
   _totalCount: 0,
+  _versionInfoDone: false,
   onPageShow: function gVersionInfoPage_onPageShow()
   {
     gUpdateWizard.setButtonLabels(null, true,
                                   "nextButtonText", true,
                                   "cancelButtonText", false);
 
     try {
       var hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID);
     }
     catch (e) { }
 
     // Retrieve all add-ons in order to sync their app compatibility information
     AddonManager.getAllAddons(function gVersionInfoPage_getAllAddons(aAddons) {
-      gUpdateWizard.addons = aAddons.filter(function gVersionInfoPage_filterAddons(a) {
-        return a.type != "plugin" && a.id != hotfixID;
-      });
+      if (gUpdateWizard.shuttingDown) {
+        logger.debug("getAllAddons completed after dialog closed");
+      }
+
+      gUpdateWizard.addons = [a for (a of aAddons)
+                               if (a.type != "plugin" && a.id != hotfixID)];
 
       gVersionInfoPage._totalCount = gUpdateWizard.addons.length;
 
       // Ensure compatibility overrides are up to date before checking for
       // individual addon updates.
-      let ids = [addon.id for each (addon in gUpdateWizard.addons)];
+      let ids = [addon.id for (addon of gUpdateWizard.addons)];
+
+      AddonRepository.repopulateCache(ids, function gVersionInfoPage_repopulateCache() {
 
-      gInteruptable = false;
-      AddonRepository.repopulateCache(ids, function gVersionInfoPage_repolulateCache() {
-        AddonManagerPrivate.updateAddonRepositoryData(function gVersionInfoPage_updateAddonRepoData() {
-          gInteruptable = true;
-          if (gPendingClose) {
-            window.close();
-            return;
-          }
+        if (gUpdateWizard.shuttingDown) {
+          logger.debug("repopulateCache completed after dialog closed");
+        }
 
-          for (let addon of gUpdateWizard.addons)
-            addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
-        });
+        for (let addon of gUpdateWizard.addons) {
+          logger.debug("VersionInfo Finding updates for " + addon.id);
+          addon.findUpdates(gVersionInfoPage, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+        }
       }, METADATA_TIMEOUT);
     });
   },
 
   onAllUpdatesFinished: function gVersionInfoPage_onAllUpdatesFinished() {
     // Filter out any add-ons that were disabled before the application was
     // upgraded or are already compatible
-    gUpdateWizard.addons = gUpdateWizard.addons.filter(function onAllUpdatesFinished_filterAddons(a) {
-      return a.appDisabled && gUpdateWizard.inactiveAddonIDs.indexOf(a.id) < 0;
-    });
+    logger.debug("VersionInfo updates finished: inactive " +
+         gUpdateWizard.inactiveAddonIDs.toSource() + " found " +
+         [addon.id + ":" + addon.appDisabled for (addon of gUpdateWizard.addons)].toSource());
+    let filteredAddons = [];
+    for (let a of gUpdateWizard.addons) {
+      if (a.appDisabled && gUpdateWizard.inactiveAddonIDs.indexOf(a.id) < 0) {
+        logger.debug("Continuing with add-on " + a.id);
+        filteredAddons.push(a);
+      }
+      else if (gUpdateWizard.addonInstalls.has(a.id)) {
+        gUpdateWizard.addonInstalls.get(a.id).cancel();
+        gUpdateWizard.addonInstalls.delete(a.id);
+      }
+    }
+    gUpdateWizard.addons = filteredAddons;
 
-    if (gUpdateWizard.addons.length > 0) {
-      // There are still incompatible addons, inform the user.
+    if (gUpdateWizard.shuttingDown) {
+      // jump directly to updating auto-update add-ons in the background
+      if (gUpdateWizard.addonInstalls.size > 0) {
+        gInstallingPage.startInstalls([i for ([, i] of gUpdateWizard.addonInstalls)]);
+      }
+      return;
+    }
+
+    if (filteredAddons.length > 0) {
+      if (!gUpdateWizard.xpinstallEnabled && gUpdateWizard.xpinstallLocked) {
+        document.documentElement.currentPage = document.getElementById("adminDisabled");
+        return;
+      }
       document.documentElement.currentPage = document.getElementById("mismatch");
     }
     else {
+      logger.info("VersionInfo: No updates require further action");
       // VersionInfo compatibility updates resolved all compatibility problems,
       // close this window and continue starting the application...
       //XXX Bug 314754 - We need to use setTimeout to close the window due to
       // the EM using xmlHttpRequest when checking for updates.
       setTimeout(close, 0);
     }
   },
 
   /////////////////////////////////////////////////////////////////////////////
   // UpdateListener
   onUpdateFinished: function gVersionInfoPage_onUpdateFinished(aAddon, status) {
-    // If the add-on is now active then it won't have been disabled by startup
-    if (aAddon.active)
-      AddonManagerPrivate.removeStartupChange("disabled", aAddon.id);
-
-    if (status != AddonManager.UPDATE_STATUS_NO_ERROR)
-      gUpdateWizard.errorItems.push(aAddon);
-
     ++this._completeCount;
 
-    // Update the status text and progress bar
-    var updateStrings = document.getElementById("updateStrings");
-    var statusElt = document.getElementById("versioninfo.status");
-    var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
-    statusElt.setAttribute("value", statusString);
+    if (status != AddonManager.UPDATE_STATUS_NO_ERROR) {
+      logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount +
+           " failed for " + aAddon.id + ": " + status);
+      gUpdateWizard.errorItems.push(aAddon);
+    }
+    else {
+      logger.debug("VersionInfo update " + this._completeCount + " of " + this._totalCount +
+           " finished for " + aAddon.id);
+    }
 
-    // Update the status text and progress bar
-    var progress = document.getElementById("versioninfo.progress");
-    progress.mode = "normal";
-    progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+    // If we're not in the background, just make a list of add-ons that have
+    // updates available
+    if (!gUpdateWizard.shuttingDown) {
+      // If we're still in the update check window and the add-on is now active
+      // then it won't have been disabled by startup
+      if (aAddon.active)
+        AddonManagerPrivate.removeStartupChange("disabled", aAddon.id);
+
+      // Update the status text and progress bar
+      var updateStrings = document.getElementById("updateStrings");
+      var statusElt = document.getElementById("versioninfo.status");
+      var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
+      statusElt.setAttribute("value", statusString);
+
+      // Update the status text and progress bar
+      var progress = document.getElementById("versioninfo.progress");
+      progress.mode = "normal";
+      progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+    }
 
     if (this._completeCount == this._totalCount)
       this.onAllUpdatesFinished();
   },
+
+  onUpdateAvailable: function gVersionInfoPage_onUpdateAvailable(aAddon, aInstall) {
+    logger.debug("VersionInfo got an install for " + aAddon.id + ": " + aAddon.version);
+    gUpdateWizard.addonInstalls.set(aAddon.id, aInstall);
+  },
 };
 
 var gMismatchPage = {
+  waiting: false,
+
   onPageShow: function gMismatchPage_onPageShow()
   {
+    gMismatchPage.waiting = true;
     gUpdateWizard.setButtonLabels(null, true,
                                   "mismatchCheckNow", false,
                                   "mismatchDontCheck", false);
     document.documentElement.getButton("next").focus();
 
     var incompatible = document.getElementById("mismatch.incompatible");
     for (let addon of gUpdateWizard.addons) {
       var listitem = document.createElement("listitem");
@@ -247,60 +304,66 @@ var gMismatchPage = {
   }
 };
 
 var gUpdatePage = {
   _totalCount: 0,
   _completeCount: 0,
   onPageShow: function gUpdatePage_onPageShow()
   {
-    if (!gUpdateWizard.xpinstallEnabled && gUpdateWizard.xpinstallLocked) {
-      document.documentElement.currentPage = document.getElementById("adminDisabled");
-      return;
-    }
-
+    gMismatchPage.waiting = false;
     gUpdateWizard.setButtonLabels(null, true,
                                   "nextButtonText", true,
                                   "cancelButtonText", false);
     document.documentElement.getButton("next").focus();
 
     gUpdateWizard.errorItems = [];
 
     this._totalCount = gUpdateWizard.addons.length;
-    for (let addon of gUpdateWizard.addons)
+    for (let addon of gUpdateWizard.addons) {
+      logger.debug("UpdatePage requesting update for " + addon.id);
+      // Redundant call to find updates again here when we already got them
+      // in the VersionInfo page: https://bugzilla.mozilla.org/show_bug.cgi?id=960597
       addon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+    }
   },
 
   onAllUpdatesFinished: function gUpdatePage_onAllUpdatesFinished() {
+    if (gUpdateWizard.shuttingDown)
+      return;
+
     var nextPage = document.getElementById("noupdates");
     if (gUpdateWizard.addonsToUpdate.length > 0)
       nextPage = document.getElementById("found");
     document.documentElement.currentPage = nextPage;
   },
 
   /////////////////////////////////////////////////////////////////////////////
   // UpdateListener
   onUpdateAvailable: function gUpdatePage_onUpdateAvailable(aAddon, aInstall) {
+    logger.debug("UpdatePage got an update for " + aAddon.id + ": " + aAddon.version);
     gUpdateWizard.addonsToUpdate.push(aInstall);
   },
 
   onUpdateFinished: function gUpdatePage_onUpdateFinished(aAddon, status) {
     if (status != AddonManager.UPDATE_STATUS_NO_ERROR)
       gUpdateWizard.errorItems.push(aAddon);
 
     ++this._completeCount;
 
-    // Update the status text and progress bar
-    var updateStrings = document.getElementById("updateStrings");
-    var statusElt = document.getElementById("checking.status");
-    var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
-    statusElt.setAttribute("value", statusString);
+    if (!gUpdateWizard.shuttingDown) {
+      // Update the status text and progress bar
+      var updateStrings = document.getElementById("updateStrings");
+      var statusElt = document.getElementById("checking.status");
+      var statusString = updateStrings.getFormattedString("statusPrefix", [aAddon.name]);
+      statusElt.setAttribute("value", statusString);
 
-    var progress = document.getElementById("checking.progress");
-    progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+      var progress = document.getElementById("checking.progress");
+      progress.value = Math.ceil((this._completeCount / this._totalCount) * 100);
+    }
 
     if (this._completeCount == this._totalCount)
       this.onAllUpdatesFinished()
   },
 };
 
 var gFoundPage = {
   onPageShow: function gFoundPage_onPageShow()
@@ -366,94 +429,129 @@ var gFoundPage = {
 
 var gInstallingPage = {
   _installs         : [],
   _errors           : [],
   _strings          : null,
   _currentInstall   : -1,
   _installing       : false,
 
+  // Initialize fields we need for installing and tracking progress,
+  // and start iterating through the installations
+  startInstalls: function gInstallingPage_startInstalls(aInstallList) {
+    if (!gUpdateWizard.xpinstallEnabled) {
+      return;
+    }
+
+    logger.debug("Start installs for "
+                 + [i.existingAddon.id for (i of aInstallList)].toSource());
+    this._errors = [];
+    this._installs = aInstallList;
+    this._installing = true;
+    this.startNextInstall();
+  },
+
   onPageShow: function gInstallingPage_onPageShow()
   {
     gUpdateWizard.setButtonLabels(null, true,
                                   "nextButtonText", true,
                                   null, true);
-    this._errors = [];
 
     var foundUpdates = document.getElementById("found.updates");
     var updates = foundUpdates.getElementsByTagName("listitem");
+    let toInstall = [];
     for (let update of updates) {
-      if (!update.checked)
+      if (!update.checked) {
+        logger.info("User chose to cancel update of " + update.label);
+        update.install.cancel();
         continue;
-      this._installs.push(update.install);
+      }
+      toInstall.push(update.install);
     }
+    this._strings = document.getElementById("updateStrings");
 
-    this._strings = document.getElementById("updateStrings");
-    this._installing = true;
-    this.startNextInstall();
+    this.startInstalls(toInstall);
   },
 
   startNextInstall: function gInstallingPage_startNextInstall() {
     if (this._currentInstall >= 0) {
       this._installs[this._currentInstall].removeListener(this);
     }
 
     this._currentInstall++;
 
     if (this._installs.length == this._currentInstall) {
+      Services.obs.notifyObservers(null, "TEST:all-updates-done", null);
       this._installing = false;
+      if (gUpdateWizard.shuttingDown) {
+        return;
+      }
       var nextPage = this._errors.length > 0 ? "installerrors" : "finished";
       document.getElementById("installing").setAttribute("next", nextPage);
       document.documentElement.advance();
       return;
     }
 
-    this._installs[this._currentInstall].addListener(this);
-    this._installs[this._currentInstall].install();
-  },
+    let install = this._installs[this._currentInstall];
 
-  cancelInstalls: function gInstallingPage_cancelInstalls() {
-    this._installs[this._currentInstall].removeListener(this);
-    this._installs[this._currentInstall].cancel();
+    if (gUpdateWizard.shuttingDown && !AddonManager.shouldAutoUpdate(install.existingAddon)) {
+      logger.debug("Don't update " + install.existingAddon.id + " in background");
+      install.cancel();
+      this.startNextInstall();
+      return;
+    }
+    install.addListener(this);
+    install.install();
   },
 
   /////////////////////////////////////////////////////////////////////////////
   // InstallListener
   onDownloadStarted: function gInstallingPage_onDownloadStarted(aInstall) {
+    if (gUpdateWizard.shuttingDown) {
+      return;
+    }
     var strings = document.getElementById("updateStrings");
     var label = strings.getFormattedString("downloadingPrefix", [aInstall.name]);
     var actionItem = document.getElementById("actionItem");
     actionItem.value = label;
   },
 
   onDownloadProgress: function gInstallingPage_onDownloadProgress(aInstall) {
+    if (gUpdateWizard.shuttingDown) {
+      return;
+    }
     var downloadProgress = document.getElementById("downloadProgress");
     downloadProgress.value = Math.ceil(100 * aInstall.progress / aInstall.maxProgress);
   },
 
   onDownloadEnded: function gInstallingPage_onDownloadEnded(aInstall) {
   },
 
   onDownloadFailed: function gInstallingPage_onDownloadFailed(aInstall) {
     this._errors.push(aInstall);
 
     this.startNextInstall();
   },
 
   onInstallStarted: function gInstallingPage_onInstallStarted(aInstall) {
+    if (gUpdateWizard.shuttingDown) {
+      return;
+    }
     var strings = document.getElementById("updateStrings");
     var label = strings.getFormattedString("installingPrefix", [aInstall.name]);
     var actionItem = document.getElementById("actionItem");
     actionItem.value = label;
   },
 
   onInstallEnded: function gInstallingPage_onInstallEnded(aInstall, aAddon) {
-    // Remember that this add-on was updated during startup
-    AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
-                                         aAddon.id);
+    if (!gUpdateWizard.shuttingDown) {
+      // Remember that this add-on was updated during startup
+      AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED,
+                                           aAddon.id);
+    }
 
     this.startNextInstall();
   },
 
   onInstallFailed: function gInstallingPage_onInstallFailed(aInstall) {
     this._errors.push(aInstall);
 
     this.startNextInstall();
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -469,18 +469,20 @@ AddonSearchResult.prototype = {
  */
 this.AddonRepository = {
   /**
    * Whether caching is currently enabled
    */
   get cacheEnabled() {
     // Act as though caching is disabled if there was an unrecoverable error
     // openning the database.
-    if (!AddonDatabase.databaseOk)
+    if (!AddonDatabase.databaseOk) {
+      logger.warn("Cache is disabled because database is not OK");
       return false;
+    }
 
     let preference = PREF_GETADDONS_CACHE_ENABLED;
     let enabled = false;
     try {
       enabled = Services.prefs.getBoolPref(preference);
     } catch(e) {
       logger.warn("cacheEnabled: Couldn't get pref: " + preference);
     }
@@ -606,61 +608,69 @@ this.AddonRepository = {
    * @param  aTimeout
    *         (Optional) timeout in milliseconds to abandon the XHR request
    *         if we have not received a response from the server.
    */
   repopulateCache: function(aIds, aCallback, aTimeout) {
     this._repopulateCacheInternal(aIds, aCallback, false, aTimeout);
   },
 
-  _repopulateCacheInternal: function(aIds, aCallback, aSendPerformance, aTimeout) {
+  _repopulateCacheInternal: function (aIds, aCallback, aSendPerformance, aTimeout) {
+    // Always call AddonManager updateAddonRepositoryData after we refill the cache
+    function repopulateAddonManager() {
+      AddonManagerPrivate.updateAddonRepositoryData(aCallback);
+    }
+
+    logger.debug("Repopulate add-on cache with " + aIds.toSource());
     // Completely remove cache if caching is not enabled
     if (!this.cacheEnabled) {
+      logger.debug("Clearing cache because it is disabled");
       this._addons = null;
       this._pendingCallbacks = null;
-      AddonDatabase.delete(aCallback);
+      AddonDatabase.delete(repopulateAddonManager);
       return;
     }
 
     let self = this;
     getAddonsToCache(aIds, function repopulateCache_getAddonsToCache(aAddons) {
       // Completely remove cache if there are no add-ons to cache
       if (aAddons.length == 0) {
+        logger.debug("Clearing cache because 0 add-ons were requested");
         self._addons = null;
         self._pendingCallbacks = null;
-        AddonDatabase.delete(aCallback);
+        AddonDatabase.delete(repopulateAddonManager);
         return;
       }
 
       self._beginGetAddons(aAddons, {
         searchSucceeded: function repopulateCacheInternal_searchSucceeded(aAddons) {
           self._addons = {};
           aAddons.forEach(function(aAddon) { self._addons[aAddon.id] = aAddon; });
-          AddonDatabase.repopulate(aAddons, aCallback);
+          AddonDatabase.repopulate(aAddons, repopulateAddonManager);
         },
         searchFailed: function repopulateCacheInternal_searchFailed() {
           logger.warn("Search failed when repopulating cache");
-          if (aCallback)
-            aCallback();
+          repopulateAddonManager();
         }
       }, aSendPerformance, aTimeout);
     });
   },
 
   /**
    * Asynchronously add add-ons to the cache corresponding to the specified
    * ids. If caching is disabled, the cache is unchanged and the callback is
-   * immediatly called if it is defined.
+   * immediately called if it is defined.
    *
    * @param  aIds
    *         The array of add-on ids to add to the cache
    * @param  aCallback
    *         The optional callback to call once complete
    */
   cacheAddons: function AddonRepo_cacheAddons(aIds, aCallback) {
+    logger.debug("cacheAddons: enabled " + this.cacheEnabled + " IDs " + aIds.toSource());
     if (!this.cacheEnabled) {
       if (aCallback)
         aCallback();
       return;
     }
 
     let self = this;
     getAddonsToCache(aIds, function cacheAddons_getAddonsToCache(aAddons) {
@@ -1392,16 +1402,18 @@ this.AddonRepository = {
     let compatData = {};
     Array.forEach(aElements, this._parseAddonCompatElement.bind(this, compatData));
     return compatData;
   },
 
   // Begins a new search if one isn't currently executing
   _beginSearch: function(aURI, aMaxResults, aCallback, aHandleResults, aTimeout) {
     if (this._searching || aURI == null || aMaxResults <= 0) {
+      logger.warn("AddonRepository search failed: searching " + this._searching + " aURI " + aURI +
+                  " aMaxResults " + aMaxResults);
       aCallback.searchFailed();
       return;
     }
 
     this._searching = true;
     this._callback = aCallback;
     this._maxResults = aMaxResults;
 
--- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
@@ -40,17 +40,17 @@ XPCOMUtils.defineLazyGetter(this, "CertU
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
 var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
            getService(Ci.nsIRDFService);
 
 Cu.import("resource://gre/modules/Log.jsm");
-const LOGGER_ID = "addons.updates";
+const LOGGER_ID = "addons.update-checker";
 
 // Create a new logger for use by the Addons Update Checker
 // (Requires AddonManager.jsm)
 let logger = Log.repository.getLogger(LOGGER_ID);
 
 /**
  * A serialisation method for RDF data that produces an identical string
  * for matching RDF graphs.
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3720,38 +3720,40 @@ var XPIProvider = {
    *
    * @param  aCallback
    *         Function to call when operation is complete.
    */
   updateAddonRepositoryData: function XPI_updateAddonRepositoryData(aCallback) {
     let self = this;
     XPIDatabase.getVisibleAddons(null, function UARD_getVisibleAddonsCallback(aAddons) {
       let pending = aAddons.length;
+      logger.debug("updateAddonRepositoryData found " + pending + " visible add-ons");
       if (pending == 0) {
         aCallback();
         return;
       }
 
       function notifyComplete() {
         if (--pending == 0)
           aCallback();
       }
 
-      aAddons.forEach(function UARD_forEachCallback(aAddon) {
-        AddonRepository.getCachedAddonByID(aAddon.id,
+      for (let addon of aAddons) {
+        AddonRepository.getCachedAddonByID(addon.id,
                                            function UARD_getCachedAddonCallback(aRepoAddon) {
           if (aRepoAddon) {
-            aAddon._repositoryAddon = aRepoAddon;
-            aAddon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
-            self.updateAddonDisabledState(aAddon);
+            logger.debug("updateAddonRepositoryData got info for " + addon.id);
+            addon._repositoryAddon = aRepoAddon;
+            addon.compatibilityOverrides = aRepoAddon.compatibilityOverrides;
+            self.updateAddonDisabledState(addon);
           }
 
           notifyComplete();
         });
-      });
+      };
     });
   },
 
   /**
    * When the previously selected theme is removed this method will be called
    * to enable the default theme.
    */
   enableDefaultTheme: function XPI_enableDefaultTheme() {
@@ -4790,19 +4792,20 @@ AddonInstall.prototype = {
    * be installed by this AddonInstall instance, the rest will have new
    * AddonInstall instances created for them.
    *
    * @param  aZipReader
    *         An open nsIZipReader for the multi-package XPI's files. This will
    *         be closed before this method returns.
    * @param  aCallback
    *         A function to call when all of the add-on manifests have been
-   *         loaded.
+   *         loaded. Because this loadMultipackageManifests is an internal API
+   *         we don't exception-wrap this callback
    */
-  loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
+  _loadMultipackageManifests: function AI_loadMultipackageManifests(aZipReader,
                                                                    aCallback) {
     let files = [];
     let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
     while (entries.hasMore()) {
       let entryName = entries.getNext();
       var target = getTemporaryFile();
       try {
         aZipReader.extract(entryName, target);
@@ -4837,17 +4840,17 @@ AddonInstall.prototype = {
       catch (e) {
         logger.warn(this.file.leafName + " cannot be installed from multi-package " +
              "XPI", e);
       }
     }
 
     if (!addon) {
       // No valid add-on was found
-      makeSafe(aCallback)();
+      aCallback();
       return;
     }
 
     this.addon = addon;
 
     this.updateAddonURIs();
 
     this.addon._install = this;
@@ -4886,17 +4889,17 @@ AddonInstall.prototype = {
 
           count++;
           if (count == files.length)
             aCallback();
         }, file);
       }, this);
     }
     else {
-      makeSafe(aCallback)();
+      aCallback();
     }
   },
 
   /**
    * Called after the add-on is a local file and the signature and install
    * manifest can be read.
    *
    * @param  aCallback
@@ -4966,17 +4969,17 @@ AddonInstall.prototype = {
       this.addon = loadManifestFromZipReader(zipreader);
     }
     catch (e) {
       zipreader.close();
       throw e;
     }
 
     if (this.addon.type == "multipackage") {
-      this.loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
+      this._loadMultipackageManifests(zipreader, function loadManifest_loadMultipackageManifests() {
         addRepositoryData(self.addon);
       });
       return;
     }
 
     zipreader.close();
 
     this.updateAddonURIs();
@@ -5246,17 +5249,17 @@ AddonInstall.prototype = {
    * Notify listeners that the download failed.
    *
    * @param  aReason
    *         Something to log about the failure
    * @param  error
    *         The error code to pass to the listeners
    */
   downloadFailed: function AI_downloadFailed(aReason, aError) {
-    logger.warn("Download failed", aError);
+    logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
     this.state = AddonManager.STATE_DOWNLOAD_FAILED;
     this.error = aReason;
     XPIProvider.removeActiveInstall(this);
     AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
                                              this.wrapper);
 
     // If the listener hasn't restarted the download then remove any temporary
     // file
@@ -5322,28 +5325,30 @@ AddonInstall.prototype = {
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
                                                this.listeners, this.wrapper)
       return;
     }
 
     // Find and cancel any pending installs for the same add-on in the same
     // install location
-    XPIProvider.installs.forEach(function(aInstall) {
+    for (let aInstall of XPIProvider.installs) {
       if (aInstall.state == AddonManager.STATE_INSTALLED &&
           aInstall.installLocation == this.installLocation &&
-          aInstall.addon.id == this.addon.id)
+          aInstall.addon.id == this.addon.id) {
+        logger.debug("Cancelling previous pending install of " + aInstall.addon.id);
         aInstall.cancel();
-    }, this);
+      }
+    }
 
     let isUpgrade = this.existingAddon &&
                     this.existingAddon._installLocation == this.installLocation;
     let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
 
-    logger.debug("Starting install of " + this.sourceURI.spec);
+    logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
     AddonManagerPrivate.callAddonListeners("onInstalling",
                                            createWrapper(this.addon),
                                            requiresRestart);
 
     let stagingDir = this.installLocation.getStagingDir();
     let stagedAddon = stagingDir.clone();
 
     Task.spawn((function() {
@@ -5389,17 +5394,17 @@ AddonInstall.prototype = {
           converter.init(stream, "UTF-8", 0, 0x0000);
           converter.writeString(JSON.stringify(this.addon));
         }
         finally {
           converter.close();
           stream.close();
         }
 
-        logger.debug("Staged install of " + this.sourceURI.spec + " ready; waiting for restart.");
+        logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
         this.state = AddonManager.STATE_INSTALLED;
         if (isUpgrade) {
           delete this.existingAddon.pendingUpgrade;
           this.existingAddon.pendingUpgrade = this.addon;
         }
         AddonManagerPrivate.callInstallListeners("onInstallEnded",
                                                  this.listeners, this.wrapper,
                                                  createWrapper(this.addon));
@@ -5851,18 +5856,20 @@ UpdateChecker.prototype = {
         // Skip installs that don't match the available update
         if (currentInstall.existingAddon != this.addon ||
             currentInstall.version != update.version)
           continue;
 
         // If the existing install has not yet started downloading then send an
         // available update notification. If it is already downloading then
         // don't send any available update notification
-        if (currentInstall.state == AddonManager.STATE_AVAILABLE)
+        if (currentInstall.state == AddonManager.STATE_AVAILABLE) {
+          logger.debug("Found an existing AddonInstall for " + this.addon.id);
           sendUpdateAvailableMessages(this, currentInstall);
+        }
         else
           sendUpdateAvailableMessages(this, null);
         return;
       }
 
       let self = this;
       AddonInstall.createUpdate(function onUpdateCheckComplete_createUpdate(aInstall) {
         sendUpdateAvailableMessages(self, aInstall);
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -1306,18 +1306,16 @@ this.XPIDatabase = {
   },
 
   /**
    * Synchronously marks a DBAddonInternal as visible marking all other
    * instances with the same ID as not visible.
    *
    * @param  aAddon
    *         The DBAddonInternal to make visible
-   * @param  callback
-   *         A callback to pass the DBAddonInternal to
    */
   makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
     logger.debug("Make addon " + aAddon._key + " visible");
     for (let [, otherAddon] of this.addonDB) {
       if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
         logger.debug("Hide addon " + otherAddon._key);
         otherAddon.visible = false;
       }
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 support-files =
   addon_about.xul
   addon_prefs.xul
+  cancelCompatCheck.sjs
   discovery.html
   signed_hotfix.rdf
   signed_hotfix.xpi
   unsigned_hotfix.rdf
   unsigned_hotfix.xpi
   more_options.xul
   options.xul
   plugin_test.html
@@ -30,16 +31,17 @@ support-files =
   browser_install.xml
   browser_install1_3.xpi
   browser_eula.xml
   browser_purchase.xml
 
 [browser_addonrepository_performance.js]
 [browser_bug557956.js]
 [browser_bug616841.js]
+[browser_cancelCompatCheck.js]
 [browser_checkAddonCompatibility.js]
 [browser_hotfix.js]
 [browser_installssl.js]
 [browser_newaddon.js]
 [browser_select_compatoverrides.js]
 [browser_select_confirm.js]
 [browser_select_selection.js]
 [browser_select_update.js]
--- a/toolkit/mozapps/extensions/test/browser/browser_bug557956.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956.js
@@ -420,17 +420,17 @@ add_test(function() {
             EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
           });
         });
       });
     });
   });
 });
 
-// Tests that compatibility overrides are retreived and affect addon
+// Tests that compatibility overrides are retrieved and affect addon
 // compatibility.
 add_test(function() {
   Services.prefs.setBoolPref(PREF_STRICT_COMPAT, false);
   Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
   is(AddonManager.strictCompatibility, false, "Strict compatibility should be disabled");
 
   // Use a blank update URL
   Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js
@@ -0,0 +1,536 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we can cancel the add-on compatibility check while it is
+// in progress (bug 772484).
+// Test framework copied from browser_bug557956.js
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+
+const PREF_GETADDONS_BYIDS            = "extensions.getAddons.get.url";
+const PREF_MIN_PLATFORM_COMPAT        = "extensions.minCompatiblePlatformVersion";
+
+let repo = {};
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm", repo);
+
+/**
+ * Test add-ons:
+ *
+ * Addon    minVersion   maxVersion   Notes
+ * addon1   0            *
+ * addon2   0            0
+ * addon3   0            0
+ * addon4   1            *
+ * addon5   0            0            Made compatible by update check
+ * addon6   0            0            Made compatible by update check
+ * addon7   0            0            Has a broken update available
+ * addon8   0            0            Has an update available
+ * addon9   0            0            Has an update available
+ * addon10  0            0            Made incompatible by override check
+ */
+
+// describe the addons
+let ao1 = { file: "browser_bug557956_1", id: "addon1@tests.mozilla.org"};
+let ao2 = { file: "browser_bug557956_2", id: "addon2@tests.mozilla.org"};
+let ao3 = { file: "browser_bug557956_3", id: "addon3@tests.mozilla.org"};
+let ao4 = { file: "browser_bug557956_4", id: "addon4@tests.mozilla.org"};
+let ao5 = { file: "browser_bug557956_5", id: "addon5@tests.mozilla.org"};
+let ao6 = { file: "browser_bug557956_6", id: "addon6@tests.mozilla.org"};
+let ao7 = { file: "browser_bug557956_7", id: "addon7@tests.mozilla.org"};
+let ao8 = { file: "browser_bug557956_8_1", id: "addon8@tests.mozilla.org"};
+let ao9 = { file: "browser_bug557956_9_1", id: "addon9@tests.mozilla.org"};
+let ao10 = { file: "browser_bug557956_10", id: "addon10@tests.mozilla.org"};
+
+// Return a promise that resolves after the specified delay in MS
+function delayMS(aDelay) {
+  let deferred = Promise.defer();
+  setTimeout(deferred.resolve, aDelay);
+  return deferred.promise;
+}
+
+// Return a promise that resolves when the specified observer topic is notified
+function promise_observer(aTopic) {
+  let deferred = Promise.defer();
+  Services.obs.addObserver(function observe(aSubject, aObsTopic, aData) {
+    Services.obs.removeObserver(arguments.callee, aObsTopic);
+    deferred.resolve([aSubject, aData]);
+  }, aTopic, false);
+  return deferred.promise;
+}
+
+// Install a set of addons using a bogus update URL so that we can force
+// the compatibility update to happen later
+// @param aUpdateURL The real update URL to use after the add-ons are installed
+function promise_install_test_addons(aAddonList, aUpdateURL) {
+  info("Starting add-on installs");
+  var installs = [];
+  let deferred = Promise.defer();
+
+  // Use a blank update URL
+  Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+  for (let addon of aAddonList) {
+    AddonManager.getInstallForURL(TESTROOT + "addons/" + addon.file + ".xpi", function(aInstall) {
+      installs.push(aInstall);
+    }, "application/x-xpinstall");
+  }
+
+  var listener = {
+    installCount: 0,
+
+    onInstallEnded: function() {
+      this.installCount++;
+      if (this.installCount == installs.length) {
+        info("Done add-on installs");
+        // Switch to the test update URL
+        Services.prefs.setCharPref(PREF_UPDATEURL, aUpdateURL);
+        deferred.resolve();
+      }
+    }
+  };
+
+  for (let install of installs) {
+    install.addListener(listener);
+    install.install();
+  }
+
+  return deferred.promise;
+}
+
+function promise_addons_by_ids(aAddonIDs) {
+  info("promise_addons_by_ids " + aAddonIDs.toSource());
+  let deferred = Promise.defer();
+  AddonManager.getAddonsByIDs(aAddonIDs, deferred.resolve);
+  return deferred.promise;
+}
+
+function* promise_uninstall_test_addons() {
+  info("Starting add-on uninstalls");
+  let addons = yield promise_addons_by_ids([ao1.id, ao2.id, ao3.id, ao4.id, ao5.id,
+                                            ao6.id, ao7.id, ao8.id, ao9.id, ao10.id]);
+  let deferred = Promise.defer();
+  let uninstallCount = addons.length;
+  let listener = {
+    onUninstalled: function(aAddon) {
+      if (aAddon) {
+        info("Finished uninstalling " + aAddon.id);
+      }
+      if (--uninstallCount == 0) {
+        info("Done add-on uninstalls");
+        AddonManager.removeAddonListener(listener);
+        deferred.resolve();
+      }
+    }};
+  AddonManager.addAddonListener(listener);
+  for (let addon of addons) {
+    if (addon)
+      addon.uninstall();
+    else
+      listener.onUninstalled(null);
+  }
+  yield deferred.promise;
+}
+
+// Returns promise{window}, resolves with a handle to the compatibility
+// check window
+function promise_open_compatibility_window(aInactiveAddonIds) {
+  let deferred = Promise.defer();
+  // This will reset the longer timeout multiplier to 2 which will give each
+  // test that calls open_compatibility_window a minimum of 60 seconds to
+  // complete.
+  requestLongerTimeout(100 /* XXX was 2 */);
+
+  var variant = Cc["@mozilla.org/variant;1"].
+                createInstance(Ci.nsIWritableVariant);
+  variant.setFromVariant(aInactiveAddonIds);
+
+  // Cannot be modal as we want to interract with it, shouldn't cause problems
+  // with testing though.
+  var features = "chrome,centerscreen,dialog,titlebar";
+  var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+           getService(Ci.nsIWindowWatcher);
+  var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+  win.addEventListener("load", function() {
+    function page_shown(aEvent) {
+      if (aEvent.target.pageid)
+        info("Page " + aEvent.target.pageid + " shown");
+    }
+
+    win.removeEventListener("load", arguments.callee, false);
+
+    info("Compatibility dialog opened");
+
+    win.addEventListener("pageshow", page_shown, false);
+    win.addEventListener("unload", function() {
+      win.removeEventListener("unload", arguments.callee, false);
+      win.removeEventListener("pageshow", page_shown, false);
+      dump("Compatibility dialog closed\n");
+    }, false);
+
+    deferred.resolve(win);
+  }, false);
+  return deferred.promise;
+}
+
+function promise_window_close(aWindow) {
+  let deferred = Promise.defer();
+  aWindow.addEventListener("unload", function() {
+    aWindow.removeEventListener("unload", arguments.callee, false);
+    deferred.resolve(aWindow);
+  }, false);
+  return deferred.promise;
+}
+
+function promise_page(aWindow, aPageId) {
+  let deferred = Promise.defer();
+  var page = aWindow.document.getElementById(aPageId);
+  page.addEventListener("pageshow", function() {
+    page.removeEventListener("pageshow", arguments.callee, false);
+    executeSoon(function() {
+      deferred.resolve(aWindow);
+    });
+  }, false);
+  return deferred.promise;
+}
+
+function get_list_names(aList) {
+  var items = [];
+  for (let listItem of aList.childNodes)
+    items.push(listItem.label);
+  items.sort();
+  return items;
+}
+
+// These add-ons were inactive in the old application
+let inactiveAddonIds = [
+  ao2.id,
+  ao4.id,
+  ao5.id,
+  ao10.id
+];
+
+// Make sure the addons in the list are not installed
+function* check_addons_uninstalled(aAddonList) {
+  let foundList = yield promise_addons_by_ids([addon.id for (addon of aAddonList)]);
+  for (let i = 0; i < aAddonList.length; i++) {
+    ok(!foundList[i], "Addon " + aAddonList[i].id + " is not installed");
+  }
+  info("Add-on uninstall check complete");
+  yield true;
+}
+
+
+// Tests that the right add-ons show up in the mismatch dialog and updates can
+// be installed
+// This is a task-based rewrite of the first test in browser_bug557956.js
+// kept here to show the whole process so that other tests in this file can
+// pick and choose which steps to perform, but disabled since the logic is already
+// tested in browser_bug557956.js.
+// add_task(
+    function start_update() {
+  // Don't pull compatibility data during add-on install
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+  let addonList = [ao3, ao5, ao6, ao7, ao8, ao9];
+  yield promise_install_test_addons(addonList, TESTROOT + "cancelCompatCheck.sjs");
+
+
+  // Check that the addons start out not compatible.
+  let [a5, a6, a8, a9] = yield promise_addons_by_ids([ao5.id, ao6.id, ao8.id, ao9.id]);
+  ok(!a5.isCompatible, "addon5 should not be compatible");
+  ok(!a6.isCompatible, "addon6 should not be compatible");
+  ok(!a8.isCompatible, "addon8 should not be compatible");
+  ok(!a9.isCompatible, "addon9 should not be compatible");
+
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  // Check that opening the compatibility window loads and applies
+  // the compatibility update
+  let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+  var doc = compatWindow.document;
+  compatWindow = yield promise_page(compatWindow, "mismatch");
+  var items = get_list_names(doc.getElementById("mismatch.incompatible"));
+  is(items.length, 4, "Should have seen 4 still incompatible items");
+  is(items[0], "Addon3 1.0", "Should have seen addon3 still incompatible");
+  is(items[1], "Addon7 1.0", "Should have seen addon7 still incompatible");
+  is(items[2], "Addon8 1.0", "Should have seen addon8 still incompatible");
+  is(items[3], "Addon9 1.0", "Should have seen addon9 still incompatible");
+
+  ok(a5.isCompatible, "addon5 should be compatible");
+  ok(a6.isCompatible, "addon6 should be compatible");
+
+  // Click next to start finding updates for the addons that are still incompatible
+  var button = doc.documentElement.getButton("next");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  compatWindow = yield promise_page(compatWindow, "found");
+  ok(doc.getElementById("xpinstallDisabledAlert").hidden,
+     "Install should be allowed");
+
+  var list = doc.getElementById("found.updates");
+  var items = get_list_names(list);
+  is(items.length, 3, "Should have seen 3 updates available");
+  is(items[0], "Addon7 2.0", "Should have seen update for addon7");
+  is(items[1], "Addon8 2.0", "Should have seen update for addon8");
+  is(items[2], "Addon9 2.0", "Should have seen update for addon9");
+
+  ok(!doc.documentElement.getButton("next").disabled,
+     "Next button should be enabled");
+
+  // Uncheck all
+  for (let listItem of list.childNodes)
+    EventUtils.synthesizeMouse(listItem, 2, 2, { }, compatWindow);
+
+  ok(doc.documentElement.getButton("next").disabled,
+     "Next button should not be enabled");
+
+  // Check the ones we want to install
+  for (let listItem of list.childNodes) {
+    if (listItem.label != "Addon7 2.0")
+      EventUtils.synthesizeMouse(listItem, 2, 2, { }, compatWindow);
+  }
+
+  var button = doc.documentElement.getButton("next");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  compatWindow = yield promise_page(compatWindow, "finished");
+  var button = doc.documentElement.getButton("finish");
+  ok(!button.hidden, "Finish button should not be hidden");
+  ok(!button.disabled, "Finish button should not be disabled");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  compatWindow = yield promise_window_close(compatWindow);
+
+  // Check that the appropriate add-ons have been updated
+  let [a8, a9] = yield promise_addons_by_ids(["addon8@tests.mozilla.org",
+                                              "addon9@tests.mozilla.org"]);
+  is(a8.version, "2.0", "addon8 should have updated");
+  is(a9.version, "2.0", "addon9 should have updated");
+
+  yield promise_uninstall_test_addons();
+}
+// );
+
+// Test what happens when the user cancels during AddonRepository.repopulateCache()
+// Add-ons that have updates available should not update if they were disabled before
+// For this test, addon8 became disabled during update and addon9 was previously disabled,
+// so addon8 should update and addon9 should not
+add_task(function cancel_during_repopulate() {
+  Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+  Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+  Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+  let installsDone = promise_observer("TEST:all-updates-done");
+
+  // Don't pull compatibility data during add-on install
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+  // Set up our test addons so that the server-side JS has a 500ms delay to make
+  // sure we cancel the dialog before we get the data we want to refill our
+  // AddonRepository cache
+  let addonList = [ao5, ao8, ao9, ao10];
+  yield promise_install_test_addons(addonList,
+                                    TESTROOT + "cancelCompatCheck.sjs?500");
+
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, TESTROOT + "browser_bug557956.xml");
+
+  let [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+  ok(!a5.isCompatible, "addon5 should not be compatible");
+  ok(!a8.isCompatible, "addon8 should not be compatible");
+  ok(!a9.isCompatible, "addon9 should not be compatible");
+
+  let compatWindow = yield promise_open_compatibility_window([ao9.id, ...inactiveAddonIds]);
+  var doc = compatWindow.document;
+  yield promise_page(compatWindow, "versioninfo");
+
+  // Brief delay to let the update window finish requesting all add-ons and start
+  // reloading the addon repository
+  yield delayMS(50);
+
+  info("Cancel the compatibility check dialog");
+  var button = doc.documentElement.getButton("cancel");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  info("Waiting for installs to complete");
+  yield installsDone;
+  ok(!repo.AddonRepository.isSearching, "Background installs are done");
+
+  // There should be no active updates
+  let getInstalls = Promise.defer();
+  AddonManager.getAllInstalls(getInstalls.resolve);
+  let installs = yield getInstalls.promise;
+  is (installs.length, 0, "There should be no active installs after background installs are done");
+
+  // addon8 should have updated in the background,
+  // addon9 was listed as previously disabled so it should not have updated
+  let [a5, a8, a9, a10] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id, ao10.id]);
+  ok(a5.isCompatible, "addon5 should be compatible");
+  ok(a8.isCompatible, "addon8 should have been upgraded");
+  ok(!a9.isCompatible, "addon9 should not have been upgraded");
+  ok(!a10.isCompatible, "addon10 should not be compatible");
+
+  info("Updates done");
+  yield promise_uninstall_test_addons();
+  info("done uninstalling add-ons");
+});
+
+// User cancels after repopulateCache, while we're waiting for the addon.findUpdates()
+// calls in gVersionInfoPage_onPageShow() to complete
+// For this test, both addon8 and addon9 were disabled by this update, but addon8
+// is set to not auto-update, so only addon9 should update in the background
+add_task(function cancel_during_findUpdates() {
+  Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+  Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+  let observeUpdateDone = promise_observer("TEST:addon-repository-data-updated");
+  let installsDone = promise_observer("TEST:all-updates-done");
+
+  // Don't pull compatibility data during add-on install
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+  // No delay on the .sjs this time because we want the cache to repopulate
+  let addonList = [ao3, ao5, ao6, ao7, ao8, ao9];
+  yield promise_install_test_addons(addonList,
+                                    TESTROOT + "cancelCompatCheck.sjs");
+
+  let [a8] = yield promise_addons_by_ids([ao8.id]);
+  a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+  var doc = compatWindow.document;
+  yield promise_page(compatWindow, "versioninfo");
+
+  info("Waiting for repository-data-updated");
+  yield observeUpdateDone;
+
+  // Quick wait to make sure the findUpdates calls get queued
+  yield delayMS(5);
+
+  info("Cancel the compatibility check dialog");
+  var button = doc.documentElement.getButton("cancel");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  info("Waiting for installs to complete 2");
+  yield installsDone;
+  ok(!repo.AddonRepository.isSearching, "Background installs are done 2");
+
+  // addon8 should have updated in the background,
+  // addon9 was listed as previously disabled so it should not have updated
+  let [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+  ok(a5.isCompatible, "addon5 should be compatible");
+  ok(!a8.isCompatible, "addon8 should not have been upgraded");
+  ok(a9.isCompatible, "addon9 should have been upgraded");
+
+  let getInstalls = Promise.defer();
+  AddonManager.getAllInstalls(getInstalls.resolve);
+  let installs = yield getInstalls.promise;
+  is (installs.length, 0, "There should be no active installs after the dialog is cancelled 2");
+
+  info("findUpdates done");
+  yield promise_uninstall_test_addons();
+});
+
+// Cancelling during the 'mismatch' screen allows add-ons that can auto-update
+// to continue updating in the background and cancels any other updates
+// Same conditions as the previous test - addon8 and addon9 have updates available,
+// addon8 is set to not auto-update so only addon9 should become compatible
+add_task(function cancel_mismatch() {
+  Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+  Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+  let observeUpdateDone = promise_observer("TEST:addon-repository-data-updated");
+  let installsDone = promise_observer("TEST:all-updates-done");
+
+  // Don't pull compatibility data during add-on install
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+  // No delay on the .sjs this time because we want the cache to repopulate
+  let addonList = [ao3, ao5, ao6, ao7, ao8, ao9];
+  yield promise_install_test_addons(addonList,
+                                    TESTROOT + "cancelCompatCheck.sjs");
+
+  let [a8] = yield promise_addons_by_ids([ao8.id]);
+  a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+  // Check that the addons start out not compatible.
+  let [a3, a7, a8, a9] = yield promise_addons_by_ids([ao3.id, ao7.id, ao8.id, ao9.id]);
+  ok(!a3.isCompatible, "addon3 should not be compatible");
+  ok(!a7.isCompatible, "addon7 should not be compatible");
+  ok(!a8.isCompatible, "addon8 should not be compatible");
+  ok(!a9.isCompatible, "addon9 should not be compatible");
+
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+  var doc = compatWindow.document;
+  info("Wait for mismatch page");
+  yield promise_page(compatWindow, "mismatch");
+  info("Click the Don't Check button");
+  var button = doc.documentElement.getButton("cancel");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  yield promise_window_close(compatWindow);
+  info("Waiting for installs to complete in cancel_mismatch");
+  yield installsDone;
+
+  // addon8 should not have updated in the background,
+  // addon9 was listed as previously disabled so it should not have updated
+  let [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+  ok(a5.isCompatible, "addon5 should be compatible");
+  ok(!a8.isCompatible, "addon8 should not have been upgraded");
+  ok(a9.isCompatible, "addon9 should have been upgraded");
+
+  // Make sure there are no pending addon installs
+  let pInstalls = Promise.defer();
+  AddonManager.getAllInstalls(pInstalls.resolve);
+  let installs = yield pInstalls.promise;
+  ok(installs.length == 0, "No remaining add-on installs (" + installs.toSource() + ")");
+
+  yield promise_uninstall_test_addons();
+  yield check_addons_uninstalled(addonList);
+});
+
+// Cancelling during the 'mismatch' screen with only add-ons that have
+// no updates available
+add_task(function cancel_mismatch_no_updates() {
+  Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+  Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+  let observeUpdateDone = promise_observer("TEST:addon-repository-data-updated");
+
+  // Don't pull compatibility data during add-on install
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+  // No delay on the .sjs this time because we want the cache to repopulate
+  let addonList = [ao3, ao5, ao6];
+  yield promise_install_test_addons(addonList,
+                                    TESTROOT + "cancelCompatCheck.sjs");
+
+  // Check that the addons start out not compatible.
+  let [a3, a5, a6] = yield promise_addons_by_ids([ao3.id, ao5.id, ao6.id]);
+  ok(!a3.isCompatible, "addon3 should not be compatible");
+  ok(!a5.isCompatible, "addon7 should not be compatible");
+  ok(!a6.isCompatible, "addon8 should not be compatible");
+
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+  var doc = compatWindow.document;
+  info("Wait for mismatch page");
+  yield promise_page(compatWindow, "mismatch");
+  info("Click the Don't Check button");
+  var button = doc.documentElement.getButton("cancel");
+  EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+  yield promise_window_close(compatWindow);
+
+  let [a3, a5, a6] = yield promise_addons_by_ids([ao3.id, ao5.id, ao6.id]);
+  ok(!a3.isCompatible, "addon3 should not be compatible");
+  ok(a5.isCompatible, "addon5 should have become compatible");
+  ok(a6.isCompatible, "addon6 should have become compatible");
+
+  // Make sure there are no pending addon installs
+  let pInstalls = Promise.defer();
+  AddonManager.getAllInstalls(pInstalls.resolve);
+  let installs = yield pInstalls.promise;
+  ok(installs.length == 0, "No remaining add-on installs (" + installs.toSource() + ")");
+
+  yield promise_uninstall_test_addons();
+  yield check_addons_uninstalled(addonList);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Delay before responding to an HTTP call attempting to read
+// an addon update RDF file
+
+function handleRequest(req, resp) {
+  resp.processAsync();
+  resp.setHeader("Cache-Control", "no-cache, no-store", false);
+  resp.setHeader("Content-Type", "text/xml;charset=utf-8", false);
+
+  let file = null;
+  getObjectState("SERVER_ROOT", function(serverRoot)
+  {
+    file = serverRoot.getFile("browser/toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf");
+  });
+  dump("*** cancelCompatCheck.sjs: " + file.path + "\n");
+  let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+                  createInstance(Components.interfaces.nsIFileInputStream);
+  fstream.init(file, -1, 0, 0);
+  let cstream = null;
+  cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+          createInstance(Components.interfaces.nsIConverterInputStream);
+  cstream.init(fstream, "UTF-8", 0, 0);
+
+  // The delay can be passed on the query string
+  let delay = req.queryString + 0;
+
+  timer = Components.classes["@mozilla.org/timer;1"].
+          createInstance(Components.interfaces.nsITimer);
+  timer.init(function sendFile() {
+    dump("cancelCompatCheck: starting to send file\n");
+    let (str = {}) {
+      let read = 0;
+      do {
+        // read as much as we can and put it in str.value
+        read = cstream.readString(0xffffffff, str);
+        resp.write(str.value);
+      } while (read != 0);
+    }
+    cstream.close();
+    resp.finish();
+  }, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+}
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -84,48 +84,48 @@ for (let pref of gRestorePrefs) {
     pref.value = Services.prefs.getIntPref(pref.name);
   else if (pref.type == Services.prefs.PREF_STRING)
     pref.value = Services.prefs.getCharPref(pref.name);
 }
 
 // Turn logging on for all tests
 Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
 
+// Helper to register test failures and close windows if any are left open
+function checkOpenWindows(aWindowID) {
+  let windows = Services.wm.getEnumerator(aWindowID);
+  let found = false;
+  while (windows.hasMoreElements()) {
+    let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+    if (!win.closed) {
+      found = true;
+      win.close();
+    }
+  }
+  if (found)
+    ok(false, "Found unexpected " + aWindowID + " window still open");
+}
+
 registerCleanupFunction(function() {
   // Restore prefs
   for (let pref of gRestorePrefs) {
     if (pref.type == "clear")
       Services.prefs.clearUserPref(pref.name);
     else if (pref.type == Services.prefs.PREF_BOOL)
       Services.prefs.setBoolPref(pref.name, pref.value);
     else if (pref.type == Services.prefs.PREF_INT)
       Services.prefs.setIntPref(pref.name, pref.value);
     else if (pref.type == Services.prefs.PREF_STRING)
       Services.prefs.setCharPref(pref.name, pref.value);
   }
 
   // Throw an error if the add-ons manager window is open anywhere
-  var windows = Services.wm.getEnumerator("Addons:Manager");
-  if (windows.hasMoreElements())
-    ok(false, "Found unexpected add-ons manager window still open");
-  while (windows.hasMoreElements())
-    windows.getNext().QueryInterface(Ci.nsIDOMWindow).close();
-
-  windows = Services.wm.getEnumerator("Addons:Compatibility");
-  if (windows.hasMoreElements())
-    ok(false, "Found unexpected add-ons compatibility window still open");
-  while (windows.hasMoreElements())
-    windows.getNext().QueryInterface(Ci.nsIDOMWindow).close();
-
-  windows = Services.wm.getEnumerator("Addons:Install");
-  if (windows.hasMoreElements())
-    ok(false, "Found unexpected add-ons installation window still open");
-  while (windows.hasMoreElements())
-    windows.getNext().QueryInterface(Ci.nsIDOMWindow).close();
-
+  checkOpenWindows("Addons:Manager");
+  checkOpenWindows("Addons:Compatibility");
+  checkOpenWindows("Addons:Install");
 
   // We can for now know that getAllInstalls actually calls its callback before
   // it returns so this will complete before the next test start.
   AddonManager.getAllInstalls(function(aInstalls) {
     for (let install of aInstalls) {
       if (install instanceof MockInstall)
         continue;