diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..160c7f7 --- /dev/null +++ b/build.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/LIBS b/lib/LIBS new file mode 100644 index 0000000..2d57f1c --- /dev/null +++ b/lib/LIBS @@ -0,0 +1,2 @@ +samskivert.jar +ant.jar diff --git a/lib/manifest.mf b/lib/manifest.mf new file mode 100644 index 0000000..ed06cbd --- /dev/null +++ b/lib/manifest.mf @@ -0,0 +1 @@ +Main-Class: com.threerings.getdown.launcher.Getdown diff --git a/src/java/com/threerings/getdown/Log.java b/src/java/com/threerings/getdown/Log.java new file mode 100644 index 0000000..132fd4f --- /dev/null +++ b/src/java/com/threerings/getdown/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown; + +/** + * A placeholder class that contains a reference to the log object used by + * the Getdown code. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("getdown"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java new file mode 100644 index 0000000..296bacf --- /dev/null +++ b/src/java/com/threerings/getdown/data/Application.java @@ -0,0 +1,185 @@ +// +// $Id: Application.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.data; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.samskivert.io.NestableIOException; +import com.samskivert.text.MessageUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.getdown.Log; +import com.threerings.getdown.util.ConfigUtil; + +/** + * Parses and provide access to the information contained in the + * getdown.txt configuration file. + */ +public class Application +{ + /** The name of our configuration file. */ + public static final String CONFIG_FILE = "getdown.txt"; + + /** + * Creates an application instance which records the location of the + * getdown.txt configuration file from the supplied + * application directory. + */ + public Application (File appdir) + { + _appdir = appdir; + _config = new File(appdir, CONFIG_FILE); + } + + /** + * Returns a resource that refers to the application configuration + * file itself. + */ + public Resource getConfigResource () + { + try { + return new Resource(_appbase, _appdir, CONFIG_FILE); + } catch (Exception e) { + throw new RuntimeException("Booched appbase '" + _appbase + "'!?"); + } + } + + /** + * Returns a list of the code {@link Resource} objects used by this + * application. + */ + public List getCodeResources () + { + return _codes; + } + + /** + * Returns a list of the non-code {@link Resource} objects used by + * this application. + */ + public List getResources () + { + return _resources; + } + + /** + * Instructs the application to parse its getdown.txt + * configuration and prepare itself for operation. The application + * base URL will be parsed first so that if there are errors + * discovered later, the caller can use the application base to + * download a new config.txt file and try again. + * + * @exception IOException thrown if there is an error reading the file + * or an error encountered during its parsing. + */ + public void init () + throws IOException + { + // parse our configuration file + HashMap cdata = ConfigUtil.parseConfig(_config); + + // first determine our application base, this way if anything goes + // wrong later in the process, our caller can use the appbase to + // download a new configuration file + String appbase = (String)cdata.get("appbase"); + if (appbase == null) { + throw new IOException("m.missing_appbase"); + } + try { + // make sure there's a trailing slash + if (!appbase.endsWith("/")) { + appbase = appbase + "/"; + } + _appbase = new URL(appbase); + } catch (Exception e) { + String err = MessageUtil.tcompose("m.invalid_appbase", appbase); + throw new NestableIOException(err, e); + } + + // extract our version information + String vstr = (String)cdata.get("version"); + if (vstr != null) { + try { + _version = Integer.parseInt(vstr); + } catch (Exception e) { + String err = MessageUtil.tcompose("m.invalid_version", vstr); + throw new NestableIOException(err, e); + } + } + + // determine our application class name + _class = (String)cdata.get("class"); + if (_class == null) { + throw new IOException("m.missing_class"); + } + + // parse our code resources + String[] codes = ConfigUtil.getMultiValue(cdata, "code"); + if (codes == null) { + throw new IOException("m.missing_code"); + } + for (int ii = 0; ii < codes.length; ii++) { + try { + _codes.add(new Resource(_appbase, _appdir, codes[ii])); + } catch (Exception e) { + Log.warning("Invalid code resource '" + codes[ii] + "'."); + } + } + + // parse our non-code resources + String[] rsrcs = ConfigUtil.getMultiValue(cdata, "resource"); + if (rsrcs != null) { + for (int ii = 0; ii < rsrcs.length; ii++) { + try { + _resources.add(new Resource(_appbase, _appdir, rsrcs[ii])); + } catch (Exception e) { + Log.warning("Invalid resource '" + rsrcs[ii] + "'."); + } + } + } + + // transfer our JVM arguments + String[] jvmargs = ConfigUtil.getMultiValue(cdata, "jvmarg"); + if (jvmargs != null) { + for (int ii = 0; ii < jvmargs.length; ii++) { + _jvmargs.add(jvmargs[ii]); + } + } + + // transfer our application arguments + String[] appargs = ConfigUtil.getMultiValue(cdata, "apparg"); + if (appargs != null) { + for (int ii = 0; ii < appargs.length; ii++) { + _appargs.add(appargs[ii]); + } + } + +// Log.info("Parsed application " + _appbase); +// Log.info("Version: " + _version); +// Log.info("Class: " + _class); +// Log.info("Code: " + StringUtil.toString(_codes.iterator())); +// Log.info("Resources: " + StringUtil.toString(_resources.iterator())); +// Log.info("JVM Args: " + StringUtil.toString(_jvmargs.iterator())); +// Log.info("App Args: " + StringUtil.toString(_appargs.iterator())); + } + + protected File _appdir; + protected File _config; + + protected int _version = -1; + protected URL _appbase; + protected String _class; + + protected ArrayList _codes = new ArrayList(); + protected ArrayList _resources = new ArrayList(); + + protected ArrayList _jvmargs = new ArrayList(); + protected ArrayList _appargs = new ArrayList(); +} diff --git a/src/java/com/threerings/getdown/data/Digest.java b/src/java/com/threerings/getdown/data/Digest.java new file mode 100644 index 0000000..8322728 --- /dev/null +++ b/src/java/com/threerings/getdown/data/Digest.java @@ -0,0 +1,121 @@ +// +// $Id: Digest.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.data; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.samskivert.text.MessageUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.getdown.util.ConfigUtil; + +/** + * Manages the digest.txt file and the computing and + * processing of MD5 digests for an application. + */ +public class Digest +{ + /** The name of our MD5 digest file. */ + public static final String DIGEST_FILE = "digest.txt"; + + /** + * Creates a digest instance which will parse and validate the + * digest.txt in the supplied application directory. + */ + public Digest (File appdir) + throws IOException + { + // parse and validate our digest file contents + String metaDigest = ""; + StringBuffer data = new StringBuffer(); + List pairs = ConfigUtil.parsePairs(new File(appdir, DIGEST_FILE)); + for (Iterator iter = pairs.iterator(); iter.hasNext(); ) { + String[] pair = (String[])iter.next(); + if (pair[0].equals(DIGEST_FILE)) { + metaDigest = pair[1]; + break; + } + _digests.put(pair[0], pair[1]); + note(data, pair[0], pair[1]); + } + + // we've reached the end, validate our contents + MessageDigest md = getMessageDigest(); + 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); + throw new IOException(err); + } + } + + /** + * 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); + } + + /** + * 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(); + StringBuffer data = new StringBuffer(); + PrintWriter pout = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(output), "UTF-8")); + + // compute and append the MD5 digest of each resource in the list + for (Iterator iter = resources.iterator(); iter.hasNext(); ) { + Resource rsrc = (Resource)iter.next(); + String path = rsrc.getPath(); + String digest = rsrc.computeDigest(md); + note(data, path, digest); + pout.println(path + " = " + digest); + } + + // 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))); + + pout.close(); + } + + /** Used by {@link #createDigest} and {@link Digest}. */ + protected static void note (StringBuffer data, String path, String digest) + { + data.append(path).append(" = ").append(digest).append("\n"); + } + + /** Used by {@link #createDigest} and {@link Digest}. */ + protected static MessageDigest getMessageDigest () + { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException("JVM does not support MD5. Gurp!"); + } + } + + protected HashMap _digests = new HashMap(); +} diff --git a/src/java/com/threerings/getdown/data/Resource.java b/src/java/com/threerings/getdown/data/Resource.java new file mode 100644 index 0000000..5700cc9 --- /dev/null +++ b/src/java/com/threerings/getdown/data/Resource.java @@ -0,0 +1,86 @@ +// +// $Id: Resource.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import java.net.MalformedURLException; +import java.net.URL; + +import java.security.MessageDigest; + +import com.samskivert.util.StringUtil; + +/** + * Models a single file resource used by an {@link Application}. + */ +public class Resource +{ + /** + * Creates a resource with the supplied application base URL and + * resource path. + */ + public Resource (URL appbase, File appdir, String path) + throws MalformedURLException + { + _path = path; + _remote = new URL(appbase, path); + _local = new File(appdir, path); + } + + /** + * Returns the path associated with this resource. + */ + public String getPath () + { + return _path; + } + + /** + * Computes the MD5 hash of this resource's underlying file. + * Note: This is both CPU and I/O intensive. + */ + public String computeDigest (MessageDigest md) + throws IOException + { + md.reset(); + byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; + int read; + FileInputStream fin = new FileInputStream(_local); + while ((read = fin.read(buffer)) != -1) { + md.update(buffer, 0, read); + } + return StringUtil.hexlate(md.digest()); + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + // return _path; + // return _netloc.toString(); + 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; + + protected final static int DIGEST_BUFFER_SIZE = 5 * 1025; +} diff --git a/src/java/com/threerings/getdown/launcher/Getdown.java b/src/java/com/threerings/getdown/launcher/Getdown.java new file mode 100644 index 0000000..1674aa7 --- /dev/null +++ b/src/java/com/threerings/getdown/launcher/Getdown.java @@ -0,0 +1,55 @@ +// +// $Id: Getdown.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.launcher; + +import java.io.File; + +import com.threerings.getdown.Log; +import com.threerings.getdown.data.Application; + +/** + * Manages the main control for the Getdown application updater and + * deployment system. + */ +public class Getdown +{ + public Getdown (File appDir) + { + _app = new Application(appDir); + } + + public void run () + { + try { + _app.init(); + } catch (Exception e) { + Log.logStackTrace(e); + } + } + + public static void main (String[] args) + { + // ensure the proper parameters are passed + if (args.length != 1) { + System.err.println("Usage: java -jar getdown.jar app_dir"); + System.exit(-1); + } + + // ensure a valid directory was supplied + File appDir = new File(args[0]); + if (!appDir.exists() || !appDir.isDirectory()) { + Log.warning("Invalid app_dir '" + args[0] + "'."); + System.exit(-1); + } + + try { + Getdown app = new Getdown(appDir); + app.run(); + } catch (Exception e) { + Log.logStackTrace(e); + } + } + + protected Application _app; +} diff --git a/src/java/com/threerings/getdown/tools/DigesterTask.java b/src/java/com/threerings/getdown/tools/DigesterTask.java new file mode 100644 index 0000000..6834637 --- /dev/null +++ b/src/java/com/threerings/getdown/tools/DigesterTask.java @@ -0,0 +1,71 @@ +// +// $Id: DigesterTask.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.tools; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.tools.ant.BuildException; +import org.apache.tools.ant.Task; + +import com.samskivert.util.CollectionUtil; + +import com.threerings.getdown.data.Application; +import com.threerings.getdown.data.Digest; + +/** + * An ant task used to create a digest.txt for a Getdown + * application deployment. + */ +public class DigesterTask extends Task +{ + /** + * Sets the application directory. + */ + public void setAppdir (File appdir) + { + _appdir = appdir; + } + + /** + * Performs the actual work of the task. + */ + public void execute () throws BuildException + { + // make sure appdir is set + if (_appdir == null) { + String errmsg = "Must specify the path to the application " + + "directory via the 'appdir' attribute."; + throw new BuildException(errmsg); + } + File target = new File(_appdir, Digest.DIGEST_FILE); + System.out.println("Generating digest file '" + target + "'..."); + + // create our application and instruct it to parse its business + Application app = new Application(_appdir); + try { + app.init(); + } catch (IOException ioe) { + throw new BuildException("Error parsing getdown.txt: " + + ioe.getMessage(), ioe); + } + + ArrayList rsrcs = new ArrayList(); + rsrcs.add(app.getConfigResource()); + CollectionUtil.addAll(rsrcs, app.getCodeResources().iterator()); + CollectionUtil.addAll(rsrcs, app.getResources().iterator()); + + // now generate the digest file + try { + Digest.createDigest(rsrcs, target); + } catch (IOException ioe) { + throw new BuildException("Error creating digest: " + + ioe.getMessage(), ioe); + } + } + + /** The application directory in which we're creating a digest file. */ + protected File _appdir; +} diff --git a/src/java/com/threerings/getdown/util/ConfigUtil.java b/src/java/com/threerings/getdown/util/ConfigUtil.java new file mode 100644 index 0000000..9a69aa4 --- /dev/null +++ b/src/java/com/threerings/getdown/util/ConfigUtil.java @@ -0,0 +1,119 @@ +// +// $Id: ConfigUtil.java,v 1.1 2004/07/02 11:01:21 mdb Exp $ + +package com.threerings.getdown.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.samskivert.util.StringUtil; + +/** + * Parses a file containing key/value pairs and returns a {@link HashMap} + * with the values. Keys may be repeated, in which case they will be made + * to reference an array of values. + */ +public class ConfigUtil +{ + /** + * Parses a configuration file containing key/value pairs. The file + * must be in the UTF-8 encoding. + * + * @return a list of String[] instances containing the + * key/value pairs in the order they were parsed from the file. + */ + public static List parsePairs (File config) + throws IOException + { + 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); + } + + // 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); + } + + return pairs; + } + + /** + * Parses a configuration file containing key/value pairs. The file + * must be in the UTF-8 encoding. + * + * @return a map from keys to values, where a value will be an array + * of strings if more than one key/value pair in the config file was + * associated with the same key. + */ + public static HashMap parseConfig (File config) + throws IOException + { + List pairs = parsePairs(config); + HashMap data = new HashMap(); + + for (Iterator iter = pairs.iterator(); iter.hasNext(); ) { + String[] pair = (String[])iter.next(); + Object value = data.get(pair[0]); + if (value == null) { + data.put(pair[0], pair[1]); + } else if (value instanceof String) { + data.put(pair[0], new String[] { (String)value, pair[1] }); + } else if (value instanceof String[]) { + String[] values = (String[])value; + String[] nvalues = new String[values.length+1]; + System.arraycopy(values, 0, nvalues, 0, values.length); + nvalues[values.length] = pair[1]; + data.put(pair[0], nvalues); + } + } + + return data; + } + + /** + * Massages a single string into an array and leaves existing array + * values as is. Simplifies access to parameters that are expected to + * be arrays. + */ + public static String[] getMultiValue (HashMap data, String name) + { + Object value = data.get(name); + if (value instanceof String) { + return new String[] { (String)value }; + } else { + return (String[])value; + } + } +}