Split Getdown into multiple (Maven) modules.
- core: the main Getdown logic code - tools: code to create digest.txt & patch files - launcher: the standalone launcher/updater app - ant: the Ant task for creating digest.txt files This paves the way for a proper Jigsaw-ification of the Getdown code. I may further factor code out of getdown-launcher and into getdown-core to enable the use-case where an app embeds Getdown completely and does not use the launcher app/UI. That will also make it easier to create a JavaFX UI and retire the old Swing UI. This also moves the obsolete applet code into a separate applet module, which is merely a temporary holding area. It will be deleted in the next commit.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.threerings</groupId>
|
||||
<artifactId>getdown</artifactId>
|
||||
<version>1.7.2-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>getdown-core</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Getdown Core</name>
|
||||
<description>Core Getdown functionality</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.samskivert</groupId>
|
||||
<artifactId>samskivert</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>1.10.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource> <!-- include the LICENSE file in the jar -->
|
||||
<directory>..</directory>
|
||||
<includes><include>LICENSE</include></includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown;
|
||||
|
||||
import com.samskivert.util.Logger;
|
||||
|
||||
/**
|
||||
* A placeholder class that contains a reference to the log object used by the Getdown code.
|
||||
*/
|
||||
public class Log
|
||||
{
|
||||
/** We dispatch our log messages through this logger. */
|
||||
public static final Logger log = Logger.getLogger("com.threerings.getdown");
|
||||
}
|
||||
@@ -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<File> classPathEntries)
|
||||
{
|
||||
_classPathEntries = Collections.unmodifiableSet(classPathEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class path as an java command line argument string, e.g.
|
||||
*
|
||||
* <pre>
|
||||
* /path/to/a.jar:/path/to/b.jar
|
||||
* </pre>
|
||||
*/
|
||||
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<File> 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<File> _classPathEntries;
|
||||
}
|
||||
@@ -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() ? buildCachedClassPath(app) : buildDefaultClassPath(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<File> classPathEntries = new LinkedHashSet<File>();
|
||||
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<File> classPathEntries = new LinkedHashSet<File>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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()) {
|
||||
FileUtil.deleteHarder(cachedFile);
|
||||
} else {
|
||||
FileUtil.deleteHarder(lastAccessedFile);
|
||||
}
|
||||
} else if (shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
|
||||
FileUtil.deleteHarder(lastAccessedFile);
|
||||
FileUtil.deleteHarder(cachedFile);
|
||||
}
|
||||
|
||||
File folder = file.getParentFile();
|
||||
if (folder != null) {
|
||||
String[] children = folder.list();
|
||||
if (children != null && children.length == 0) {
|
||||
FileUtil.deleteHarder(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(".")));
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import com.samskivert.text.MessageUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.getdown.util.Config;
|
||||
import com.threerings.getdown.util.ProgressObserver;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
* Manages the <code>digest.txt</code> file and the computing and processing of digests for an
|
||||
* application.
|
||||
*/
|
||||
public class Digest
|
||||
{
|
||||
/** The current version of the digest protocol. */
|
||||
public static final int VERSION = 2;
|
||||
|
||||
/**
|
||||
* Returns the name of the digest file for the specified protocol version.
|
||||
*/
|
||||
public static String digestFile (int version) {
|
||||
String infix = version > 1 ? String.valueOf(version) : "";
|
||||
return FILE_NAME + infix + FILE_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the crypto algorithm used to sign digest files of the specified version.
|
||||
*/
|
||||
public static String sigAlgorithm (int version) {
|
||||
switch (version) {
|
||||
case 1: return "SHA1withRSA";
|
||||
case 2: return "SHA256withRSA";
|
||||
default: throw new IllegalArgumentException("Invalid digest version " + version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest file at the specified location using the supplied list of resources.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public static void createDigest (int version, List<Resource> resources, File output)
|
||||
throws IOException
|
||||
{
|
||||
// first compute the digests for all the resources in parallel
|
||||
ExecutorService exec = Executors.newFixedThreadPool(
|
||||
Runtime.getRuntime().availableProcessors());
|
||||
final Map<Resource, String> digests = new ConcurrentHashMap<>();
|
||||
final BlockingQueue<Object> completed = new LinkedBlockingQueue<>();
|
||||
final int fversion = version;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
Set<Resource> pending = new HashSet<>(resources);
|
||||
for (final Resource rsrc : resources) {
|
||||
exec.execute(new Runnable() {
|
||||
public void run () {
|
||||
try {
|
||||
MessageDigest md = getMessageDigest(fversion);
|
||||
digests.put(rsrc, rsrc.computeDigest(fversion, md, null));
|
||||
completed.add(rsrc);
|
||||
} catch (Throwable t) {
|
||||
completed.add(new IOException("Error computing digest for: " + rsrc).
|
||||
initCause(t));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// queue a shutdown of the thread pool when the tasks are done
|
||||
exec.shutdown();
|
||||
|
||||
try {
|
||||
while (pending.size() > 0) {
|
||||
Object done = completed.poll(600, TimeUnit.SECONDS);
|
||||
if (done instanceof IOException) {
|
||||
throw (IOException)done;
|
||||
} else if (done instanceof Resource) {
|
||||
pending.remove((Resource)done);
|
||||
} else {
|
||||
throw new AssertionError("What is this? " + done);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
throw new IOException("Timeout computing digests. Wow.");
|
||||
}
|
||||
|
||||
StringBuilder data = new StringBuilder();
|
||||
try (FileOutputStream fos = new FileOutputStream(output);
|
||||
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
|
||||
PrintWriter pout = new PrintWriter(osw)) {
|
||||
// compute and append the digest of each resource in the list
|
||||
for (Resource rsrc : resources) {
|
||||
String path = rsrc.getPath();
|
||||
String digest = digests.get(rsrc);
|
||||
note(data, path, digest);
|
||||
pout.println(path + " = " + digest);
|
||||
}
|
||||
// finally compute and append the digest for the file contents
|
||||
MessageDigest md = getMessageDigest(version);
|
||||
byte[] contents = data.toString().getBytes(UTF_8);
|
||||
String filename = digestFile(version);
|
||||
pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
log.debug("Computed digests [rsrcs=" + resources.size() + ", time=" + elapsed + "ms]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains an appropriate message digest instance for use by the Getdown system.
|
||||
*/
|
||||
public static MessageDigest getMessageDigest (int version)
|
||||
{
|
||||
String algo = version > 1 ? "SHA-256" : "MD5";
|
||||
try {
|
||||
return MessageDigest.getInstance(algo);
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
throw new RuntimeException("JVM does not support " + algo + ". Gurp!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest instance which will parse and validate the digest in the supplied
|
||||
* application directory, using the current digest version.
|
||||
*/
|
||||
public Digest (File appdir, boolean strictComments) throws IOException {
|
||||
this(appdir, VERSION, strictComments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a digest instance which will parse and validate the digest in the supplied
|
||||
* application directory.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public Digest (File appdir, int version, boolean strictComments) throws IOException
|
||||
{
|
||||
// parse and validate our digest file contents
|
||||
String filename = digestFile(version);
|
||||
StringBuilder data = new StringBuilder();
|
||||
File dfile = new File(appdir, filename);
|
||||
Config.ParseOpts opts = Config.createOpts(false);
|
||||
opts.strictComments = strictComments;
|
||||
// bias = toward key: the key is the filename and could conceivably contain = signs, value
|
||||
// is the hex encoded hash which will not contain =
|
||||
opts.biasToKey = true;
|
||||
for (String[] pair : Config.parsePairs(dfile, opts)) {
|
||||
if (pair[0].equals(filename)) {
|
||||
_metaDigest = pair[1];
|
||||
break;
|
||||
}
|
||||
_digests.put(pair[0], pair[1]);
|
||||
note(data, pair[0], pair[1]);
|
||||
}
|
||||
|
||||
// we've reached the end, validate our contents
|
||||
MessageDigest md = getMessageDigest(version);
|
||||
byte[] contents = data.toString().getBytes(UTF_8);
|
||||
String hash = StringUtil.hexlate(md.digest(contents));
|
||||
if (!hash.equals(_metaDigest)) {
|
||||
String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, hash);
|
||||
throw new IOException(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the digest for the digest file.
|
||||
*/
|
||||
public String getMetaDigest ()
|
||||
{
|
||||
return _metaDigest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the hash of the specified resource and compares it with the value parsed from
|
||||
* the digest file. Logs a message if the resource fails validation.
|
||||
*
|
||||
* @return true if the resource is valid, false if it failed the digest check or if an I/O
|
||||
* error was encountered during the validation process.
|
||||
*/
|
||||
public boolean validateResource (Resource resource, ProgressObserver obs)
|
||||
{
|
||||
try {
|
||||
String chash = resource.computeDigest(VERSION, getMessageDigest(VERSION), obs);
|
||||
String ehash = _digests.get(resource.getPath());
|
||||
if (chash.equals(ehash)) {
|
||||
return true;
|
||||
}
|
||||
log.info("Resource failed digest check",
|
||||
"rsrc", resource, "computed", chash, "expected", ehash);
|
||||
} catch (Throwable t) {
|
||||
log.info("Resource failed digest check", "rsrc", resource, "error", t);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the digest of the given {@code resource}.
|
||||
*/
|
||||
public String getDigest (Resource resource)
|
||||
{
|
||||
return _digests.get(resource.getPath());
|
||||
}
|
||||
|
||||
/** Used by {@link #createDigest} and {@link Digest}. */
|
||||
protected static void note (StringBuilder data, String path, String digest)
|
||||
{
|
||||
data.append(path).append(" = ").append(digest).append("\n");
|
||||
}
|
||||
|
||||
protected HashMap<String, String> _digests = new HashMap<>();
|
||||
protected String _metaDigest = "";
|
||||
|
||||
protected static final String FILE_NAME = "digest";
|
||||
protected static final String FILE_SUFFIX = ".txt";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.data;
|
||||
|
||||
/**
|
||||
* System property constants associated with Getdown.
|
||||
*/
|
||||
public class Properties
|
||||
{
|
||||
/** This property will be set to "true" on the application when it is being run by getdown. */
|
||||
public static final String GETDOWN = "com.threerings.getdown";
|
||||
|
||||
/** If accepting connections from the launched application, this property
|
||||
* will be set to the connection server port. */
|
||||
public static final String CONNECT_PORT = "com.threerings.getdown.connectPort";
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.data;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.getdown.util.FileUtil;
|
||||
import com.threerings.getdown.util.ProgressObserver;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* Models a single file resource used by an {@link Application}.
|
||||
*/
|
||||
public class Resource implements Comparable<Resource>
|
||||
{
|
||||
/** Defines special attributes for resources. */
|
||||
public static enum Attr {
|
||||
/** Indicates that the resource should be unpacked. */
|
||||
UNPACK,
|
||||
/** Indicates that the resource should be marked executable. */
|
||||
EXEC
|
||||
};
|
||||
|
||||
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> EXEC = EnumSet.of(Attr.EXEC);
|
||||
|
||||
/**
|
||||
* Computes the MD5 hash of the supplied file.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public static String computeDigest (int version, File target, MessageDigest md,
|
||||
ProgressObserver obs)
|
||||
throws IOException
|
||||
{
|
||||
md.reset();
|
||||
byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
|
||||
int read;
|
||||
|
||||
boolean isJar = isJar(target.getPath());
|
||||
boolean isPacked200Jar = isPacked200Jar(target.getPath());
|
||||
|
||||
// if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
|
||||
// manner to properly correlate jardiff patched jars with their unpatched originals
|
||||
if (isJar || isPacked200Jar){
|
||||
File tmpJarFile = null;
|
||||
JarFile jar = null;
|
||||
try {
|
||||
// if this is a compressed jar file, uncompress it to compute the jar file digest
|
||||
if (isPacked200Jar){
|
||||
tmpJarFile = new File(target.getPath() + ".tmp");
|
||||
FileUtil.unpackPacked200Jar(target, tmpJarFile);
|
||||
jar = new JarFile(tmpJarFile);
|
||||
} else{
|
||||
jar = new JarFile(target);
|
||||
}
|
||||
|
||||
List<JarEntry> entries = Collections.list(jar.entries());
|
||||
Collections.sort(entries, ENTRY_COMP);
|
||||
|
||||
int eidx = 0;
|
||||
for (JarEntry entry : entries) {
|
||||
// old versions of the digest code skipped metadata
|
||||
if (version < 2) {
|
||||
if (entry.getName().startsWith("META-INF")) {
|
||||
updateProgress(obs, eidx, entries.size());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try (InputStream in = jar.getInputStream(entry)) {
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(obs, eidx, entries.size());
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (jar != null) {
|
||||
try {
|
||||
jar.close();
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
|
||||
}
|
||||
}
|
||||
if (tmpJarFile != null) {
|
||||
FileUtil.deleteHarder(tmpJarFile);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
long totalSize = target.length(), position = 0L;
|
||||
try (FileInputStream fin = new FileInputStream(target)) {
|
||||
while ((read = fin.read(buffer)) != -1) {
|
||||
md.update(buffer, 0, read);
|
||||
position += read;
|
||||
updateProgress(obs, position, totalSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringUtil.hexlate(md.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a resource with the supplied remote URL and local path.
|
||||
*/
|
||||
public Resource (String path, URL remote, File local, EnumSet<Attr> attrs)
|
||||
{
|
||||
_path = path;
|
||||
_remote = remote;
|
||||
_local = local;
|
||||
_localNew = new File(local.toString() + "_new");
|
||||
String lpath = _local.getPath();
|
||||
_marker = new File(lpath + "v");
|
||||
|
||||
_attrs = attrs;
|
||||
_isJar = isJar(lpath);
|
||||
_isPacked200Jar = isPacked200Jar(lpath);
|
||||
boolean unpack = attrs.contains(Attr.UNPACK);
|
||||
if (unpack && _isJar) {
|
||||
_unpacked = _local.getParentFile();
|
||||
} else if(unpack && _isPacked200Jar) {
|
||||
String dotJar = ".jar", lname = _local.getName();
|
||||
String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length());
|
||||
_unpacked = new File(_local.getParent(), uname);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path associated with this resource.
|
||||
*/
|
||||
public String getPath ()
|
||||
{
|
||||
return _path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local location of this resource.
|
||||
*/
|
||||
public File getLocal ()
|
||||
{
|
||||
return _local;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the to-be-installed new version of this resource.
|
||||
*/
|
||||
public File getLocalNew ()
|
||||
{
|
||||
return _localNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the unpacked resource.
|
||||
*/
|
||||
public File getUnpacked ()
|
||||
{
|
||||
return _unpacked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final target of this resource, whether it has been unpacked or not.
|
||||
*/
|
||||
public File getFinalTarget ()
|
||||
{
|
||||
return shouldUnpack() ? getUnpacked() : getLocal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the remote location of this resource.
|
||||
*/
|
||||
public URL getRemote ()
|
||||
{
|
||||
return _remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this resource should be unpacked as a part of the validation process.
|
||||
*/
|
||||
public boolean shouldUnpack ()
|
||||
{
|
||||
return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MD5 hash of this resource's underlying file.
|
||||
* <em>Note:</em> This is both CPU and I/O intensive.
|
||||
* @param version the version of the digest protocol to use.
|
||||
*/
|
||||
public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
|
||||
throws IOException
|
||||
{
|
||||
File file;
|
||||
if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) {
|
||||
file = _local;
|
||||
} else {
|
||||
file = _localNew.exists() ? _localNew : _local;
|
||||
}
|
||||
return computeDigest(version, file, md, obs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this resource has an associated "validated" marker
|
||||
* file.
|
||||
*/
|
||||
public boolean isMarkedValid ()
|
||||
{
|
||||
if (!_local.exists()) {
|
||||
clearMarker();
|
||||
return false;
|
||||
}
|
||||
return _marker.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "validated" marker file for this resource to indicate
|
||||
* that its MD5 hash has been computed and compared with the value in
|
||||
* the digest file.
|
||||
*
|
||||
* @throws IOException if we fail to create the marker file.
|
||||
*/
|
||||
public void markAsValid ()
|
||||
throws IOException
|
||||
{
|
||||
_marker.createNewFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any "validated" marker file associated with this resource.
|
||||
*/
|
||||
public void clearMarker ()
|
||||
{
|
||||
if (_marker.exists() && !FileUtil.deleteHarder(_marker)) {
|
||||
log.warning("Failed to erase marker file '" + _marker + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the {@code getLocalNew} version of this resource to {@code getLocal}.
|
||||
*/
|
||||
public void install () throws IOException {
|
||||
File source = getLocalNew(), dest = getLocal();
|
||||
log.info("- " + source);
|
||||
if (!FileUtil.renameTo(source, dest)) {
|
||||
throw new IOException("Failed to rename " + source + " to " + dest);
|
||||
}
|
||||
applyAttrs();
|
||||
markAsValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks this resource file into the directory that contains it.
|
||||
*/
|
||||
public void unpack () throws IOException
|
||||
{
|
||||
// sanity check
|
||||
if (!_isJar && !_isPacked200Jar) {
|
||||
throw new IOException("Requested to unpack non-jar file '" + _local + "'.");
|
||||
}
|
||||
if (_isJar) {
|
||||
try (JarFile jar = new JarFile(_local)) {
|
||||
FileUtil.unpackJar(jar, _unpacked);
|
||||
}
|
||||
} else {
|
||||
FileUtil.unpackPacked200Jar(_local, _unpacked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies this resources special attributes: unpacks this resource if needed, marks it as
|
||||
* executable if needed.
|
||||
*/
|
||||
public void applyAttrs () throws IOException {
|
||||
if (shouldUnpack()) {
|
||||
unpack();
|
||||
}
|
||||
if (_attrs.contains(Attr.EXEC)) {
|
||||
FileUtil.makeExecutable(_local);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes this resource file along with any "validated" marker file that may be associated with
|
||||
* it.
|
||||
*/
|
||||
public void erase ()
|
||||
{
|
||||
clearMarker();
|
||||
if (_local.exists() && !FileUtil.deleteHarder(_local)) {
|
||||
log.warning("Failed to erase resource '" + _local + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int compareTo (Resource other) {
|
||||
return _path.compareTo(other._path);
|
||||
}
|
||||
|
||||
@Override public boolean equals (Object other)
|
||||
{
|
||||
if (other instanceof Resource) {
|
||||
return _path.equals(((Resource)other)._path);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int hashCode ()
|
||||
{
|
||||
return _path.hashCode();
|
||||
}
|
||||
|
||||
@Override public String toString ()
|
||||
{
|
||||
return _path;
|
||||
}
|
||||
|
||||
/** Helper function to simplify the process of reporting progress. */
|
||||
protected static void updateProgress (ProgressObserver obs, long pos, long total)
|
||||
{
|
||||
if (obs != null) {
|
||||
obs.progress((int)(100 * pos / total));
|
||||
}
|
||||
}
|
||||
|
||||
protected static boolean isJar (String path)
|
||||
{
|
||||
return path.endsWith(".jar") || path.endsWith(".jar_new");
|
||||
}
|
||||
|
||||
protected static boolean isPacked200Jar (String path)
|
||||
{
|
||||
return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
|
||||
path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
|
||||
}
|
||||
|
||||
protected String _path;
|
||||
protected URL _remote;
|
||||
protected File _local, _localNew, _marker, _unpacked;
|
||||
protected EnumSet<Attr> _attrs;
|
||||
protected boolean _isJar, _isPacked200Jar;
|
||||
|
||||
/** Used to sort the entries in a jar file. */
|
||||
protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
|
||||
@Override public int compare (JarEntry e1, JarEntry e2) {
|
||||
return e1.getName().compareTo(e2.getName());
|
||||
}
|
||||
};
|
||||
|
||||
protected static final int DIGEST_BUFFER_SIZE = 5 * 1025;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.data;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.threerings.getdown.util.VersionUtil;
|
||||
|
||||
/**
|
||||
* This class encapsulates all system properties that are read and processed by Getdown. Don't
|
||||
* stick a call to {@code System.getProperty} randomly into the code, put it in here and give it an
|
||||
* accessor so that it's easy to see all of the secret system property arguments that Getdown makes
|
||||
* use of.
|
||||
*/
|
||||
public class SysProps
|
||||
{
|
||||
/** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */
|
||||
public static String appDir () {
|
||||
return System.getProperty("appdir");
|
||||
}
|
||||
|
||||
/** Configures the appid (in lieu of passing it in argv). Usage: {@code -Dappid=foo}. */
|
||||
public static String appId () {
|
||||
return System.getProperty("appid");
|
||||
}
|
||||
|
||||
/** Configures the appbase (in lieu of providing a skeleton getdown.txt, and as a last resort
|
||||
* fallback). Usage: {@code -Dappbase=URL}. */
|
||||
public static String appBase () {
|
||||
return System.getProperty("appbase");
|
||||
}
|
||||
|
||||
/** If true, disables redirection of logging into {@code launcher.log}.
|
||||
* Usage: {@code -Dno_log_redir}. */
|
||||
public static boolean noLogRedir () {
|
||||
return System.getProperty("no_log_redir") != null;
|
||||
}
|
||||
|
||||
/** Overrides the domain on {@code appbase}. Usage: {@code -Dappbase_domain=foo}. */
|
||||
public static String appbaseDomain () {
|
||||
return System.getProperty("appbase_domain");
|
||||
}
|
||||
|
||||
/** Overrides enter {@code appbase}. Usage: {@code -Dappbase_override=URL}. */
|
||||
public static String appbaseOverride () {
|
||||
return System.getProperty("appbase_override");
|
||||
}
|
||||
|
||||
/** If true, Getdown installs the app without ever bringing up a UI, except in the event of an
|
||||
* error. NOTE: it does not launch the app. See {@link #launchInSilent}.
|
||||
* Usage: {@code -Dsilent}. */
|
||||
public static boolean silent () {
|
||||
return System.getProperty("silent") != null;
|
||||
}
|
||||
|
||||
/** If true, Getdown does not automatically install updates after downloading them. It waits
|
||||
* for the application to call `Getdown.install`.
|
||||
* Usage: {@code -Dno_install}. */
|
||||
public static boolean noInstall () {
|
||||
return System.getProperty("no_install") != null;
|
||||
}
|
||||
|
||||
/** If true, Getdown installs the app without ever bringing up a UI and then launches it.
|
||||
* Usage: {@code -Dsilent=launch}. */
|
||||
public static boolean launchInSilent () {
|
||||
return "launch".equals(System.getProperty("silent"));
|
||||
}
|
||||
|
||||
/** Specifies the a delay (in minutes) to wait before starting the update and install process.
|
||||
* Usage: {@code -Ddelay=N}. */
|
||||
public static int startDelay () {
|
||||
return Integer.getInteger("delay", 0);
|
||||
}
|
||||
|
||||
/** If true, Getdown will not unpack {@code uresource} jars. Usage: {@code -Dno_unpack}. */
|
||||
public static boolean noUnpack () {
|
||||
return Boolean.getBoolean("no_unpack");
|
||||
}
|
||||
|
||||
/** If true, Getdown will run the application in the same VM in which Getdown is running. If
|
||||
* false (the default), Getdown will fork a new VM. Note that reusing the same VM prevents
|
||||
* Getdown from configuring some launch-time-only VM parameters (like -mxN etc.).
|
||||
* Usage: {@code -Ddirect}. */
|
||||
public static boolean direct () {
|
||||
return Boolean.getBoolean("direct");
|
||||
}
|
||||
|
||||
/** Specifies the connection timeout (in seconds) to use when downloading control files from
|
||||
* the server. This is chiefly useful when you are running in versionless mode and want Getdown
|
||||
* to more quickly timeout its startup update check if the server with which it is
|
||||
* communicating is not available. Usage: {@code -Dconnect_timeout=N}. */
|
||||
public static int connectTimeout () {
|
||||
return Integer.getInteger("connect_timeout", 0);
|
||||
}
|
||||
|
||||
/** Specifies the read timeout (in seconds) to use when downloading all files from the server.
|
||||
* The default is 30 seconds, meaning that if a download stalls for more than 30 seconds, the
|
||||
* update process wil fail. Setting the timeout to zero (or a negative value) will disable it.
|
||||
* Usage: {@code -Dread_timeout=N}. */
|
||||
public static int readTimeout () {
|
||||
return Integer.getInteger("read_timeout", 30);
|
||||
}
|
||||
|
||||
/** Parses a Java version system property using the supplied regular expression. The numbers
|
||||
* extracted from the regexp will be placed in each consecutive hundreds position in the
|
||||
* returned value.
|
||||
*
|
||||
* <p>For example, {@code java.version} takes the form {@code 1.8.0_31}, and with the regexp
|
||||
* {@code (\d+)\.(\d+)\.(\d+)(_\d+)?} we would parse {@code 1, 8, 0, 31} and combine them into
|
||||
* the final value {@code 1080031}.
|
||||
*
|
||||
* <p>Note that non-numeric characters matched by the regular expression will simply be
|
||||
* ignored, and optional groups which do not match are treated as zero in the final version
|
||||
* calculation.
|
||||
*
|
||||
* <p>One can instead parse {@code java.runtime.version} which takes the form {@code
|
||||
* 1.8.0_31-b13}. Using regexp {@code (\d+)\.(\d+)\.(\d+)_(\d+)-b(\d+)} we would parse
|
||||
* {@code 1, 8, 0, 31, 13} and combine them into a final value {@code 108003113}.
|
||||
*
|
||||
* <p>Other (or future) JVMs may provide different version properties which can be parsed as
|
||||
* desired using this general scheme as long as the numbers appear from left to right in order
|
||||
* of significance.
|
||||
*
|
||||
* @throws IllegalArgumentException if no system named {@code propName} exists, or if
|
||||
* {@code propRegex} does not match the returned version string.
|
||||
*/
|
||||
public static long parseJavaVersion (String propName, String propRegex) {
|
||||
String verstr = System.getProperty(propName);
|
||||
if (verstr == null) throw new IllegalArgumentException(
|
||||
"No system property '" + propName + "'.");
|
||||
|
||||
long vers = VersionUtil.parseJavaVersion(propRegex, verstr);
|
||||
if (vers == 0L) throw new IllegalArgumentException(
|
||||
"Regexp '" + propRegex + "' does not match '" + verstr + "' (from " + propName + ")");
|
||||
return vers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@code appbase_override} or {@code appbase_domain} if they are set.
|
||||
*/
|
||||
public static String overrideAppbase (String appbase) {
|
||||
String appbaseOverride = appbaseOverride();
|
||||
if (appbaseOverride != null) {
|
||||
return appbaseOverride;
|
||||
} else {
|
||||
return replaceDomain(appbase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If appbase_domain property is set, replace the domain on the provided string.
|
||||
*/
|
||||
public static String replaceDomain (String appbase)
|
||||
{
|
||||
String appbaseDomain = appbaseDomain();
|
||||
if (appbaseDomain != null) {
|
||||
Matcher m = Pattern.compile("(https?://[^/]+)(.*)").matcher(appbase);
|
||||
appbase = m.replaceAll(appbaseDomain + "$2");
|
||||
}
|
||||
return appbase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.net;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Used to terminate the download process in its midst.
|
||||
*/
|
||||
public class DownloadAbortedException extends IOException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.net;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.threerings.getdown.data.Resource;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* Handles the download of a collection of files, first issuing HTTP head requests to obtain size
|
||||
* information and then downloading the files individually, reporting progress back via a callback
|
||||
* interface.
|
||||
*/
|
||||
public abstract class Downloader extends Thread
|
||||
{
|
||||
/**
|
||||
* An interface used to communicate status back to an external entity. <em>Note:</em> these
|
||||
* methods are all called on the download thread, so implementors must take care to only
|
||||
* execute thread-safe code or simply pass a message to the AWT thread, for example.
|
||||
*/
|
||||
public interface Observer
|
||||
{
|
||||
/**
|
||||
* Called before the downloader begins the series of HTTP head requests to determine the
|
||||
* size of the files it needs to download.
|
||||
*/
|
||||
public void resolvingDownloads ();
|
||||
|
||||
/**
|
||||
* Called to inform the observer of ongoing progress toward completion of the overall
|
||||
* downloading task. The caller is guaranteed to get at least one call reporting 100%
|
||||
* completion.
|
||||
*
|
||||
* @param percent the percent completion, in terms of total file size, of the downloads.
|
||||
* @param remaining the estimated download time remaining in seconds, or <code>-1</code> if
|
||||
* the time can not yet be determined.
|
||||
*
|
||||
* @return true if the download should continue, false if it should be aborted.
|
||||
*/
|
||||
public boolean downloadProgress (int percent, long remaining);
|
||||
|
||||
/**
|
||||
* Called if a failure occurs while checking for an update or downloading a file.
|
||||
*
|
||||
* @param rsrc the resource that was being downloaded when the error occurred, or
|
||||
* <code>null</code> if the failure occurred while resolving downloads.
|
||||
* @param e the exception detailing the failure.
|
||||
*/
|
||||
public void downloadFailed (Resource rsrc, Exception e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a downloader that will download the supplied list of resources and communicate with
|
||||
* the specified observer. The {@link #download} method must be called on the downloader to
|
||||
* initiate the download process.
|
||||
*/
|
||||
public Downloader (Collection<Resource> resources, Observer obs)
|
||||
{
|
||||
super("Downloader");
|
||||
_resources = resources;
|
||||
_obs = obs;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked as the downloader thread and performs the actual downloading.
|
||||
*/
|
||||
@Override
|
||||
public void run ()
|
||||
{
|
||||
download();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start downloading the resources in this downloader.
|
||||
*
|
||||
* @return true if the download completed or failed for unexpected reasons (in which case the
|
||||
* observer will have been notified), false if it was aborted by the observer.
|
||||
*/
|
||||
public boolean download ()
|
||||
{
|
||||
Resource current = null;
|
||||
try {
|
||||
// let the observer know that we're computing download size
|
||||
if (_obs != null) {
|
||||
_obs.resolvingDownloads();
|
||||
}
|
||||
|
||||
// first compute the total size of our download
|
||||
for (Resource resource : _resources) {
|
||||
discoverSize(resource);
|
||||
}
|
||||
|
||||
long totalSize = sum(_sizes.values());
|
||||
log.info("Downloading " + totalSize + " bytes...");
|
||||
|
||||
// make a note of the time at which we started the download
|
||||
_start = System.currentTimeMillis();
|
||||
|
||||
// now actually download the files
|
||||
for (Resource resource : _resources) {
|
||||
download(resource);
|
||||
}
|
||||
|
||||
// finally report our download completion if we did not already do so when downloading
|
||||
// our final resource
|
||||
if (_obs != null && !_complete && !_obs.downloadProgress(100, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (DownloadAbortedException e) {
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
if (_obs != null) {
|
||||
_obs.downloadFailed(current, e);
|
||||
} else {
|
||||
log.warning("Observer failed.", e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes the amount of data needed to download the given resource..
|
||||
*/
|
||||
protected void discoverSize (Resource rsrc)
|
||||
throws IOException
|
||||
{
|
||||
_sizes.put(rsrc, Math.max(checkSize(rsrc), 0L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the protocol-specific portion of checking download size.
|
||||
*/
|
||||
protected abstract long checkSize (Resource rsrc) throws IOException;
|
||||
|
||||
/**
|
||||
* Downloads the specified resource from its remote location to its local location.
|
||||
*/
|
||||
protected void download (Resource rsrc)
|
||||
throws IOException
|
||||
{
|
||||
// make sure the resource's target directory exists
|
||||
File parent = new File(rsrc.getLocal().getParent());
|
||||
if (!parent.exists() && !parent.mkdirs()) {
|
||||
log.warning("Failed to create target directory for resource '" + rsrc + "'. " +
|
||||
"Download will certainly fail.");
|
||||
}
|
||||
doDownload(rsrc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically called by the protocol-specific downloaders to update their progress. This
|
||||
* should be called at least once for each resource to be downloaded, with the total downloaded
|
||||
* size for that resource. It can also be called periodically along the way for each resource
|
||||
* to communicate incremental progress.
|
||||
*
|
||||
* @param rsrc the resource currently being downloaded.
|
||||
* @param currentSize the number of bytes currently downloaded for said resource.
|
||||
* @param actualSize the size reported for this resource now that we're actually downloading
|
||||
* it. Some web servers lie about Content-length when doing a HEAD request, so by reporting
|
||||
* updated sizes here we can recover from receiving bogus information in the earlier {@link
|
||||
* #checkSize} phase.
|
||||
*/
|
||||
protected void updateObserver (Resource rsrc, long currentSize, long actualSize)
|
||||
throws IOException
|
||||
{
|
||||
// update the actual size for this resource (but don't let it shrink)
|
||||
_sizes.put(rsrc, actualSize = Math.max(actualSize, _sizes.get(rsrc)));
|
||||
|
||||
// update the current downloaded size for said resource; don't allow the downloaded bytes
|
||||
// to exceed the original claimed size of the resource, otherwise our progress will get
|
||||
// booched and we'll end up back on the Daily WTF: http://tinyurl.com/29wt4oq
|
||||
_downloaded.put(rsrc, Math.min(actualSize, currentSize));
|
||||
|
||||
// notify the observer if it's been sufficiently long since our last notification
|
||||
long now = System.currentTimeMillis();
|
||||
if ((now - _lastUpdate) >= UPDATE_DELAY) {
|
||||
_lastUpdate = now;
|
||||
|
||||
// total up our current and total bytes
|
||||
long downloaded = sum(_downloaded.values());
|
||||
long totalSize = sum(_sizes.values());
|
||||
|
||||
// compute our bytes per second
|
||||
long secs = (now - _start) / 1000L;
|
||||
long bps = (secs == 0) ? 0 : (downloaded / secs);
|
||||
|
||||
// compute our percentage completion
|
||||
int pctdone = (totalSize == 0) ? 0 : (int)((downloaded * 100f) / totalSize);
|
||||
|
||||
// estimate our time remaining
|
||||
long remaining = (bps <= 0 || totalSize == 0) ? -1 : (totalSize - downloaded) / bps;
|
||||
|
||||
// make sure we only report 100% exactly once
|
||||
if (pctdone < 100 || !_complete) {
|
||||
_complete = (pctdone == 100);
|
||||
if (!_obs.downloadProgress(pctdone, remaining)) {
|
||||
throw new DownloadAbortedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the supplied values.
|
||||
*/
|
||||
protected static long sum (Iterable<Long> values)
|
||||
{
|
||||
long acc = 0L;
|
||||
for (Long value : values) {
|
||||
acc += value;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accomplishes the copying of the resource from remote location to local location using
|
||||
* protocol-specific code
|
||||
*/
|
||||
protected abstract void doDownload (Resource rsrc) throws IOException;
|
||||
|
||||
/** The resources to be downloaded. */
|
||||
protected Collection<Resource> _resources;
|
||||
|
||||
/** The reported sizes of our resources. */
|
||||
protected Map<Resource, Long> _sizes = new HashMap<>();
|
||||
|
||||
/** The bytes downloaded for each resource. */
|
||||
protected Map<Resource, Long> _downloaded = new HashMap<>();
|
||||
|
||||
/** The observer with whom we are communicating. */
|
||||
protected Observer _obs;
|
||||
|
||||
/** Used while downloading. */
|
||||
protected byte[] _buffer = new byte[4096];
|
||||
|
||||
/** The time at which the file transfer began. */
|
||||
protected long _start;
|
||||
|
||||
/** The current transfer rate in bytes per second. */
|
||||
protected long _bytesPerSecond;
|
||||
|
||||
/** The time at which the last progress update was posted to the progress observer. */
|
||||
protected long _lastUpdate;
|
||||
|
||||
/** Whether the download has completed and the progress observer notified. */
|
||||
protected boolean _complete;
|
||||
|
||||
/** The delay in milliseconds between notifying progress observers of file download
|
||||
* progress. */
|
||||
protected static final long UPDATE_DELAY = 500L;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.net;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Collection;
|
||||
|
||||
import com.threerings.getdown.data.Resource;
|
||||
import com.threerings.getdown.util.ConnectionUtil;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* Implements downloading files over HTTP
|
||||
*/
|
||||
public class HTTPDownloader extends Downloader
|
||||
{
|
||||
public HTTPDownloader (Collection<Resource> resources, Observer obs)
|
||||
{
|
||||
super(resources, obs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long checkSize (Resource rsrc)
|
||||
throws IOException
|
||||
{
|
||||
URLConnection conn = ConnectionUtil.open(rsrc.getRemote(), 0, 0);
|
||||
try {
|
||||
// if we're accessing our data via HTTP, we only need a HEAD request
|
||||
if (conn instanceof HttpURLConnection) {
|
||||
HttpURLConnection hcon = (HttpURLConnection)conn;
|
||||
hcon.setRequestMethod("HEAD");
|
||||
hcon.connect();
|
||||
// make sure we got a satisfactory response code
|
||||
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
throw new IOException("Unable to check up-to-date for " +
|
||||
rsrc.getRemote() + ": " + hcon.getResponseCode());
|
||||
}
|
||||
}
|
||||
return conn.getContentLength();
|
||||
|
||||
} finally {
|
||||
// let it be known that we're done with this connection
|
||||
conn.getInputStream().close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doDownload (Resource rsrc)
|
||||
throws IOException
|
||||
{
|
||||
// download the resource from the specified URL
|
||||
URLConnection conn = ConnectionUtil.open(rsrc.getRemote(), 0, 0);
|
||||
conn.connect();
|
||||
|
||||
// make sure we got a satisfactory response code
|
||||
if (conn instanceof HttpURLConnection) {
|
||||
HttpURLConnection hcon = (HttpURLConnection)conn;
|
||||
if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " +
|
||||
hcon.getResponseCode());
|
||||
}
|
||||
}
|
||||
|
||||
long actualSize = conn.getContentLength();
|
||||
log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize);
|
||||
long currentSize = 0L;
|
||||
try (InputStream in = conn.getInputStream();
|
||||
FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) {
|
||||
|
||||
// TODO: look to see if we have a download info file
|
||||
// containing info on potentially partially downloaded data;
|
||||
// if so, use a "Range: bytes=HAVE-" header.
|
||||
|
||||
// read in the file data
|
||||
int read;
|
||||
while ((read = in.read(_buffer)) != -1) {
|
||||
// write it out to our local copy
|
||||
out.write(_buffer, 0, read);
|
||||
|
||||
// if we have no observer, then don't bother computing download statistics
|
||||
if (_obs == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// note that we've downloaded some data
|
||||
currentSize += read;
|
||||
updateObserver(rsrc, currentSize, actualSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,731 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* 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.threerings.getdown.util;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
/**
|
||||
* Utilities for encoding and decoding the Base64 representation of
|
||||
* binary data. See RFCs <a
|
||||
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
|
||||
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
|
||||
*/
|
||||
public class Base64 {
|
||||
/**
|
||||
* Default values for encoder/decoder flags.
|
||||
*/
|
||||
public static final int DEFAULT = 0;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit the padding '=' characters at the end
|
||||
* of the output (if any).
|
||||
*/
|
||||
public static final int NO_PADDING = 1;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit all line terminators (i.e., the output
|
||||
* will be on one long line).
|
||||
*/
|
||||
public static final int NO_WRAP = 2;
|
||||
|
||||
/**
|
||||
* Encoder flag bit to indicate lines should be terminated with a
|
||||
* CRLF pair instead of just an LF. Has no effect if {@code
|
||||
* NO_WRAP} is specified as well.
|
||||
*/
|
||||
public static final int CRLF = 4;
|
||||
|
||||
/**
|
||||
* Encoder/decoder flag bit to indicate using the "URL and
|
||||
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
||||
* {@code -} and {@code _} are used in place of {@code +} and
|
||||
* {@code /}.
|
||||
*/
|
||||
public static final int URL_SAFE = 8;
|
||||
|
||||
/**
|
||||
* Flag to pass to {@link Base64OutputStream} to indicate that it
|
||||
* should not close the output stream it is wrapping when it
|
||||
* itself is closed.
|
||||
*/
|
||||
public static final int NO_CLOSE = 16;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// shared code
|
||||
// --------------------------------------------------------
|
||||
|
||||
/* package */ static abstract class Coder {
|
||||
public byte[] output;
|
||||
public int op;
|
||||
|
||||
/**
|
||||
* Encode/decode another block of input data. this.output is
|
||||
* provided by the caller, and must be big enough to hold all
|
||||
* the coded data. On exit, this.opwill be set to the length
|
||||
* of the coded data.
|
||||
*
|
||||
* @param finish true if this is the final call to process for
|
||||
* this object. Will finalize the coder state and
|
||||
* include any final bytes in the output.
|
||||
*
|
||||
* @return true if the input so far is good; false if some
|
||||
* error has been detected in the input stream..
|
||||
*/
|
||||
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
|
||||
|
||||
/**
|
||||
* @return the maximum number of bytes a call to process()
|
||||
* could produce for the given number of input bytes. This may
|
||||
* be an overestimate.
|
||||
*/
|
||||
public abstract int maxOutputSize(int len);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// decoding
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param str the input String to decode, which is converted to
|
||||
* bytes using ASCII
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(String str, int flags) {
|
||||
return decode(str.getBytes(US_ASCII), flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the input array to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(byte[] input, int flags) {
|
||||
return decode(input, 0, input.length, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
* <p>The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the data to decode
|
||||
* @param offset the position within the input array at which to start
|
||||
* @param len the number of bytes of input to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass {@code DEFAULT} to decode standard Base64.
|
||||
*
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
public static byte[] decode(byte[] input, int offset, int len, int flags) {
|
||||
// Allocate space for the most data the input could represent.
|
||||
// (It could contain less if it contains whitespace, etc.)
|
||||
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
|
||||
|
||||
if (!decoder.process(input, offset, len, true)) {
|
||||
throw new IllegalArgumentException("bad base-64");
|
||||
}
|
||||
|
||||
// Maybe we got lucky and allocated exactly enough output space.
|
||||
if (decoder.op == decoder.output.length) {
|
||||
return decoder.output;
|
||||
}
|
||||
|
||||
// Need to shorten the array, so allocate a new one of the
|
||||
// right size and copy.
|
||||
byte[] temp = new byte[decoder.op];
|
||||
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
|
||||
return temp;
|
||||
}
|
||||
|
||||
/* package */ static class Decoder extends Coder {
|
||||
/**
|
||||
* Lookup table for turning bytes into their position in the
|
||||
* Base64 alphabet.
|
||||
*/
|
||||
private static final int DECODE[] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode lookup table for the "web safe" variant (RFC 3548
|
||||
* sec. 4) where - and _ replace + and /.
|
||||
*/
|
||||
private static final int DECODE_WEBSAFE[] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
};
|
||||
|
||||
/** Non-data values in the DECODE arrays. */
|
||||
private static final int SKIP = -1;
|
||||
private static final int EQUALS = -2;
|
||||
|
||||
/**
|
||||
* States 0-3 are reading through the next input tuple.
|
||||
* State 4 is having read one '=' and expecting exactly
|
||||
* one more.
|
||||
* State 5 is expecting no more data or padding characters
|
||||
* in the input.
|
||||
* State 6 is the error state; an error has been detected
|
||||
* in the input and no future input can "fix" it.
|
||||
*/
|
||||
private int state; // state number (0 to 6)
|
||||
private int value;
|
||||
|
||||
final private int[] alphabet;
|
||||
|
||||
public Decoder(int flags, byte[] output) {
|
||||
this.output = output;
|
||||
|
||||
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
|
||||
state = 0;
|
||||
value = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes {@code
|
||||
* len} bytes could decode to.
|
||||
*/
|
||||
public int maxOutputSize(int len) {
|
||||
return len * 3/4 + 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode another block of input data.
|
||||
*
|
||||
* @return true if the state machine is still healthy. false if
|
||||
* bad base-64 data has been detected in the input stream.
|
||||
*/
|
||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||
if (this.state == 6) return false;
|
||||
|
||||
int p = offset;
|
||||
len += offset;
|
||||
|
||||
// Using local variables makes the decoder about 12%
|
||||
// faster than if we manipulate the member variables in
|
||||
// the loop. (Even alphabet makes a measurable
|
||||
// difference, which is somewhat surprising to me since
|
||||
// the member variable is final.)
|
||||
int state = this.state;
|
||||
int value = this.value;
|
||||
int op = 0;
|
||||
final byte[] output = this.output;
|
||||
final int[] alphabet = this.alphabet;
|
||||
|
||||
while (p < len) {
|
||||
// Try the fast path: we're starting a new tuple and the
|
||||
// next four bytes of the input stream are all data
|
||||
// bytes. This corresponds to going through states
|
||||
// 0-1-2-3-0. We expect to use this method for most of
|
||||
// the data.
|
||||
//
|
||||
// If any of the next four bytes of input are non-data
|
||||
// (whitespace, etc.), value will end up negative. (All
|
||||
// the non-data values in decode are small negative
|
||||
// numbers, so shifting any of them up and or'ing them
|
||||
// together will result in a value with its top bit set.)
|
||||
//
|
||||
// You can remove this whole block and the output should
|
||||
// be the same, just slower.
|
||||
if (state == 0) {
|
||||
while (p+4 <= len &&
|
||||
(value = ((alphabet[input[p] & 0xff] << 18) |
|
||||
(alphabet[input[p+1] & 0xff] << 12) |
|
||||
(alphabet[input[p+2] & 0xff] << 6) |
|
||||
(alphabet[input[p+3] & 0xff]))) >= 0) {
|
||||
output[op+2] = (byte) value;
|
||||
output[op+1] = (byte) (value >> 8);
|
||||
output[op] = (byte) (value >> 16);
|
||||
op += 3;
|
||||
p += 4;
|
||||
}
|
||||
if (p >= len) break;
|
||||
}
|
||||
|
||||
// The fast path isn't available -- either we've read a
|
||||
// partial tuple, or the next four input bytes aren't all
|
||||
// data, or whatever. Fall back to the slower state
|
||||
// machine implementation.
|
||||
|
||||
int d = alphabet[input[p++] & 0xff];
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
if (d >= 0) {
|
||||
value = d;
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (d >= 0) {
|
||||
value = (value << 6) | d;
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (d >= 0) {
|
||||
value = (value << 6) | d;
|
||||
++state;
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect exactly one more padding character.
|
||||
output[op++] = (byte) (value >> 4);
|
||||
state = 4;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (d >= 0) {
|
||||
// Emit the output triple and return to state 0.
|
||||
value = (value << 6) | d;
|
||||
output[op+2] = (byte) value;
|
||||
output[op+1] = (byte) (value >> 8);
|
||||
output[op] = (byte) (value >> 16);
|
||||
op += 3;
|
||||
state = 0;
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect no further data or padding characters.
|
||||
output[op+1] = (byte) (value >> 2);
|
||||
output[op] = (byte) (value >> 10);
|
||||
op += 2;
|
||||
state = 5;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
if (d == EQUALS) {
|
||||
++state;
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 5:
|
||||
if (d != SKIP) {
|
||||
this.state = 6;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finish) {
|
||||
// We're out of input, but a future call could provide
|
||||
// more.
|
||||
this.state = state;
|
||||
this.value = value;
|
||||
this.op = op;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Done reading input. Now figure out where we are left in
|
||||
// the state machine and finish up.
|
||||
|
||||
switch (state) {
|
||||
case 0:
|
||||
// Output length is a multiple of three. Fine.
|
||||
break;
|
||||
case 1:
|
||||
// Read one extra input byte, which isn't enough to
|
||||
// make another output byte. Illegal.
|
||||
this.state = 6;
|
||||
return false;
|
||||
case 2:
|
||||
// Read two extra input bytes, enough to emit 1 more
|
||||
// output byte. Fine.
|
||||
output[op++] = (byte) (value >> 4);
|
||||
break;
|
||||
case 3:
|
||||
// Read three extra input bytes, enough to emit 2 more
|
||||
// output bytes. Fine.
|
||||
output[op++] = (byte) (value >> 10);
|
||||
output[op++] = (byte) (value >> 2);
|
||||
break;
|
||||
case 4:
|
||||
// Read one padding '=' when we expected 2. Illegal.
|
||||
this.state = 6;
|
||||
return false;
|
||||
case 5:
|
||||
// Read all the padding '='s we expected and no more.
|
||||
// Fine.
|
||||
break;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this.op = op;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// encoding
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static String encodeToString(byte[] input, int flags) {
|
||||
return new String(encode(input, flags), US_ASCII);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static String encodeToString(byte[] input, int offset, int len, int flags) {
|
||||
return new String(encode(input, offset, len, flags), US_ASCII);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static byte[] encode(byte[] input, int flags) {
|
||||
return encode(input, 0, input.length, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing {@code DEFAULT} results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
public static byte[] encode(byte[] input, int offset, int len, int flags) {
|
||||
Encoder encoder = new Encoder(flags, null);
|
||||
|
||||
// Compute the exact length of the array we will produce.
|
||||
int output_len = len / 3 * 4;
|
||||
|
||||
// Account for the tail of the data and the padding bytes, if any.
|
||||
if (encoder.do_padding) {
|
||||
if (len % 3 > 0) {
|
||||
output_len += 4;
|
||||
}
|
||||
} else {
|
||||
switch (len % 3) {
|
||||
case 0: break;
|
||||
case 1: output_len += 2; break;
|
||||
case 2: output_len += 3; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Account for the newlines, if any.
|
||||
if (encoder.do_newline && len > 0) {
|
||||
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
||||
(encoder.do_cr ? 2 : 1);
|
||||
}
|
||||
|
||||
encoder.output = new byte[output_len];
|
||||
encoder.process(input, offset, len, true);
|
||||
|
||||
assert encoder.op == output_len;
|
||||
|
||||
return encoder.output;
|
||||
}
|
||||
|
||||
/* package */ static class Encoder extends Coder {
|
||||
/**
|
||||
* Emit a new line every this many output tuples. Corresponds to
|
||||
* a 76-character line length (the maximum allowable according to
|
||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
|
||||
*/
|
||||
public static final int LINE_GROUPS = 19;
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private static final byte ENCODE[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
|
||||
};
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private static final byte ENCODE_WEBSAFE[] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
|
||||
};
|
||||
|
||||
final private byte[] tail;
|
||||
/* package */ int tailLen;
|
||||
private int count;
|
||||
|
||||
final public boolean do_padding;
|
||||
final public boolean do_newline;
|
||||
final public boolean do_cr;
|
||||
final private byte[] alphabet;
|
||||
|
||||
public Encoder(int flags, byte[] output) {
|
||||
this.output = output;
|
||||
|
||||
do_padding = (flags & NO_PADDING) == 0;
|
||||
do_newline = (flags & NO_WRAP) == 0;
|
||||
do_cr = (flags & CRLF) != 0;
|
||||
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
|
||||
|
||||
tail = new byte[2];
|
||||
tailLen = 0;
|
||||
|
||||
count = do_newline ? LINE_GROUPS : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes {@code
|
||||
* len} bytes could encode to.
|
||||
*/
|
||||
public int maxOutputSize(int len) {
|
||||
return len * 8/5 + 10;
|
||||
}
|
||||
|
||||
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||
// Using local variables makes the encoder about 9% faster.
|
||||
final byte[] alphabet = this.alphabet;
|
||||
final byte[] output = this.output;
|
||||
int op = 0;
|
||||
int count = this.count;
|
||||
|
||||
int p = offset;
|
||||
len += offset;
|
||||
int v = -1;
|
||||
|
||||
// First we need to concatenate the tail of the previous call
|
||||
// with any input bytes available now and see if we can empty
|
||||
// the tail.
|
||||
|
||||
switch (tailLen) {
|
||||
case 0:
|
||||
// There was no tail.
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (p+2 <= len) {
|
||||
// A 1-byte tail with at least 2 bytes of
|
||||
// input available now.
|
||||
v = ((tail[0] & 0xff) << 16) |
|
||||
((input[p++] & 0xff) << 8) |
|
||||
(input[p++] & 0xff);
|
||||
tailLen = 0;
|
||||
};
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (p+1 <= len) {
|
||||
// A 2-byte tail with at least 1 byte of input.
|
||||
v = ((tail[0] & 0xff) << 16) |
|
||||
((tail[1] & 0xff) << 8) |
|
||||
(input[p++] & 0xff);
|
||||
tailLen = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (v != -1) {
|
||||
output[op++] = alphabet[(v >> 18) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (--count == 0) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
count = LINE_GROUPS;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either there is no tail, or there are fewer
|
||||
// than 3 bytes of input available.
|
||||
|
||||
// The main loop, turning 3 input bytes into 4 output bytes on
|
||||
// each iteration.
|
||||
while (p+3 <= len) {
|
||||
v = ((input[p] & 0xff) << 16) |
|
||||
((input[p+1] & 0xff) << 8) |
|
||||
(input[p+2] & 0xff);
|
||||
output[op] = alphabet[(v >> 18) & 0x3f];
|
||||
output[op+1] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op+2] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op+3] = alphabet[v & 0x3f];
|
||||
p += 3;
|
||||
op += 4;
|
||||
if (--count == 0) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
count = LINE_GROUPS;
|
||||
}
|
||||
}
|
||||
|
||||
if (finish) {
|
||||
// Finish up the tail of the input. Note that we need to
|
||||
// consume any bytes in tail before any bytes
|
||||
// remaining in input; there should be at most two bytes
|
||||
// total.
|
||||
|
||||
if (p-tailLen == len-1) {
|
||||
int t = 0;
|
||||
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
|
||||
tailLen -= t;
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (do_padding) {
|
||||
output[op++] = '=';
|
||||
output[op++] = '=';
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
} else if (p-tailLen == len-2) {
|
||||
int t = 0;
|
||||
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
|
||||
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
|
||||
tailLen -= t;
|
||||
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||
output[op++] = alphabet[v & 0x3f];
|
||||
if (do_padding) {
|
||||
output[op++] = '=';
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
||||
if (do_cr) output[op++] = '\r';
|
||||
output[op++] = '\n';
|
||||
}
|
||||
|
||||
assert tailLen == 0;
|
||||
assert p == len;
|
||||
} else {
|
||||
// Save the leftovers in tail to be consumed on the next
|
||||
// call to encodeInternal.
|
||||
|
||||
if (p == len-1) {
|
||||
tail[tailLen++] = input[p];
|
||||
} else if (p == len-2) {
|
||||
tail[tailLen++] = input[p];
|
||||
tail[tailLen++] = input[p+1];
|
||||
}
|
||||
}
|
||||
|
||||
this.op = op;
|
||||
this.count = count;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private Base64() { } // don't instantiate
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Rectangle;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.samskivert.util.ArrayUtil;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* Handles parsing and runtime access for Getdown's config files (mainly {@code getdown.txt}).
|
||||
* These files contain zero or more mappings for a particular string key. Config values can be
|
||||
* fetched as single strings, lists of strings, or parsed into primitives or compound data types
|
||||
* like colors and rectangles.
|
||||
*/
|
||||
public class Config
|
||||
{
|
||||
/** Options that control the {@link #parsePairs} function. */
|
||||
public static class ParseOpts {
|
||||
// these should be tweaked as desired by the caller
|
||||
public boolean biasToKey = false;
|
||||
public boolean strictComments = false;
|
||||
|
||||
// these are filled in by parseConfig
|
||||
public String osname = null;
|
||||
public String osarch = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a parse configuration, filling in the platform filters (or not) depending on the
|
||||
* value of {@code checkPlatform}.
|
||||
*/
|
||||
public static ParseOpts createOpts (boolean checkPlatform) {
|
||||
ParseOpts opts = new ParseOpts();
|
||||
if (checkPlatform) {
|
||||
opts.osname = StringUtil.deNull(System.getProperty("os.name")).toLowerCase(Locale.ROOT);
|
||||
opts.osarch = StringUtil.deNull(System.getProperty("os.arch")).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a configuration file containing key/value pairs. The file must be in the UTF-8
|
||||
* encoding.
|
||||
*
|
||||
* @param opts options that influence the parsing. See {@link #createOpts}.
|
||||
*
|
||||
* @return a list of <code>String[]</code> instances containing the key/value pairs in the
|
||||
* order they were parsed from the file.
|
||||
*/
|
||||
public static List<String[]> parsePairs (File source, ParseOpts opts)
|
||||
throws IOException
|
||||
{
|
||||
// annoyingly FileReader does not allow encoding to be specified (uses platform default)
|
||||
try (FileInputStream fis = new FileInputStream(source);
|
||||
InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
|
||||
return parsePairs(input, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #parsePairs(File,ParseOpts)}.
|
||||
*/
|
||||
public static List<String[]> parsePairs (Reader source, ParseOpts opts) throws IOException
|
||||
{
|
||||
List<String[]> pairs = new ArrayList<>();
|
||||
for (String line : FileUtil.readLines(source)) {
|
||||
// nix comments
|
||||
int cidx = line.indexOf("#");
|
||||
if (opts.strictComments ? cidx == 0 : cidx != -1) {
|
||||
line = line.substring(0, cidx);
|
||||
}
|
||||
|
||||
// trim whitespace and skip blank lines
|
||||
line = line.trim();
|
||||
if (StringUtil.isBlank(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// parse our key/value pair
|
||||
String[] pair = new String[2];
|
||||
// if we're biasing toward key, put all the extra = in the key rather than the value
|
||||
int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("=");
|
||||
if (eidx != -1) {
|
||||
pair[0] = line.substring(0, eidx).trim();
|
||||
pair[1] = line.substring(eidx+1).trim();
|
||||
} else {
|
||||
pair[0] = line;
|
||||
pair[1] = "";
|
||||
}
|
||||
|
||||
// if the pair has an os qualifier, we need to process it
|
||||
if (pair[1].startsWith("[")) {
|
||||
int qidx = pair[1].indexOf("]");
|
||||
if (qidx == -1) {
|
||||
log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]);
|
||||
continue; // omit the pair entirely
|
||||
}
|
||||
// if we're checking qualifiers and the os doesn't match this qualifier, skip it
|
||||
String quals = pair[1].substring(1, qidx);
|
||||
if (opts.osname != null && !checkQualifiers(quals, opts.osname, opts.osarch)) {
|
||||
log.debug("Skipping", "quals", quals,
|
||||
"osname", opts.osname, "osarch", opts.osarch,
|
||||
"key", pair[0], "value", pair[1]);
|
||||
continue;
|
||||
}
|
||||
// otherwise filter out the qualifier text
|
||||
pair[1] = pair[1].substring(qidx+1).trim();
|
||||
}
|
||||
|
||||
pairs.add(pair);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a comma-separated String of four integers and returns a rectangle using those ints as
|
||||
* the its x, y, width, and height.
|
||||
*/
|
||||
public static Rectangle parseRect (String name, String value)
|
||||
{
|
||||
if (!StringUtil.isBlank(value)) {
|
||||
int[] v = StringUtil.parseIntArray(value);
|
||||
if (v != null && v.length == 4) {
|
||||
return new Rectangle(v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
log.warning("Ignoring invalid rect '" + name + "' config '" + value + "'.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given hex color value (e.g. FFFFF) and returns a Color object with that value.
|
||||
* If the given value is null of not a valid hexadecimal number, this will return null.
|
||||
*/
|
||||
public static Color parseColor (String hexValue)
|
||||
{
|
||||
if (!StringUtil.isBlank(hexValue)) {
|
||||
try {
|
||||
int rgba = Integer.parseInt(hexValue, 16);
|
||||
boolean hasAlpha = hexValue.length() > 6;
|
||||
return new Color(rgba, hasAlpha);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warning("Ignoring invalid color", "hexValue", hexValue, "exception", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a configuration file containing key/value pairs. The file must be in the UTF-8
|
||||
* encoding.
|
||||
*
|
||||
* @return a map from keys to values, where a value will be an array of strings if more than
|
||||
* one key/value pair in the config file was associated with the same key.
|
||||
*/
|
||||
public static Config parseConfig (File source, ParseOpts opts)
|
||||
throws IOException
|
||||
{
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// I thought that we could use HashMap<String, String[]> and put new String[] {pair[1]} for
|
||||
// the null case, but it mysteriously dies on launch, so leaving it as HashMap<String,
|
||||
// Object> for now
|
||||
for (String[] pair : parsePairs(source, opts)) {
|
||||
Object value = data.get(pair[0]);
|
||||
if (value == null) {
|
||||
data.put(pair[0], pair[1]);
|
||||
} else if (value instanceof String) {
|
||||
data.put(pair[0], new String[] { (String)value, pair[1] });
|
||||
} else if (value instanceof String[]) {
|
||||
String[] values = (String[])value;
|
||||
String[] nvalues = new String[values.length+1];
|
||||
System.arraycopy(values, 0, nvalues, 0, values.length);
|
||||
nvalues[values.length] = pair[1];
|
||||
data.put(pair[0], nvalues);
|
||||
}
|
||||
}
|
||||
|
||||
// special magic for the getdown.txt config: if the parsed data contains 'strict_comments =
|
||||
// true' then we reparse the file with strict comments (i.e. # is only assumed to start a
|
||||
// comment in column 0)
|
||||
if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) {
|
||||
opts.strictComments = true;
|
||||
return parseConfig(source, opts);
|
||||
}
|
||||
|
||||
return new Config(data);
|
||||
}
|
||||
|
||||
public Config (Map<String, Object> data) {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@code name} has a value in this config.
|
||||
*/
|
||||
public boolean hasValue (String name) {
|
||||
return _data.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw-value for {@code name}. This may be a {@code String}, {@code String[]}, or
|
||||
* {@code null}.
|
||||
*/
|
||||
public Object getRaw (String name) {
|
||||
return _data.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified config value as a string, or {@code null}.
|
||||
*/
|
||||
public String getString (String name) {
|
||||
return (String)_data.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified config value as a string, or {@code def}.
|
||||
*/
|
||||
public String getString (String name, String def) {
|
||||
String value = (String)_data.get(name);
|
||||
return value == null ? def : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the specified config value as a boolean.
|
||||
*/
|
||||
public boolean getBoolean (String name) {
|
||||
return Boolean.parseBoolean(getString(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Massages a single string into an array and leaves existing array values as is. Simplifies
|
||||
* access to parameters that are expected to be arrays.
|
||||
*/
|
||||
public String[] getMultiValue (String name)
|
||||
{
|
||||
Object value = _data.get(name);
|
||||
if (value instanceof String) {
|
||||
return new String[] { (String)value };
|
||||
} else {
|
||||
return (String[])value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to parse rectangle specifications from the config file. */
|
||||
public Rectangle getRect (String name, Rectangle def)
|
||||
{
|
||||
String value = getString(name);
|
||||
Rectangle rect = parseRect(name, value);
|
||||
return (rect == null) ? def : rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and returns the config value for {@code name} as an int. If no value is provided,
|
||||
* {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
|
||||
* returned.
|
||||
*/
|
||||
public int getInt (String name, int def) {
|
||||
String value = getString(name);
|
||||
try {
|
||||
return value == null ? def : Integer.parseInt(value);
|
||||
} catch (Exception e) {
|
||||
log.warning("Ignoring invalid int '" + name + "' config '" + value + "',");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and returns the config value for {@code name} as a long. If no value is provided,
|
||||
* {@code def} is returned. If the value is invalid, a warning is logged and {@code def} is
|
||||
* returned.
|
||||
*/
|
||||
public long getLong (String name, long def) {
|
||||
String value = getString(name);
|
||||
try {
|
||||
return value == null ? def : Long.parseLong(value);
|
||||
} catch (Exception e) {
|
||||
log.warning("Ignoring invalid long '" + name + "' config '" + value + "',");
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to parse color specifications from the config file. */
|
||||
public Color getColor (String name, Color def)
|
||||
{
|
||||
String value = getString(name);
|
||||
Color color = parseColor(value);
|
||||
return (color == null) ? def : color;
|
||||
}
|
||||
|
||||
/** Parses a list of strings from the config file. */
|
||||
public String[] getList (String name)
|
||||
{
|
||||
String value = getString(name);
|
||||
return (value == null) ? ArrayUtil.EMPTY_STRING : StringUtil.parseStringArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URL from the config file, checking first for a localized version.
|
||||
*/
|
||||
public String getUrl (String name, String def)
|
||||
{
|
||||
String value = getString(name + "." + Locale.getDefault().getLanguage());
|
||||
if (!StringUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
value = getString(name);
|
||||
return StringUtil.isBlank(value) ? def : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function for {@link #parsePairs(Reader,ParseOpts)}. Qualifiers have the following
|
||||
* form:
|
||||
* <pre>
|
||||
* id = os[-arch]
|
||||
* ids = id | id,ids
|
||||
* quals = !id | ids
|
||||
* </pre>
|
||||
* Examples: [linux-amd64,linux-x86_64], [windows], [mac os x], [!windows]. Negative qualifiers
|
||||
* must appear alone, they cannot be used with other qualifiers (positive or negative).
|
||||
*/
|
||||
protected static boolean checkQualifiers (String quals, String osname, String osarch)
|
||||
{
|
||||
if (quals.startsWith("!")) {
|
||||
if (quals.indexOf(",") != -1) { // sanity check
|
||||
log.warning("Multiple qualifiers cannot be used when one of the qualifiers " +
|
||||
"is negative", "quals", quals);
|
||||
return false;
|
||||
}
|
||||
return !checkQualifier(quals.substring(1), osname, osarch);
|
||||
}
|
||||
for (String qual : quals.split(",")) {
|
||||
if (checkQualifier(qual, osname, osarch)) {
|
||||
return true; // if we have a positive match, we can immediately return true
|
||||
}
|
||||
}
|
||||
return false; // we had no positive matches, so return false
|
||||
}
|
||||
|
||||
/** A helper function for {@link #checkQualifiers}. */
|
||||
protected static boolean checkQualifier (String qual, String osname, String osarch)
|
||||
{
|
||||
String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-");
|
||||
String os = bits[0], arch = (bits.length > 1) ? bits[1] : "";
|
||||
return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1);
|
||||
}
|
||||
|
||||
private final Map<String, Object> _data;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLDecoder;
|
||||
import com.threerings.getdown.data.SysProps;
|
||||
import com.threerings.getdown.util.Base64;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class ConnectionUtil
|
||||
{
|
||||
/**
|
||||
* Opens a connection to a URL, setting the authentication header if user info is present.
|
||||
* @param url the URL to which to open a connection.
|
||||
* @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the
|
||||
* connection. If {@code 0} is supplied, the connection timeout specified via system properties
|
||||
* will be used instead.
|
||||
* @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from
|
||||
* the connection. If {@code 0} is supplied, the read timeout specified via system properties
|
||||
* will be used instead.
|
||||
*/
|
||||
public static URLConnection open (URL url, int connectTimeout, int readTimeout)
|
||||
throws IOException
|
||||
{
|
||||
URLConnection conn = url.openConnection();
|
||||
|
||||
// configure a connect timeout, if requested
|
||||
int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout();
|
||||
if (ctimeout > 0) {
|
||||
conn.setConnectTimeout(ctimeout * 1000);
|
||||
}
|
||||
|
||||
// configure a read timeout, if requested
|
||||
int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout();
|
||||
if (rtimeout > 0) {
|
||||
conn.setReadTimeout(rtimeout * 1000);
|
||||
}
|
||||
|
||||
// If URL has a username:password@ before hostname, use HTTP basic auth
|
||||
String userInfo = url.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
// Remove any percent-encoding in the username/password
|
||||
userInfo = URLDecoder.decode(userInfo, "UTF-8");
|
||||
// Now base64 encode the auth info and make it a single line
|
||||
String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT).
|
||||
replaceAll("\\n","").replaceAll("\\r", "");
|
||||
conn.setRequestProperty("Authorization", "Basic " + encoded);
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a connection to a http or https URL, setting the authentication header if user info is
|
||||
* present. Throws a class cast exception if the connection returned is not the right type. See
|
||||
* {@link #open} for parameter documentation.
|
||||
*/
|
||||
public static HttpURLConnection openHttp (URL url, int connectTimeout, int readTimeout)
|
||||
throws IOException
|
||||
{
|
||||
return (HttpURLConnection)open(url, connectTimeout, readTimeout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.jar.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import com.samskivert.io.StreamUtil;
|
||||
import com.samskivert.util.Logger;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* File related utilities.
|
||||
*/
|
||||
public class FileUtil
|
||||
{
|
||||
/**
|
||||
* Gets the specified source file to the specified destination file by hook or crook. Windows
|
||||
* has all sorts of problems which we work around in this method.
|
||||
*
|
||||
* @return true if we managed to get the job done, false otherwise.
|
||||
*/
|
||||
public static boolean renameTo (File source, File dest)
|
||||
{
|
||||
// if we're on a civilized operating system we may be able to simple rename it
|
||||
if (source.renameTo(dest)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fall back to trying to rename the old file out of the way, rename the new file into
|
||||
// place and then delete the old file
|
||||
if (dest.exists()) {
|
||||
File temp = new File(dest.getPath() + "_old");
|
||||
if (temp.exists() && !deleteHarder(temp)) {
|
||||
log.warning("Failed to delete old intermediate file " + temp + ".");
|
||||
// the subsequent code will probably fail
|
||||
}
|
||||
if (dest.renameTo(temp) && source.renameTo(dest)) {
|
||||
if (!deleteHarder(temp)) {
|
||||
log.warning("Failed to delete intermediate file " + temp + ".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// as a last resort, try copying the old data over the new
|
||||
try (FileInputStream fin = new FileInputStream(source);
|
||||
FileOutputStream fout = new FileOutputStream(dest)) {
|
||||
|
||||
StreamUtil.copy(fin, fout);
|
||||
|
||||
// close the input stream now so we can delete 'source'
|
||||
fin.close();
|
||||
if (!deleteHarder(source)) {
|
||||
log.warning("Failed to delete " + source +
|
||||
" after brute force copy to " + dest + ".");
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Failed to copy " + source + " to " + dest + ": " + ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Tries harder" to delete {@code file} than just calling {@code delete} on it. Presently this
|
||||
* just means "try a second time if the first time fails, and if that fails then try to delete
|
||||
* when the virtual machine terminates." On Windows Vista, sometimes deletes fail but then
|
||||
* succeed if you just try again. Given that delete failure is a rare occurrence, we can
|
||||
* implement this hacky workaround without any negative consequences for normal behavior.
|
||||
*/
|
||||
public static boolean deleteHarder (File file) {
|
||||
// if at first you don't succeed... try, try again
|
||||
boolean deleted = (file.delete() || file.delete());
|
||||
if (!deleted) {
|
||||
file.deleteOnExit();
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of the supplied input stream into a list of lines. Closes the reader on
|
||||
* successful or failed completion.
|
||||
*/
|
||||
public static List<String> readLines (Reader in)
|
||||
throws IOException
|
||||
{
|
||||
List<String> lines = new ArrayList<>();
|
||||
try (BufferedReader bin = new BufferedReader(in)) {
|
||||
for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks the specified jar file into the specified target directory.
|
||||
*/
|
||||
public static void unpackJar (JarFile jar, File target) throws IOException
|
||||
{
|
||||
Enumeration<?> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = (JarEntry)entries.nextElement();
|
||||
File efile = new File(target, entry.getName());
|
||||
|
||||
// if we're unpacking a normal jar file, it will have special path
|
||||
// entries that allow us to create our directories first
|
||||
if (entry.isDirectory()) {
|
||||
if (!efile.exists() && !efile.mkdir()) {
|
||||
log.warning("Failed to create jar entry path", "jar", jar, "entry", entry);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// but some do not, so we want to ensure that our directories exist
|
||||
// prior to getting down and funky
|
||||
File parent = new File(efile.getParent());
|
||||
if (!parent.exists() && !parent.mkdirs()) {
|
||||
log.warning("Failed to create jar entry parent", "jar", jar, "parent", parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile));
|
||||
InputStream jin = jar.getInputStream(entry)) {
|
||||
StreamUtil.copy(jin, fout);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(
|
||||
Logger.format("Failure unpacking", "jar", jar, "entry", efile), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code
|
||||
* packedJar} has a {@code .gz} extension, it will be gunzipped first.
|
||||
*/
|
||||
public static void unpackPacked200Jar (File packedJar, File target) throws IOException
|
||||
{
|
||||
try (InputStream packJarIn = new FileInputStream(packedJar);
|
||||
JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) {
|
||||
boolean gz = (packedJar.getName().endsWith(".gz") ||
|
||||
packedJar.getName().endsWith(".gz_new"));
|
||||
try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) {
|
||||
Pack200.Unpacker unpacker = Pack200.newUnpacker();
|
||||
unpacker.unpack(packJarIn2, jarOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the given {@code source} file to the given {@code target}.
|
||||
*/
|
||||
public static void copy (File source, File target) throws IOException {
|
||||
try (FileInputStream in = new FileInputStream(source);
|
||||
FileOutputStream out = new FileOutputStream(target)) {
|
||||
StreamUtil.copy(in, out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks {@code file} as executable, if it exists. Catches and logs any errors that occur.
|
||||
*/
|
||||
public static void makeExecutable (File file) {
|
||||
try {
|
||||
if (file.exists()) {
|
||||
if (!file.setExecutable(true, false)) {
|
||||
log.warning("Failed to mark as executable", "file", file);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warning("Failed to mark as executable", "file", file, "error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
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);
|
||||
File[] currentChildren = currentFile.listFiles();
|
||||
if (currentChildren != null) {
|
||||
for (File file : currentChildren) {
|
||||
stack.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.samskivert.util.RunAnywhere;
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import static com.threerings.getdown.Log.log;
|
||||
|
||||
/**
|
||||
* Useful routines for launching Java applications from within other Java
|
||||
* applications.
|
||||
*/
|
||||
public class LaunchUtil
|
||||
{
|
||||
/** The directory into which a local VM installation should be unpacked. */
|
||||
public static final String LOCAL_JAVA_DIR = "java_vm";
|
||||
|
||||
/**
|
||||
* Writes a <code>version.txt</code> file into the specified application directory and
|
||||
* attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
|
||||
* specified version and relaunch the application.
|
||||
*
|
||||
* @param appdir the directory in which the application is installed.
|
||||
* @param getdownJarName the name of the getdown jar file in the application directory. This is
|
||||
* probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
|
||||
* the results of the standard build.
|
||||
* @param newVersion the new version to which Getdown will update when it is executed.
|
||||
*
|
||||
* @return true if the relaunch succeeded, false if we were unable to relaunch due to being on
|
||||
* Windows 9x where we cannot launch subprocesses without waiting around for them to exit,
|
||||
* reading their stdout and stderr all the while. If true is returned, the application may exit
|
||||
* after making this call as it will be upgraded and restarted. If false is returned, the
|
||||
* application should tell the user that they must restart the application manually.
|
||||
*
|
||||
* @exception IOException thrown if we were unable to create the <code>version.txt</code> file
|
||||
* in the supplied application directory. If the version.txt file cannot be created, restarting
|
||||
* Getdown will not cause the application to be upgraded, so the application will have to
|
||||
* resort to telling the user that it is in a bad way.
|
||||
*/
|
||||
public static boolean updateVersionAndRelaunch (
|
||||
File appdir, String getdownJarName, String newVersion)
|
||||
throws IOException
|
||||
{
|
||||
// create the file that instructs Getdown to upgrade
|
||||
File vfile = new File(appdir, "version.txt");
|
||||
try (PrintStream ps = new PrintStream(new FileOutputStream(vfile))) {
|
||||
ps.println(newVersion);
|
||||
}
|
||||
|
||||
// make sure that we can find our getdown.jar file and can safely launch children
|
||||
File pro = new File(appdir, getdownJarName);
|
||||
if (mustMonitorChildren() || !pro.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// do the deed
|
||||
String[] args = new String[] {
|
||||
getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
|
||||
};
|
||||
log.info("Running " + StringUtil.join(args, "\n "));
|
||||
try {
|
||||
Runtime.getRuntime().exec(args, null);
|
||||
return true;
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Failed to run getdown", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs the path to the JVM used to launch this process.
|
||||
*/
|
||||
public static String getJVMPath (File appdir)
|
||||
{
|
||||
return getJVMPath(appdir, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstructs the path to the JVM used to launch this process.
|
||||
*
|
||||
* @param windebug if true we will use java.exe instead of javaw.exe on Windows.
|
||||
*/
|
||||
public static String getJVMPath (File appdir, boolean windebug)
|
||||
{
|
||||
// first look in our application directory for an installed VM
|
||||
String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug);
|
||||
|
||||
// then fall back to the VM in which we're already running
|
||||
if (vmpath == null) {
|
||||
vmpath = checkJVMPath(System.getProperty("java.home"), windebug);
|
||||
}
|
||||
|
||||
// then throw up our hands and hope for the best
|
||||
if (vmpath == null) {
|
||||
log.warning("Unable to find java [appdir=" + appdir +
|
||||
", java.home=" + System.getProperty("java.home") + "]!");
|
||||
vmpath = "java";
|
||||
}
|
||||
|
||||
// Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched
|
||||
// from /usr/bin/java, and not if launched by directly referring to <java.home>/bin/java,
|
||||
// even though the former is a symlink to the latter! To work around this, see if the
|
||||
// desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead.
|
||||
if (RunAnywhere.isMacOS()) {
|
||||
try {
|
||||
File localVM = new File("/usr/bin/java").getCanonicalFile();
|
||||
if (localVM.equals(new File(vmpath).getCanonicalFile())) {
|
||||
vmpath = "/usr/bin/java";
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Failed to check Mac OS canonical VM path.", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
return vmpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrades Getdown by moving an installation managed copy of the Getdown jar file over the
|
||||
* non-managed copy (which would be used to run Getdown itself).
|
||||
*
|
||||
* <p> If the upgrade fails for a variety of reasons, warnings are logged but no other actions
|
||||
* are taken. There's not much else one can do other than try again next time around.
|
||||
*/
|
||||
public static void upgradeGetdown (File oldgd, File curgd, File newgd)
|
||||
{
|
||||
// we assume getdown's jar file size changes with every upgrade, this is not guaranteed,
|
||||
// but in reality it will, and it allows us to avoid pointlessly upgrading getdown every
|
||||
// time the client is updated which is unnecessarily flirting with danger
|
||||
if (!newgd.exists() || newgd.length() == curgd.length()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Updating Getdown with " + newgd + "...");
|
||||
|
||||
// clear out any old getdown
|
||||
if (oldgd.exists()) {
|
||||
FileUtil.deleteHarder(oldgd);
|
||||
}
|
||||
|
||||
// now try updating using renames
|
||||
if (!curgd.exists() || curgd.renameTo(oldgd)) {
|
||||
if (newgd.renameTo(curgd)) {
|
||||
FileUtil.deleteHarder(oldgd); // yay!
|
||||
try {
|
||||
// copy the moved file back to getdown-dop-new.jar so that we don't end up
|
||||
// downloading another copy next time
|
||||
FileUtil.copy(curgd, newgd);
|
||||
} catch (IOException e) {
|
||||
log.warning("Error copying updated Getdown back: " + e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.warning("Unable to renameTo(" + oldgd + ").");
|
||||
// try to unfuck ourselves
|
||||
if (!oldgd.renameTo(curgd)) {
|
||||
log.warning("Oh God, why dost thee scorn me so.");
|
||||
}
|
||||
}
|
||||
|
||||
// that didn't work, let's try copying it
|
||||
log.info("Attempting to upgrade by copying over " + curgd + "...");
|
||||
try {
|
||||
FileUtil.copy(newgd, curgd);
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Mayday! Brute force copy method also failed.", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if, on this operating system, we have to stick around and read the stderr from
|
||||
* our children processes to prevent them from filling their output buffers and hanging.
|
||||
*/
|
||||
public static boolean mustMonitorChildren ()
|
||||
{
|
||||
String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
|
||||
return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a Java Virtual Machine can be located in the supplied path.
|
||||
*/
|
||||
protected static String checkJVMPath (String vmhome, boolean windebug)
|
||||
{
|
||||
String vmbase = vmhome + File.separator + "bin" + File.separator;
|
||||
String vmpath = vmbase + "java";
|
||||
if (new File(vmpath).exists()) {
|
||||
return vmpath;
|
||||
}
|
||||
|
||||
if (!windebug) {
|
||||
vmpath = vmbase + "javaw.exe";
|
||||
if (new File(vmpath).exists()) {
|
||||
return vmpath;
|
||||
}
|
||||
}
|
||||
|
||||
vmpath = vmbase + "java.exe";
|
||||
if (new File(vmpath).exists()) {
|
||||
return vmpath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
/**
|
||||
* Accumulates the progress from a number of (potentially parallel) elements into a single smoothly
|
||||
* progressing progress.
|
||||
*/
|
||||
public class ProgressAggregator
|
||||
{
|
||||
public ProgressAggregator (ProgressObserver target, long[] sizes) {
|
||||
_target = target;
|
||||
_sizes = sizes;
|
||||
_progress = new int[sizes.length];
|
||||
}
|
||||
|
||||
public ProgressObserver startElement (final int index) {
|
||||
return new ProgressObserver() {
|
||||
public void progress (int percent) {
|
||||
_progress[index] = percent;
|
||||
updateAggProgress();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected void updateAggProgress () {
|
||||
long totalSize = 0L, currentSize = 0L;
|
||||
synchronized (this) {
|
||||
for (int ii = 0, ll = _sizes.length; ii < ll; ii++) {
|
||||
long size = _sizes[ii];
|
||||
totalSize += size;
|
||||
currentSize += (int)((size * _progress[ii])/100.0);
|
||||
}
|
||||
}
|
||||
_target.progress((int)(100.0*currentSize / totalSize));
|
||||
}
|
||||
|
||||
protected static long sum (long[] sizes) {
|
||||
long totalSize = 0L;
|
||||
for (long size : sizes) totalSize += size;
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
protected ProgressObserver _target;
|
||||
protected long[] _sizes;
|
||||
protected int[] _progress;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
/**
|
||||
* Used to communicate progress.
|
||||
*/
|
||||
public interface ProgressObserver
|
||||
{
|
||||
/**
|
||||
* Informs the observer that we have completed the specified
|
||||
* percentage of the process.
|
||||
*/
|
||||
public void progress (int percent);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.samskivert.util.StringUtil;
|
||||
|
||||
import com.threerings.getdown.data.SysProps;
|
||||
import static com.threerings.getdown.Log.log;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
/**
|
||||
* Version related utilities.
|
||||
*/
|
||||
public class VersionUtil
|
||||
{
|
||||
/**
|
||||
* Reads a version number from a file.
|
||||
*/
|
||||
public static long readVersion (File vfile)
|
||||
{
|
||||
long fileVersion = -1;
|
||||
try (BufferedReader bin =
|
||||
new BufferedReader(new InputStreamReader(new FileInputStream(vfile), UTF_8))) {
|
||||
String vstr = bin.readLine();
|
||||
if (!StringUtil.isBlank(vstr)) {
|
||||
fileVersion = Long.parseLong(vstr);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("Unable to read version file: " + e.getMessage());
|
||||
}
|
||||
|
||||
return fileVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a version number to a file.
|
||||
*/
|
||||
public static void writeVersion (File vfile, long version) throws IOException
|
||||
{
|
||||
try (PrintStream out = new PrintStream(new FileOutputStream(vfile))) {
|
||||
out.println(version);
|
||||
} catch (Exception e) {
|
||||
log.warning("Unable to write version file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses {@code versStr} using {@code versRegex} into a (long) integer version number.
|
||||
* @see SysProps#parseJavaVersion
|
||||
*/
|
||||
public static long parseJavaVersion (String versRegex, String versStr)
|
||||
{
|
||||
Matcher m = Pattern.compile(versRegex).matcher(versStr);
|
||||
if (!m.matches()) return 0L;
|
||||
|
||||
long vers = 0L;
|
||||
for (int ii = 1; ii <= m.groupCount(); ii++) {
|
||||
String valstr = m.group(ii);
|
||||
int value = (valstr == null) ? 0 : parseInt(valstr);
|
||||
vers *= 100;
|
||||
vers += value;
|
||||
}
|
||||
return vers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the version from the {@code release} file bundled with a JVM.
|
||||
*/
|
||||
public static long readReleaseVersion (File relfile, String versRegex)
|
||||
{
|
||||
try (BufferedReader in =
|
||||
new BufferedReader(new InputStreamReader(new FileInputStream(relfile), UTF_8))) {
|
||||
String line = null, relvers = null;
|
||||
while ((line = in.readLine()) != null) {
|
||||
if (line.startsWith("JAVA_VERSION=")) {
|
||||
relvers = line.substring("JAVA_VERSION=".length()).replace('"', ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (relvers == null) {
|
||||
log.warning("No JAVA_VERSION line in 'release' file", "file", relfile);
|
||||
return 0L;
|
||||
}
|
||||
return parseJavaVersion(versRegex, relvers);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("Failed to read version from 'release' file", "file", relfile, e);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt (String str) {
|
||||
int value = 0;
|
||||
for (int ii = 0, ll = str.length(); ii < ll; ii++) {
|
||||
char c = str.charAt(ii);
|
||||
if (c >= '0' && c <= '9') {
|
||||
value *= 10;
|
||||
value += (c - '0');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -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<File> classPathEntries = new LinkedHashSet<File>();
|
||||
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;
|
||||
}
|
||||
@@ -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.samskivert.util.FileUtil;
|
||||
import com.threerings.getdown.data.Application;
|
||||
import com.threerings.getdown.data.Resource;
|
||||
|
||||
@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();
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.data;
|
||||
|
||||
import org.junit.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class SysPropsTest {
|
||||
|
||||
@After public void clearProps () {
|
||||
System.clearProperty("appbase_domain");
|
||||
System.clearProperty("appbase_override");
|
||||
}
|
||||
|
||||
public static String[] APPBASES = {
|
||||
"http://foobar.com/myapp",
|
||||
"https://foobar.com/myapp",
|
||||
"http://foobar.com:8080/myapp",
|
||||
"https://foobar.com:8080/myapp"
|
||||
};
|
||||
|
||||
@Test public void testAppbaseDomain () {
|
||||
System.setProperty("appbase_domain", "https://barbaz.com");
|
||||
for (String appbase : APPBASES) {
|
||||
assertEquals("https://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
|
||||
}
|
||||
System.setProperty("appbase_domain", "http://barbaz.com");
|
||||
for (String appbase : APPBASES) {
|
||||
assertEquals("http://barbaz.com/myapp", SysProps.overrideAppbase(appbase));
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void testAppbaseOverride () {
|
||||
System.setProperty("appbase_override", "https://barbaz.com/newapp");
|
||||
for (String appbase : APPBASES) {
|
||||
assertEquals("https://barbaz.com/newapp", SysProps.overrideAppbase(appbase));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
|
||||
import com.samskivert.util.RandomUtil;
|
||||
|
||||
import org.junit.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests {@link Config}.
|
||||
*/
|
||||
public class ConfigTest
|
||||
{
|
||||
public static class Pair {
|
||||
public final String key;
|
||||
public final String value;
|
||||
public Pair (String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static final Pair[] SIMPLE_PAIRS = {
|
||||
new Pair("one", "two"),
|
||||
new Pair("three", "four"),
|
||||
new Pair("five", "six"),
|
||||
new Pair("seven", "eight"),
|
||||
new Pair("nine", "ten"),
|
||||
};
|
||||
|
||||
@Test public void testSimplePairs () throws IOException
|
||||
{
|
||||
List<String[]> pairs = Config.parsePairs(
|
||||
toReader(SIMPLE_PAIRS), Config.createOpts(true));
|
||||
for (int ii = 0; ii < SIMPLE_PAIRS.length; ii++) {
|
||||
assertEquals(SIMPLE_PAIRS[ii].key, pairs.get(ii)[0]);
|
||||
assertEquals(SIMPLE_PAIRS[ii].value, pairs.get(ii)[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void testQualifiedPairs () throws IOException
|
||||
{
|
||||
Pair linux = new Pair("one", "[linux] two");
|
||||
Pair mac = new Pair("three", "[mac os x] four");
|
||||
Pair linuxAndMac = new Pair("five", "[linux, mac os x] six");
|
||||
Pair linux64 = new Pair("seven", "[linux-x86_64] eight");
|
||||
Pair linux64s = new Pair("nine", "[linux-x86_64, linux-amd64] ten");
|
||||
Pair mac64 = new Pair("eleven", "[mac os x-x86_64] twelve");
|
||||
Pair win64 = new Pair("thirteen", "[windows-x86_64] fourteen");
|
||||
Pair notWin = new Pair("fifteen", "[!windows] sixteen");
|
||||
Pair[] pairs = { linux, mac, linuxAndMac, linux64, linux64s, mac64, win64, notWin };
|
||||
|
||||
Config.ParseOpts opts = Config.createOpts(false);
|
||||
opts.osname = "linux";
|
||||
opts.osarch = "i386";
|
||||
List<String[]> parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(!exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(exists(parsed, notWin.key));
|
||||
|
||||
opts.osarch = "x86_64";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(exists(parsed, linuxAndMac.key));
|
||||
assertTrue(exists(parsed, linux64.key));
|
||||
assertTrue(exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(exists(parsed, notWin.key));
|
||||
|
||||
opts.osarch = "amd64";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(exists(parsed, notWin.key));
|
||||
|
||||
opts.osname = "mac os x";
|
||||
opts.osarch = "x86_64";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(!exists(parsed, linux.key));
|
||||
assertTrue(exists(parsed, mac.key));
|
||||
assertTrue(exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(!exists(parsed, linux64s.key));
|
||||
assertTrue(exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(exists(parsed, notWin.key));
|
||||
|
||||
opts.osname = "windows";
|
||||
opts.osarch = "i386";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(!exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(!exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(!exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(!exists(parsed, notWin.key));
|
||||
|
||||
opts.osarch = "x86_64";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(!exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(!exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(!exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(exists(parsed, win64.key));
|
||||
assertTrue(!exists(parsed, notWin.key));
|
||||
|
||||
opts.osarch = "amd64";
|
||||
parsed = Config.parsePairs(toReader(pairs), opts);
|
||||
assertTrue(!exists(parsed, linux.key));
|
||||
assertTrue(!exists(parsed, mac.key));
|
||||
assertTrue(!exists(parsed, linuxAndMac.key));
|
||||
assertTrue(!exists(parsed, linux64.key));
|
||||
assertTrue(!exists(parsed, linux64s.key));
|
||||
assertTrue(!exists(parsed, mac64.key));
|
||||
assertTrue(!exists(parsed, win64.key));
|
||||
assertTrue(!exists(parsed, notWin.key));
|
||||
}
|
||||
|
||||
protected static boolean exists (List<String[]> pairs, String key)
|
||||
{
|
||||
for (String[] pair : pairs) {
|
||||
if (pair[0].equals(key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static StringReader toReader (Pair[] pairs)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Pair pair : pairs) {
|
||||
// throw some whitespace in to ensure it's trimmed
|
||||
builder.append(whitespace()).append(pair.key).
|
||||
append(whitespace()).append("=").
|
||||
append(whitespace()).append(pair.value).
|
||||
append(whitespace()).append("\n");
|
||||
}
|
||||
return new StringReader(builder.toString());
|
||||
}
|
||||
|
||||
protected static String whitespace ()
|
||||
{
|
||||
return RandomUtil.getBoolean() ? " " : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
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.*;
|
||||
|
||||
/**
|
||||
* Tests {@link FileUtil}.
|
||||
*/
|
||||
public class FileUtilTest
|
||||
{
|
||||
@Test public void testReadLines () throws IOException
|
||||
{
|
||||
String data = "This is a test\nof a file with\na few lines\n";
|
||||
List<String> lines = FileUtil.readLines(new StringReader(data));
|
||||
String[] linesBySplit = data.split("\n");
|
||||
assertEquals(linesBySplit.length, lines.size());
|
||||
for (int ii = 0; ii < lines.size(); ii++) {
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Getdown - application installer, patcher and launcher
|
||||
// Copyright (C) 2004-2016 Getdown authors
|
||||
// https://github.com/threerings/getdown/blob/master/LICENSE
|
||||
|
||||
package com.threerings.getdown.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class VersionUtilTest {
|
||||
|
||||
@Test
|
||||
public void shouldParseJavaVersion ()
|
||||
{
|
||||
long version = VersionUtil.parseJavaVersion(
|
||||
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8.0_152");
|
||||
assertEquals(1_080_152, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJavaVersion8 ()
|
||||
{
|
||||
long version = VersionUtil.parseJavaVersion(
|
||||
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "1.8");
|
||||
assertEquals(1_080_000, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJavaVersion9 ()
|
||||
{
|
||||
long version = VersionUtil.parseJavaVersion(
|
||||
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "9");
|
||||
assertEquals(9_000_000, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJavaVersion10 ()
|
||||
{
|
||||
long version = VersionUtil.parseJavaVersion(
|
||||
"(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(_\\d+)?)?)?", "10");
|
||||
assertEquals(10_000_000, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldParseJavaRuntimeVersion ()
|
||||
{
|
||||
long version = VersionUtil.parseJavaVersion(
|
||||
"(\\d+)\\.(\\d+)\\.(\\d+)(_\\d+)?(-b\\d+)?", "1.8.0_131-b11");
|
||||
assertEquals(108_013_111, version);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user