| /* |
| * Copyright 2014 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.javac; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.TreeLogger.Type; |
| import com.google.gwt.core.ext.UnableToCompleteException; |
| import com.google.gwt.dev.jjs.impl.GwtAstBuilder; |
| import com.google.gwt.dev.util.CompilerVersion; |
| import com.google.gwt.dev.util.StringInterningObjectInputStream; |
| 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.thirdparty.guava.common.annotations.VisibleForTesting; |
| import com.google.gwt.thirdparty.guava.common.collect.Lists; |
| import com.google.gwt.util.tools.Utility; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * The directory containing persistent unit cache files. |
| * (Helper class for {@link PersistentUnitCache}.) |
| */ |
| class PersistentUnitCacheDir { |
| |
| private static final String DIRECTORY_NAME = "gwt-unitCache"; |
| private static final String CACHE_FILE_PREFIX = "gwt-unitCache-"; |
| |
| static final String CURRENT_VERSION_CACHE_FILE_PREFIX = |
| CACHE_FILE_PREFIX + CompilerVersion.getHash(); |
| |
| private final TreeLogger logger; |
| private final File dir; |
| private final String filePrefix; |
| |
| // Non-null when a a cache file is open for writing. (Always true in normal operation.) |
| private OpenFile openFile; |
| |
| /** |
| * Finds the child directory where the cache files will be stored and opens a new cache |
| * file for appending. |
| */ |
| PersistentUnitCacheDir(TreeLogger logger, File parentDir, String cacheFilePrefix) |
| throws UnableToCompleteException { |
| this.logger = logger; |
| this.filePrefix = CURRENT_VERSION_CACHE_FILE_PREFIX + "-" + cacheFilePrefix + "-"; |
| |
| /* |
| * We must canonicalize the path here, otherwise we might set cacheDirectory |
| * to something like "/path/to/x/../gwt-unitCache". If this were to happen, |
| * the mkdirs() call below would create "/path/to/gwt-unitCache" but |
| * not "/path/to/x". |
| * Further accesses via the uncanonicalized path will fail if "/path/to/x" |
| * had not been created by other means. |
| * |
| * Fixes issue 6443 |
| */ |
| try { |
| parentDir = parentDir.getCanonicalFile(); |
| } catch (IOException e) { |
| logger.log(TreeLogger.WARN, "Can't get canonical directory for " |
| + parentDir.getAbsolutePath(), e); |
| throw new UnableToCompleteException(); |
| } |
| |
| dir = chooseCacheDir(parentDir); |
| if (!dir.isDirectory() && !dir.mkdirs()) { |
| logger.log(TreeLogger.WARN, "Can't create directory: " + dir.getAbsolutePath()); |
| throw new UnableToCompleteException(); |
| } |
| |
| if (!dir.canRead()) { |
| logger.log(Type.WARN, "Can't read directory: " + dir.getAbsolutePath()); |
| throw new UnableToCompleteException(); |
| } |
| |
| logger.log(TreeLogger.TRACE, "Persistent unit cache dir set to: " + dir.getAbsolutePath()); |
| |
| openFile = new OpenFile(logger, createEmptyCacheFile(logger, dir, filePrefix)); |
| } |
| |
| /** |
| * Returns the absolute path of the directory where cache files are stored. |
| */ |
| String getPath() { |
| return dir.getAbsolutePath(); |
| } |
| |
| /** |
| * Returns the number of files written to the cache directory and closed. |
| */ |
| synchronized int getClosedCacheFileCount() { |
| return selectClosedFiles(listFiles(filePrefix)).size(); |
| } |
| |
| /** |
| * Load everything cached on disk into memory. |
| */ |
| synchronized void loadUnitMap(PersistentUnitCache destination) { |
| Event loadPersistentUnitEvent = |
| SpeedTracerLogger.start(DevModeEventType.LOAD_PERSISTENT_UNIT_CACHE); |
| if (logger.isLoggable(TreeLogger.TRACE)) { |
| logger.log(TreeLogger.TRACE, "Looking for previously cached Compilation Units in " |
| + getPath()); |
| } |
| try { |
| List<File> files = selectClosedFiles(listFiles(filePrefix)); |
| for (File cacheFile : files) { |
| loadOrDeleteCacheFile(cacheFile, destination); |
| } |
| } finally { |
| loadPersistentUnitEvent.end(); |
| } |
| } |
| |
| /** |
| * Delete all cache files in the directory except for the currently open file. |
| */ |
| synchronized void deleteClosedCacheFiles() { |
| SpeedTracerLogger.Event deleteEvent = SpeedTracerLogger.start(DevModeEventType.DELETE_CACHE); |
| logger.log(TreeLogger.TRACE, "Deleting cache files from " + dir); |
| |
| // We want to delete cache files from previous versions as well. |
| List<File> allVersionsList = listFiles(CACHE_FILE_PREFIX); |
| int deleteCount = 0; |
| for (File candidate : allVersionsList) { |
| if (deleteUnlessOpen(candidate)) { |
| deleteCount++; |
| } |
| } |
| |
| logger.log(TreeLogger.TRACE, "Deleted " + deleteCount + " cache files from " + dir); |
| deleteEvent.end(); |
| } |
| |
| /** |
| * Closes the current cache file and opens a new one. |
| */ |
| synchronized void rotate() throws UnableToCompleteException { |
| logger.log(Type.TRACE, "Rotating persistent unit cache"); |
| if (openFile != null) { |
| openFile.close(logger); |
| openFile = null; |
| } |
| openFile = new OpenFile(logger, createEmptyCacheFile(logger, dir, filePrefix)); |
| } |
| |
| /** |
| * Deletes the given file unless it's currently open for writing. |
| */ |
| synchronized boolean deleteUnlessOpen(File cacheFile) { |
| if (isOpen(cacheFile)) { |
| return false; |
| } |
| logger.log(Type.TRACE, "Deleting file: " + cacheFile); |
| boolean deleted = cacheFile.delete(); |
| if (!deleted) { |
| logger.log(Type.WARN, "Unable to delete file: " + cacheFile); |
| } |
| return deleted; |
| } |
| |
| /** |
| * Writes a compilation unit to the disk cache. |
| */ |
| synchronized void writeUnit(CompilationUnit unit) throws UnableToCompleteException { |
| if (openFile == null) { |
| logger.log(Type.TRACE, "Skipped writing compilation unit to cache because no file is open"); |
| return; |
| } |
| openFile.writeUnit(logger, unit); |
| } |
| |
| /** |
| * Closes the file where cache entries are written. |
| * (This should only be called at shutdown.) |
| */ |
| synchronized void closeCurrentFile() { |
| if (openFile != null) { |
| openFile.close(logger); |
| openFile = null; |
| } |
| } |
| |
| @VisibleForTesting |
| static File chooseCacheDir(File parentDir) { |
| return new File(parentDir, DIRECTORY_NAME); |
| } |
| |
| private boolean isOpen(File f) { |
| return openFile != null && openFile.file.equals(f); |
| } |
| |
| /** |
| * Loads all the units in a cache file into the given cache. |
| * Delete it if unable to read it. |
| */ |
| private void loadOrDeleteCacheFile(File cacheFile, PersistentUnitCache destination) { |
| FileInputStream fis = null; |
| BufferedInputStream bis = null; |
| ObjectInputStream inputStream = null; |
| |
| boolean ok = false; |
| int unitsLoaded = 0; |
| try { |
| fis = new FileInputStream(cacheFile); |
| bis = new BufferedInputStream(fis); |
| /* |
| * It is possible for the next call to throw an exception, leaving |
| * inputStream null and fis still live. |
| */ |
| inputStream = new StringInterningObjectInputStream(bis); |
| |
| // Read objects until we get an EOF exception. |
| while (true) { |
| CachedCompilationUnit unit = (CachedCompilationUnit) inputStream.readObject(); |
| if (unit == null) { |
| // Won't normally get here. Not sure why this check was here before. |
| logger.log(Type.WARN, "unexpected null in cache file: " + cacheFile); |
| break; |
| } |
| if (unit.getTypesSerializedVersion() != GwtAstBuilder.getSerializationVersion()) { |
| continue; |
| } |
| destination.maybeAddLoadedUnit(unit); |
| unitsLoaded++; |
| } |
| |
| } catch (EOFException ignored) { |
| // This is a normal exit. Go on to the next file. |
| ok = true; |
| } catch (IOException e) { |
| logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log " |
| + cacheFile.getAbsolutePath() + " due to read error.", e); |
| } catch (ClassNotFoundException e) { |
| logger.log(TreeLogger.TRACE, "Ignoring and deleting cache log " |
| + cacheFile.getAbsolutePath() + " due to deserialization error.", e); |
| } finally { |
| Utility.close(inputStream); |
| Utility.close(bis); |
| Utility.close(fis); |
| } |
| |
| if (ok) { |
| logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded + |
| " units from cache file: " + cacheFile.getName()); |
| } else { |
| deleteUnlessOpen(cacheFile); |
| logger.log(TreeLogger.TRACE, "Loaded " + unitsLoaded + |
| " units from invalid cache file before deleting it: " + cacheFile.getName()); |
| } |
| } |
| |
| /** |
| * Lists files in the cache directory that start with the given prefix. |
| * |
| * <p>The files will be sorted according to {@link java.io.File#compareTo}, which |
| * differs on Unix versus Windows, but is good enough to sort by age |
| * for the names we use.</p> |
| */ |
| private List<File> listFiles(String prefix) { |
| File[] files = dir.listFiles(); |
| if (files == null) { |
| // Shouldn't happen, just satisfying null check warning. |
| return Collections.emptyList(); |
| } |
| List<File> out = Lists.newArrayList(); |
| for (File file : files) { |
| if (file.getName().startsWith(prefix)) { |
| out.add(file); |
| } |
| } |
| Collections.sort(out); |
| return out; |
| } |
| |
| /** |
| * Removes the currently open file from a list of files. |
| * @return the new list. |
| */ |
| private List<File> selectClosedFiles(Iterable<File> fileList) { |
| List<File> closedFiles = Lists.newArrayList(); |
| for (File file : fileList) { |
| if (!isOpen(file)) { |
| closedFiles.add(file); |
| } |
| } |
| return closedFiles; |
| } |
| |
| /** |
| * Creates a new, empty file with a name based on the current system time. |
| */ |
| private static File createEmptyCacheFile(TreeLogger logger, File dir, String filePrefix) |
| throws UnableToCompleteException { |
| File newFile = null; |
| long timestamp = System.currentTimeMillis(); |
| try { |
| do { |
| newFile = new File(dir, filePrefix + String.format("%016X", timestamp++)); |
| } while (!newFile.createNewFile()); |
| } catch (IOException ex) { |
| logger.log(TreeLogger.WARN, "Can't create new cache log file " |
| + newFile.getAbsolutePath() + ".", ex); |
| throw new UnableToCompleteException(); |
| } |
| |
| if (!newFile.canWrite()) { |
| logger.log(TreeLogger.WARN, "Can't write to new cache log file " |
| + newFile.getAbsolutePath() + "."); |
| throw new UnableToCompleteException(); |
| } |
| |
| return newFile; |
| } |
| |
| /** |
| * The current file and stream being written to by the persistent unit cache, if any. |
| * |
| * <p>Not thread safe. (The parent class handles concurrency.) |
| */ |
| private static class OpenFile { |
| private final File file; |
| private final ObjectOutputStream stream; |
| private int unitsWritten = 0; |
| |
| /** |
| * Opens a file for writing compilation units. |
| * Overwrites the file (it's typically empty). |
| * A cache file may not already be open. |
| */ |
| OpenFile(TreeLogger logger, File toOpen) |
| throws UnableToCompleteException { |
| logger.log(Type.TRACE, "Opening cache file: " + toOpen); |
| ObjectOutputStream newStream = openObjectStream(logger, toOpen); |
| |
| this.file = toOpen; |
| this.stream = newStream; |
| unitsWritten = 0; |
| } |
| |
| /** |
| * Writes a compilation unit to the currently open file, if any. |
| * @return true if written |
| * @throws UnableToCompleteException if the file was open but we can't append. |
| */ |
| boolean writeUnit(TreeLogger logger, CompilationUnit unit) |
| throws UnableToCompleteException { |
| try { |
| stream.writeObject(unit); |
| unitsWritten++; |
| return true; |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Error saving compilation unit to cache file: " + file, e); |
| throw new UnableToCompleteException(); |
| } |
| } |
| |
| /** |
| * Closes the current file and deletes it if it's empty. If no file is open, does nothing. |
| */ |
| void close(TreeLogger logger) { |
| logger.log(Type.TRACE, |
| "Closing cache file: " + file + " (" + unitsWritten + " units written)"); |
| |
| Utility.close(stream); |
| |
| if (unitsWritten == 0) { |
| // Remove useless empty file. |
| logger.log(Type.TRACE, "Deleting empty file: " + file); |
| boolean deleted = file.delete(); |
| if (!deleted) { |
| logger.log(Type.INFO, "Couldn't delete persistent unit cache file: " + file); |
| } |
| } |
| } |
| |
| private static ObjectOutputStream openObjectStream(TreeLogger logger, File file) |
| throws UnableToCompleteException { |
| |
| FileOutputStream fstream = null; |
| try { |
| fstream = new FileOutputStream(file); |
| return new ObjectOutputStream(new BufferedOutputStream(fstream)); |
| } catch (IOException e) { |
| logger.log(Type.ERROR, "Can't open persistent unit cache file", e); |
| Utility.close(fstream); |
| throw new UnableToCompleteException(); |
| } |
| } |
| } |
| } |