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 id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdtownsend, bz, bsmedberg
bugs594058
milestone2.0b8pre
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 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();