blob: 4c2e648d4da05840aaf7e79e9731a5853db94e3f [file] [log] [blame]
/*
* 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();
}
}
}
}