diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java
index 7a71049..ca049c6 100644
--- a/src/java/com/threerings/getdown/data/Application.java
+++ b/src/java/com/threerings/getdown/data/Application.java
@@ -1,5 +1,5 @@
//
-// $Id: Application.java,v 1.3 2004/07/02 17:03:33 mdb Exp $
+// $Id: Application.java,v 1.4 2004/07/06 05:13:35 mdb Exp $
package com.threerings.getdown.data;
@@ -215,6 +215,64 @@ public class Application
return new File(_appdir, path);
}
+ /**
+ * Invokes the process associated with this application definition.
+ */
+ public Process createProcess ()
+ throws IOException
+ {
+ // create our classpath
+ StringBuffer cpbuf = new StringBuffer();
+ for (Iterator iter = _codes.iterator(); iter.hasNext(); ) {
+ if (cpbuf.length() > 0) {
+ cpbuf.append(File.pathSeparator);
+ }
+ Resource rsrc = (Resource)iter.next();
+ cpbuf.append(rsrc.getLocal().getAbsolutePath());
+ }
+
+ // we'll need the JVM, classpath, JVM args, class name and app args
+ String[] args = new String[4 + _jvmargs.size() + _appargs.size()];
+ int idx = 0;
+
+ // reconstruct the path to the JVM
+ args[idx++] = System.getProperty("java.home") +
+ File.separator + "bin" + File.separator + "java";
+
+ // add the classpath arguments
+ args[idx++] = "-classpath";
+ args[idx++] = cpbuf.toString();
+
+ // add the JVM arguments
+ for (Iterator iter = _jvmargs.iterator(); iter.hasNext(); ) {
+ args[idx++] = processArg((String)iter.next());
+ }
+
+ // add the application class name
+ args[idx++] = _class;
+
+ // finally add the application arguments
+ for (Iterator iter = _appargs.iterator(); iter.hasNext(); ) {
+ args[idx++] = processArg((String)iter.next());
+ }
+
+ Log.info("Running " + StringUtil.join(args, "\n"));
+ return Runtime.getRuntime().exec(args, null);
+ }
+
+ /** Replaces the application directory and version in any argument. */
+ protected String processArg (String arg)
+ {
+ if (arg.indexOf("%APPDIR%") != -1) {
+ arg = StringUtil.replace(
+ arg, "%APPDIR%", _appdir.getAbsolutePath());
+ }
+ if (arg.indexOf("%VERSION%") != -1) {
+ arg = StringUtil.replace(arg, "%VERSION%", "" + _version);
+ }
+ return arg;
+ }
+
/**
* Loads the digest.txt file and verifies the contents of
* both that file and the getdown.text file. Then it
diff --git a/src/java/com/threerings/getdown/data/Resource.java b/src/java/com/threerings/getdown/data/Resource.java
index dc9e9a2..162fa24 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.3 2004/07/02 17:03:33 mdb Exp $
+// $Id: Resource.java,v 1.4 2004/07/06 05:13:36 mdb Exp $
package com.threerings.getdown.data;
@@ -39,6 +39,22 @@ public class Resource
return _path;
}
+ /**
+ * Returns the local location of this resource.
+ */
+ public File getLocal ()
+ {
+ return _local;
+ }
+
+ /**
+ * Returns the remote location of this resource.
+ */
+ public URL getRemote ()
+ {
+ return _remote;
+ }
+
/**
* Computes the MD5 hash of this resource's underlying file.
* Note: This is both CPU and I/O intensive.
diff --git a/src/java/com/threerings/getdown/launcher/Downloader.java b/src/java/com/threerings/getdown/launcher/Downloader.java
new file mode 100644
index 0000000..fab5f82
--- /dev/null
+++ b/src/java/com/threerings/getdown/launcher/Downloader.java
@@ -0,0 +1,264 @@
+//
+// $Id: Downloader.java,v 1.1 2004/07/06 05:13:36 mdb Exp $
+
+package com.threerings.getdown.launcher;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.net.HttpURLConnection;
+
+import java.util.Iterator;
+import java.util.List;
+
+import com.samskivert.io.StreamUtil;
+
+import com.threerings.getdown.Log;
+import com.threerings.getdown.data.Resource;
+
+/**
+ * Handles the download of a collection of files, first issuing HTTP head
+ * requests to obtain size information and then downloading the files
+ * individually, reporting progress back via a callback interface.
+ */
+public class Downloader extends Thread
+{
+ /**
+ * An interface used to communicate status back to an external entity.
+ * Note: these methods are all called on the download thread,
+ * so implementors must take care to only execute thread-safe code or
+ * simply pass a message to the AWT thread, for example.
+ */
+ public interface Observer
+ {
+ /**
+ * Called before the downloader begins the series of HTTP head
+ * requests to determine the size of the files it needs to
+ * download.
+ */
+ public void resolvingDownloads ();
+
+ /**
+ * Called to inform the observer of ongoing progress toward
+ * completion of the overall downloading task. The caller is
+ * guaranteed to get at least one call reporting 100% completion.
+ *
+ * @param percent the percent completion, in terms of total file
+ * size, of the downloads.
+ * @param remaining the estimated download time remaining in
+ * seconds, or -1 if the time can not yet be
+ * determined.
+ */
+ public void downloadProgress (int percent, long remaining);
+
+ /**
+ * Called if a failure occurs while checking for an update or
+ * downloading a file.
+ *
+ * @param rsrc the resource that was being downloaded when the
+ * error occurred, or null if the failure occurred
+ * while resolving downloads.
+ * @param e the exception detailing the failure.
+ */
+ public void downloadFailed (Resource rsrc, Exception e);
+ }
+
+ /**
+ * Creates a downloader that will download the supplied list of
+ * resources and communicate with the specified observer. The {@link
+ * #start} method must be called on the downloader to initiate the
+ * download process.
+ */
+ public Downloader (List resources, Observer obs)
+ {
+ super("Downloader");
+ _resources = resources;
+ _obs = obs;
+ }
+
+ /**
+ * This method is invoked as the downloader thread and performs the
+ * actual downloading.
+ */
+ public void run ()
+ {
+ Resource current = null;
+ try {
+ // let the observer know that we're computing download size
+ if (_obs != null) {
+ _obs.resolvingDownloads();
+ }
+
+ // first compute the total size of our download
+ for (Iterator iter = _resources.iterator(); iter.hasNext(); ) {
+ discoverSize((Resource)iter.next());
+ }
+
+ // make a note of the time at which we started the download
+ _start = System.currentTimeMillis();
+
+ // now actually download the files
+ for (Iterator iter = _resources.iterator(); iter.hasNext(); ) {
+ current = (Resource)iter.next();
+ download(current);
+ }
+
+ // finally report our download completion if we did not
+ // already do so when downloading our final resource
+ if (_obs != null && !_complete) {
+ _obs.downloadProgress(100, 0);
+ }
+
+ } catch (Exception e) {
+ if (_obs != null) {
+ _obs.downloadFailed(current, e);
+ }
+ }
+ }
+
+ /**
+ * Issues a HEAD request for the specified resource and notes the
+ * amount of data we will be downloading to account for it.
+ */
+ protected void discoverSize (Resource rsrc)
+ throws IOException
+ {
+ // read the file information via an HTTP HEAD request
+ HttpURLConnection ucon = (HttpURLConnection)
+ rsrc.getRemote().openConnection();
+ ucon.setRequestMethod("HEAD");
+ ucon.connect();
+
+ // make sure we got a satisfactory response code
+ if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ String errmsg = "Unable to check up-to-date for " +
+ rsrc.getRemote() + ": " + ucon.getResponseCode();
+ throw new IOException(errmsg);
+ }
+
+ // add this resource's size to our total download size
+ _totalSize += ucon.getContentLength();
+ }
+
+ /**
+ * Downloads the specified resource from its remote location to its
+ * local location.
+ */
+ protected void download (Resource rsrc)
+ throws IOException
+ {
+ // make sure the resource's target directory exists
+ File parent = new File(rsrc.getLocal().getParent());
+ if (!parent.exists()) {
+ if (!parent.mkdirs()) {
+ Log.warning("Failed to create target directory for " +
+ "resource '" + rsrc + "'. Download will " +
+ "certainly fail.");
+ }
+ }
+
+ // download the resource from the specified URL
+ HttpURLConnection ucon = (HttpURLConnection)
+ rsrc.getRemote().openConnection();
+ ucon.connect();
+
+ // make sure we got a satisfactory response code
+ if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ String errmsg = "Unable to download resource " +
+ rsrc.getRemote() + ": " + ucon.getResponseCode();
+ throw new IOException(errmsg);
+ }
+
+ Log.info("Downloading resource [url=" + rsrc.getRemote() + "].");
+ InputStream in = null;
+ FileOutputStream out = null;
+ try {
+ in = ucon.getInputStream();
+ out = new FileOutputStream(rsrc.getLocal());
+ int read;
+
+ // TODO: look to see if we have a download info file
+ // containing info on potentially partially downloaded data;
+ // if so, use a "Range: bytes=HAVE-" header.
+
+ // read in the file data
+ while ((read = in.read(_buffer)) != -1) {
+ // write it out to our local copy
+ out.write(_buffer, 0, read);
+
+ // if we have no observer, then don't bother computing
+ // download statistics
+ if (_obs == null) {
+ continue;
+ }
+
+ // note that we've downloaded some data
+ _currentSize += read;
+
+ // notify the observer if it's been sufficiently long
+ // since our last notification
+ long now = System.currentTimeMillis();
+ if ((now - _lastUpdate) >= UPDATE_DELAY) {
+ _lastUpdate = now;
+
+ // compute our bytes per second
+ long secs = (now - _start) / 1000L;
+ long bps = (secs == 0) ? 0 : (_currentSize / secs);
+
+ // compute our percentage completion
+ int pctdone = (int)(
+ (_currentSize / (float)_totalSize) * 100f);
+
+ // estimate our time remaining
+ long remaining = (bps <= 0) ? -1 :
+ (_totalSize - _currentSize) / bps;
+
+ // make sure we only report 100% exactly once
+ if (pctdone < 100 || !_complete) {
+ _complete = (pctdone == 100);
+ _obs.downloadProgress(pctdone, remaining);
+ }
+ }
+ }
+
+ } finally {
+ StreamUtil.close(in);
+ StreamUtil.close(out);
+ }
+ }
+
+ /** The list of resources to be downloaded. */
+ protected List _resources;
+
+ /** The observer with whom we are communicating. */
+ protected Observer _obs;
+
+ /** Used while downloading. */
+ protected byte[] _buffer = new byte[4096];
+
+ /** The total file size in bytes to be transferred. */
+ protected long _totalSize;
+
+ /** The file size in bytes transferred thus far. */
+ protected long _currentSize;
+
+ /** The time at which the file transfer began. */
+ protected long _start;
+
+ /** The current transfer rate in bytes per second. */
+ protected long _bytesPerSecond;
+
+ /** The time at which the last progress update was posted to the
+ * progress observer. */
+ protected long _lastUpdate;
+
+ /** Whether the download has completed and the progress observer
+ * notified. */
+ protected boolean _complete;
+
+ /** The delay in milliseconds between notifying progress observers of
+ * file download progress. */
+ protected static final long UPDATE_DELAY = 2500L;
+}
diff --git a/src/java/com/threerings/getdown/launcher/Getdown.java b/src/java/com/threerings/getdown/launcher/Getdown.java
index a5e8dec..276c8c8 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.3 2004/07/02 17:03:33 mdb Exp $
+// $Id: Getdown.java,v 1.4 2004/07/06 05:13:36 mdb Exp $
package com.threerings.getdown.launcher;
@@ -14,6 +14,7 @@ import org.apache.commons.io.TeeOutputStream;
import com.threerings.getdown.Log;
import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Resource;
/**
* Manages the main control for the Getdown application updater and
@@ -29,26 +30,101 @@ public class Getdown
public void run ()
{
try {
- _app.init();
+ for (int ii = 0; ii < MAX_LOOPS; ii++) {
+ _app.init();
- if (_app.verifyMetadata()) {
- Log.info("Application requires update.");
+ if (_app.verifyMetadata()) {
+ Log.info("Application requires update.");
- } else {
- Log.info("Metadata verified.");
- List failures = _app.verifyResources();
- if (failures == null) {
- Log.info("Resources verified.");
} else {
- Log.info(failures.size() + " resources require update.");
+ Log.info("Metadata verified.");
+ List failures = _app.verifyResources();
+ if (failures == null) {
+ Log.info("Resources verified.");
+ launch();
+ return;
+
+ } else {
+ Log.info(failures.size() + " rsrcs require update.");
+ download(failures);
+ // now we'll loop back and try it all again
+ }
}
}
+ Log.warning("Pants! We couldn't get the job done.");
} catch (Exception e) {
Log.logStackTrace(e);
}
}
+ /**
+ * Called if the application is determined to be of an old version.
+ */
+ protected void update ()
+ {
+ Log.info("We're needin' an update Cap'n!");
+ }
+
+ /**
+ * Called if the application is determined to require resource
+ * downloads.
+ */
+ protected void download (List resources)
+ {
+ final Object lock = new Object();
+
+ // create a downloader to download our resources
+ Downloader.Observer obs = new Downloader.Observer() {
+ public void resolvingDownloads () {
+ Log.info("Resolving downloads...");
+ }
+
+ public void downloadProgress (int percent, long remaining) {
+ Log.info("Download progress " + percent + "% " +
+ remaining + "s remaining.");
+ if (percent == 100) {
+ synchronized (lock) {
+ lock.notify();
+ }
+ }
+ }
+
+ public void downloadFailed (Resource rsrc, Exception e) {
+ Log.warning("Download failed [rsrc=" + rsrc + "].");
+ Log.logStackTrace(e);
+ synchronized (lock) {
+ lock.notify();
+ }
+ }
+ };
+ Downloader dl = new Downloader(resources, obs);
+ dl.start();
+
+ // now wait for it to complete
+ synchronized (lock) {
+ try {
+ lock.wait();
+ } catch (InterruptedException ie) {
+ Log.warning("Waitus interruptus " + ie + ".");
+ }
+ }
+ }
+
+ /**
+ * Called to launch the application if everything is determined to be
+ * ready to go.
+ */
+ protected void launch ()
+ {
+ Log.info("All systems go!");
+ try {
+ Process proc = _app.createProcess();
+ } catch (IOException ioe) {
+ Log.logStackTrace(ioe);
+ }
+ }
+
public static void main (String[] args)
{
// ensure the proper parameters are passed
@@ -85,4 +161,6 @@ public class Getdown
}
protected Application _app;
+
+ protected static final int MAX_LOOPS = 5;
}