Bug 594058 - invalidate cache by statting contents of extensions directory r=dtownsend r=bz a=bsmedberg
authorBenedict Hsieh <bhsieh@mozilla.com>
Tue, 14 Sep 2010 17:39:07 -0700
changeset 55169 5ede38e20e254404461e0660eeb68a549c849ae5
parent 55168 4fae29935080d5ac5d9fdc19f8f83fd0d7c18895
child 55170 c11770ffab99bd69e4dde54600152f50692e0796
push idunknown
push userunknown
push dateunknown
reviewersdtownsend, bz, bsmedberg
bugs594058
milestone2.0b8pre
Bug 594058 - invalidate cache by statting contents of extensions directory r=dtownsend r=bz a=bsmedberg
content/xul/document/src/nsXULPrototypeCache.cpp
startupcache/StartupCache.cpp
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/test/addons/test_bug594058/directory/file1
toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js
toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
toolkit/mozapps/extensions/test/xpcshell/test_startup.js
toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
--- a/content/xul/document/src/nsXULPrototypeCache.cpp
+++ b/content/xul/document/src/nsXULPrototypeCache.cpp
@@ -178,16 +178,19 @@ nsXULPrototypeCache::Observe(nsISupports
                              const PRUnichar *aData)
 {
     if (!strcmp(aTopic, "chrome-flush-skin-caches")) {
         FlushSkinFiles();
     }
     else if (!strcmp(aTopic, "chrome-flush-caches")) {
         Flush();
     }
+    else if (!strcmp(aTopic, "startupcache-invalidate")) {
+        AbortFastLoads();
+    }
     else {
         NS_WARNING("Unexpected observer topic.");
     }
     return NS_OK;
 }
 
 nsXULPrototypeDocument*
 nsXULPrototypeCache::GetPrototype(nsIURI* aURI)
--- a/startupcache/StartupCache.cpp
+++ b/startupcache/StartupCache.cpp
@@ -170,16 +170,19 @@ StartupCache::Init()
     NS_WARNING("Could not get observerService.");
     return NS_ERROR_UNEXPECTED;
   }
   
   mListener = new StartupCacheListener();  
   rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID,
                                      PR_FALSE);
   NS_ENSURE_SUCCESS(rv, rv);
+  rv = mObserverService->AddObserver(mListener, "startupcache-invalidate",
+                                     PR_FALSE);
+  NS_ENSURE_SUCCESS(rv, rv);
   
   rv = LoadArchive();
   
   // Sometimes we don't have a cache yet, that's ok.
   // If it's corrupted, just remove it and start over.
   if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
     NS_WARNING("Failed to load startupcache file correctly, removing!");
     InvalidateCache();
@@ -415,16 +418,20 @@ StartupCache::WriteTimeout(nsITimer *aTi
 NS_IMPL_THREADSAFE_ISUPPORTS1(StartupCacheListener, nsIObserver)
 
 nsresult
 StartupCacheListener::Observe(nsISupports *subject, const char* topic, const PRUnichar* data)
 {
   nsresult rv = NS_OK;
   if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
     StartupCache::gShutdownInitiated = PR_TRUE;
+  } else if (strcmp(topic, "startupcache-invalidate") == 0) {
+    StartupCache* sc = StartupCache::GetSingleton();
+    if (sc)
+      sc->InvalidateCache();
   }
   return rv;
 } 
 
 nsresult
 StartupCache::GetDebugObjectOutputStream(nsIObjectOutputStream* aStream,
                                          nsIObjectOutputStream** aOutStream) 
 {
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -884,16 +884,44 @@ function resultRows(aStatement) {
       yield aStatement.row;
   }
   finally {
     aStatement.reset();
   }
 }
 
 /**
+  * Returns the timestamp of the most recently modified file in a directory,
+  * or simply the file's own timestamp if it is not a directory.
+  * 
+  * @param aFile
+  * A non-null nsIFile object
+  * @return Epoch time, as described above. 0 for an empty directory.
+  */
+function recursiveLastModifiedTime(aFile) {
+  if (aFile.isFile())
+    return aFile.lastModifiedTime;
+
+  if (aFile.isDirectory()) {
+    let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+    let entry, time;
+    let maxTime = aFile.lastModifiedTime;
+    while (entry = entries.nextFile) {
+      time = recursiveLastModifiedTime(entry);
+      maxTime = Math.max(time, maxTime);
+    }
+    entries.close();
+    return maxTime;
+  }
+  
+  // If the file is something else, just ignore it.
+  return 0;
+}
+
+/**
  * A helpful wrapper around the prefs service that allows for default values
  * when requested values aren't set.
  */
 var Prefs = {
   /**
    * Gets a preference from the default branch ignoring user-set values.
    *
    * @param  aName
@@ -1291,44 +1319,45 @@ var XPIProvider = {
     for (let id in this.bootstrappedAddons)
       data += (data ? "," : "") + id + ":" + this.bootstrappedAddons[id].version;
 
     try {
       Services.appinfo.annotateCrashReport("Add-ons", data);
     }
     catch (e) { }
   },
-
+  
   /**
-   * Gets the add-on states for an install location.
+   * Gets the add-on states for an install location. 
+   * This function may be expensive because of the recursiveLastModifiedTime call.
    *
    * @param  location
    *         The install location to retrieve the add-on states for
    * @return a dictionary mapping add-on IDs to objects with a descriptor
    *         property which contains the add-ons dir/file descriptor and an
    *         mtime property which contains the add-on's last modified time as
    *         the number of milliseconds since the epoch.
    */
   getAddonStates: function XPI_getAddonStates(aLocation) {
     let addonStates = {};
     aLocation.addonLocations.forEach(function(file) {
       let id = aLocation.getIDForLocation(file);
       addonStates[id] = {
         descriptor: file.persistentDescriptor,
-        mtime: file.lastModifiedTime
+        mtime: recursiveLastModifiedTime(file)
       };
     });
 
     return addonStates;
   },
 
   /**
    * Gets an array of install location states which uniquely describes all
    * installed add-ons with the add-on's InstallLocation name and last modified
-   * time.
+   * time. This function may be expensive because of the getAddonStates() call.
    *
    * @return an array of add-on states for each install location. Each state
    *         is an object with a name property holding the location's name and
    *         an addons property holding the add-on states for the location
    */
   getInstallLocationStates: function XPI_getInstallLocationStates() {
     let states = [];
     this.installLocations.forEach(function(aLocation) {
@@ -1456,17 +1485,18 @@ var XPIProvider = {
       }
     });
     return changed;
   },
 
   /**
    * Compares the add-ons that are currently installed to those that were
    * known to be installed when the application last ran and applies any
-   * changes found to the database.
+   * changes found to the database. Also sends "startupcache-invalidate" signal to 
+   * observerservice if it detects that data may have changed.
    *
    * @param  aState
    *         The array of current install location states
    * @param  aManifests
    *         A dictionary of cached AddonInstalls for add-ons that have been
    *         installed
    * @param  aUpdateCompatibility
    *         true to update add-ons appDisabled property when the application
@@ -1864,16 +1894,22 @@ var XPIProvider = {
       addons.forEach(function(aOldAddon) {
         changed = removeMetadata(aLocation, aOldAddon) || changed;
       }, this);
     }, this);
 
     // Cache the new install location states
     cache = JSON.stringify(this.getInstallLocationStates());
     Services.prefs.setCharPref(PREF_INSTALL_CACHE, cache);
+    
+    if (changed) {
+      // 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);
+    }
     return changed;
   },
 
   /**
    * Imports the xpinstall permissions from preferences into the permissions
    * manager for the user to change later.
    */
   importPermissions: function XPI_importPermissions() {
@@ -4970,17 +5006,17 @@ AddonInstall.prototype = {
           }
         }
 
         // Install the new add-on into its final location
         let file = this.installLocation.installAddon(this.addon.id, stagedAddon);
 
         // Update the metadata in the database
         this.addon._installLocation = this.installLocation;
-        this.addon.updateDate = file.lastModifiedTime;
+        this.addon.updateDate = recursiveLastModifiedTime(file);
         this.addon.visible = true;
         if (isUpgrade) {
           XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
                                           file.persistentDescriptor);
         }
         else {
           this.addon.installDate = this.addon.updateDate;
           XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>bug594058@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+    <em:name>bug 594058</em:name>
+    <em:description>stat-based invalidation</em:description>
+    <em:unpack>true</em:unpack>
+  </Description>      
+</RDF>
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -567,16 +567,33 @@ function writeInstallRDFForExtension(aDa
                       stream, false);
   if (aExtraFile)
     zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
                         stream, false);
   zipW.close();
   return dir;
 }
 
+/**
+ * Sets the last modified time of the extension, usually to trigger an update
+ * of its metadata. If the extension is unpacked, this function assumes that
+ * the extension contains only the install.rdf file.
+ *
+ * @param aExt   a file pointing to either the packed extension or its unpacked directory.
+ * @param aTime  the time to which we set the lastModifiedTime of the extension
+ */
+function setExtensionModifiedTime(aExt, aTime) {
+  aExt.lastModifiedTime = aTime;
+  if (aExt.isDirectory()) {
+    aExt = aExt.clone();
+    aExt.append("install.rdf");
+    aExt.lastModifiedTime = aTime;
+  }
+}
+
 function registerDirectory(aKey, aDir) {
   var dirProvider = {
     getFile: function(aProp, aPersistent) {
       aPersistent.value = true;
       if (aProp == aKey)
         return aDir.clone();
       return null;
     },
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
@@ -19,17 +19,17 @@ function run_test() {
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }]
   }, profileDir);
   // Attempt to make this look like it was added some time in the past so
   // the update makes the last modified time change.
-  dest.lastModifiedTime -= 5000;
+  setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
 
   startupManager();
 
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a) {
     do_check_neq(a, null);
     do_check_eq(a.version, "1.0");
     do_check_false(a.userDisabled);
     do_check_true(a.appDisabled);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests is modifying a file in an unpacked extension
+// causes cache invalidation.
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+const Ci = Components.interfaces;
+const extDir = gProfD.clone();
+extDir.append("extensions");
+
+/**
+ * Start the test by installing extensions.
+ */
+function run_test() {
+  do_test_pending();
+  let cachePurged = false;
+
+  let obs = AM_Cc["@mozilla.org/observer-service;1"].
+    getService(AM_Ci.nsIObserverService);
+  obs.addObserver({
+    observe: function(aSubject, aTopic, aData) {
+      cachePurged = true;
+    }
+  }, "startupcache-invalidate", false);
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+  startupManager();
+
+  installAllFiles([do_get_addon("test_bug594058")], function() {
+    restartManager();
+    do_check_true(cachePurged);
+    cachePurged = false;
+
+    // Now, make it look like we've updated the file. First, start the EM
+    // so it records the bogus old time, then update the file and restart.
+    let extFile = extDir.clone();
+    let pastTime = extFile.lastModifiedTime - 5000;
+    extFile.append("bug594058@tests.mozilla.org");
+    setExtensionModifiedTime(extFile, pastTime);
+    let otherFile = extFile.clone();
+    otherFile.append("directory");
+    otherFile.lastModifiedTime = pastTime;
+    otherFile.append("file1");
+    otherFile.lastModifiedTime = pastTime;
+
+    restartManager();
+    cachePurged = false;
+
+    otherFile.lastModifiedTime = pastTime + 5000;
+    restartManager();
+    do_check_true(cachePurged);
+    cachePurged = false;
+
+    restartManager();
+    do_check_true(!cachePurged);
+
+    do_test_finished();
+  });  
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
@@ -215,17 +215,17 @@ function run_test_5() {
   restartManager();
 
   AddonManager.getAddonByID(addon1.id, function(a1) {
     do_check_neq(a1, null);
     do_check_eq(a1.version, "1.0");
 
     var dest = writeInstallRDFForExtension(addon2, sourceDir, addon1.id);
     // Make sure the modification time changes enough to be detected.
-    dest.lastModifiedTime -= 5000;
+    setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
 
     restartManager();
 
     AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                  "addon2@tests.mozilla.org"], function([a1, a2]) {
       do_check_eq(a1, null);
       do_check_eq(a2, null);
 
@@ -305,17 +305,17 @@ function run_test_8() {
   restartManager();
 
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
     do_check_neq(a1, null);
     do_check_eq(a1.version, "1.0");
 
     var dest = writeInstallRDFForExtension(addon1_2, sourceDir);
     // Make sure the modification time changes enough to be detected.
-    dest.lastModifiedTime -= 5000;
+    setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
 
     restartManager();
 
     AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
       do_check_neq(a1, null);
       do_check_eq(a1.version, "2.0");
 
       a1.uninstall();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
@@ -115,17 +115,18 @@ function end_test() {
 }
 
 // Try to install all the items into the profile
 function run_test_1() {
   writeInstallRDFForExtension(addon1, profileDir);
   var dest = writeInstallRDFForExtension(addon2, profileDir);
   // Attempt to make this look like it was added some time in the past so
   // the change in run_test_2 makes the last modified time change.
-  dest.lastModifiedTime -= 5000;
+  setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
+
   writeInstallRDFForExtension(addon3, profileDir);
   writeInstallRDFForExtension(addon4, profileDir);
   writeInstallRDFForExtension(addon5, profileDir);
 
   restartManager();
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
--- a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
@@ -70,17 +70,17 @@ function run_test() {
     version: "1.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 4",
   }, globalDir);
-  dest.lastModifiedTime = gInstallTime;
+  setExtensionModifiedTime(dest, gInstallTime);
 
   do_test_pending();
 
   run_test_1();
 }
 
 function end_test() {
   if (!gGlobalExisted) {
@@ -128,17 +128,17 @@ function run_test_2() {
     version: "2.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "2",
       maxVersion: "2"
     }],
     name: "Test Addon 4",
   }, globalDir);
-  dest.lastModifiedTime = gInstallTime;
+  setExtensionModifiedTime(dest, gInstallTime);
 
   restartManager("2");
   AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
                                "addon2@tests.mozilla.org",
                                "addon3@tests.mozilla.org",
                                "addon4@tests.mozilla.org"],
                                function([a1, a2, a3, a4]) {
 
@@ -167,17 +167,17 @@ function run_test_3() {
     version: "3.0",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "3",
       maxVersion: "3"
     }],
     name: "Test Addon 4",
   }, globalDir);
-  dest.lastModifiedTime = gInstallTime;
+  setExtensionModifiedTime(dest, gInstallTime);
 
   // Simulates a simple Build ID change, the platform deletes extensions.ini
   // whenever the application is changed.
   var file = gProfD.clone();
   file.append("extensions.ini");
   file.remove(true);
   restartManager();