diff --git a/README.md b/README.md
index c975499..cc3f16b 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,8 @@ applications.
The latest version of Getdown can be obtained thusly:
* Download the pre-built jar file from Maven Central:
- [getdown-1.5.jar](http://repo2.maven.org/maven2/com/threerings/getdown/1.5/getdown-1.5.jar)
- * Obtain the jar artifact via Maven with the following identifier: `com.threerings:getdown:1.5`.
+ [getdown-1.6.jar](http://repo2.maven.org/maven2/com/threerings/getdown/1.6/getdown-1.6.jar)
+ * Obtain the jar artifact via Maven with the following identifier: `com.threerings:getdown:1.6`.
* [Check out the code](https://github.com/threerings/getdown) and build it yourself.
You can also:
diff --git a/pom.xml b/pom.xml
index acb34bd..9815d4b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@
com.threerings
getdown
jar
- 1.6-SNAPSHOT
+ 1.7-SNAPSHOT
getdown
An application installer and updater.
diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java
index ce72f50..c6dd522 100644
--- a/src/main/java/com/threerings/getdown/data/Application.java
+++ b/src/main/java/com/threerings/getdown/data/Application.java
@@ -904,7 +904,7 @@ public class Application
// now re-download our control files; we download the digest first so that if it fails,
// our config file will still reference the old version and re-running the updater will
// start the whole process over again
- downloadDigestFile();
+ downloadDigestFiles();
downloadConfigFile();
} catch (IOException ex) {
@@ -1171,7 +1171,7 @@ public class Application
String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
try {
status.updateStatus("m.checking");
- downloadDigestFile();
+ downloadDigestFiles();
_digest = new Digest(_appdir);
if (!olddig.equals(_digest.getMetaDigest())) {
log.info("Unversioned digest changed. Revalidating...");
@@ -1189,7 +1189,7 @@ public class Application
// exceptions to propagate up to the caller as there is nothing else we can do
if (_digest == null) {
status.updateStatus("m.updating_metadata");
- downloadDigestFile();
+ downloadDigestFiles();
_digest = new Digest(_appdir);
}
@@ -1200,7 +1200,7 @@ public class Application
// attempt to redownload both of our metadata files; again we pass errors up to our
// caller because there's nothing we can do to automatically recover
downloadConfigFile();
- downloadDigestFile();
+ downloadDigestFiles();
_digest = new Digest(_appdir);
// revalidate everything if we end up downloading new metadata
clearValidationMarkers();
@@ -1459,13 +1459,15 @@ public class Application
}
/**
- * Downloads a copy of Digest.DIGEST_FILE and validates its signature.
+ * Downloads the digest files and validates their signature.
* @throws IOException
*/
- protected void downloadDigestFile ()
+ protected void downloadDigestFiles ()
throws IOException
{
- downloadControlFile(Digest.DIGEST_FILE, true);
+ for (int version = 1; version <= Digest.VERSION; version++) {
+ downloadControlFile(Digest.digestFile(version), true);
+ }
}
/**
@@ -1507,7 +1509,7 @@ public class Application
FileInputStream dataInput = null;
try {
dataInput = new FileInputStream(target);
- Signature sig = Signature.getInstance("SHA1withRSA");
+ Signature sig = Signature.getInstance(Digest.SIG_ALGO);
sig.initVerify(cert);
while ((length = dataInput.read(buffer)) != -1) {
sig.update(buffer, 0, length);
diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java
index 9d30c6f..7eb74d2 100644
--- a/src/main/java/com/threerings/getdown/data/Digest.java
+++ b/src/main/java/com/threerings/getdown/data/Digest.java
@@ -25,26 +25,96 @@ import com.threerings.getdown.util.ProgressObserver;
import static com.threerings.getdown.Log.log;
/**
- * Manages the digest.txt file and the computing and processing of MD5 digests for an
+ * Manages the digest.txt file and the computing and processing of digests for an
* application.
*/
public class Digest
{
- /** The name of our MD5 digest file. */
- public static final String DIGEST_FILE = "digest.txt";
+ /** The current version of the digest protocol. */
+ public static final int VERSION = 2;
+
+ /** The current algorithm used to sign digest files. */
+ public static final String SIG_ALGO = "SHA256withRSA";
/**
- * Creates a digest instance which will parse and validate the digest.txt in the
- * supplied application directory.
+ * Returns the name of the digest file for the specified protocol version.
*/
- public Digest (File appdir)
+ public static String digestFile (int version) {
+ String infix = version > 1 ? String.valueOf(version) : "";
+ return FILE_NAME + infix + FILE_SUFFIX;
+ }
+
+ /**
+ * 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 resources, File output)
throws IOException
{
- // parse and validate our digest file contents
+ MessageDigest md = getMessageDigest(version);
StringBuilder data = new StringBuilder();
- File dfile = new File(appdir, DIGEST_FILE);
+ PrintWriter pout = null;
+ try {
+ pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
+
+ // compute and append the digest of each resource in the list
+ for (Resource rsrc : resources) {
+ String path = rsrc.getPath();
+ try {
+ String digest = rsrc.computeDigest(version, md, null);
+ note(data, path, digest);
+ pout.println(path + " = " + digest);
+ } catch (Throwable t) {
+ throw (IOException) new IOException(
+ "Error computing digest for: " + rsrc).initCause(t);
+ }
+ }
+
+ // finally compute and append the digest for the file contents
+ md.reset();
+ byte[] contents = data.toString().getBytes("UTF-8");
+ String filename = digestFile(version);
+ pout.println(filename + " = " + StringUtil.hexlate(md.digest(contents)));
+
+ } finally {
+ StreamUtil.close(pout);
+ }
+ }
+
+ /**
+ * 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) throws IOException {
+ this(appdir, VERSION);
+ }
+
+ /**
+ * 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) throws IOException
+ {
+ // parse and validate our digest file contents
+ String filename = digestFile(version);
+ StringBuilder data = new StringBuilder();
+ File dfile = new File(appdir, filename);
for (String[] pair : ConfigUtil.parsePairs(dfile, false)) {
- if (pair[0].equals(DIGEST_FILE)) {
+ if (pair[0].equals(filename)) {
_metaDigest = pair[1];
break;
}
@@ -53,11 +123,11 @@ public class Digest
}
// we've reached the end, validate our contents
- MessageDigest md = getMessageDigest();
+ MessageDigest md = getMessageDigest(version);
byte[] contents = data.toString().getBytes("UTF-8");
- String md5 = StringUtil.hexlate(md.digest(contents));
- if (!md5.equals(_metaDigest)) {
- String err = MessageUtil.tcompose("m.invalid_digest_file", _metaDigest, md5);
+ 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);
}
}
@@ -71,7 +141,7 @@ public class Digest
}
/**
- * Computes the MD5 hash of the specified resource and compares it with the value parsed from
+ * 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
@@ -80,13 +150,13 @@ public class Digest
public boolean validateResource (Resource resource, ProgressObserver obs)
{
try {
- String cmd5 = resource.computeDigest(getMessageDigest(), obs);
- String emd5 = _digests.get(resource.getPath());
- if (cmd5.equals(emd5)) {
+ 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", cmd5, "expected", emd5);
+ "rsrc", resource, "computed", chash, "expected", ehash);
} catch (Throwable t) {
log.info("Resource failed digest check", "rsrc", resource, "error", t);
}
@@ -101,53 +171,6 @@ public class Digest
return _digests.get(resource.getPath());
}
- /**
- * Creates a digest file at the specified location using the supplied list of resources.
- */
- public static void createDigest (List resources, File output)
- throws IOException
- {
- MessageDigest md = getMessageDigest();
- StringBuilder data = new StringBuilder();
- PrintWriter pout = null;
- try {
- pout = new PrintWriter(new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
-
- // compute and append the MD5 digest of each resource in the list
- for (Resource rsrc : resources) {
- String path = rsrc.getPath();
- try {
- String digest = rsrc.computeDigest(md, null);
- note(data, path, digest);
- pout.println(path + " = " + digest);
- } catch (Throwable t) {
- throw (IOException) new IOException(
- "Error computing digest for: " + rsrc).initCause(t);
- }
- }
-
- // finally compute and append the digest for the file contents
- md.reset();
- byte[] contents = data.toString().getBytes("UTF-8");
- pout.println(DIGEST_FILE + " = " + StringUtil.hexlate(md.digest(contents)));
-
- } finally {
- StreamUtil.close(pout);
- }
- }
-
- /**
- * Obtains an appropriate message digest instance for use by the Getdown system.
- */
- public static MessageDigest getMessageDigest ()
- {
- try {
- return MessageDigest.getInstance("MD5");
- } catch (NoSuchAlgorithmException nsae) {
- throw new RuntimeException("JVM does not support MD5. Gurp!");
- }
- }
-
/** Used by {@link #createDigest} and {@link Digest}. */
protected static void note (StringBuilder data, String path, String digest)
{
@@ -156,4 +179,7 @@ public class Digest
protected HashMap _digests = new HashMap();
protected String _metaDigest = "";
+
+ protected static final String FILE_NAME = "digest";
+ protected static final String FILE_SUFFIX = ".txt";
}
diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java
index 7ad0a1e..df18c52 100644
--- a/src/main/java/com/threerings/getdown/data/Resource.java
+++ b/src/main/java/com/threerings/getdown/data/Resource.java
@@ -27,6 +27,91 @@ import static com.threerings.getdown.Log.log;
*/
public class Resource
{
+ /**
+ * 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 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;
+ }
+ }
+
+ InputStream in = null;
+ try {
+ in = jar.getInputStream(entry);
+ while ((read = in.read(buffer)) != -1) {
+ md.update(buffer, 0, read);
+ }
+ } finally {
+ StreamUtil.close(in);
+ }
+ 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) {
+ tmpJarFile.delete();
+ }
+ }
+
+ } else {
+ long totalSize = target.length(), position = 0L;
+ FileInputStream fin = null;
+ try {
+ fin = new FileInputStream(target);
+ while ((read = fin.read(buffer)) != -1) {
+ md.update(buffer, 0, read);
+ position += read;
+ updateProgress(obs, position, totalSize);
+ }
+ } finally {
+ StreamUtil.close(fin);
+ }
+ }
+ return StringUtil.hexlate(md.digest());
+ }
+
/**
* Creates a resource with the supplied remote URL and local path.
*/
@@ -35,6 +120,7 @@ public class Resource
_path = path;
_remote = remote;
_local = local;
+ _localNew = new File(local.toString() + "_new");
String lpath = _local.getPath();
_marker = new File(lpath + "v");
@@ -66,6 +152,14 @@ public class Resource
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.
*/
@@ -102,11 +196,18 @@ public class Resource
/**
* Computes the MD5 hash of this resource's underlying file.
* Note: This is both CPU and I/O intensive.
+ * @param version the version of the digest protocol to use.
*/
- public String computeDigest (MessageDigest md, ProgressObserver obs)
+ public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
throws IOException
{
- return computeDigest(_local, md, obs);
+ File file;
+ if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) {
+ file = _local;
+ } else {
+ file = _localNew.exists() ? _localNew : _local;
+ }
+ return computeDigest(version, file, md, obs);
}
/**
@@ -199,88 +300,6 @@ public class Resource
return _path;
}
- /**
- * Computes the MD5 hash of the supplied file.
- */
- public static String computeDigest (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 entries = Collections.list(jar.entries());
- Collections.sort(entries, ENTRY_COMP);
-
- int eidx = 0;
- for (JarEntry entry : entries) {
- // skip metadata; we just want the goods
- if (entry.getName().startsWith("META-INF")) {
- updateProgress(obs, eidx, entries.size());
- continue;
- }
-
- // add this file's data to the MD5 hash
- InputStream in = null;
- try {
- in = jar.getInputStream(entry);
- while ((read = in.read(buffer)) != -1) {
- md.update(buffer, 0, read);
- }
- } finally {
- StreamUtil.close(in);
- }
- 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) {
- tmpJarFile.delete();
- }
- }
-
- } else {
- long totalSize = target.length(), position = 0L;
- FileInputStream fin = null;
- try {
- fin = new FileInputStream(target);
- while ((read = fin.read(buffer)) != -1) {
- md.update(buffer, 0, read);
- position += read;
- updateProgress(obs, position, totalSize);
- }
- } finally {
- StreamUtil.close(fin);
- }
- }
- return StringUtil.hexlate(md.digest());
- }
-
/** Helper function to simplify the process of reporting progress. */
protected static void updateProgress (ProgressObserver obs, long pos, long total)
{
@@ -291,7 +310,7 @@ public class Resource
protected static boolean isJar (String path)
{
- return path.endsWith(".jar");
+ return path.endsWith(".jar") || path.endsWith(".jar_new");
}
protected static boolean isPacked200Jar (String path)
@@ -301,7 +320,7 @@ public class Resource
protected String _path;
protected URL _remote;
- protected File _local, _marker, _unpacked;
+ protected File _local, _localNew, _marker, _unpacked;
protected boolean _unpack, _isJar, _isPacked200Jar;
/** Used to sort the entries in a jar file. */
diff --git a/src/main/java/com/threerings/getdown/data/SysProps.java b/src/main/java/com/threerings/getdown/data/SysProps.java
index 6dfdf9c..f983c4d 100644
--- a/src/main/java/com/threerings/getdown/data/SysProps.java
+++ b/src/main/java/com/threerings/getdown/data/SysProps.java
@@ -6,6 +6,7 @@
package com.threerings.getdown.data;
import com.threerings.getdown.util.VersionUtil;
+import com.threerings.getdown.launcher.Getdown;
/**
* This class encapsulates all system properties that are read and processed by Getdown. Don't
@@ -49,6 +50,13 @@ public class SysProps
return System.getProperty("silent") != null;
}
+ /** If true, Getdown does not automatically install updates after downloading them. It waits
+ * for the application to call {@link 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 () {
diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java
index 3afc3a9..1cd00bb 100644
--- a/src/main/java/com/threerings/getdown/launcher/Getdown.java
+++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java
@@ -68,6 +68,7 @@ import com.threerings.getdown.net.HTTPDownloader;
import com.threerings.getdown.tools.Patcher;
import com.threerings.getdown.util.ConfigUtil;
import com.threerings.getdown.util.ConnectionUtil;
+import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.ProgressAggregator;
import com.threerings.getdown.util.ProgressObserver;
@@ -104,6 +105,7 @@ public abstract class Getdown extends Thread
_launchInSilent = SysProps.launchInSilent();
}
_delay = SysProps.startDelay();
+ _noInstall = SysProps.noInstall();
} catch (SecurityException se) {
// don't freak out, just assume non-silent and no delay; we're probably already
// recovering from a security failure
@@ -126,6 +128,39 @@ public abstract class Getdown extends Thread
_startup = System.currentTimeMillis();
}
+ /**
+ * Returns true if there are pending new resources, waiting to be installed.
+ */
+ public boolean isUpdateAvailable ()
+ {
+ return _readyToInstall && !_toBeInstalledResouces.isEmpty();
+ }
+
+ /**
+ * Installs the currently pending new resources.
+ */
+ public void install () throws IOException, InterruptedException
+ {
+ if (_readyToInstall) {
+ log.info("Installing downloaded resources:");
+ for (Resource resource : _toBeInstalledResouces) {
+ File source = resource.getLocalNew(), dest = resource.getLocal();
+ log.info("- " + source);
+ if (!FileUtil.renameTo(source, dest)) {
+ throw new IOException("Failed to rename " + source + " to " + dest);
+ }
+ if (Thread.interrupted()) {
+ throw new InterruptedException("m.applet_stopped");
+ }
+ }
+ _toBeInstalledResouces.clear();
+ _readyToInstall = false;
+ log.info("Install completed.");
+ } else {
+ log.info("Nothing to install.");
+ }
+ }
+
/**
* This is used by the applet which always needs a user interface and wants to load it as soon
* as possible.
@@ -410,6 +445,9 @@ public abstract class Getdown extends Thread
// we'll keep track of all the resources we unpack
Set unpacked = new HashSet();
+ _toBeInstalledResouces = new ArrayList();
+ _readyToInstall = false;
+
//setStep(Step.START);
for (int ii = 0; ii < MAX_LOOPS; ii++) {
// if we aren't running in a JVM that meets our version requirements, either
@@ -471,6 +509,12 @@ public abstract class Getdown extends Thread
}
}
+ // assuming we're not doing anything funny, install the update
+ _readyToInstall = true;
+ if (!_noInstall) {
+ install();
+ }
+
// Only launch if we aren't in silent mode. Some mystery program starting out
// of the blue would be disconcerting.
if (!_silent || _launchInSilent) {
@@ -486,6 +530,13 @@ public abstract class Getdown extends Thread
return;
}
+ // we have failures, those will be redownloaded so we note them as to-be-installed
+ for (Resource r : failures) {
+ if (!_toBeInstalledResouces.contains(r)) {
+ _toBeInstalledResouces.add(r);
+ }
+ }
+
try {
// if any of our resources have already been marked valid this is not a first
// time install and we don't want to enable tracking
@@ -499,6 +550,7 @@ public abstract class Getdown extends Thread
download(failures);
reportTrackingEvent("app_complete", -1);
+
} finally {
_enableTracking = false;
}
@@ -595,21 +647,10 @@ public abstract class Getdown extends Thread
}
vmjar.markAsValid();
- // Sun, why dost thou spite me? Java doesn't know anything about file permissions (and by
- // extension then, neither does Jar), so on Joonix we have to hackily make java_vm/bin/java
- // executable by execing chmod; a pox on their children!
- if (!RunAnywhere.isWindows()) {
- String vmbin = LaunchUtil.LOCAL_JAVA_DIR + File.separator + "bin" +
- File.separator + "java";
- String cmd = "chmod a+rx " + _app.getLocalPath(vmbin);
- try {
- log.info("Please smack a Java engineer. Running: " + cmd);
- Runtime.getRuntime().exec(cmd);
- } catch (Exception e) {
- log.warning("Failed to mark VM binary as executable", "cmd", cmd, "error", e);
- // we should do something like tell the user or something but fucking fuck
- }
- }
+ // these only run on non-Windows platforms, so we use Unix file separators
+ String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
+ makeExecutable(localJavaDir + "bin/java");
+ makeExecutable(localJavaDir + "lib/amd64/jspawnhelper");
// lastly regenerate the .jsa dump file that helps Java to start up faster
String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
@@ -617,12 +658,29 @@ public abstract class Getdown extends Thread
log.info("Regenerating classes.jsa for " + vmpath + "...");
Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
} catch (Exception e) {
- log.warning("Failed to regenerate .jsa dum file", "error", e);
+ log.warning("Failed to regenerate .jsa dump file", "error", e);
}
reportTrackingEvent("jvm_complete", -1);
}
+ protected void makeExecutable (String path) {
+ // Java doesn't know anything about file permissions (and by extension then,
+ // neither does Jar), so on Unix we have to hackily do so via chmod
+ if (!RunAnywhere.isWindows()) {
+ File target = _app.getLocalPath(path);
+ String cmd = "chmod a+rx " + target;
+ try {
+ if (target.exists()) {
+ log.info("Running: " + cmd);
+ Runtime.getRuntime().exec(cmd);
+ }
+ } catch (Exception e) {
+ log.warning("Failed to mark VM binary as executable", "cmd", cmd, "error", e);
+ }
+ }
+ }
+
/**
* Called if the application is determined to be of an old version.
*/
@@ -1219,9 +1277,13 @@ public abstract class Getdown extends Thread
protected boolean _dead;
protected boolean _silent;
+ protected boolean _noInstall;
protected boolean _launchInSilent;
protected long _startup;
+ protected List _toBeInstalledResouces;
+ protected boolean _readyToInstall;
+
protected boolean _enableTracking = true;
protected int _reportedProgress = 0;
diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
index 57818ff..fb68080 100644
--- a/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
+++ b/src/main/java/com/threerings/getdown/launcher/GetdownApp.java
@@ -11,11 +11,16 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedOutputStream;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import javax.swing.JFrame;
@@ -26,6 +31,7 @@ import com.samskivert.util.ArrayUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
+import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.SysProps;
import static com.threerings.getdown.Log.log;
@@ -36,6 +42,20 @@ public class GetdownApp
{
public static void main (String[] argv)
{
+ try {
+ start(argv);
+ } catch (Exception e) {
+ log.warning("main() failed.", e);
+ }
+ }
+
+ /**
+ * Runs Getdown as an application, using the arguments supplie as {@code argv}.
+ * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been
+ * called on it.
+ * @throws Exception if anything goes wrong starting Getdown.
+ */
+ public static Getdown start (String[] argv) throws Exception {
int aidx = 0;
List args = Arrays.asList(argv);
@@ -66,6 +86,22 @@ public class GetdownApp
System.exit(-1);
}
+ // load X.509 certificate if it exists
+ File crtFile = new File(appDir, Digest.digestFile(Digest.VERSION) + ".crt");
+ List crts = new ArrayList();
+ if (crtFile.exists()) {
+ try {
+ FileInputStream fis = new FileInputStream(crtFile);
+ X509Certificate certificate = (X509Certificate)
+ CertificateFactory.getInstance("X.509").generateCertificate(fis);
+ fis.close();
+ crts.add(certificate);
+ } catch (Exception e) {
+ log.warning("Certificate error: " + e.getMessage());
+ System.exit(-1);
+ }
+ }
+
// pipe our output into a file in the application directory
if (!SysProps.noLogRedir()) {
File logFile = new File(appDir, "launcher.log");
@@ -91,118 +127,114 @@ public class GetdownApp
log.info("-- Cur dir: " + System.getProperty("user.dir"));
log.info("---------------------------------------------");
- try {
- Getdown app = new Getdown(appDir, appId, null, null, appArgs) {
- @Override
- protected Container createContainer () {
- // create our user interface, and display it
- String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name;
- if (_frame == null) {
- _frame = new JFrame(title);
- _frame.addWindowListener(new WindowAdapter() {
- @Override
- public void windowClosing (WindowEvent evt) {
- handleWindowClose();
- }
- });
- _frame.setUndecorated(_ifc.hideDecorations);
- _frame.setResizable(false);
- } else {
- _frame.setTitle(title);
- _frame.getContentPane().removeAll();
- }
-
- if (_ifc.iconImages != null) {
- ArrayList icons = new ArrayList();
- for (String path : _ifc.iconImages) {
- Image img = loadImage(path);
- if (img == null) {
- log.warning("Error loading icon image", "path", path);
- } else {
- icons.add(img);
- }
+ Getdown app = new Getdown(appDir, appId, crts, null, appArgs) {
+ @Override
+ protected Container createContainer () {
+ // create our user interface, and display it
+ String title = StringUtil.isBlank(_ifc.name) ? "" : _ifc.name;
+ if (_frame == null) {
+ _frame = new JFrame(title);
+ _frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing (WindowEvent evt) {
+ handleWindowClose();
}
- if (icons.isEmpty()) {
- log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
+ });
+ _frame.setUndecorated(_ifc.hideDecorations);
+ _frame.setResizable(false);
+ } else {
+ _frame.setTitle(title);
+ _frame.getContentPane().removeAll();
+ }
+
+ if (_ifc.iconImages != null) {
+ ArrayList icons = new ArrayList();
+ for (String path : _ifc.iconImages) {
+ Image img = loadImage(path);
+ if (img == null) {
+ log.warning("Error loading icon image", "path", path);
} else {
- SwingUtil.setFrameIcons(_frame, icons);
+ icons.add(img);
}
}
-
- _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
- return _frame.getContentPane();
- }
-
- @Override
- protected void showContainer () {
- if (_frame != null) {
- _frame.pack();
- SwingUtil.centerWindow(_frame);
- _frame.setVisible(true);
- }
- }
-
- @Override
- protected void disposeContainer () {
- if (_frame != null) {
- _frame.dispose();
- _frame = null;
- }
- }
-
- @Override
- protected void showDocument (String url) {
- String[] cmdarray;
- if (RunAnywhere.isWindows()) {
- String osName = System.getProperty("os.name");
- if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
- cmdarray = new String[] {
- "command.com", "/c", "start", "\"" + url + "\"" };
- } else {
- cmdarray = new String[] {
- "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
- }
- } else if (RunAnywhere.isMacOS()) {
- cmdarray = new String[] { "open", url };
- } else { // Linux, Solaris, etc.
- cmdarray = new String[] { "firefox", url };
- }
- try {
- Runtime.getRuntime().exec(cmdarray);
- } catch (Exception e) {
- log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
- }
- }
-
- @Override
- protected void exit (int exitCode) {
- // if we're running the app in the same JVM, don't call System.exit, but do
- // make double sure that the download window is closed.
- if (invokeDirect()) {
- disposeContainer();
+ if (icons.isEmpty()) {
+ log.warning("Failed to load any icons", "iconImages", _ifc.iconImages);
} else {
- System.exit(exitCode);
+ SwingUtil.setFrameIcons(_frame, icons);
}
}
- @Override
- protected void fail (String message) {
- // if the frame was set to be undecorated, make window decoration available
- // to allow the user to close the window
- if (_frame != null && _frame.isUndecorated()) {
- _frame.dispose();
- _frame.setUndecorated(false);
- showContainer();
- }
- super.fail(message);
+ _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+ return _frame.getContentPane();
+ }
+
+ @Override
+ protected void showContainer () {
+ if (_frame != null) {
+ _frame.pack();
+ SwingUtil.centerWindow(_frame);
+ _frame.setVisible(true);
}
+ }
- protected JFrame _frame;
- };
- app.start();
+ @Override
+ protected void disposeContainer () {
+ if (_frame != null) {
+ _frame.dispose();
+ _frame = null;
+ }
+ }
- } catch (Exception e) {
- log.warning("main() failed.", e);
- }
+ @Override
+ protected void showDocument (String url) {
+ String[] cmdarray;
+ if (RunAnywhere.isWindows()) {
+ String osName = System.getProperty("os.name");
+ if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) {
+ cmdarray = new String[] {
+ "command.com", "/c", "start", "\"" + url + "\"" };
+ } else {
+ cmdarray = new String[] {
+ "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" };
+ }
+ } else if (RunAnywhere.isMacOS()) {
+ cmdarray = new String[] { "open", url };
+ } else { // Linux, Solaris, etc.
+ cmdarray = new String[] { "firefox", url };
+ }
+ try {
+ Runtime.getRuntime().exec(cmdarray);
+ } catch (Exception e) {
+ log.warning("Failed to open browser.", "cmdarray", cmdarray, e);
+ }
+ }
+
+ @Override
+ protected void exit (int exitCode) {
+ // if we're running the app in the same JVM, don't call System.exit, but do
+ // make double sure that the download window is closed.
+ if (invokeDirect()) {
+ disposeContainer();
+ } else {
+ System.exit(exitCode);
+ }
+ }
+
+ @Override
+ protected void fail (String message) {
+ // if the frame was set to be undecorated, make window decoration available
+ // to allow the user to close the window
+ if (_frame != null && _frame.isUndecorated()) {
+ _frame.dispose();
+ _frame.setUndecorated(false);
+ showContainer();
+ }
+ super.fail(message);
+ }
+
+ protected JFrame _frame;
+ };
+ app.start();
+ return app;
}
}
diff --git a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
index 738d064..286ebc5 100644
--- a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
+++ b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java
@@ -78,7 +78,7 @@ public class HTTPDownloader extends Downloader
long currentSize = 0L;
try {
in = conn.getInputStream();
- out = new FileOutputStream(rsrc.getLocal());
+ out = new FileOutputStream(rsrc.getLocalNew());
int read;
// TODO: look to see if we have a download info file
diff --git a/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java b/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java
index ab83a30..78cac26 100644
--- a/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java
+++ b/src/main/java/com/threerings/getdown/tools/AppletParamSigner.java
@@ -13,6 +13,8 @@ import java.security.Signature;
import org.apache.commons.codec.binary.Base64;
+import com.threerings.getdown.data.Digest;
+
/**
* Produces a signed hash of the appbase, appname, and image path to ensure that signed copies of
* Getdown are not hijacked to run malicious code.
@@ -41,7 +43,7 @@ public class AppletParamSigner
store.load(new BufferedInputStream(new FileInputStream(keystore)),
storepass.toCharArray());
PrivateKey key = (PrivateKey)store.getKey(alias, keypass.toCharArray());
- Signature sig = Signature.getInstance("SHA1withRSA");
+ Signature sig = Signature.getInstance(Digest.SIG_ALGO);
sig.initSign(key);
sig.update(params.getBytes());
String signed = new String(Base64.encodeBase64(sig.sign()));
diff --git a/src/main/java/com/threerings/getdown/tools/Differ.java b/src/main/java/com/threerings/getdown/tools/Differ.java
index 28fe715..fbde2a9 100644
--- a/src/main/java/com/threerings/getdown/tools/Differ.java
+++ b/src/main/java/com/threerings/getdown/tools/Differ.java
@@ -94,7 +94,8 @@ public class Differ
ArrayList nrsrcs, boolean verbose)
throws IOException
{
- MessageDigest md = Digest.getMessageDigest();
+ int version = Digest.VERSION;
+ MessageDigest md = Digest.getMessageDigest(version);
JarOutputStream jout = null;
try {
jout = new JarOutputStream(
@@ -107,8 +108,8 @@ public class Differ
Resource orsrc = (oidx == -1) ? null : orsrcs.remove(oidx);
if (orsrc != null) {
// first see if they are the same
- String odig = orsrc.computeDigest(md, null);
- String ndig = rsrc.computeDigest(md, null);
+ String odig = orsrc.computeDigest(version, md, null);
+ String ndig = rsrc.computeDigest(version, md, null);
if (odig.equals(ndig)) {
if (verbose) {
System.out.println("Unchanged: " + rsrc.getPath());
diff --git a/src/main/java/com/threerings/getdown/tools/Digester.java b/src/main/java/com/threerings/getdown/tools/Digester.java
index 8ec2411..a3738c3 100644
--- a/src/main/java/com/threerings/getdown/tools/Digester.java
+++ b/src/main/java/com/threerings/getdown/tools/Digester.java
@@ -36,24 +36,40 @@ public class Digester
public static void main (String[] args)
throws IOException, GeneralSecurityException
{
- if (args.length != 1 && args.length != 4) {
+ switch (args.length) {
+ case 1:
+ createDigests(new File(args[0]), null, null, null);
+ break;
+ case 4:
+ createDigests(new File(args[0]), new File(args[1]), args[2], args[3]);
+ break;
+ default:
System.err.println("Usage: Digester app_dir [keystore_path password alias]");
System.exit(255);
}
+ }
- createDigest(new File(args[0]));
- if (args.length == 4) {
- signDigest(new File(args[0]), new File(args[1]), args[2], args[3]);
+ /**
+ * Creates digest file(s) and optionally signs them if {@code keystore} is not null.
+ */
+ public static void createDigests (File appdir, File keystore, String password, String alias)
+ throws IOException, GeneralSecurityException
+ {
+ for (int version = 1; version <= Digest.VERSION; version++) {
+ createDigest(version, appdir);
+ if (keystore != null) {
+ signDigest(version, appdir, keystore, password, alias);
+ }
}
}
/**
* Creates a digest file in the specified application directory.
*/
- public static void createDigest (File appdir)
+ public static void createDigest (int version, File appdir)
throws IOException
{
- File target = new File(appdir, Digest.DIGEST_FILE);
+ File target = new File(appdir, Digest.digestFile(version));
System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business
@@ -70,17 +86,19 @@ public class Digester
}
// now generate the digest file
- Digest.createDigest(rsrcs, target);
+ Digest.createDigest(version, rsrcs, target);
}
/**
* Creates a digest file in the specified application directory.
*/
- public static void signDigest (File appdir, File storePath, String storePass, String storeAlias)
+ public static void signDigest (int version, File appdir,
+ File storePath, String storePass, String storeAlias)
throws IOException, GeneralSecurityException
{
- File inputFile = new File(appdir, Digest.DIGEST_FILE);
- File signatureFile = new File(appdir, Digest.DIGEST_FILE + Application.SIGNATURE_SUFFIX);
+ String filename = Digest.digestFile(version);
+ File inputFile = new File(appdir, filename);
+ File signatureFile = new File(appdir, filename + Application.SIGNATURE_SUFFIX);
FileInputStream storeInput = null, dataInput = null;
FileOutputStream signatureOutput = null;
@@ -92,7 +110,8 @@ public class Digester
PrivateKey key = (PrivateKey)store.getKey(storeAlias, storePass.toCharArray());
// sign the digest file
- Signature sig = Signature.getInstance("SHA1withRSA");
+ String algo = version > 1 ? Digest.SIG_ALGO : "SHA1withRSA";
+ Signature sig = Signature.getInstance(algo);
dataInput = new FileInputStream(inputFile);
byte[] buffer = new byte[8192];
int length;
diff --git a/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/src/main/java/com/threerings/getdown/tools/DigesterTask.java
index a059250..18bcdda 100644
--- a/src/main/java/com/threerings/getdown/tools/DigesterTask.java
+++ b/src/main/java/com/threerings/getdown/tools/DigesterTask.java
@@ -13,6 +13,8 @@ import java.security.GeneralSecurityException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
+import com.threerings.getdown.data.Digest;
+
/**
* An ant task used to create a digest.txt for a Getdown
* application deployment.
@@ -70,10 +72,7 @@ public class DigesterTask extends Task
}
try {
- Digester.createDigest(_appdir);
- if (_storepath != null) {
- Digester.signDigest(_appdir, _storepath, _storepass, _storealias);
- }
+ Digester.createDigests(_appdir, _storepath, _storepass, _storealias);
} catch (IOException ioe) {
throw new BuildException("Error creating digest: " + ioe.getMessage(), ioe);
} catch (GeneralSecurityException gse) {
diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java
index f8362eb..67c0db9 100644
--- a/src/main/java/com/threerings/getdown/util/FileUtil.java
+++ b/src/main/java/com/threerings/getdown/util/FileUtil.java
@@ -60,6 +60,9 @@ public class FileUtil extends com.samskivert.util.FileUtil
fin = new FileInputStream(source);
fout = new FileOutputStream(dest);
StreamUtil.copy(fin, fout);
+ // close the input stream now so we can delete 'source'
+ fin.close();
+ fin = null;
if (!source.delete()) {
log.warning("Failed to delete " + source +
" after brute force copy to " + dest + ".");