Bug 481603: Flush caches for restartless add-on changes. r=robstrong
authorDave Townsend <dtownsend@oxymoronical.com>
Wed, 15 Jun 2011 11:12:53 -0700
changeset 71659 274e2d2a93a8f53596951833e17a1ec5bba95992
parent 71658 210112f045f7c4d6efb5021564c11d9ffe58b8e6
child 71660 cab0115bbc48da3322890161779addd6cb28ab6e
push id159
push usereakhgari@mozilla.com
push dateTue, 16 Aug 2011 17:53:11 +0000
treeherdermozilla-beta@8786e3e49240 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobstrong
bugs481603
milestone7.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 481603: Flush caches for restartless add-on changes. r=robstrong
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js
toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm
toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js
toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm
toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js
toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm
toolkit/mozapps/extensions/test/xpcshell-unpack/head_unpack.js
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -902,16 +902,43 @@ function loadManifestFromZipFile(aXPIFil
 function loadManifestFromFile(aFile) {
   if (aFile.isFile())
     return loadManifestFromZipFile(aFile);
   else
     return loadManifestFromDir(aFile);
 }
 
 /**
+ * Gets an nsIURI for a file within another file, either a directory or an XPI
+ * file. If aFile is a directory then this will return a file: URI, if it is an
+ * XPI file then it will return a jar: URI.
+ *
+ * @param  aFile
+ *         The file containing the resources, must be either a directory or an
+ *         XPI file
+ * @param  aPath
+ *         The path to find the resource at, "/" separated. If aPath is empty
+ *         then the uri to the root of the contained files will be returned
+ * @return an nsIURI pointing at the resource
+ */
+function getURIForResourceInFile(aFile, aPath) {
+  if (aFile.isDirectory()) {
+    let resource = aFile.clone();
+    if (aPath) {
+      aPath.split("/").forEach(function(aPart) {
+        resource.append(aPart);
+      });
+    }
+    return NetUtil.newURI(resource);
+  }
+
+  return buildJarURI(aFile, aPath);
+}
+
+/**
  * Creates a jar: URI for a file inside a ZIP file.
  *
  * @param  aJarfile
  *         The ZIP file as an nsIFile
  * @param  aPath
  *         The path inside the ZIP file
  * @return an nsIURI for the file
  */
@@ -928,16 +955,22 @@ function buildJarURI(aJarfile, aPath) {
  *        The ZIP/XPI/JAR file as a nsIFile
  */
 function flushJarCache(aJarFile) {
   Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
   Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIChromeFrameMessageManager)
     .sendAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
 }
 
+function flushStartupCache() {
+  // Init this, so it will get the notification.
+  Cc["@mozilla.org/xul/xul-prototype-cache;1"].getService(Ci.nsISupports);
+  Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+}
+
 /**
  * Creates and returns a new unique temporary file. The caller should delete
  * the file when it is no longer needed.
  *
  * @return an nsIFile that points to a randomly named, initially empty file in
  *         the OS temporary files directory
  */
 function getTemporaryFile() {
@@ -1560,19 +1593,17 @@ var XPIProvider = {
         // Remember the list of add-ons that were disabled this startup so
         // the application can notify the user however it wants to
         Services.prefs.setCharPref(PREF_EM_DISABLED_ADDONS_LIST,
                                    this.startupChanges.appDisabled.join(","));
       }
     }
 
     if (flushCaches) {
-      // Init this, so it will get the notification.
-      let xulPrototypeCache = Cc["@mozilla.org/xul/xul-prototype-cache;1"].getService(Ci.nsISupports);
-      Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+      flushStartupCache();
 
       // UI displayed early in startup (like the compatibility UI) may have
       // caused us to cache parts of the skin or locale in memory. These must
       // be flushed to allow extension provided skins and locales to take full
       // effect
       Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
       Services.obs.notifyObservers(null, "chrome-flush-caches", null);
     }
@@ -1984,16 +2015,17 @@ var XPIProvider = {
               let newVersion = oldBootstrap.version;
               let uninstallReason = Services.vc.compare(newVersion, oldVersion) < 0 ?
                                     BOOTSTRAP_REASONS.ADDON_UPGRADE :
                                     BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
               this.callBootstrapMethod(existingAddonID, oldBootstrap.version,
                                        existingAddon, "uninstall", uninstallReason);
               this.unloadBootstrapScope(existingAddonID);
+              flushStartupCache();
             }
           }
           catch (e) {
           }
         }
 
         try {
           var addonInstallLocation = aLocation.installAddon(id, stageDirEntry,
@@ -2254,28 +2286,30 @@ var XPIProvider = {
 
         // If this was the active theme and it is now disabled then enable the
         // default theme
         if (aOldAddon.active && isAddonDisabled(newAddon))
           XPIProvider.enableDefaultTheme();
 
         // If the new add-on is bootstrapped and active then call its install method
         if (newAddon.active && newAddon.bootstrap) {
+          // Startup cache must be flushed before calling the bootstrap script
+          flushStartupCache();
+
           let installReason = Services.vc.compare(aOldAddon.version, newAddon.version) < 0 ?
                               BOOTSTRAP_REASONS.ADDON_UPGRADE :
                               BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
           let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
           file.persistentDescriptor = aAddonState.descriptor;
           XPIProvider.callBootstrapMethod(newAddon.id, newAddon.version, file,
                                           "install", installReason);
           return false;
         }
 
-        // Otherwise the caches will need to be invalidated
         return true;
       }
 
       return false;
     }
 
     /**
      * Updates an add-on's descriptor for when the add-on has moved in the
@@ -2440,19 +2474,17 @@ var XPIProvider = {
       XPIDatabase.removeAddonMetadata(aOldAddon);
       if (aOldAddon.active) {
 
         // Enable the default theme if the previously active theme has been
         // removed
         if (aOldAddon.type == "theme")
           XPIProvider.enableDefaultTheme();
 
-        // If this was not a bootstrapped add-on then we must force a restart.
-        if (!aOldAddon.bootstrap)
-          return true;
+        return true;
       }
 
       return false;
     }
 
     /**
      * Called when a new add-on has been detected.
      *
@@ -2581,20 +2613,26 @@ var XPIProvider = {
 
           installReason = Services.vc.compare(oldBootstrap.version, newAddon.version) < 0 ?
                           BOOTSTRAP_REASONS.ADDON_UPGRADE :
                           BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
           let oldAddonFile = Cc["@mozilla.org/file/local;1"].
                              createInstance(Ci.nsILocalFile);
           oldAddonFile.persistentDescriptor = oldBootstrap.descriptor;
+
           XPIProvider.callBootstrapMethod(newAddon.id, oldBootstrap.version,
                                           oldAddonFile, "uninstall",
                                           installReason);
           XPIProvider.unloadBootstrapScope(newAddon.id);
+
+          // If the new add-on is bootstrapped then we must flush the caches
+          // before calling the new bootstrap script
+          if (newAddon.bootstrap)
+            flushStartupCache();
         }
 
         if (!newAddon.bootstrap)
           return true;
 
         // Visible bootstrapped add-ons need to have their install method called
         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
         file.persistentDescriptor = aAddonState.descriptor;
@@ -3350,57 +3388,42 @@ var XPIProvider = {
       descriptor: aFile.persistentDescriptor
     };
     this.addAddonsToCrashReporter();
 
     let principal = Cc["@mozilla.org/systemprincipal;1"].
                     createInstance(Ci.nsIPrincipal);
     this.bootstrapScopes[aId] = new Components.utils.Sandbox(principal);
 
-    let bootstrap = aFile.clone();
-    let name = aFile.leafName;
-    let spec;
-
-    if (!bootstrap.exists()) {
+    if (!aFile.exists()) {
       ERROR("Attempted to load bootstrap scope from missing directory " + bootstrap.path);
       return;
     }
 
-    if (bootstrap.isDirectory()) {
-      bootstrap.append("bootstrap.js");
-      let uri = Services.io.newFileURI(bootstrap);
-      spec = uri.spec;
-    } else {
-      spec = buildJarURI(bootstrap, "bootstrap.js").spec;
-    }
-    if (bootstrap.exists()) {
-      let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
-                   createInstance(Ci.mozIJSSubScriptLoader);
-
-      try {
-        // As we don't want our caller to control the JS version used for the
-        // bootstrap file, we run loadSubScript within the context of the
-        // sandbox with the latest JS version set explicitly.
-        this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ = spec;
-        Components.utils.evalInSandbox(
-          "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
-                     .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
-                     .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
-      }
-      catch (e) {
-        WARN("Error loading bootstrap.js for " + aId, e);
-      }
-
-      // Copy the reason values from the global object into the bootstrap scope.
-      for (let name in BOOTSTRAP_REASONS)
-        this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
-    }
-    else {
-      WARN("Bootstrap missing for " + aId);
-    }
+    let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+                 createInstance(Ci.mozIJSSubScriptLoader);
+
+    try {
+      // As we don't want our caller to control the JS version used for the
+      // bootstrap file, we run loadSubScript within the context of the
+      // sandbox with the latest JS version set explicitly.
+      this.bootstrapScopes[aId].__SCRIPT_URI_SPEC__ =
+          getURIForResourceInFile(aFile, "bootstrap.js").spec;
+      Components.utils.evalInSandbox(
+        "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
+                   .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
+                   .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
+    }
+    catch (e) {
+      WARN("Error loading bootstrap.js for " + aId, e);
+    }
+
+    // Copy the reason values from the global object into the bootstrap scope.
+    for (let name in BOOTSTRAP_REASONS)
+      this.bootstrapScopes[aId][name] = BOOTSTRAP_REASONS[name];
   },
 
   /**
    * Unloads a bootstrap scope by dropping all references to it and then
    * updating the list of active add-ons with the crash reporter.
    *
    * @param  aId
    *         The add-on's ID
@@ -3438,17 +3461,18 @@ var XPIProvider = {
     if (!(aMethod in this.bootstrapScopes[aId])) {
       WARN("Add-on " + aId + " is missing bootstrap method " + aMethod);
       return;
     }
 
     let params = {
       id: aId,
       version: aVersion,
-      installPath: aFile.clone()
+      installPath: aFile.clone(),
+      resourceURI: getURIForResourceInFile(aFile, "")
     };
 
     LOG("Calling bootstrap method " + aMethod + " on " + aId + " version " +
         aVersion);
     try {
       this.bootstrapScopes[aId][aMethod](params, aReason);
     }
     catch (e) {
@@ -3618,19 +3642,21 @@ var XPIProvider = {
 
     if (!requiresRestart) {
       if (aAddon.bootstrap) {
         let file = aAddon._installLocation.getLocationForID(aAddon.id);
         if (aAddon.active) {
           this.callBootstrapMethod(aAddon.id, aAddon.version, file, "shutdown",
                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         }
+
         this.callBootstrapMethod(aAddon.id, aAddon.version, file, "uninstall",
                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         this.unloadBootstrapScope(aAddon.id);
+        flushStartupCache();
       }
       aAddon._installLocation.uninstallAddon(aAddon.id);
       XPIDatabase.removeAddonMetadata(aAddon);
       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
       // Reveal the highest priority add-on with the same ID
       function revealAddon(aAddon) {
         XPIDatabase.makeAddonVisible(aAddon);
@@ -6211,20 +6237,22 @@ AddonInstall.prototype = {
           if (this.existingAddon.bootstrap) {
             let file = this.existingAddon._installLocation
                            .getLocationForID(this.existingAddon.id);
             if (this.existingAddon.active) {
               XPIProvider.callBootstrapMethod(this.existingAddon.id,
                                               this.existingAddon.version,
                                               file, "shutdown", reason);
             }
+
             XPIProvider.callBootstrapMethod(this.existingAddon.id,
                                             this.existingAddon.version,
                                             file, "uninstall", reason);
             XPIProvider.unloadBootstrapScope(this.existingAddon.id);
+            flushStartupCache();
           }
 
           if (!isUpgrade && this.existingAddon.active) {
             this.existingAddon.active = false;
             XPIDatabase.updateAddonActive(this.existingAddon);
           }
         }
 
@@ -7205,31 +7233,32 @@ function AddonWrapper(aAddon) {
     let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
                     createInstance(Ci.nsIZipReader);
     zipReader.open(bundle);
     let result = zipReader.hasEntry(aPath);
     zipReader.close();
     return result;
   },
 
+  /**
+   * Returns a URI to the selected resource or to the add-on bundle if aPath
+   * is null. URIs to the bundle will always be file: URIs. URIs to resources
+   * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
+   * still an XPI file.
+   *
+   * @param  aPath
+   *         The path in the add-on to get the URI for or null to get a URI to
+   *         the file or directory the add-on is installed as.
+   * @return an nsIURI
+   */
   this.getResourceURI = function(aPath) {
-    let bundle = aAddon._sourceBundle.clone();
-
-    if (bundle.isDirectory()) {
-      if (aPath) {
-        aPath.split("/").forEach(function(aPart) {
-          bundle.append(aPart);
-        });
-      }
-      return Services.io.newFileURI(bundle);
-    }
-
     if (!aPath)
-      return Services.io.newFileURI(bundle);
-    return buildJarURI(bundle, aPath);
+      return NetUtil.newURI(aAddon._sourceBundle);
+
+    return getURIForResourceInFile(aAddon._sourceBundle, aPath);
   }
 }
 
 /**
  * An object which identifies a directory install location for add-ons. The
  * location consists of a directory which contains the add-ons installed in the
  * location.
  *
--- a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js
@@ -1,18 +1,22 @@
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 function install(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.installed_version", 1);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function startup(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.active_version", 1);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function shutdown(data, reason) {
   Services.prefs.setIntPref("bootstraptest.active_version", 0);
   Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
 }
 
 function uninstall(data, reason) {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm
@@ -0,0 +1,3 @@
+var EXPORTED_SYMBOLS = ["VERSION"];
+
+var VERSION = 1;
--- a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js
@@ -1,18 +1,22 @@
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 function install(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.installed_version", 2);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function startup(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.active_version", 2);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function shutdown(data, reason) {
   Services.prefs.setIntPref("bootstraptest.active_version", 0);
   Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
 }
 
 function uninstall(data, reason) {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm
@@ -0,0 +1,3 @@
+var EXPORTED_SYMBOLS = ["VERSION"];
+
+var VERSION = 2;
--- a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js
@@ -1,18 +1,22 @@
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 function install(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.installed_version", 3);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function startup(data, reason) {
-  Services.prefs.setIntPref("bootstraptest.active_version", 3);
+  Components.utils.import(data.resourceURI.spec + "version.jsm");
+  Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
   Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+  Components.utils.unload(data.resourceURI.spec + "version.jsm");
 }
 
 function shutdown(data, reason) {
   Services.prefs.setIntPref("bootstraptest.active_version", 0);
   Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
 }
 
 function uninstall(data, reason) {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm
@@ -0,0 +1,3 @@
+var EXPORTED_SYMBOLS = ["VERSION"];
+
+var VERSION = 3;
--- a/toolkit/mozapps/extensions/test/xpcshell-unpack/head_unpack.js
+++ b/toolkit/mozapps/extensions/test/xpcshell-unpack/head_unpack.js
@@ -1,1 +1,2 @@
 Services.prefs.setBoolPref("extensions.alwaysUnpack", true);
+TEST_UNPACKED = true;
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -14,16 +14,18 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
 var gInternalManager = null;
 var gAppInfo = null;
 var gAddonsList;
 
+var TEST_UNPACKED = false;
+
 function createAppInfo(id, name, version, platformVersion) {
   gAppInfo = {
     // nsIXULAppInfo
     vendor: "Mozilla",
     name: name,
     ID: id,
     version: version,
     appBuildID: "2007010101",
@@ -156,17 +158,17 @@ function do_get_addon_root_uri(aProfileD
     return "jar:" + Services.io.newFileURI(path).spec + "!/";
   }
   else {
     return Services.io.newFileURI(path).spec;
   }
 }
 
 function do_get_expected_addon_name(aId) {
-  if (Services.prefs.getBoolPref("extensions.alwaysUnpack"))
+  if (TEST_UNPACKED)
     return aId;
   return aId + ".xpi";
 }
 
 /**
  * Check that an array of actual add-ons is the same as an array of
  * expected add-ons.
  *
@@ -564,17 +566,17 @@ function writeInstallRDFToDir(aData, aDi
  *          An optional dummy file to create in the extension
  * @return  A file pointing to where the extension was installed
  */
 function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
   var id = aId ? aId : aData.id
 
   var dir = aDir.clone();
 
-  if (Services.prefs.getBoolPref("extensions.alwaysUnpack")) {
+  if (TEST_UNPACKED) {
     dir.append(id);
     writeInstallRDFToDir(aData, dir, aExtraFile);
     return dir;
   }
 
   if (!dir.exists())
     dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
   dir.append(id + ".xpi");
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
@@ -68,16 +68,56 @@ function getShutdownReason() {
 function getInstallReason() {
   return Services.prefs.getIntPref("bootstraptest.install_reason");
 }
 
 function getUninstallReason() {
   return Services.prefs.getIntPref("bootstraptest.uninstall_reason");
 }
 
+function manuallyInstall(aXPIFile, aInstallLocation, aID) {
+  if (TEST_UNPACKED) {
+    let dir = aInstallLocation.clone();
+    dir.append(aID);
+    dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
+              createInstance(AM_Ci.nsIZipReader);
+    zip.open(aXPIFile);
+    let entries = zip.findEntries(null);
+    while (entries.hasMore()) {
+      let entry = entries.getNext();
+      let target = dir.clone();
+      entry.split("/").forEach(function(aPart) {
+        target.append(aPart);
+      });
+      zip.extract(entry, target);
+    }
+    zip.close();
+
+    return dir;
+  }
+  else {
+    let target = aInstallLocation.clone();
+    target.append(aID + ".xpi");
+    aXPIFile.copyTo(target.parent, target.leafName);
+    return target;
+  }
+}
+
+function manuallyUninstall(aInstallLocation, aID) {
+  let file = getFileForAddon(aInstallLocation, aID);
+
+  // In reality because the app is restarted a flush isn't necessary for XPIs
+  // removed outside the app, but for testing we must flush manually.
+  if (file.isFile())
+    Services.obs.notifyObservers(file, "flush-cache-entry", null);
+
+  file.remove(true);
+}
+
 function run_test() {
   do_test_pending();
 
   resetPrefs();
 
   // Create and configure the HTTP server.
   testserver = new nsHttpServer();
   testserver.registerDirectory("/addons/", do_get_file("addons"));
@@ -364,28 +404,18 @@ function check_test_7() {
   });
 }
 
 // Test that a bootstrapped extension dropped into the profile loads properly
 // on startup and doesn't cause an EM restart
 function run_test_8() {
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_1"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+                  "bootstrap1@tests.mozilla.org");
 
   startupManager(false);
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     do_check_neq(b1, null);
     do_check_eq(b1.version, "1.0");
     do_check_false(b1.appDisabled);
     do_check_false(b1.userDisabled);
@@ -398,19 +428,18 @@ function run_test_8() {
     run_test_9();
   });
 }
 
 // Test that items detected as removed during startup get removed properly
 function run_test_9() {
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.remove(true);
+  manuallyUninstall(profileDir, "bootstrap1@tests.mozilla.org");
+
   startupManager(false);
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     do_check_eq(b1, null);
     do_check_not_in_crash_annotation("bootstrap1@tests.mozilla.org", "1.0");
 
     run_test_10();
   });
@@ -543,28 +572,18 @@ function check_test_11() {
   run_test_12();
 }
 
 // Tests that bootstrapped extensions are correctly loaded even if the app is
 // upgraded at the same time
 function run_test_12() {
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_1"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+                  "bootstrap1@tests.mozilla.org");
 
   startupManager(true);
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     do_check_neq(b1, null);
     do_check_eq(b1.version, "1.0");
     do_check_false(b1.appDisabled);
     do_check_false(b1.userDisabled);
@@ -649,28 +668,18 @@ function check_test_13() {
   });
 }
 
 // Tests that a bootstrapped extension with an invalid target application entry
 // does not get loaded when detected during startup
 function run_test_14() {
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_3"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  manuallyInstall(do_get_addon("test_bootstrap1_3"), profileDir,
+                  "bootstrap1@tests.mozilla.org");
 
   startupManager(false);
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     do_check_neq(b1, null);
     do_check_eq(b1.version, "3.0");
     do_check_true(b1.appDisabled);
     do_check_false(b1.userDisabled);
@@ -803,28 +812,18 @@ function run_test_16() {
     });
   });
 }
 
 // Check that a bootstrapped extension in a non-profile location is loaded
 function run_test_17() {
   shutdownManager();
 
-  let dir = userExtDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_1"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  manuallyInstall(do_get_addon("test_bootstrap1_1"), userExtDir,
+                  "bootstrap1@tests.mozilla.org");
 
   resetPrefs();
   startupManager();
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     // Should have installed and started
     do_check_eq(getInstalledVersion(), 1);
     do_check_eq(getActiveVersion(), 1);
@@ -897,28 +896,18 @@ function check_test_19() {
 }
 
 // Check that a new profile extension detected at startup replaces the non-profile
 // one
 function run_test_20() {
   resetPrefs();
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_2"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir,
+                  "bootstrap1@tests.mozilla.org");
 
   startupManager();
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     // Should have installed and started
     do_check_eq(getInstalledVersion(), 2);
     do_check_eq(getActiveVersion(), 2);
     do_check_neq(b1, null);
@@ -934,19 +923,17 @@ function run_test_20() {
   });
 }
 
 // Check that a detected removal reveals the non-profile one
 function run_test_21() {
   resetPrefs();
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.remove(true);
+  manuallyUninstall(profileDir, "bootstrap1@tests.mozilla.org");
 
   startupManager();
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     // Should have installed and started
     do_check_eq(getInstalledVersion(), 1);
     do_check_eq(getActiveVersion(), 1);
     do_check_neq(b1, null);
@@ -959,71 +946,50 @@ function run_test_21() {
     // uninstall it properly
     do_check_eq(getUninstallReason(), -1);
 
     // TODO this reason should probably be ADDON_DOWNGRADE (bug 607818)
     do_check_eq(getInstallReason(), ADDON_INSTALL);
 
     do_check_eq(getStartupReason(), APP_STARTUP);
 
-    dir = userExtDir.clone();
-    dir.append("bootstrap1@tests.mozilla.org");
-    dir.remove(true);
+    manuallyUninstall(userExtDir, "bootstrap1@tests.mozilla.org");
 
     restartManager();
 
     run_test_22();
   });
 }
 
 // Check that an upgrade from the filesystem is detected and applied correctly
 function run_test_22() {
   shutdownManager();
 
-  let dir = profileDir.clone();
-  dir.append("bootstrap1@tests.mozilla.org");
-  dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-  let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-            createInstance(AM_Ci.nsIZipReader);
-  zip.open(do_get_addon("test_bootstrap1_1"));
-  dir.append("install.rdf");
-  zip.extract("install.rdf", dir);
-  dir = dir.parent;
-  dir.append("bootstrap.js");
-  zip.extract("bootstrap.js", dir);
-  zip.close();
+  let file = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+                             "bootstrap1@tests.mozilla.org");
 
   // Make it look old so changes are detected
-  setExtensionModifiedTime(dir.parent, dir.parent.lastModifiedTime - 5000);
+  setExtensionModifiedTime(file, file.lastModifiedTime - 5000);
 
   startupManager();
 
   AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
     // Should have installed and started
     do_check_eq(getInstalledVersion(), 1);
     do_check_eq(getActiveVersion(), 1);
     do_check_neq(b1, null);
     do_check_eq(b1.version, "1.0");
     do_check_true(b1.isActive);
 
     resetPrefs();
     shutdownManager();
 
-    dir = dir.parent;
-    dir.remove(true);
-    dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0755);
-    let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
-              createInstance(AM_Ci.nsIZipReader);
-    zip.open(do_get_addon("test_bootstrap1_2"));
-    dir.append("install.rdf");
-    zip.extract("install.rdf", dir);
-    dir = dir.parent;
-    dir.append("bootstrap.js");
-    zip.extract("bootstrap.js", dir);
-    zip.close();
+    manuallyUninstall(profileDir, "bootstrap1@tests.mozilla.org");
+    manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir,
+                    "bootstrap1@tests.mozilla.org");
 
     startupManager();
 
     AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
       // Should have installed and started
       do_check_eq(getInstalledVersion(), 2);
       do_check_eq(getActiveVersion(), 2);
       do_check_neq(b1, null);