diff --git a/LICENSE b/LICENSE index 883d10c..0d9b255 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Getdown - application installer, patcher and launcher -Copyright (C) 2004-2014 Three Rings Design, Inc. +Copyright (C) 2004-2016 Getdown authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/pom.xml b/pom.xml index 96de9f2..acb34bd 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,8 @@ - GNU Lesser General Public License (LGPL), Version 2.1 - http://www.fsf.org/licensing/licenses/lgpl.txt + The (New) BSD License + http://www.opensource.org/licenses/bsd-license.php repo @@ -90,6 +90,12 @@ 4.8.1 test + + org.mockito + mockito-core + 1.10.19 + test + @@ -109,8 +115,8 @@ maven-compiler-plugin 3.3 - 1.5 - 1.5 + 1.6 + 1.6 true true true @@ -211,6 +217,7 @@ * * * + true diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPath.java b/src/main/java/com/threerings/getdown/classpath/ClassPath.java new file mode 100644 index 0000000..d96b712 --- /dev/null +++ b/src/main/java/com/threerings/getdown/classpath/ClassPath.java @@ -0,0 +1,71 @@ +package com.threerings.getdown.classpath; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Represents the class path and it's elements of the application to be launched. The class path can + * either be represented as an {@link #asArgumentString() argument string} for the java command line + * or as an {@link #asUrls() array of URLs} to be used by a {@link URLClassLoader}. + */ +public class ClassPath +{ + public ClassPath (LinkedHashSet 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(); }