Merge pull request #173 from vatsal22/native_cache

Use nresource=[native jar] to use native jars
This commit is contained in:
Michael Bayne
2018-12-06 10:32:40 -08:00
committed by GitHub
8 changed files with 233 additions and 17 deletions
@@ -17,7 +17,6 @@ import com.threerings.getdown.data.Resource;
public class ClassPaths public class ClassPaths
{ {
static final String CACHE_DIR = ".cache";
/** /**
* Builds either a default or cached classpath based on {@code app}'s configuration. * Builds either a default or cached classpath based on {@code app}'s configuration.
@@ -48,23 +47,20 @@ public class ClassPaths
*/ */
public static ClassPath buildCachedClassPath (Application app) throws IOException public static ClassPath buildCachedClassPath (Application app) throws IOException
{ {
File cacheDir = new File(app.getAppDir(), CACHE_DIR); File codeCacheDir = new File(app.getAppDir(), Application.CACHE_DIR + "/code");
// a negative value of code_cache_retention_days allows to clean up the cache forcefully // a negative value of code_cache_retention_days allows to clean up the cache forcefully
if (app.getCodeCacheRetentionDays() <= 0) { if (app.getCodeCacheRetentionDays() != 0) {
runGarbageCollection(app, cacheDir); runGarbageCollection(app, codeCacheDir);
} }
ResourceCache cache = new ResourceCache(cacheDir); ResourceCache cache = new ResourceCache(codeCacheDir);
LinkedHashSet<File> classPathEntries = new LinkedHashSet<File>(); LinkedHashSet<File> classPathEntries = new LinkedHashSet<>();
for (Resource resource: app.getActiveCodeResources()) { for (Resource resource: app.getActiveCodeResources()) {
File entry = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource)); File entry = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource));
classPathEntries.add(entry); classPathEntries.add(entry);
} }
if (app.getCodeCacheRetentionDays() > 0) {
runGarbageCollection(app, cacheDir);
}
return new ClassPath(classPathEntries); return new ClassPath(classPathEntries);
} }
@@ -0,0 +1,83 @@
package com.threerings.getdown.classpath;
import com.threerings.getdown.classpath.cache.GarbageCollector;
import com.threerings.getdown.classpath.cache.ResourceCache;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.FileUtil;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;
import static com.threerings.getdown.Log.log;
/**
* Similar to {@link ClassPath}, except used to represent directories containing native libraries instead.
*/
public class NativeLibPath extends ClassPath {
public NativeLibPath(LinkedHashSet<File> entries) {
super(entries);
}
/**
* Builds a {@link NativeLibPath} instance by first caching all native jars (indicated by
* nresource=[native jar]), unpacking them, and referencing the locations of each of the unpacked files.
* Also performs garbage collection similar to {@link ClassPaths#buildCachedClassPath}
*
* @param app used to determine native jars and related information
* @param addCurrentLibraryPath if true, it adds the locations referenced by
* {@code System.getProperty("java.library.path")} as well
*/
public static NativeLibPath buildLibsPath(Application app, boolean addCurrentLibraryPath) throws IOException {
List<Resource> resources = app.getNativeJars();
String[] curPaths = System.getProperty("java.library.path").split(File.pathSeparator);
LinkedHashSet<File> nativedirs = new LinkedHashSet<>();
File nativeCacheDir = new File(app.getAppDir(), Application.CACHE_DIR + "/native");
ResourceCache cache = new ResourceCache(nativeCacheDir);
// negative value forces total garbage collection, 0 avoids garbage collection at all
if (app.getCodeCacheRetentionDays() != 0) {
runGarbageCollection(app, nativeCacheDir);
}
for (Resource resource : resources) {
// Use untruncated directory names because in the off chance that two native jars share a directory AND
// contain files with the same names, we'll get overwriting issues when unpacking
File cachedFile = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource), true);
if (!getUnpackedIndicator(cachedFile).exists()) {
try {
FileUtil.unpackJar(new JarFile(cachedFile), cachedFile.getParentFile(), false);
getUnpackedIndicator(cachedFile).createNewFile();
} catch (IOException ioe) {
log.warning("Failed to unpack native jar", "File", cachedFile.getAbsolutePath(), ioe);
// Keep going and unpack the other jars...
}
}
nativedirs.add(cachedFile.getParentFile());
}
if (addCurrentLibraryPath) {
for (String path : curPaths) {
nativedirs.add(new File(path));
}
}
return new NativeLibPath(nativedirs);
}
private static void runGarbageCollection(Application app, File nativeCacheDir) {
long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
GarbageCollector.collectNative(nativeCacheDir, retainMillis);
}
private static File getUnpackedIndicator(File cachedFile) {
return new File(cachedFile.getParent(), cachedFile.getName() + ".unpacked");
}
}
@@ -1,6 +1,8 @@
package com.threerings.getdown.classpath.cache; package com.threerings.getdown.classpath.cache;
import java.io.File; import java.io.File;
import java.io.FileFilter;
import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.FileUtil;
/** /**
@@ -40,6 +42,42 @@ public class GarbageCollector
}); });
} }
/**
* Collect and delete garbage in the native cache. It tries to find a jar file with a matching
* lastmodified file, and deletes the entire directory accordingly.
*/
public static void collectNative (File cacheDir, final long retentionPeriodMillis) {
FileUtil.walkDirectChildren(cacheDir, new FileUtil.Visitor() {
@Override
public void visit(File file) {
if (file.isDirectory()) {
// Get all the native jars in the directory (there should only be one though)
File[] nativejars = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".jar");
}
});
for (File nativejar : nativejars) {
if (nativejar==null || !nativejar.exists()) {
continue;
}
File cachedFile = getCachedFile(nativejar);
File lastAccessedFile = getLastAccessedFile(nativejar);
if (!cachedFile.exists() || !lastAccessedFile.exists() || shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
FileUtil.deleteDirHarder(file);
}
}
} else {
// @TODO There shouldn't be any loose files in native/ but if there are then what? Delete them?
// file.delete();
}
}
});
}
private static boolean shouldDelete (File lastAccessedFile, long retentionMillis) private static boolean shouldDelete (File lastAccessedFile, long retentionMillis)
{ {
return System.currentTimeMillis() - lastAccessedFile.lastModified() > retentionMillis; return System.currentTimeMillis() - lastAccessedFile.lastModified() > retentionMillis;
@@ -26,13 +26,34 @@ public class ResourceCache
} }
/** /**
* Caches the given file under it's {@code digest}. * Caches the given file under it's {@code digest}. Same as calling {@link #cacheFile(File, String, boolean)}
* * with {@code false}
* @return the cached file * @return the cached file
*/ */
public File cacheFile (File fileToCache, String digest) throws IOException public File cacheFile (File fileToCache, String digest) throws IOException
{ {
File cacheLocation = new File(_cacheDir, digest.substring(0, 2)); return cacheFile(fileToCache, digest, false);
}
/**
* Caches the given file under it's {@code digest}.
* @param fileToCache file to cache
* @param digest used to determine the name of the directory to store file in
* @param useFullHashName if true, the name of the cache directory will not be truncated.
* This is useful if you need to avoid the possibility of two files
* sharing a directory.
* @return the cached file
*/
public File cacheFile (File fileToCache, String digest, boolean useFullHashName) throws IOException
{
File cacheLocation;
if (useFullHashName) {
cacheLocation = new File(_cacheDir, digest);
} else {
cacheLocation = new File(_cacheDir, digest.substring(0, 2));
}
createDirectoryIfNecessary(cacheLocation); createDirectoryIfNecessary(cacheLocation);
File cachedFile = new File(cacheLocation, digest + getFileSuffix(fileToCache)); File cachedFile = new File(cacheLocation, digest + getFileSuffix(fileToCache));
@@ -25,6 +25,7 @@ import java.util.zip.GZIPInputStream;
import com.threerings.getdown.classpath.ClassPaths; import com.threerings.getdown.classpath.ClassPaths;
import com.threerings.getdown.classpath.ClassPath; import com.threerings.getdown.classpath.ClassPath;
import com.threerings.getdown.classpath.NativeLibPath;
import com.threerings.getdown.util.*; import com.threerings.getdown.util.*;
// avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+ // avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+
import com.threerings.getdown.util.Base64; import com.threerings.getdown.util.Base64;
@@ -54,6 +55,9 @@ public class Application
/** A special classname that means 'use -jar code.jar' instead of a classname. */ /** A special classname that means 'use -jar code.jar' instead of a classname. */
public static final String MANIFEST_CLASS = "manifest"; public static final String MANIFEST_CLASS = "manifest";
/** Name of directory to store cached files in. */
public static final String CACHE_DIR = ".cache";
/** Used to communicate information about the UI displayed when updating the application. */ /** Used to communicate information about the UI displayed when updating the application. */
public static final class UpdateInterface public static final class UpdateInterface
{ {
@@ -374,6 +378,22 @@ public class Application
return codes; return codes;
} }
/**
* Returns all jar resources indicated to contain native library files (.dll, .so etc.)
*/
public List<Resource> getNativeJars() {
List<Resource> nativeJars = new ArrayList<>();
for (Resource resource: _resources) {
if (resource.isNativeJar()) {
nativeJars.add(resource);
}
}
return nativeJars;
}
/** /**
* Returns all non-code resources and all resources from active auxiliary resource groups. * Returns all non-code resources and all resources from active auxiliary resource groups.
*/ */
@@ -673,6 +693,7 @@ public class Application
parseResources(config, "uresource", Resource.UNPACK, _resources); parseResources(config, "uresource", Resource.UNPACK, _resources);
parseResources(config, "xresource", Resource.EXEC, _resources); parseResources(config, "xresource", Resource.EXEC, _resources);
parseResources(config, "presource", Resource.PRELOAD, _resources); parseResources(config, "presource", Resource.PRELOAD, _resources);
parseResources(config, "nresource", Resource.NATIVE, _resources);
// parse our auxiliary resource groups // parse our auxiliary resource groups
for (String auxgroup : config.getList("auxgroups")) { for (String auxgroup : config.getList("auxgroups")) {
@@ -915,6 +936,10 @@ public class Application
// add the -classpath arguments if we're not in -jar mode // add the -classpath arguments if we're not in -jar mode
ClassPath classPath = ClassPaths.buildClassPath(this); ClassPath classPath = ClassPaths.buildClassPath(this);
// get the -Djava.library.path value to pass
// @TODO optional getdown.txt parameter to set addCurrentLibraryPath to true or false?
NativeLibPath nativeLibPaths = NativeLibPath.buildLibsPath(this, true);
if (!dashJarMode) { if (!dashJarMode) {
args.add("-classpath"); args.add("-classpath");
args.add(classPath.asArgumentString()); args.add(classPath.asArgumentString());
@@ -938,6 +963,9 @@ public class Application
// add the marker indicating the app is running in getdown // add the marker indicating the app is running in getdown
args.add("-D" + Properties.GETDOWN + "=true"); args.add("-D" + Properties.GETDOWN + "=true");
// set the native library path
args.add("-Djava.library.path=" + nativeLibPaths.asArgumentString());
// pass along any pass-through arguments // pass along any pass-through arguments
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) { for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
String key = (String)entry.getKey(); String key = (String)entry.getKey();
@@ -37,13 +37,16 @@ public class Resource implements Comparable<Resource>
/** Indicates that the resource should be marked executable. */ /** Indicates that the resource should be marked executable. */
EXEC, EXEC,
/** Indicates that the resource should be downloaded before a UI is displayed. */ /** Indicates that the resource should be downloaded before a UI is displayed. */
PRELOAD PRELOAD,
/** Indicates that the resource is a jar containing native libs. */
NATIVE
}; };
public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class); public static final EnumSet<Attr> NORMAL = EnumSet.noneOf(Attr.class);
public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK); public static final EnumSet<Attr> UNPACK = EnumSet.of(Attr.UNPACK);
public static final EnumSet<Attr> EXEC = EnumSet.of(Attr.EXEC); public static final EnumSet<Attr> EXEC = EnumSet.of(Attr.EXEC);
public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD); public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD);
public static final EnumSet<Attr> NATIVE = EnumSet.of(Attr.NATIVE);
/** /**
* Computes the MD5 hash of the supplied file. * Computes the MD5 hash of the supplied file.
@@ -212,6 +215,15 @@ public class Resource implements Comparable<Resource>
return _attrs.contains(Attr.PRELOAD); return _attrs.contains(Attr.PRELOAD);
} }
/**
* Returns true if this resource is a native lib jar.
*/
public boolean isNativeJar ()
{
return _attrs.contains(Attr.NATIVE);
}
/** /**
* Computes the MD5 hash of this resource's underlying file. * Computes the MD5 hash of this resource's underlying file.
* <em>Note:</em> This is both CPU and I/O intensive. * <em>Note:</em> This is both CPU and I/O intensive.
@@ -77,6 +77,27 @@ public class FileUtil
return deleted; return deleted;
} }
/**
* Force deletes {@code file} and all of its children recursively using {@link #deleteHarder}.
* Note that some children may still be deleted even if {@code false} is returned. Also, since {@link #deleteHarder}
* is used, the {@code file} could be deleted once the jvm exits even if {@code false} is returned.
*
* @param file file to delete
* @return true iff {@code file} was successfully deleted.
*/
public static boolean deleteDirHarder(File file) {
if (file.isDirectory()) {
for (File child : file.listFiles()) {
if (child.isDirectory()) {
deleteDirHarder(child);
} else {
deleteHarder(child);
}
}
}
return deleteHarder(file);
}
/** /**
* Reads the contents of the supplied input stream into a list of lines. Closes the reader on * Reads the contents of the supplied input stream into a list of lines. Closes the reader on
* successful or failed completion. * successful or failed completion.
@@ -190,7 +211,7 @@ public class FileUtil
} }
/** /**
* Used by {@link #walkTree}. * Used by {@link #walkTree} and {@link #walkDirectChildren}.
*/ */
public interface Visitor public interface Visitor
{ {
@@ -218,4 +239,21 @@ public class FileUtil
} }
} }
} }
/**
* Walks all direct sub-files of {@code root}, calling {@code visitor} on each file.
*/
public static void walkDirectChildren (final File root, Visitor visitor) {
File[] children = root.listFiles();
if (children == null) return;
Deque<File> stack = new ArrayDeque<>(Arrays.asList(children));
while (!stack.isEmpty()) {
File currentFile = stack.pop();
if (currentFile.exists()) {
visitor.visit(currentFile);
}
}
}
} }
@@ -46,10 +46,10 @@ public class ClassPathsTest
when(_application.getCodeCacheRetentionDays()).thenReturn(1); when(_application.getCodeCacheRetentionDays()).thenReturn(1);
Path firstCachedJarFile = _appdir.getRoot().toPath(). Path firstCachedJarFile = _appdir.getRoot().toPath().
resolve(ClassPaths.CACHE_DIR).resolve("fi").resolve("first.jar"); resolve(Application.CACHE_DIR + "/code").resolve("fi").resolve("first.jar");
Path secondCachedJarFile = _appdir.getRoot().toPath(). Path secondCachedJarFile = _appdir.getRoot().toPath().
resolve(ClassPaths.CACHE_DIR).resolve("se").resolve("second.jar"); resolve(Application.CACHE_DIR + "/code").resolve("se").resolve("second.jar");
String expectedClassPath = firstCachedJarFile.toAbsolutePath() + File.pathSeparator + String expectedClassPath = firstCachedJarFile.toAbsolutePath() + File.pathSeparator +
secondCachedJarFile.toAbsolutePath(); secondCachedJarFile.toAbsolutePath();