classPathEntries)
+ {
+ _classPathEntries = Collections.unmodifiableSet(classPathEntries);
+ }
+
+ /**
+ * Returns the class path as an java command line argument string, e.g.
+ *
+ *
+ * /path/to/a.jar:/path/to/b.jar
+ *
+ */
+ public String asArgumentString ()
+ {
+ StringBuilder builder = new StringBuilder();
+ String delimiter = "";
+ for (File entry: _classPathEntries) {
+ builder.append(delimiter).append(entry.getAbsolutePath());
+ delimiter = File.pathSeparator;
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Returns the class path entries as an array of URLs to be used for example by an
+ * {@link URLClassLoader}.
+ */
+ public URL[] asUrls ()
+ {
+ URL[] urls = new URL[_classPathEntries.size()];
+ int i = 0;
+ for (File entry : _classPathEntries) {
+ urls[i++] = getURL(entry);
+ }
+ return urls;
+ }
+
+ public Set getClassPathEntries ()
+ {
+ return _classPathEntries;
+ }
+
+
+ private static URL getURL (File file)
+ {
+ try {
+ return file.toURI().toURL();
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException("URL of file is illegal: " + file.getAbsolutePath(), e);
+ }
+ }
+
+ private final Set _classPathEntries;
+}
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPaths.java b/src/main/java/com/threerings/getdown/classpath/ClassPaths.java
new file mode 100644
index 0000000..84b5607
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPaths.java
@@ -0,0 +1,76 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2016 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.concurrent.TimeUnit;
+
+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;
+
+public class ClassPaths
+{
+ static final String CACHE_DIR = ".cache";
+
+ /**
+ * Builds either a default or cached classpath based on {@code app}'s configuration.
+ */
+ public static ClassPath buildClassPath (Application app) throws IOException
+ {
+ return app.useCodeCache() ? buildDefaultClassPath(app) : buildCachedClassPath(app);
+ }
+
+ /**
+ * Builds a {@link ClassPath} instance for {@code app} using the code resources in place in
+ * the app directory.
+ */
+ public static ClassPath buildDefaultClassPath (Application app)
+ {
+ LinkedHashSet classPathEntries = new LinkedHashSet();
+ for (Resource resource: app.getActiveCodeResources()) {
+ classPathEntries.add(resource.getFinalTarget());
+ }
+ return new ClassPath(classPathEntries);
+ }
+
+ /**
+ * Builds a {@link ClassPath} instance for {@code app} by first copying the code resources into
+ * a cache directory and then referencing them from there. This avoids problems with
+ * overwriting in-use classpath elements when the application is later updated. This also
+ * "garbage collects" expired caches if necessary.
+ */
+ public static ClassPath buildCachedClassPath (Application app) throws IOException
+ {
+ File cacheDir = new File(app.getAppdir(), CACHE_DIR);
+ // a negative value of code_cache_retention_days allows to clean up the cache forcefully
+ if (app.getCodeCacheRetentionDays() <= 0) {
+ runGarbageCollection(app, cacheDir);
+ }
+
+ ResourceCache cache = new ResourceCache(cacheDir);
+ LinkedHashSet classPathEntries = new LinkedHashSet();
+ for (Resource resource: app.getActiveCodeResources()) {
+ File entry = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource));
+ classPathEntries.add(entry);
+ }
+
+ if (app.getCodeCacheRetentionDays() > 0) {
+ runGarbageCollection(app, cacheDir);
+ }
+
+ return new ClassPath(classPathEntries);
+ }
+
+ private static void runGarbageCollection (Application app, File cacheDir)
+ {
+ long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+ GarbageCollector.collect(cacheDir, retainMillis);
+ }
+}
diff --git a/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java b/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
new file mode 100644
index 0000000..6c1dbc7
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
@@ -0,0 +1,61 @@
+package com.threerings.getdown.classpath.cache;
+
+import java.io.File;
+import com.threerings.getdown.util.FileUtil;
+
+/**
+ * Collects elements in the {@link ResourceCache cache} which became unused and deletes them
+ * afterwards.
+ */
+public class GarbageCollector
+{
+ /**
+ * Collect and delete the garbage in the cache.
+ */
+ public static void collect (File cacheDir, final long retentionPeriodMillis)
+ {
+ FileUtil.walkTree(cacheDir, new FileUtil.Visitor() {
+ @Override public void visit (File file) {
+ File cachedFile = getCachedFile(file);
+ File lastAccessedFile = getLastAccessedFile(file);
+ if (!cachedFile.exists() || !lastAccessedFile.exists()) {
+ if (cachedFile.exists()) {
+ cachedFile.delete();
+ } else {
+ lastAccessedFile.delete();
+ }
+ } else if (shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
+ lastAccessedFile.delete();
+ cachedFile.delete();
+ }
+
+ File folder = file.getParentFile();
+ if (folder.list().length == 0) {
+ folder.delete();
+ }
+ }
+ });
+ }
+
+ private static boolean shouldDelete (File lastAccessedFile, long retentionMillis)
+ {
+ return System.currentTimeMillis() - lastAccessedFile.lastModified() > retentionMillis;
+ }
+
+ private static File getLastAccessedFile (File file)
+ {
+ return isLastAccessedFile(file) ? file : new File(
+ file.getParentFile(), file.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ private static boolean isLastAccessedFile (File file)
+ {
+ return file.getName().endsWith(ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ private static File getCachedFile (File file)
+ {
+ return !isLastAccessedFile(file) ? file : new File(
+ file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
+ }
+}
diff --git a/src/main/java/com/threerings/getdown/classpath/cache/ResourceCache.java b/src/main/java/com/threerings/getdown/classpath/cache/ResourceCache.java
new file mode 100644
index 0000000..6e35b3f
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/cache/ResourceCache.java
@@ -0,0 +1,73 @@
+package com.threerings.getdown.classpath.cache;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.threerings.getdown.util.FileUtil;
+
+/**
+ * Maintains a cache of code resources. The cache allows multiple application instances of different
+ * versions to open at the same time.
+ */
+public class ResourceCache
+{
+ public ResourceCache (File _cacheDir) throws IOException
+ {
+ this._cacheDir = _cacheDir;
+
+ createDirectoryIfNecessary(_cacheDir);
+ }
+
+ private void createDirectoryIfNecessary (File dir) throws IOException
+ {
+ if (!dir.exists() && !dir.mkdirs()) {
+ throw new IOException("unable to create directory: " + dir.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Caches the given file under it's {@code digest}.
+ *
+ * @return the cached file
+ */
+ public File cacheFile (File fileToCache, String digest) throws IOException
+ {
+ File cacheLocation = new File(_cacheDir, digest.substring(0, 2));
+ createDirectoryIfNecessary(cacheLocation);
+
+ File cachedFile = new File(cacheLocation, digest + getFileSuffix(fileToCache));
+ File lastAccessedFile = new File(
+ cacheLocation, cachedFile.getName() + LAST_ACCESSED_FILE_SUFFIX);
+
+ if (!cachedFile.exists()) {
+ createNewFile(cachedFile);
+ FileUtil.copy(fileToCache, cachedFile);
+ }
+
+ if (lastAccessedFile.exists()) {
+ lastAccessedFile.setLastModified(System.currentTimeMillis());
+ } else {
+ createNewFile(lastAccessedFile);
+ }
+
+ return cachedFile;
+ }
+
+ private void createNewFile (File fileToCreate) throws IOException
+ {
+ if (!fileToCreate.exists() && !fileToCreate.createNewFile()) {
+ throw new IOException("unable to create new file: " + fileToCreate.getAbsolutePath());
+ }
+ }
+
+ private String getFileSuffix (File fileToCache) {
+ String fileName = fileToCache.getName();
+ int index = fileName.lastIndexOf(".");
+
+ return index > -1 ? fileName.substring(index) : "";
+ }
+
+ private final File _cacheDir;
+
+ static final String LAST_ACCESSED_FILE_SUFFIX = ".lastAccessed";
+}
diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java
index 9b6e961..ce72f50 100644
--- a/src/main/java/com/threerings/getdown/data/Application.java
+++ b/src/main/java/com/threerings/getdown/data/Application.java
@@ -33,6 +33,8 @@ import com.samskivert.util.StringUtil;
import org.apache.commons.codec.binary.Base64;
+import com.threerings.getdown.classpath.ClassPaths;
+import com.threerings.getdown.classpath.ClassPath;
import com.threerings.getdown.launcher.RotatingBackgrounds;
import com.threerings.getdown.util.*;
@@ -232,6 +234,32 @@ public class Application
_extraAppArgs = (appargs == null) ? ArrayUtil.EMPTY_STRING : appargs;
}
+ /**
+ * Returns the configured application directory.
+ */
+ public File getAppdir()
+ {
+ return _appdir;
+ }
+
+ /**
+ * Returns whether the application should cache code resources prior to launching the
+ * application.
+ */
+ public boolean useCodeCache ()
+ {
+ return _useCodeCache;
+ }
+
+ /**
+ * Returns the number of days a cached code resource is allowed to stay unused before it
+ * becomes eligible for deletion.
+ */
+ public int getCodeCacheRetentionDays ()
+ {
+ return _codeCacheRetentionDays;
+ }
+
/**
* Returns a resource that refers to the application configuration file itself.
*/
@@ -260,6 +288,14 @@ public class Application
return _resources;
}
+ /**
+ * Returns the digest of the given {@code resource}.
+ */
+ public String getDigest (Resource resource)
+ {
+ return _digest.getDigest(resource);
+ }
+
/**
* Returns a list of all the active {@link Resource} objects used by this application (code and
* non-code).
@@ -656,6 +692,11 @@ public class Application
// obtain a thread dump of the running JVM
_windebug = getLocalPath("debug.txt").exists();
+ // whether to cache code resources and launch from cache
+ _useCodeCache = Boolean.parseBoolean((String) cdata.get("use_code_cache"));
+ _codeCacheRetentionDays = cdata.containsKey("code_cache_retention_days") ?
+ Integer.parseInt((String) cdata.get("use_code_cache")) : 7;
+
// parse and return our application config
UpdateInterface ui = new UpdateInterface();
_name = ui.name = (String)cdata.get("ui.name");
@@ -901,16 +942,11 @@ public class Application
boolean dashJarMode = MANIFEST_CLASS.equals(_class);
// add the -classpath arguments if we're not in -jar mode
- StringBuilder cpbuf = new StringBuilder();
- for (Resource rsrc : getActiveCodeResources()) {
- if (cpbuf.length() > 0) {
- cpbuf.append(File.pathSeparator);
- }
- cpbuf.append(rsrc.getFinalTarget().getAbsolutePath());
- }
+ ClassPath classPath = ClassPaths.buildClassPath(this);
+
if (!dashJarMode) {
args.add("-classpath");
- args.add(cpbuf.toString());
+ args.add(classPath.asArgumentString());
}
// we love our Mac users, so we do nice things to preserve our application identity
@@ -960,7 +996,7 @@ public class Application
// if we're in -jar mode add those arguments, otherwise add the app class name
if (dashJarMode) {
args.add("-jar");
- args.add(cpbuf.toString());
+ args.add(classPath.asArgumentString());
} else {
args.add(_class);
}
@@ -1007,20 +1043,13 @@ public class Application
/**
* Runs this application directly in the current VM.
*/
- public void invokeDirect (JApplet applet)
+ public void invokeDirect (JApplet applet) throws IOException
{
- // create a custom class loader
- ArrayList jars = new ArrayList();
- for (Resource rsrc : getActiveCodeResources()) {
- try {
- jars.add(new URL("file", "", rsrc.getFinalTarget().getAbsolutePath()));
- } catch (Exception e) {
- e.printStackTrace(System.err);
- }
- }
- URLClassLoader loader = new URLClassLoader(
- jars.toArray(new URL[jars.size()]),
- ClassLoader.getSystemClassLoader()) {
+ ClassPath classPath = ClassPaths.buildClassPath(this);
+ URL[] jarUrls = classPath.asUrls();
+
+ // create custom class loader
+ URLClassLoader loader = new URLClassLoader(jarUrls, ClassLoader.getSystemClassLoader()) {
@Override protected PermissionCollection getPermissions (CodeSource code) {
Permissions perms = new Permissions();
perms.add(new AllPermission());
@@ -1030,7 +1059,7 @@ public class Application
Thread.currentThread().setContextClassLoader(loader);
log.info("Configured URL class loader:");
- for (URL url : jars) log.info(" " + url);
+ for (URL url : jarUrls) log.info(" " + url);
// configure any system properties that we can
for (String jvmarg : _jvmargs) {
@@ -1126,12 +1155,6 @@ public class Application
log.info("Verifying application: " + _vappbase);
log.info("Version: " + _version);
log.info("Class: " + _class);
-// log.info("Code: " +
-// StringUtil.toString(getCodeResources().iterator()));
-// log.info("Resources: " +
-// StringUtil.toString(getActiveResources().iterator()));
-// log.info("JVM Args: " + StringUtil.toString(_jvmargs.iterator()));
-// log.info("App Args: " + StringUtil.toString(_appargs.iterator()));
// this will read in the contents of the digest file and validate itself
try {
@@ -1780,6 +1803,9 @@ public class Application
protected List _codes = new ArrayList();
protected List _resources = new ArrayList();
+ protected boolean _useCodeCache;
+ protected int _codeCacheRetentionDays;
+
protected Map _auxgroups = new HashMap();
protected Map _auxactive = new HashMap();
diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java
index ecdde99..9d30c6f 100644
--- a/src/main/java/com/threerings/getdown/data/Digest.java
+++ b/src/main/java/com/threerings/getdown/data/Digest.java
@@ -10,10 +10,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
-
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-
import java.util.HashMap;
import java.util.List;
@@ -95,6 +93,14 @@ public class Digest
return false;
}
+ /**
+ * Returns the digest of the given {@code resource}.
+ */
+ public String getDigest (Resource resource)
+ {
+ return _digests.get(resource.getPath());
+ }
+
/**
* Creates a digest file at the specified location using the supplied list of resources.
*/
diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
index 06c8e5a..57818ff 100644
--- a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
+++ b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
@@ -184,6 +184,19 @@ public class GetdownApp
System.exit(exitCode);
}
}
+
+ @Override
+ protected void fail (String message) {
+ // if the frame was set to be undecorated, make window decoration available
+ // to allow the user to close the window
+ if (_frame != null && _frame.isUndecorated()) {
+ _frame.dispose();
+ _frame.setUndecorated(false);
+ showContainer();
+ }
+ super.fail(message);
+ }
+
protected JFrame _frame;
};
app.start();
diff --git a/src/main/java/com/threerings/getdown/util/ConfigUtil.java b/src/main/java/com/threerings/getdown/util/ConfigUtil.java
index ae82b4e..0a8bf2b 100644
--- a/src/main/java/com/threerings/getdown/util/ConfigUtil.java
+++ b/src/main/java/com/threerings/getdown/util/ConfigUtil.java
@@ -142,8 +142,8 @@ public class ConfigUtil
// if we're checking qualifiers and the os doesn't match this qualifier, skip it
String quals = pair[1].substring(1, qidx);
if (osname != null && !checkQualifiers(quals, osname, osarch)) {
- log.info("Skipping", "quals", quals, "osname", osname, "osarch", osarch,
- "key", pair[0], "value", pair[1]);
+ log.debug("Skipping", "quals", quals, "osname", osname, "osarch", osarch,
+ "key", pair[0], "value", pair[1]);
continue;
}
// otherwise filter out the qualifier text
diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java
index efde120..05b026d 100644
--- a/src/main/java/com/threerings/getdown/util/FileUtil.java
+++ b/src/main/java/com/threerings/getdown/util/FileUtil.java
@@ -6,7 +6,10 @@
package com.threerings.getdown.util;
import java.io.*;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
import java.util.List;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
@@ -121,4 +124,47 @@ public class FileUtil extends com.samskivert.util.FileUtil
StreamUtil.close(packedJarIn);
}
}
+
+ /**
+ * Copies the given {@code source} file to the given {@code target}.
+ */
+ public static void copy (File source, File target) throws IOException {
+ FileInputStream in = null;
+ FileOutputStream out = null;
+ try {
+ in = new FileInputStream(source);
+ out = new FileOutputStream(target);
+ StreamUtil.copy(in, out);
+ } finally {
+ StreamUtil.close(in);
+ StreamUtil.close(out);
+ }
+ }
+
+ /**
+ * Used by {@link #walkTree}.
+ */
+ public interface Visitor
+ {
+ void visit (File file);
+ }
+
+ /**
+ * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
+ */
+ public static void walkTree (File root, Visitor visitor)
+ {
+ Deque stack = new ArrayDeque(Arrays.asList(root.listFiles()));
+ while (!stack.isEmpty()) {
+ File currentFile = stack.pop();
+ if (currentFile.exists()) {
+ visitor.visit(currentFile);
+ if (currentFile.isDirectory()) {
+ for (File file: currentFile.listFiles()) {
+ stack.push(file);
+ }
+ }
+ }
+ }
+ }
}
diff --git a/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/src/main/java/com/threerings/getdown/util/LaunchUtil.java
index 1aeabdf..716961d 100644
--- a/src/main/java/com/threerings/getdown/util/LaunchUtil.java
+++ b/src/main/java/com/threerings/getdown/util/LaunchUtil.java
@@ -10,7 +10,6 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
-import com.samskivert.io.StreamUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
@@ -155,7 +154,7 @@ public class LaunchUtil
try {
// copy the moved file back to getdown-dop-new.jar so that we don't end up
// downloading another copy next time
- StreamUtil.copy(new FileInputStream(curgd), new FileOutputStream(newgd));
+ FileUtil.copy(curgd, newgd);
} catch (IOException e) {
log.warning("Error copying updated Getdown back: " + e);
}
@@ -172,7 +171,7 @@ public class LaunchUtil
// that didn't work, let's try copying it
log.info("Attempting to upgrade by copying over " + curgd + "...");
try {
- StreamUtil.copy(new FileInputStream(newgd), new FileOutputStream(curgd));
+ FileUtil.copy(newgd, curgd);
} catch (IOException ioe) {
log.warning("Mayday! Brute force copy method also failed.", ioe);
}
diff --git a/src/main/resources/com/threerings/getdown/messages.properties b/src/main/resources/com/threerings/getdown/messages.properties
index b73f08e..c219af9 100644
--- a/src/main/resources/com/threerings/getdown/messages.properties
+++ b/src/main/resources/com/threerings/getdown/messages.properties
@@ -56,7 +56,7 @@ m.init_failed = Our configuration file is missing or corrupt. Attempting \
to download a new copy...
m.java_download_failed = We were unable to automatically download the \
- necessary verson of Java for your computer.\n\n\
+ necessary version of Java for your computer.\n\n\
Please go to www.java.com and download the latest version of \
Java, then try running the application again.
@@ -99,7 +99,7 @@ m.corrupt_digest_signature_error = We couldn't verify the application's digital
m.default_install_error = the support section of the website
m.another_getdown_running = Multiple instances of this application's \
- installer are running. This one will stop and let another complete.
+ installer are running. This one will stop and let another complete.
m.applet_stopped = Getdown's applet was told to stop working.
diff --git a/src/main/resources/com/threerings/getdown/messages_de.properties b/src/main/resources/com/threerings/getdown/messages_de.properties
index 5326af8..8d9b83c 100644
--- a/src/main/resources/com/threerings/getdown/messages_de.properties
+++ b/src/main/resources/com/threerings/getdown/messages_de.properties
@@ -66,7 +66,7 @@ Anwendung erneut starten.
m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \
entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \
-Festplatte frei sind und versuche dann, die Anwendung erneut zu \
+Festplatte frei sind und versuche dann die Anwendung erneut zu \
starten.\n\n\ \
Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \
Javaversion herunterladen und installieren und dann erneut versuchen.
@@ -113,10 +113,10 @@ m.applet_stopped = Die Anwendung wurde beendet.
# application/digest errors
-m.missing_appbase = In der Konfigurations-Datei fehlt die 'appbase'.
-m.invalid_version = In der Konfigurations-Datei steht die falsche Version.
-m.invalid_appbase = In der Konfigurations-Datei steht die falsche 'appbase'.
-m.missing_class = In der Konfigurations-Datei fehlt die Anwendungs-Klasse.
-m.missing_code = Die Konfigurations-Datei enth\u00e4lt keine Code-Quellen.
-m.invalid_digest_file = Die \u00dcbersichts-Datei ist ung\u00fcltig.
+m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'.
+m.invalid_version = In der Konfigurationsdatei steht die falsche Version.
+m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'.
+m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse.
+m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen.
+m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig.
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java b/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
new file mode 100644
index 0000000..fb90a80
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
@@ -0,0 +1,49 @@
+package com.threerings.getdown.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.LinkedHashSet;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for {@link ClassPath}.
+ */
+public class ClassPathTest
+{
+ @Before public void createJarsAndSetupClassPath () throws IOException
+ {
+ _firstJar = _folder.newFile("a.jar");
+ _secondJar = _folder.newFile("b.jar");
+
+ LinkedHashSet classPathEntries = new LinkedHashSet();
+ classPathEntries.add(_firstJar);
+ classPathEntries.add(_secondJar);
+ _classPath = new ClassPath(classPathEntries);
+ }
+
+ @Test public void shouldCreateValidArgumentString ()
+ {
+ assertEquals(
+ _firstJar.getAbsolutePath() + File.pathSeparator + _secondJar.getAbsolutePath(),
+ _classPath.asArgumentString());
+ }
+
+ @Test public void shouldProvideJarUrls () throws MalformedURLException, URISyntaxException
+ {
+ URL[] actualUrls = _classPath.asUrls();
+ assertEquals(_firstJar, new File(actualUrls[0].toURI()));
+ assertEquals(_secondJar, new File(actualUrls[1].toURI()));
+ }
+
+ @Rule public TemporaryFolder _folder = new TemporaryFolder();
+
+ private File _firstJar, _secondJar;
+ private ClassPath _classPath;
+}
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java b/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java
new file mode 100644
index 0000000..956585a
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java
@@ -0,0 +1,68 @@
+package com.threerings.getdown.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import static org.junit.Assert.assertEquals;
+
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import static org.mockito.Mockito.when;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Resource;
+import com.threerings.getdown.util.FileUtil;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClassPathsTest
+{
+ @Before public void setupFilesAndResources () throws IOException
+ {
+ _firstJarFile = _appdir.newFile("a.jar");
+ _secondJarFile = _appdir.newFile("b.jar");
+
+ when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
+ when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
+ when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
+ when(_application.getAppdir()).thenReturn(_appdir.getRoot());
+ }
+
+ @Test public void shouldBuildDefaultClassPath () throws IOException
+ {
+ ClassPath classPath = ClassPaths.buildDefaultClassPath(_application);
+ String expectedClassPath = _firstJarFile.getAbsolutePath() + File.pathSeparator +
+ _secondJarFile.getAbsolutePath();
+ assertEquals(expectedClassPath, classPath.asArgumentString());
+ }
+
+ @Test public void shouldBuildCachedClassPath () throws IOException
+ {
+ when(_application.getDigest(_firstJar)).thenReturn("first");
+ when(_application.getDigest(_secondJar)).thenReturn("second");
+ when(_application.getCodeCacheRetentionDays()).thenReturn(1);
+
+ File firstCachedJarFile = FileUtil.newFile(
+ _appdir.getRoot(), ClassPaths.CACHE_DIR, "fi", "first.jar");
+
+ File secondCachedJarFile = FileUtil.newFile(
+ _appdir.getRoot(), ClassPaths.CACHE_DIR, "se", "second.jar");
+
+ String expectedClassPath = firstCachedJarFile.getAbsolutePath() + File.pathSeparator +
+ secondCachedJarFile.getAbsolutePath();
+
+ ClassPath classPath = ClassPaths.buildCachedClassPath(_application);
+ assertEquals(expectedClassPath, classPath.asArgumentString());
+ }
+
+ @Mock protected Application _application;
+ @Mock protected Resource _firstJar;
+ @Mock protected Resource _secondJar;
+
+ protected File _firstJarFile, _secondJarFile;
+
+ @Rule public TemporaryFolder _appdir = new TemporaryFolder();
+}
diff --git a/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java b/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
new file mode 100644
index 0000000..ee86685
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
@@ -0,0 +1,66 @@
+package com.threerings.getdown.classpath.cache;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Validates that cache garbage is collected and deleted correctly.
+ */
+public class GarbageCollectorTest
+{
+ @Before public void setupFiles () throws IOException
+ {
+ _cachedFile = _folder.newFile("abc123.jar");
+ _lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ @Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
+ {
+ gcNow();
+ assertFalse(_cachedFile.exists());
+ assertFalse(_lastAccessedFile.exists());
+ }
+
+ @Test public void shouldDeleteCacheFolderIfFolderIsEmpty ()
+ {
+ gcNow();
+ assertFalse(_folder.getRoot().exists());
+ }
+
+ private void gcNow() {
+ GarbageCollector.collect(_folder.getRoot(), -1);
+ }
+
+ @Test public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
+ {
+ GarbageCollector.collect(_folder.getRoot(), TimeUnit.DAYS.toMillis(1));
+ assertTrue(_cachedFile.exists());
+ assertTrue(_lastAccessedFile.exists());
+ }
+
+ @Test public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
+ {
+ assumeTrue(_lastAccessedFile.delete());
+ gcNow();
+ assertFalse(_cachedFile.exists());
+ }
+
+ @Test public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
+ {
+ assumeTrue(_cachedFile.delete());
+ gcNow();
+ assertFalse(_lastAccessedFile.exists());
+ }
+
+ @Rule public TemporaryFolder _folder = new TemporaryFolder();
+
+ private File _cachedFile;
+ private File _lastAccessedFile;
+}
diff --git a/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java b/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
new file mode 100644
index 0000000..8002f0b
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
@@ -0,0 +1,67 @@
+package com.threerings.getdown.classpath.cache;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.*;
+
+/**
+ * Asserts the correct functionality of the {@link ResourceCache}.
+ */
+public class ResourceCacheTest
+{
+ @Before public void setupCache () throws IOException {
+ _fileToCache = _folder.newFile("filetocache.jar");
+ _cache = new ResourceCache(_folder.newFolder(".cache"));
+ }
+
+ @Test public void shouldCacheFile () throws IOException
+ {
+ assertEquals("abc123.jar", cacheFile().getName());
+ }
+
+ private File cacheFile() throws IOException
+ {
+ return _cache.cacheFile(_fileToCache, "abc123");
+ }
+
+ @Test public void shouldTrackFileUsage () throws IOException
+ {
+ String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
+ File lastAccessedFile = new File(cacheFile().getParentFile(), name);
+ assertTrue(lastAccessedFile.exists());
+ }
+
+ @Test public void shouldNotCacheTheSameFile () throws Exception
+ {
+ File cachedFile = cacheFile();
+ cachedFile.setLastModified(YESTERDAY);
+ long expectedLastModified = cachedFile.lastModified();
+ // caching it another time
+ File sameCachedFile = cacheFile();
+ assertEquals(expectedLastModified, sameCachedFile.lastModified());
+ }
+
+ @Test public void shouldRememberWhenFileWasRequested () throws Exception
+ {
+ File cachedFile = cacheFile();
+ String name = cachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
+ File lastAccessedFile = new File(cachedFile.getParentFile(), name);
+ lastAccessedFile.setLastModified(YESTERDAY);
+ long lastAccessed = lastAccessedFile.lastModified();
+ // caching it another time
+ cacheFile();
+ assertTrue(lastAccessedFile.lastModified() > lastAccessed);
+ }
+
+ @Rule public TemporaryFolder _folder = new TemporaryFolder();
+
+ private File _fileToCache;
+ private ResourceCache _cache;
+
+ private static final long YESTERDAY = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+}
diff --git a/src/test/java/com/threerings/getdown/data/SysPropsTest.java b/src/test/java/com/threerings/getdown/data/SysPropsTest.java
index 2b83ba1..342f4f6 100644
--- a/src/test/java/com/threerings/getdown/data/SysPropsTest.java
+++ b/src/test/java/com/threerings/getdown/data/SysPropsTest.java
@@ -5,16 +5,25 @@
package com.threerings.getdown.data;
-import org.junit.*;
+import org.junit.Test;
-public class SysPropsTest {
+import static org.junit.Assert.assertTrue;
- @Test public void testParseJavaVersion () {
- long vers = SysProps.parseJavaVersion("java.version", "(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?");
- assert(vers > 1060000);
+public class SysPropsTest
+{
+ @Test
+ public void shouldParseJavaVersion ()
+ {
+ long version = SysProps.parseJavaVersion(
+ "java.version", "(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?");
+ assertTrue(version > 1060000);
+ }
- long runVers = SysProps.parseJavaVersion("java.runtime.version",
- "(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?(-b\\d+)?");
- assert(runVers > 106000000);
- }
+ @Test
+ public void shouldParseJavaRuntimeVersion ()
+ {
+ long version = SysProps.parseJavaVersion(
+ "java.runtime.version", "(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?(-b\\d+)?");
+ assertTrue(version > 106000000);
+ }
}
diff --git a/src/test/java/com/threerings/getdown/util/FileUtilTest.java b/src/test/java/com/threerings/getdown/util/FileUtilTest.java
index c5c64f4..b9d68cd 100644
--- a/src/test/java/com/threerings/getdown/util/FileUtilTest.java
+++ b/src/test/java/com/threerings/getdown/util/FileUtilTest.java
@@ -5,11 +5,14 @@
package com.threerings.getdown.util;
+import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+
import static org.junit.Assert.*;
/**
@@ -27,4 +30,31 @@ public class FileUtilTest
assertEquals(linesBySplit[ii], lines.get(ii));
}
}
+
+ @Test public void shouldCopyFile () throws IOException
+ {
+ File source = _folder.newFile("source.txt");
+ File target = new File(_folder.getRoot(), "target.txt");
+ assertFalse(target.exists());
+ FileUtil.copy(source, target);
+ assertTrue(target.exists());
+ }
+
+ @Test public void shouldRecursivelyWalkOverFilesAndFolders () throws IOException
+ {
+ _folder.newFile("a.txt");
+ new File(_folder.newFolder("b"), "b.txt").createNewFile();
+
+ class CountingVisitor implements FileUtil.Visitor {
+ int fileCount = 0;
+ @Override public void visit(File file) {
+ fileCount++;
+ }
+ }
+ CountingVisitor visitor = new CountingVisitor();
+ FileUtil.walkTree(_folder.getRoot(), visitor);
+ assertEquals(3, visitor.fileCount);
+ }
+
+ @Rule public TemporaryFolder _folder = new TemporaryFolder();
}