Bug 724513 - Part 1 - Add StartupCache method for disregarding disk file. r=mwu
authorGraeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
Thu, 11 Oct 2012 09:17:15 +0100
changeset 120085 eb5c570b8a0bf064a26b46cb6e8274b52b75eaa0
parent 120084 e692328bf6a33db14810fa19c51d7ba105db386a
child 120086 6ec71de7ba2d66438d9ee4ee03628ada27999c3c
push idunknown
push userunknown
push dateunknown
reviewersmwu
bugs724513
milestone19.0a1
Bug 724513 - Part 1 - Add StartupCache method for disregarding disk file. r=mwu
startupcache/StartupCache.cpp
startupcache/StartupCache.h
startupcache/nsIStartupCache.idl
startupcache/test/TestStartupCache.cpp
--- a/startupcache/StartupCache.cpp
+++ b/startupcache/StartupCache.cpp
@@ -117,16 +117,17 @@ StartupCache::InitSingleton()
     delete StartupCache::gStartupCache;
     StartupCache::gStartupCache = nullptr;
   }
   return rv;
 }
 
 StartupCache* StartupCache::gStartupCache;
 bool StartupCache::gShutdownInitiated;
+bool StartupCache::gIgnoreDiskCache;
 enum StartupCache::TelemetrifyAge StartupCache::gPostFlushAgeAction = StartupCache::IGNORE_AGE;
 
 StartupCache::StartupCache() 
   : mArchive(NULL), mStartupWriteInitiated(false), mWriteThread(NULL),
     mMappingMemoryReporter(nullptr), mDataMemoryReporter(nullptr) { }
 
 StartupCache::~StartupCache() 
 {
@@ -198,22 +199,22 @@ StartupCache::Init()
   
   mListener = new StartupCacheListener();  
   rv = mObserverService->AddObserver(mListener, NS_XPCOM_SHUTDOWN_OBSERVER_ID,
                                      false);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mObserverService->AddObserver(mListener, "startupcache-invalidate",
                                      false);
   NS_ENSURE_SUCCESS(rv, rv);
-  
+
   rv = LoadArchive(RECORD_AGE);
   
   // 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) {
+  if (gIgnoreDiskCache || (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND)) {
     NS_WARNING("Failed to load startupcache file correctly, removing!");
     InvalidateCache();
   }
 
   mMappingMemoryReporter = new NS_MEMORY_REPORTER_NAME(StartupCacheMapping);
   mDataMemoryReporter    = new NS_MEMORY_REPORTER_NAME(StartupCacheData);
   (void)::NS_RegisterMemoryReporter(mMappingMemoryReporter);
   (void)::NS_RegisterMemoryReporter(mDataMemoryReporter);
@@ -222,16 +223,19 @@ StartupCache::Init()
 }
 
 /** 
  * LoadArchive can be called from the main thread or while reloading cache on write thread.
  */
 nsresult
 StartupCache::LoadArchive(enum TelemetrifyAge flag)
 {
+  if (gIgnoreDiskCache)
+    return NS_ERROR_FAILURE;
+
   bool exists;
   mArchive = NULL;
   nsresult rv = mFile->Exists(&exists);
   if (NS_FAILED(rv) || !exists)
     return NS_ERROR_FILE_NOT_FOUND;
   
   mArchive = new nsZipArchive();
   rv = mArchive->OpenArchive(mFile);
@@ -453,32 +457,49 @@ StartupCache::WriteToDisk()
   holder.time = now;
 
   mTable.Enumerate(CacheCloseHelper, &holder);
 
   // Close the archive so Windows doesn't choke.
   mArchive = NULL;
   zipW->Close();
 
+  // We succesfully wrote the archive to disk; mark the disk file as trusted
+  gIgnoreDiskCache = false;
+
   // Our reader's view of the archive is outdated now, reload it.
   LoadArchive(gPostFlushAgeAction);
   
   return;
 }
 
 void
 StartupCache::InvalidateCache() 
 {
   WaitOnWriteThread();
   mTable.Clear();
   mArchive = NULL;
-  mFile->Remove(false);
+  nsresult rv = mFile->Remove(false);
+  if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST &&
+      rv != NS_ERROR_FILE_NOT_FOUND) {
+    gIgnoreDiskCache = true;
+    return;
+  }
+  gIgnoreDiskCache = false;
   LoadArchive(gPostFlushAgeAction);
 }
 
+void
+StartupCache::IgnoreDiskCache()
+{
+  gIgnoreDiskCache = true;
+  if (gStartupCache)
+    gStartupCache->InvalidateCache();
+}
+
 /*
  * WaitOnWriteThread() is called from a main thread to wait for the worker
  * thread to finish. However since the same code is used in the worker thread and
  * main thread, the worker thread can also call WaitOnWriteThread() which is a no-op.
  */
 void
 StartupCache::WaitOnWriteThread()
 {
@@ -708,16 +729,23 @@ StartupCacheWrapper::InvalidateCache()
   StartupCache* sc = StartupCache::GetSingleton();
   if (!sc) {
     return NS_ERROR_NOT_INITIALIZED;
   }
   sc->InvalidateCache();
   return NS_OK;
 }
 
+nsresult
+StartupCacheWrapper::IgnoreDiskCache()
+{
+  StartupCache::IgnoreDiskCache();
+  return NS_OK;
+}
+
 nsresult 
 StartupCacheWrapper::GetDebugObjectOutputStream(nsIObjectOutputStream* stream,
                                                 nsIObjectOutputStream** outStream) 
 {
   StartupCache* sc = StartupCache::GetSingleton();
   if (!sc) {
     return NS_ERROR_NOT_INITIALIZED;
   }
--- a/startupcache/StartupCache.h
+++ b/startupcache/StartupCache.h
@@ -39,16 +39,22 @@
  * stored in the cache (if any), and PutBuffer() inserts a buffer into the cache.
  * GetBuffer returns a new buffer, and the caller must take ownership of it.
  * PutBuffer will assert if the client attempts to insert a buffer with the same name as
  * an existing entry. The cache makes a copy of the passed-in buffer, so client
  * retains ownership.
  *
  * InvalidateCache() may be called if a client suspects data corruption 
  * or wishes to invalidate for any other reason. This will remove all existing cache data.
+ * Additionally, the static method IgnoreDiskCache() can be called if it is
+ * believed that the on-disk cache file is itself corrupt. This call implicitly
+ * calls InvalidateCache (if the singleton has been initialized) to ensure any
+ * data already read from disk is discarded. The cache will not load data from
+ * the disk file until a successful write occurs.
+ *
  * Finally, getDebugObjectOutputStream() allows debug code to wrap an objectstream
  * with a debug objectstream, to check for multiply-referenced objects. These will
  * generally fail to deserialize correctly, unless they are stateless singletons or the 
  * client maintains their own object data map for deserialization.
  *
  * Writes before the final-ui-startup notification are placed in an intermediate
  * cache in memory, then written out to disk at a later time, to get writes off the
  * startup path. In any case, clients should not rely on being able to GetBuffer()
@@ -109,16 +115,19 @@ public:
   nsresult GetBuffer(const char* id, char** outbuf, uint32_t* length);
 
   // Stores a buffer. Caller keeps ownership, we make a copy.
   nsresult PutBuffer(const char* id, const char* inbuf, uint32_t length);
 
   // Removes the cache file.
   void InvalidateCache();
 
+  // Signal that data should not be loaded from the cache file
+  static void IgnoreDiskCache();
+
   // In DEBUG builds, returns a stream that will attempt to check for
   // and disallow multiple writes of the same object.
   nsresult GetDebugObjectOutputStream(nsIObjectOutputStream* aStream,
                                       nsIObjectOutputStream** outStream);
 
   nsresult RecordAgesAlways();
 
   static StartupCache* GetSingleton();
@@ -162,16 +171,17 @@ private:
   nsCOMPtr<nsIObserverService> mObserverService;
   nsRefPtr<StartupCacheListener> mListener;
   nsCOMPtr<nsITimer> mTimer;
 
   bool mStartupWriteInitiated;
 
   static StartupCache *gStartupCache;
   static bool gShutdownInitiated;
+  static bool gIgnoreDiskCache;
   PRThread *mWriteThread;
 #ifdef DEBUG
   nsTHashtable<nsISupportsHashKey> mWriteObjectMap;
 #endif
 
   nsIMemoryReporter* mMappingMemoryReporter;
   nsIMemoryReporter* mDataMemoryReporter;
 };
--- a/startupcache/nsIStartupCache.idl
+++ b/startupcache/nsIStartupCache.idl
@@ -4,31 +4,33 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsIInputStream.idl"
 #include "nsISupports.idl"
 #include "nsIObserver.idl"
 #include "nsIObjectOutputStream.idl"
 
-[uuid(c1b3796b-33af-4ff0-b83d-8eb0ca2c080f)]
+[uuid(25957820-90a1-428c-8739-b0845d3cc534)]
 interface nsIStartupCache : nsISupports
 {
 
   /** This interface is provided for testing purposes only, basically
    *  just to solve link vagaries. See docs in StartupCache.h
    *  GetBuffer, PutBuffer, and InvalidateCache act as described 
    *  in that file. */
 
   uint32_t getBuffer(in string aID, out charPtr aBuffer);
   void putBuffer(in string aID, in string aBuffer, 
                             in uint32_t aLength);
  
   void invalidateCache();
   
+  void ignoreDiskCache();
+
   /** In debug builds, wraps this object output stream with a stream that will 
    *  detect and prevent the write of a multiply-referenced non-singleton object 
    *  during serialization. In non-debug, returns an add-ref'd pointer to
    *  original stream, unwrapped. */
   nsIObjectOutputStream getDebugObjectOutputStream(in nsIObjectOutputStream aStream);
 
   /* Allows clients to check whether the one-time writeout after startup 
    * has finished yet, and also to set this variable as needed (so test
--- a/startupcache/test/TestStartupCache.cpp
+++ b/startupcache/test/TestStartupCache.cpp
@@ -17,16 +17,17 @@
 #include "nsIObjectInputStream.h"
 #include "nsIObjectOutputStream.h"
 #include "nsIURI.h"
 #include "nsStringAPI.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
 #include "nsITelemetry.h"
 #include "jsapi.h"
+#include "prio.h"
 
 namespace mozilla {
 namespace scache {
 
 NS_IMPORT nsresult
 NewObjectInputStreamFromBuffer(char* buffer, uint32_t len, 
                                nsIObjectInputStream** stream);
 
@@ -51,17 +52,17 @@ if (0 != strcmp(str1, str2)) {          
   return NS_ERROR_FAILURE;                         \
 }                                                  \
 passed("passed " testname);                        \
 PR_END_MACRO
 
 nsresult
 WaitForStartupTimer() {
   nsresult rv;
-  nsCOMPtr<nsIStartupCache> sc 
+  nsCOMPtr<nsIStartupCache> sc
     = do_GetService("@mozilla.org/startupcache/cache;1");
   PR_Sleep(10 * PR_TicksPerSecond());
   
   bool complete;
   while (true) {
     
     NS_ProcessPendingEvents(nullptr);
     rv = sc->StartupWriteComplete(&complete);
@@ -70,17 +71,17 @@ WaitForStartupTimer() {
     PR_Sleep(1 * PR_TicksPerSecond());
   }
   return rv;
 }
 
 nsresult
 TestStartupWriteRead() {
   nsresult rv;
-  nsCOMPtr<nsIStartupCache> sc 
+  nsCOMPtr<nsIStartupCache> sc
     = do_GetService("@mozilla.org/startupcache/cache;1", &rv);
   if (!sc) {
     fail("didn't get a pointer...");
     return NS_ERROR_FAILURE;
   } else {
     passed("got a pointer?");
   }
   sc->InvalidateCache();
@@ -113,17 +114,17 @@ TestStartupWriteRead() {
 
 nsresult
 TestWriteInvalidateRead() {
   nsresult rv;
   const char* buf = "BeardBook competitive analysis";
   const char* id = "id";
   char* outbuf = NULL;
   uint32_t len;
-  nsCOMPtr<nsIStartupCache> sc 
+  nsCOMPtr<nsIStartupCache> sc
     = do_GetService("@mozilla.org/startupcache/cache;1", &rv);
   sc->InvalidateCache();
 
   rv = sc->PutBuffer(id, buf, strlen(buf) + 1);
   NS_ENSURE_SUCCESS(rv, rv);
 
   sc->InvalidateCache();
 
@@ -243,19 +244,119 @@ TestWriteObject() {
     return rv;
   }
   
   passed("write object");
   return NS_OK;
 }
 
 nsresult
+LockCacheFile(bool protect, nsIFile* profileDir) {
+  NS_ENSURE_ARG(profileDir);
+
+  nsCOMPtr<nsIFile> startupCache;
+  profileDir->Clone(getter_AddRefs(startupCache));
+  NS_ENSURE_STATE(startupCache);
+  startupCache->AppendNative(NS_LITERAL_CSTRING("startupCache"));
+
+  nsresult rv;
+#ifndef XP_WIN
+  static uint32_t oldPermissions;
+#else
+  static PRFileDesc* fd = nullptr;
+#endif
+
+  // To prevent deletion of the startupcache file, we change the containing
+  // directory's permissions on Linux/Mac, and hold the file open on Windows
+  if (protect) {
+#ifndef XP_WIN
+    rv = startupCache->GetPermissions(&oldPermissions);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = startupCache->SetPermissions(0555);
+    NS_ENSURE_SUCCESS(rv, rv);
+#else
+    // Filename logic from StartupCache.cpp
+    #ifdef IS_BIG_ENDIAN
+    #define SC_ENDIAN "big"
+    #else
+    #define SC_ENDIAN "little"
+    #endif
+
+    #if PR_BYTES_PER_WORD == 4
+    #define SC_WORDSIZE "4"
+    #else
+    #define SC_WORDSIZE "8"
+    #endif
+    char sStartupCacheName[] = "startupCache." SC_WORDSIZE "." SC_ENDIAN;
+    startupCache->AppendNative(NS_LITERAL_CSTRING(sStartupCacheName));
+
+    rv = startupCache->OpenNSPRFileDesc(PR_RDONLY, 0, &fd);
+    NS_ENSURE_SUCCESS(rv, rv);
+#endif
+  } else {
+#ifndef XP_WIN
+    rv = startupCache->SetPermissions(oldPermissions);
+    NS_ENSURE_SUCCESS(rv, rv);
+#else
+   PR_Close(fd);
+#endif
+  }
+
+  return NS_OK;
+}
+
+nsresult
+TestIgnoreDiskCache(nsIFile* profileDir) {
+  nsresult rv;
+  nsCOMPtr<nsIStartupCache> sc
+    = do_GetService("@mozilla.org/startupcache/cache;1", &rv);
+  sc->InvalidateCache();
+  
+  const char* buf = "Get a Beardbook app for your smartphone";
+  const char* id = "id";
+  char* outbuf = NULL;
+  PRUint32 len;
+  
+  rv = sc->PutBuffer(id, buf, strlen(buf) + 1);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = sc->ResetStartupWriteTimer();
+  rv = WaitForStartupTimer();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Prevent StartupCache::InvalidateCache from deleting the disk file
+  rv = LockCacheFile(true, profileDir);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  sc->IgnoreDiskCache();
+
+  rv = sc->GetBuffer(id, &outbuf, &len);
+
+  nsresult r = LockCacheFile(false, profileDir);
+  NS_ENSURE_SUCCESS(r, r);
+
+  delete[] outbuf;
+
+  if (rv == NS_ERROR_NOT_AVAILABLE) {
+    passed("buffer not available after ignoring disk cache");
+  } else if (NS_SUCCEEDED(rv)) {
+    fail("GetBuffer succeeded unexpectedly after ignoring disk cache");
+    return NS_ERROR_UNEXPECTED;
+  } else {
+    fail("GetBuffer gave an unexpected failure, expected NOT_AVAILABLE");
+    return rv;
+  }
+
+  sc->InvalidateCache();
+  return NS_OK;
+}
+
+nsresult
 TestEarlyShutdown() {
   nsresult rv;
-  nsCOMPtr<nsIStartupCache> sc 
+  nsCOMPtr<nsIStartupCache> sc
     = do_GetService("@mozilla.org/startupcache/cache;1", &rv);
   sc->InvalidateCache();
 
   const char* buf = "Find your soul beardmate on BeardBook";
   const char* id = "id";
   uint32_t len;
   char* outbuf = NULL;
   
@@ -411,16 +512,19 @@ int main(int argc, char** argv)
   else
     sc->RecordAgesAlways();
   if (NS_FAILED(TestStartupWriteRead()))
     rv = 1;
   if (NS_FAILED(TestWriteInvalidateRead()))
     rv = 1;
   if (NS_FAILED(TestWriteObject()))
     rv = 1;
+  nsCOMPtr<nsIFile> profileDir = xpcom.GetProfileDirectory();
+  if (NS_FAILED(TestIgnoreDiskCache(profileDir)))
+    rv = 1;
   if (NS_FAILED(TestEarlyShutdown()))
     rv = 1;
 
   JS::AutoValueRooter after_counts(cx);
   if (use_js && !GetHistogramCounts("STARTUP_CACHE_AGE_HOURS histogram after test",
                                     cx, after_counts.addr()))
     use_js = false;