diff --git a/lib/LIBS b/lib/LIBS
index 2d57f1c..e226736 100644
--- a/lib/LIBS
+++ b/lib/LIBS
@@ -1,2 +1,3 @@
samskivert.jar
ant.jar
+commons-io.jar
diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java
index 296bacf..f568345 100644
--- a/src/java/com/threerings/getdown/data/Application.java
+++ b/src/java/com/threerings/getdown/data/Application.java
@@ -1,20 +1,31 @@
//
-// $Id: Application.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+// $Id: Application.java,v 1.2 2004/07/02 15:22:49 mdb Exp $
package com.threerings.getdown.data;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import java.net.MalformedURLException;
import java.net.URL;
+import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.samskivert.io.NestableIOException;
+import com.samskivert.io.StreamUtil;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
+import org.apache.commons.io.StreamUtils;
+
import com.threerings.getdown.Log;
import com.threerings.getdown.util.ConfigUtil;
@@ -27,6 +38,9 @@ public class Application
/** The name of our configuration file. */
public static final String CONFIG_FILE = "getdown.txt";
+ /** The name of our target version file. */
+ public static final String VERSION_FILE = "version.txt";
+
/**
* Creates an application instance which records the location of the
* getdown.txt configuration file from the supplied
@@ -35,7 +49,7 @@ public class Application
public Application (File appdir)
{
_appdir = appdir;
- _config = new File(appdir, CONFIG_FILE);
+ _config = getLocalPath(CONFIG_FILE);
}
/**
@@ -45,7 +59,7 @@ public class Application
public Resource getConfigResource ()
{
try {
- return new Resource(_appbase, _appdir, CONFIG_FILE);
+ return createResource(CONFIG_FILE);
} catch (Exception e) {
throw new RuntimeException("Booched appbase '" + _appbase + "'!?");
}
@@ -114,12 +128,31 @@ public class Application
}
}
+ // if we are a versioned deployment, create a versioned appbase
+ if (_version < 0) {
+ _vappbase = _appbase;
+ } else {
+ try {
+ _vappbase = new URL(
+ StringUtil.replace(_appbase.toString(), "%VERSION%", vstr));
+ } catch (MalformedURLException mue) {
+ String err = MessageUtil.tcompose("m.invalid_appbase", appbase);
+ throw new NestableIOException(err, mue);
+ }
+ }
+
// determine our application class name
_class = (String)cdata.get("class");
if (_class == null) {
throw new IOException("m.missing_class");
}
+ // clear our arrays as we may be reinitializing
+ _codes.clear();
+ _resources.clear();
+ _jvmargs.clear();
+ _appargs.clear();
+
// parse our code resources
String[] codes = ConfigUtil.getMultiValue(cdata, "code");
if (codes == null) {
@@ -127,9 +160,9 @@ public class Application
}
for (int ii = 0; ii < codes.length; ii++) {
try {
- _codes.add(new Resource(_appbase, _appdir, codes[ii]));
+ _codes.add(createResource(codes[ii]));
} catch (Exception e) {
- Log.warning("Invalid code resource '" + codes[ii] + "'.");
+ Log.warning("Invalid code resource '" + codes[ii] + "'." + e);
}
}
@@ -138,9 +171,9 @@ public class Application
if (rsrcs != null) {
for (int ii = 0; ii < rsrcs.length; ii++) {
try {
- _resources.add(new Resource(_appbase, _appdir, rsrcs[ii]));
+ _resources.add(createResource(rsrcs[ii]));
} catch (Exception e) {
- Log.warning("Invalid resource '" + rsrcs[ii] + "'.");
+ Log.warning("Invalid resource '" + rsrcs[ii] + "'. " + e);
}
}
}
@@ -170,11 +203,151 @@ public class Application
// Log.info("App Args: " + StringUtil.toString(_appargs.iterator()));
}
+ /**
+ * Returns a URL from which the specified path can be fetched. Our
+ * application base URL is properly versioned and combined with the
+ * supplied path.
+ */
+ public URL getRemoteURL (String path)
+ throws MalformedURLException
+ {
+ return new URL(_vappbase, path);
+ }
+
+ /**
+ * Returns the local path to the specified resource.
+ */
+ public File getLocalPath (String path)
+ {
+ return new File(_appdir, path);
+ }
+
+ /**
+ * Loads the digest.txt file and verifies the contents of
+ * both that file and the getdown.text file. Then it
+ * loads the version.txt and decides whether or not the
+ * application needs to be updated or whether we can proceed to
+ * verification and execution.
+ *
+ * @return true if the application needs to be updated, false if it is
+ * up to date and can be verified and executed.
+ *
+ * @exception IOException thrown if we encounter an unrecoverable
+ * error while verifying the metadata.
+ */
+ public boolean verifyMetadata ()
+ throws IOException
+ {
+ // create our digester which will read in the contents of the
+ // digest file and validate itself
+ try {
+ _digest = new Digest(_appdir);
+ } catch (IOException ioe) {
+ Log.info("Failed to load digest: " + ioe.getMessage() + ". " +
+ "Attempting recovery...");
+ }
+
+ // if we failed to load the digest, try to redownload the digest
+ // file and give it another good college try; this time we allow
+ // exceptions to propagate up to the caller as there is nothing
+ // else we can do to recover
+ if (_digest == null) {
+ downloadControlFile(Digest.DIGEST_FILE);
+ _digest = new Digest(_appdir);
+ }
+
+ // now verify the contents of our main config file
+ Resource crsrc = getConfigResource();
+ if (!_digest.validateResource(crsrc)) {
+ // attempt to redownload the file; again we pass errors up to
+ // our caller because we have no recourse to recovery
+ downloadControlFile(CONFIG_FILE);
+ // if the new copy validates, reinitialize ourselves;
+ // otherwise report baffling hoseage
+ if (_digest.validateResource(crsrc)) {
+ init();
+ }
+ }
+
+ // start by assuming we are happy with our version
+ _targetVersion = _version;
+
+ // now read in the contents of the version.txt file (if any)
+ File vfile = getLocalPath(VERSION_FILE);
+ FileInputStream fin = null;
+ try {
+ fin = new FileInputStream(vfile);
+ BufferedReader bin = new BufferedReader(
+ new InputStreamReader(fin));
+ String vstr = bin.readLine();
+ if (!StringUtil.blank(vstr)) {
+ _targetVersion = Integer.parseInt(vstr);
+ }
+ } catch (Exception e) {
+ Log.info("Unable to read version file: " + e.getMessage());
+ } finally {
+ StreamUtil.close(fin);
+ }
+
+ // finally let the caller know if we need an update
+ return _version != _targetVersion;
+ }
+
+ /**
+ * Downloads a new copy of the specified control file and, if the
+ * download is successful, moves it over the old file on the
+ * filesystem.
+ */
+ protected void downloadControlFile (String path)
+ throws IOException
+ {
+ File target = getLocalPath(path + "_new");
+ URL targetURL = null;
+ try {
+ targetURL = getRemoteURL(path);
+ } catch (Exception e) {
+ Log.warning("Requested to download invalid control file " +
+ "[appbase=" + _appbase + ", path=" + path +
+ ", error=" + e + "].");
+ throw new NestableIOException("Invalid path '" + path + "'.", e);
+ }
+
+ Log.info("Attempting to refetch '" + path + "'.");
+
+ // stream the URL into our temporary file
+ InputStream fin = null;
+ FileOutputStream fout = null;
+ try {
+ fin = targetURL.openStream();
+ fout = new FileOutputStream(target);
+ StreamUtils.pipe(fin, fout);
+ } finally {
+ StreamUtil.close(fin);
+ StreamUtil.close(fout);
+ }
+
+ // now attempt to replace the current file with the new one
+ File original = getLocalPath(path);
+ if (!target.renameTo(original)) {
+ throw new IOException(
+ "Failed to rename(" + target + ", " + original + ")");
+ }
+ }
+
+ /** Helper function for creating {@link Resource} instances. */
+ protected Resource createResource (String path)
+ throws MalformedURLException
+ {
+ return new Resource(path, getRemoteURL(path), getLocalPath(path));
+ }
+
protected File _appdir;
protected File _config;
+ protected Digest _digest;
protected int _version = -1;
- protected URL _appbase;
+ protected int _targetVersion = -1;
+ protected URL _appbase, _vappbase;
protected String _class;
protected ArrayList _codes = new ArrayList();
diff --git a/src/java/com/threerings/getdown/data/Digest.java b/src/java/com/threerings/getdown/data/Digest.java
index 8322728..4f5a7f7 100644
--- a/src/java/com/threerings/getdown/data/Digest.java
+++ b/src/java/com/threerings/getdown/data/Digest.java
@@ -1,5 +1,5 @@
//
-// $Id: Digest.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+// $Id: Digest.java,v 1.2 2004/07/02 15:22:49 mdb Exp $
package com.threerings.getdown.data;
@@ -19,6 +19,7 @@ import java.util.List;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
+import com.threerings.getdown.Log;
import com.threerings.getdown.util.ConfigUtil;
/**
@@ -62,13 +63,39 @@ public class Digest
}
}
+// /**
+// * Returns the stored digest value for the file with the supplied
+// * path, or null if no digest exists for a file with that path.
+// */
+// public String getDigest (String path)
+// {
+// return (String)_digests.get(path);
+// }
+
/**
- * Returns the stored digest value for the file with the supplied
- * path, or null if no digest exists for a file with that path.
+ * Computes the MD5 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 String getDigest (String path)
+ public boolean validateResource (Resource resource)
{
- return (String)_digests.get(path);
+ try {
+ String cmd5 = resource.computeDigest(getMessageDigest());
+ String emd5 = (String)_digests.get(resource.getPath());
+ if (cmd5.equals(emd5)) {
+ return true;
+ }
+ Log.info("Resource failed digest check [rsrc=" + resource +
+ ", computed=" + cmd5 + ", expected=" + emd5 + "].");
+ } catch (IOException ioe) {
+ Log.info("Resource failed digest check [rsrc=" + resource +
+ ", error=" + ioe + "].");
+ }
+ return false;
}
/**
diff --git a/src/java/com/threerings/getdown/data/Resource.java b/src/java/com/threerings/getdown/data/Resource.java
index 5700cc9..1988399 100644
--- a/src/java/com/threerings/getdown/data/Resource.java
+++ b/src/java/com/threerings/getdown/data/Resource.java
@@ -1,5 +1,5 @@
//
-// $Id: Resource.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+// $Id: Resource.java,v 1.2 2004/07/02 15:22:49 mdb Exp $
package com.threerings.getdown.data;
@@ -20,15 +20,13 @@ import com.samskivert.util.StringUtil;
public class Resource
{
/**
- * Creates a resource with the supplied application base URL and
- * resource path.
+ * Creates a resource with the supplied remote URL and local path.
*/
- public Resource (URL appbase, File appdir, String path)
- throws MalformedURLException
+ public Resource (String path, URL remote, File local)
{
_path = path;
- _remote = new URL(appbase, path);
- _local = new File(appdir, path);
+ _remote = remote;
+ _local = local;
}
/**
@@ -66,18 +64,6 @@ public class Resource
return _local.getPath();
}
- public static void main (String[] args)
- {
- try {
- Resource rsrc = new Resource(
- new URL("file:/"), new File("."), args[0]);
- System.out.println("MD5: " + rsrc.computeDigest(
- MessageDigest.getInstance("MD5")));
- } catch (Exception e) {
- e.printStackTrace(System.err);
- }
- }
-
protected String _path;
protected URL _remote;
protected File _local;
diff --git a/src/java/com/threerings/getdown/launcher/Getdown.java b/src/java/com/threerings/getdown/launcher/Getdown.java
index 1674aa7..aead770 100644
--- a/src/java/com/threerings/getdown/launcher/Getdown.java
+++ b/src/java/com/threerings/getdown/launcher/Getdown.java
@@ -1,5 +1,5 @@
//
-// $Id: Getdown.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+// $Id: Getdown.java,v 1.2 2004/07/02 15:22:49 mdb Exp $
package com.threerings.getdown.launcher;
@@ -23,6 +23,13 @@ public class Getdown
{
try {
_app.init();
+
+ if (_app.verifyMetadata()) {
+ Log.info("Application requires update.");
+ } else {
+ Log.info("Metadata verified.");
+ }
+
} catch (Exception e) {
Log.logStackTrace(e);
}
diff --git a/src/java/com/threerings/getdown/util/ConfigUtil.java b/src/java/com/threerings/getdown/util/ConfigUtil.java
index 9a69aa4..5bee01f 100644
--- a/src/java/com/threerings/getdown/util/ConfigUtil.java
+++ b/src/java/com/threerings/getdown/util/ConfigUtil.java
@@ -1,5 +1,5 @@
//
-// $Id: ConfigUtil.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
+// $Id: ConfigUtil.java,v 1.2 2004/07/02 15:22:49 mdb Exp $
package com.threerings.getdown.util;
@@ -14,6 +14,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import com.samskivert.io.StreamUtil;
import com.samskivert.util.StringUtil;
/**
@@ -36,34 +37,41 @@ public class ConfigUtil
ArrayList pairs = new ArrayList();
// parse our configuration file
- BufferedReader bin = new BufferedReader(
- new InputStreamReader(new FileInputStream(config), "UTF-8"));
- String line = null;
- while ((line = bin.readLine()) != null) {
- // nix comments
- int cidx = line.indexOf("#");
- if (cidx != -1) {
- line = line.substring(0, cidx);
+ FileInputStream fin = null;
+ try {
+ fin = new FileInputStream(config);
+ BufferedReader bin =
+ new BufferedReader(new InputStreamReader(fin, "UTF-8"));
+ String line = null;
+ while ((line = bin.readLine()) != null) {
+ // nix comments
+ int cidx = line.indexOf("#");
+ if (cidx != -1) {
+ line = line.substring(0, cidx);
+ }
+
+ // trim whitespace and skip blank lines
+ line = line.trim();
+ if (StringUtil.blank(line)) {
+ continue;
+ }
+
+ // parse our key/value pair
+ String[] pair = new String[2];
+ int eidx = 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] = "";
+ }
+
+ pairs.add(pair);
}
- // trim whitespace and skip blank lines
- line = line.trim();
- if (StringUtil.blank(line)) {
- continue;
- }
-
- // parse our key/value pair
- String[] pair = new String[2];
- int eidx = 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] = "";
- }
-
- pairs.add(pair);
+ } finally {
+ StreamUtil.close(fin);
}
return pairs;