From 3795d51e152041becd7727d77905067d7299acc9 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 6 Jul 2004 05:13:36 +0000 Subject: [PATCH] We have liftoff! There's no fancy GUI yet, but it now downloads invalid or missing resources and launches the application like a champ. --- .../threerings/getdown/data/Application.java | 60 +++- .../com/threerings/getdown/data/Resource.java | 18 +- .../getdown/launcher/Downloader.java | 264 ++++++++++++++++++ .../threerings/getdown/launcher/Getdown.java | 98 ++++++- 4 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 src/java/com/threerings/getdown/launcher/Downloader.java 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; }