diff --git a/src/java/com/threerings/getdown/data/Application.java b/src/java/com/threerings/getdown/data/Application.java index 123dc33..323f7ac 100644 --- a/src/java/com/threerings/getdown/data/Application.java +++ b/src/java/com/threerings/getdown/data/Application.java @@ -57,6 +57,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import com.samskivert.io.StreamUtil; +import com.samskivert.jdbc.depot.clause.UpdateClause; import com.samskivert.text.MessageUtil; import com.samskivert.util.ArrayIntSet; import com.samskivert.util.RunAnywhere; @@ -66,6 +67,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import com.threerings.getdown.Log; +import com.threerings.getdown.launcher.MultipleGetdownRunning; import com.threerings.getdown.launcher.RotatingBackgrounds; import com.threerings.getdown.util.ConfigUtil; import com.threerings.getdown.util.FileUtil; @@ -659,7 +661,7 @@ public class Application throws IOException { status.updateStatus("m.updating_metadata"); - downloadControlFile(CONFIG_FILE); + downloadConfigFile(); } /** @@ -680,8 +682,8 @@ public class Application // now re-download our control files; we download the digest first so that if it fails, our // config file will still reference the old version and re-running the updater will start // the whole process over again - downloadControlFile(Digest.DIGEST_FILE, true); - downloadControlFile(CONFIG_FILE); + downloadDigestFile(); + downloadConfigFile(); } /** @@ -870,7 +872,7 @@ public class Application String olddig = (_digest == null) ? "" : _digest.getMetaDigest(); try { status.updateStatus("m.checking"); - downloadControlFile(Digest.DIGEST_FILE, true); + downloadDigestFile(); _digest = new Digest(_appdir); if (!olddig.equals(_digest.getMetaDigest())) { Log.info("Unversioned digest changed. Revalidating..."); @@ -888,7 +890,7 @@ public class Application // exceptions to propagate up to the caller as there is nothing else we can do if (_digest == null) { status.updateStatus("m.updating_metadata"); - downloadControlFile(Digest.DIGEST_FILE, true); + downloadDigestFile(); _digest = new Digest(_appdir); } @@ -898,8 +900,8 @@ public class Application status.updateStatus("m.updating_metadata"); // attempt to redownload both of our metadata files; again we pass errors up to our // caller because there's nothing we can do to automatically recover - downloadControlFile(CONFIG_FILE); - downloadControlFile(Digest.DIGEST_FILE, true); + downloadConfigFile(); + downloadDigestFile(); _digest = new Digest(_appdir); // revalidate everything if we end up downloading new metadata clearValidationMarkers(); @@ -1020,14 +1022,54 @@ public class Application } /** - * Downloads a new copy of the specified control file and does not check any signature. + * Downloads a new copy of CONFIG_FILE. */ - protected void downloadControlFile (String path) + protected void downloadConfigFile () throws IOException { - downloadControlFile(path, false); + checkForAnotherGetdown(); + downloadControlFile(CONFIG_FILE, false); + updateConfigModtime(); } + /** + * Checks the modtime on CONFIG_FILE and if it's changed since the last time this was called, + * raise a MultipleGetdownRunning exception + */ + public void checkForAnotherGetdown () + throws MultipleGetdownRunning + { + File config = getLocalPath(CONFIG_FILE); + if (_lastConfigModtime != -1 && _lastConfigModtime < config.lastModified()) { + throw new MultipleGetdownRunning(); + } + _lastConfigModtime = config.lastModified(); + } + + /** + * Updates the modtime on CONFIG_FILE to now and notes that this application saw it at that + * time for use in checkForAnotherGetdown + */ + public void updateConfigModtime () + { + File config = getLocalPath(CONFIG_FILE); + _lastConfigModtime = System.currentTimeMillis(); + if(!config.setLastModified(_lastConfigModtime) && !_warnedAboutSetLastModified){ + Log.warning("Unable to set modtime on config file, will be unable to check for other instances of getdown running"); + _warnedAboutSetLastModified = true; + } + } + + /** + * Downloads a copy of Digest.DIGEST_FILE and validates its signature. + * @throws IOException + */ + protected void downloadDigestFile () + throws IOException + { + downloadControlFile(Digest.DIGEST_FILE, true); + } + /** * Downloads a new copy of the specified control file, optionally validating its signature. * If the download is successful, moves it over the old file on the filesystem. @@ -1106,6 +1148,11 @@ public class Application } } } + + // Check that another getdown hasn't started running since we started downloading this + // file. The rename will obliterate the modtime we're tracking to keep multiple instances + // from running. + checkForAnotherGetdown(); // now move the temporary file over the original File original = getLocalPath(path); @@ -1245,6 +1292,12 @@ public class Application protected ArrayList _appargs = new ArrayList(); protected Object[] _signers; + + /** If a warning has been issued about not being able to set modtimes. */ + protected boolean _warnedAboutSetLastModified; + + /** The modtime on CONFIG_FILE last time it was checked, or -1 if it hasn't been checked. */ + protected long _lastConfigModtime = -1; protected static final String[] SA_PROTO = new String[0]; } diff --git a/src/java/com/threerings/getdown/launcher/Downloader.java b/src/java/com/threerings/getdown/launcher/Downloader.java index 6487b14..cbfc167 100644 --- a/src/java/com/threerings/getdown/launcher/Downloader.java +++ b/src/java/com/threerings/getdown/launcher/Downloader.java @@ -33,7 +33,7 @@ import com.threerings.getdown.data.Resource; * requests to obtain size information and then downloading the files * individually, reporting progress back via a callback interface. */ -public abstract class Downloader extends Thread +public abstract class Downloader { /** * An interface used to communicate status back to an external entity. @@ -60,8 +60,9 @@ public abstract class Downloader extends Thread * @param remaining the estimated download time remaining in * seconds, or -1 if the time can not yet be * determined. + * @throws IOException if getdown shouldn't continue in its current state. */ - public void downloadProgress (int percent, long remaining); + public void downloadProgress (int percent, long remaining) throws IOException; /** * Called if a failure occurs while checking for an update or @@ -71,28 +72,30 @@ public abstract class Downloader extends Thread * error occurred, or null if the failure occurred * while resolving downloads. * @param e the exception detailing the failure. + * @throws IOException if getdown shouldn't continue after this failure. */ - public void downloadFailed (Resource rsrc, Exception e); + public void downloadFailed (Resource rsrc, Exception e) throws IOException; } /** * 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} 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. + * Start downloading the resources in this Downloader. + * + * @throws IOException if an unrecoverable error was discovered in dowloading. */ - public void run () + public void download () + throws IOException { Resource current = null; try { @@ -168,8 +171,9 @@ public abstract class Downloader extends Thread /** * Periodically called by the protocol-specific downloaders * to update their progress. + * @throws IOException */ - protected void updateObserver () + protected void updateObserver () throws IOException { // notify the observer if it's been sufficiently long // since our last notification diff --git a/src/java/com/threerings/getdown/launcher/Getdown.java b/src/java/com/threerings/getdown/launcher/Getdown.java index b82084a..ca1c27a 100644 --- a/src/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/java/com/threerings/getdown/launcher/Getdown.java @@ -353,7 +353,9 @@ public abstract class Getdown extends Thread // now force our UI to be recreated with the updated info createInterface(true); } - + _app.checkForAnotherGetdown(); + // Update the modtime here to stake a claim that we're going to getdown eventually + _app.updateConfigModtime(); if (_delay > 0) { try { Log.info("Waiting " + _delay + " minutes before beginning actual work"); @@ -405,6 +407,7 @@ public abstract class Getdown extends Thread // Only launch if we aren't in silent mode. Some mystery program starting out // of the blue would be disconcerting. if (!_silent) { + _app.checkForAnotherGetdown(); launch(); } return; @@ -447,7 +450,9 @@ public abstract class Getdown extends Thread "m.init_error", MessageUtil.taint(msg), _ifc.installError); } } - updateStatus(msg); + // Since we're dead, clear off the 'time remaining' label along with displaying the + // error message + setStatus(msg, 0, -1L, true); _dead = true; } } @@ -606,9 +611,8 @@ public abstract class Getdown extends Thread * Called if the application is determined to require resource downloads. */ protected void download (List resources) + throws IOException { - final Object lock = new Object(); - // create our user interface createInterface(false); @@ -618,26 +622,35 @@ public abstract class Getdown extends Thread updateStatus("m.resolving"); } - public void downloadProgress (int percent, long remaining) { + public void downloadProgress (int percent, long remaining) + throws IOException { + // Check for another getdown running at 0 and every 10% after that + if (_lastCheck == -1 || percent >= _lastCheck + 10) { + _app.checkForAnotherGetdown(); + _lastCheck = percent; + } setStatus("m.downloading", percent, remaining, true); if (percent > 0) { reportTrackingEvent("progress", percent); } - if (percent == 100) { - synchronized (lock) { - lock.notify(); - } - } } - public void downloadFailed (Resource rsrc, Exception e) { - updateStatus(MessageUtil.tcompose("m.failure", e.getMessage())); - Log.warning("Download failed [rsrc=" + rsrc + "]."); - Log.logStackTrace(e); - synchronized (lock) { - lock.notify(); + public void downloadFailed (Resource rsrc, Exception e) + throws IOException { + if (e instanceof MultipleGetdownRunning) { + throw (IOException)e; + } else { + updateStatus(MessageUtil.tcompose("m.failure", e.getMessage())); + Log.warning("Download failed [rsrc=" + rsrc + "]."); + Log.logStackTrace(e); } } + + /** + * The last percentage at which we checked for another getdown running, or -1 for not + * having checked at all. + */ + protected int _lastCheck = -1; }; // assume we're going to use an HTTP downloader @@ -661,14 +674,7 @@ public abstract class Getdown extends Thread } // start the download and wait for it to complete - dl.start(); - synchronized (lock) { - try { - lock.wait(); - } catch (InterruptedException ie) { - Log.warning("Waitus interruptus " + ie + "."); - } - } + dl.download(); } /** diff --git a/src/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java b/src/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java new file mode 100644 index 0000000..ab043f1 --- /dev/null +++ b/src/java/com/threerings/getdown/launcher/MultipleGetdownRunning.java @@ -0,0 +1,15 @@ +package com.threerings.getdown.launcher; + +import java.io.IOException; + +/** + * Thrown when it's detected that multiple instances of the same getdown installer are running. + */ +public class MultipleGetdownRunning extends IOException +{ + public MultipleGetdownRunning () + { + super("m.another_getdown_running"); + } + +} diff --git a/src/java/com/threerings/getdown/messages.properties b/src/java/com/threerings/getdown/messages.properties index 8ac1ed6..2febcfc 100644 --- a/src/java/com/threerings/getdown/messages.properties +++ b/src/java/com/threerings/getdown/messages.properties @@ -95,6 +95,9 @@ m.corrupt_digest_signature_error = We couldn't verify the application's digital m.default_install_error = the support section of the website +m.another_getdown_running = Multiple instances of this application's \ + installer are running. This one will stop and let another complete. + # application/digest errors m.missing_appbase = The configuration file is missing the 'appbase'. m.invalid_version = The configuration file specifies an invalid version.