Make persitent unit cache files be different for each compiler version.

Persistent unit cache filenames now include a hash of the compiler
proper to avoid deserializing state that was serialized by a different
version of the compiler.

Fix for issue 7794.

Change-Id: I6b6d0d2cafd367806ff0ea2c86ae2895aeaa1e50
diff --git a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
index 3c54f49..24703c6 100644
--- a/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
+++ b/dev/core/src/com/google/gwt/dev/javac/PersistentUnitCache.java
@@ -18,11 +18,14 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.dev.jjs.InternalCompilerException;
+import com.google.gwt.dev.jjs.ast.JNode;
 import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
 import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
 import com.google.gwt.util.tools.Utility;
+import com.google.gwt.thirdparty.guava.common.hash.Hashing;
+import com.google.gwt.thirdparty.guava.common.io.Files;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -33,6 +36,8 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.net.JarURLConnection;
+import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -64,34 +69,34 @@
  * number of files in the directory is reached
  * {@link PersistentUnitCache#CACHE_FILE_THRESHOLD} , the cache files are
  * consolidated back into a single file.
- * 
+ *
  * <p>
  * System Properties (see {@link UnitCacheFactory}).
- * 
+ *
  * <ul>
  * <li>gwt.persistentunitcache : enables the persistent cache (eventually will
  * be default)</li>
  * <li>gwt.persistentunitcachedir=<dir>: sets or overrides the cache directory</li>
  * </ul>
- * 
+ *
  * <p>
  * Known Issues:
- * 
+ *
  * <ul>
  * <li>This design uses an eager cache to load every unit in the cache on the
  * first reference to find() or add(). When the cache is large (10000 units), it
  * uses lots of heap and takes 5-10 seconds. Once the PersistentUnitCache is
  * created, it starts eagerly loading the cache in a background thread).</li>
- * 
+ *
  * <li>Although units logged to disk with the same resource path are eventually
  * cleaned up, the most recently compiled unit stays in the cache forever. This
  * means that stale units that are no longer referenced will never be purged,
  * unless by some external action (e.g. ant clean).</li>
- * 
+ *
  * <li>Unless ant builds are made aware of the cache directory, the cache will
  * persist if a user does an ant clean.</li>
  * </ul>
- * 
+ *
  */
 class PersistentUnitCache extends MemoryUnitCache {
 
@@ -100,12 +105,13 @@
    * files.
    */
   static final int CACHE_FILE_THRESHOLD = 40;
-  
+
   /**
    * Common prefix for creating directories and cache files.
    */
   static final String UNIT_CACHE_PREFIX = "gwt-unitCache";
   static final String CACHE_FILE_PREFIX = UNIT_CACHE_PREFIX + "-";
+  static final String CURRENT_VERSION_CACHE_FILE_PREFIX = computePersistentCacheFilenamePrefix();
 
   /**
    * Creates a new file with a name based on the current system time.
@@ -116,7 +122,8 @@
     long timestamp = System.currentTimeMillis();
     try {
       do {
-        newFile = new File(cacheDirectory, CACHE_FILE_PREFIX + String.format("%016X", timestamp++));
+        newFile = new File(cacheDirectory, CURRENT_VERSION_CACHE_FILE_PREFIX +
+            String.format("%016X", timestamp++));
       } while (!newFile.createNewFile());
     } catch (IOException ex) {
       logger.log(TreeLogger.WARN, "Unable to create new cache log file "
@@ -135,16 +142,17 @@
 
   /**
    * Finds all files matching a pattern in the cache directory.
-   * 
+   *
    * @return an array of sorted filenames. The file name pattern is such that
    *         sorting them alphabetically also sorts the files by age.
    */
-  private static File[] getCacheFiles(File cacheDirectory) {
+  private static File[] getCacheFiles(File cacheDirectory, boolean currentVersion) {
     if (cacheDirectory.isDirectory()) {
       File[] files = cacheDirectory.listFiles();
       List<File> cacheFiles = new ArrayList<File>();
       for (File file : files) {
-        if (file.getName().startsWith(CACHE_FILE_PREFIX)) {
+        if (file.getName().startsWith(currentVersion ? CURRENT_VERSION_CACHE_FILE_PREFIX :
+            CACHE_FILE_PREFIX)) {
           cacheFiles.add(file);
         }
       }
@@ -161,7 +169,7 @@
    */
   private Future<Boolean> purgeTaskStatus;
   private AtomicBoolean purgeInProgress = new AtomicBoolean(false);
-  
+
   private final Runnable purgeOldCacheFilesTask = new Runnable() {
     @Override
     public void run() {
@@ -169,7 +177,7 @@
         // Delete all cache files in the directory except for the currently open
         // file.
         SpeedTracerLogger.Event deleteEvent = SpeedTracerLogger.start(DevModeEventType.DELETE_CACHE);
-        File[] filesToDelete = getCacheFiles(cacheDirectory);
+        File[] filesToDelete = getCacheFiles(cacheDirectory, false);
         logger.log(TreeLogger.TRACE, "Purging cache files from " + cacheDirectory);
         for (File toDelete : filesToDelete) {
           if (!currentCacheFile.equals(toDelete)) {
@@ -179,14 +187,14 @@
           }
         }
         deleteEvent.end();
-        
+
         rotateCurrentCacheFile();
       } catch (UnableToCompleteException e) {
         backgroundService.shutdownNow();
       } finally {
         purgeInProgress.set(false);
       }
-    }      
+    }
   };
 
   private final Runnable rotateCacheFilesTask = new Runnable() {
@@ -215,7 +223,7 @@
    * Saved to be able to wait for UNIT_MAP_LOAD_TASK to complete.
    */
   private Future<Boolean> unitMapLoadStatus;
-  
+
   private final Runnable unitMapLoadTask = new Runnable() {
     @Override
     public void run() {
@@ -227,7 +235,7 @@
    * Used to execute the above Runnables in a background thread.
    */
   private final ExecutorService backgroundService;
-  
+
   private int unitsWritten = 0;
 
   private int addedSinceLastCleanup = 0;
@@ -302,7 +310,7 @@
         }
       }
     });
-    
+
     /**
      * Load up cached units from the persistent store in the background. The
      * {@link #add(CompilationUnit)} and {@link #find(String)} methods block if
@@ -340,7 +348,7 @@
   /**
    * Cleans up old cache files in the directory, migrating everything previously
    * loaded in them to the current cache file.
-   * 
+   *
    * Normally, only newly compiled units are written to the current log, but
    * when it is time to cleanup, valid units from older log files need to be
    * re-written.
@@ -353,10 +361,11 @@
       return;
     }
     boolean shouldRotate = addedSinceLastCleanup > 0;
-    logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup + " units to cache since last cleanup.");
+    logger.log(TreeLogger.TRACE, "Added " + addedSinceLastCleanup +
+        " units to cache since last cleanup.");
     addedSinceLastCleanup = 0;
     try {
-      File[] cacheFiles = getCacheFiles(cacheDirectory);
+      File[] cacheFiles = getCacheFiles(cacheDirectory, true);
       if (cacheFiles.length < CACHE_FILE_THRESHOLD) {
         if (shouldRotate) {
           backgroundService.execute(rotateCacheFilesTask);
@@ -376,7 +385,7 @@
           return;
         }
       }
-      
+
       /*
        * Resend all units read in from the in-memory cache to the background
        * thread. They will be re-written out and the old cache files removed.
@@ -438,7 +447,7 @@
 
   /**
    * For Unit testing - shutdown the persistent cache.
-   * 
+   *
    * @throws ExecutionException
    * @throws InterruptedException
    */
@@ -452,6 +461,25 @@
     }
   }
 
+  private static String computePersistentCacheFilenamePrefix() {
+    try {
+      URLConnection urlConnection =
+          JNode.class.getResource("JNode.class").openConnection();
+      if (urlConnection instanceof JarURLConnection) {
+        String gwtdevJar = ((JarURLConnection) urlConnection).getJarFile().getName();
+        return CACHE_FILE_PREFIX +
+            Files.hash(new File(gwtdevJar), Hashing.sha1()).toString() + "-";
+      }
+      System.err.println("Could not find the GWT compiler jarfile. "
+          + "Serialization errors might occur when accessing the persistent unit cache.");
+    } catch (IOException e) {
+      System.err.println("Could not compute the hash for the GWT compiler jarfile."
+          + "Serialization errors might occur when accessing the persistent unit cache.");
+      e.printStackTrace();
+    }
+    return CACHE_FILE_PREFIX;
+  }
+
   private void addImpl(final UnitCacheEntry entry) {
     try {
       backgroundService.execute(new Runnable() {
@@ -514,7 +542,7 @@
     }
     try {
       if (cacheDirectory.isDirectory() && cacheDirectory.canRead()) {
-        File[] files = getCacheFiles(cacheDirectory);
+        File[] files = getCacheFiles(cacheDirectory, true);
         for (File cacheFile : files) {
           FileInputStream fis = null;
           BufferedInputStream bis = null;
@@ -589,7 +617,8 @@
       } else {
         logger
             .log(TreeLogger.TRACE,
-                "Starting with empty Cache: CompilationUnit cache directory does not exist or is not readable.");
+                "Starting with empty Cache: CompilationUnit cache "
+                    + "directory does not exist or is not readable.");
       }
     } finally {
       loadPersistentUnitEvent.end();
diff --git a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
index d8ce8ef..09ac9ae 100644
--- a/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
+++ b/dev/core/test/com/google/gwt/dev/javac/PersistentUnitCacheTest.java
@@ -248,7 +248,8 @@
      * Create a cache file that has the right filename, but the wrong kind of
      * object in it.
      */
-    File errorFile = new File(unitCacheDir, PersistentUnitCache.CACHE_FILE_PREFIX + "12345");
+    File errorFile = new File(unitCacheDir,
+        PersistentUnitCache.CURRENT_VERSION_CACHE_FILE_PREFIX + "12345");
     ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(errorFile));
     os.writeObject(toSerialize);
     os.close();