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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user