Further progress. It makes me giddy with joy when I munge some text in the

getdown.txt configuration file and then watch the system recover and
proceed by downloading a fresh copy, emitting but a mild mannered log
message in the process.
This commit is contained in:
Michael Bayne
2004-07-02 15:22:49 +00:00
parent 2fb08f098a
commit 0d91188d7f
6 changed files with 262 additions and 60 deletions
+1
View File
@@ -1,2 +1,3 @@
samskivert.jar
ant.jar
commons-io.jar
@@ -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
* <code>getdown.txt</code> 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 <code>digest.txt</code> file and verifies the contents of
* both that file and the <code>getdown.text</code> file. Then it
* loads the <code>version.txt</code> 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();
@@ -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;
}
/**
@@ -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;
@@ -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);
}
@@ -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;