From 0d91188d7f8c520ed9e5bf94670349fcd0161230 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 2 Jul 2004 15:22:49 +0000 Subject: [PATCH] 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. --- lib/LIBS | 1 + .../threerings/getdown/data/Application.java | 189 +++++++++++++++++- .../com/threerings/getdown/data/Digest.java | 37 +++- .../com/threerings/getdown/data/Resource.java | 24 +-- .../threerings/getdown/launcher/Getdown.java | 9 +- .../threerings/getdown/util/ConfigUtil.java | 62 +++--- 6 files changed, 262 insertions(+), 60 deletions(-) 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;