Use the modtime on getdown.txt as a crude locking mechanism to keep multiple
instances of getdown from stepping on each others toes. Getdown now checks the modtime of getdown.txt when it starts, during its downloading, and before launching the real app. If at any of these points the modtime has changed from when it was last checked, getdown assumes that means another instance is running and bails to let the other instance do its magic. Mainly this is for a delayed, silent installer that wants to launch after its delay, but not if getdown has been run manually after it was launched. Also, if someone manages to get a little click happy on their installer, this will keep multiple instances from fighting for the bandwidth to download the same file simultaneously.
This commit is contained in:
@@ -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<String> _appargs = new ArrayList<String>();
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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 <code>-1</code> 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 <code>null</code> 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<Resource> 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
|
||||
|
||||
@@ -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<Resource> 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user