From 176cb8ca991eb7d00bf66d4261313dae0c1e69fc Mon Sep 17 00:00:00 2001 From: Saeid Nourian Date: Fri, 17 Jun 2016 11:03:56 -0400 Subject: [PATCH] Avoid corrupted resources when download is interrupted 1) Download resources first into a temporary file 2) Perform verification and digest comparison with the downloaded temporary file instead of the original if temporary file exist 3) Only when verification completes successfully rename all the temporary files to override the original. This step can be done automatically or manually by calling GetDown.install() which is useful for those who want to download in background and install upon program exit. --- .../com/threerings/getdown/data/Resource.java | 22 +++++++-- .../com/threerings/getdown/data/SysProps.java | 4 ++ .../threerings/getdown/launcher/Getdown.java | 47 ++++++++++++++++++- .../getdown/net/HTTPDownloader.java | 2 +- .../com/threerings/getdown/util/FileUtil.java | 1 + 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/threerings/getdown/data/Resource.java b/src/main/java/com/threerings/getdown/data/Resource.java index 688db55..b5657dc 100644 --- a/src/main/java/com/threerings/getdown/data/Resource.java +++ b/src/main/java/com/threerings/getdown/data/Resource.java @@ -13,7 +13,6 @@ import java.util.Comparator; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import java.util.jar.JarInputStream; import com.samskivert.io.StreamUtil; import com.samskivert.util.StringUtil; @@ -36,6 +35,7 @@ public class Resource _path = path; _remote = remote; _local = local; + _local_new = new File(local.toString() + "_new"); String lpath = _local.getPath(); _marker = new File(lpath + "v"); @@ -67,6 +67,11 @@ public class Resource return _local; } + public File getLocalNew () + { + return _local_new; + } + /** * Returns the location of the unpacked resource. */ @@ -107,7 +112,16 @@ public class Resource public String computeDigest (MessageDigest md, ProgressObserver obs) throws IOException { - return computeDigest(_local, md, obs); + File file; + if (_local.toString().toLowerCase().endsWith(Application.CONFIG_FILE)) + file = _local; + else { + if (_local_new.exists()) + file = _local_new; + else + file = _local; + } + return computeDigest(file, md, obs); } /** @@ -292,7 +306,7 @@ public class Resource protected static boolean isJar (String path) { - return path.endsWith(".jar"); + return path.endsWith(".jar") || path.endsWith(".jar_new"); } protected static boolean isPacked200Jar (String path) @@ -302,7 +316,7 @@ public class Resource protected String _path; protected URL _remote; - protected File _local, _marker, _unpacked; + protected File _local, _local_new, _marker, _unpacked; protected boolean _unpack, _isJar, _isPacked200Jar; /** Used to sort the entries in a jar file. */ diff --git a/src/main/java/com/threerings/getdown/data/SysProps.java b/src/main/java/com/threerings/getdown/data/SysProps.java index 6dfdf9c..39381b0 100644 --- a/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/src/main/java/com/threerings/getdown/data/SysProps.java @@ -49,6 +49,10 @@ public class SysProps return System.getProperty("silent") != null; } + public static boolean install () { + return System.getProperty("no_install") == null; + } + /** If true, Getdown installs the app without ever bringing up a UI and then launches it. * Usage: {@code -Dsilent=launch}. */ public static boolean launchInSilent () { diff --git a/src/main/java/com/threerings/getdown/launcher/Getdown.java b/src/main/java/com/threerings/getdown/launcher/Getdown.java index 3afc3a9..df5d1ad 100644 --- a/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -68,6 +68,7 @@ import com.threerings.getdown.net.HTTPDownloader; import com.threerings.getdown.tools.Patcher; import com.threerings.getdown.util.ConfigUtil; import com.threerings.getdown.util.ConnectionUtil; +import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.ProgressAggregator; import com.threerings.getdown.util.ProgressObserver; @@ -100,6 +101,7 @@ public abstract class Getdown extends Thread // If the silent property exists, install without bringing up any gui. If it equals // launch, start the application after installing. Otherwise, just install and exit. _silent = SysProps.silent(); + _install = SysProps.install(); if (_silent) { _launchInSilent = SysProps.launchInSilent(); } @@ -409,6 +411,9 @@ public abstract class Getdown extends Thread // we'll keep track of all the resources we unpack Set unpacked = new HashSet(); + + toBeInstalledResouces = new ArrayList(); + readyToInstall = false; //setStep(Step.START); for (int ii = 0; ii < MAX_LOOPS; ii++) { @@ -442,6 +447,7 @@ public abstract class Getdown extends Thread setStep(Step.VERIFY_RESOURCES); setStatusAsync("m.validating", -1, -1L, false); List failures = _app.verifyResources(_progobs, alreadyValid, unpacked); + addToBeInstalledResources(failures); if (failures == null) { log.info("Resources verified."); @@ -471,6 +477,10 @@ public abstract class Getdown extends Thread } } + readyToInstall = true; + if (_install) + install(); + // Only launch if we aren't in silent mode. Some mystery program starting out // of the blue would be disconcerting. if (!_silent || _launchInSilent) { @@ -531,7 +541,16 @@ public abstract class Getdown extends Thread } } - // documentation inherited from interface + private void addToBeInstalledResources(List resources) { + if (resources == null) + return; + else + for (Resource r : resources) + if (!toBeInstalledResouces.contains(r)) + toBeInstalledResouces.add(r); + } + + // documentation inherited from interface public void updateStatus (String message) { setStatusAsync(message, -1, -1L, true); @@ -754,7 +773,28 @@ public abstract class Getdown extends Thread throw new MultipleGetdownRunning(); } } - + + public static void install () + throws IOException, InterruptedException + { + if (readyToInstall) { + log.info("Installing downloaded resources:"); + for (Resource resource : toBeInstalledResouces) { + log.info(resource); + if (!FileUtil.renameTo(resource.getLocalNew(), resource.getLocal())) + throw new IOException("Failed to rename(" + resource.getLocalNew() + ", " + resource.getLocal() + ")"); + if (Thread.interrupted()) { + throw new InterruptedException("m.applet_stopped"); + } + } + toBeInstalledResouces.clear(); + readyToInstall = false; + log.info("Install completed."); + } else { + log.info("Nothing to install."); + } + } + /** * Called to launch the application if everything is determined to be ready to go. */ @@ -1219,6 +1259,7 @@ public abstract class Getdown extends Thread protected boolean _dead; protected boolean _silent; + protected boolean _install; protected boolean _launchInSilent; protected long _startup; @@ -1239,4 +1280,6 @@ public abstract class Getdown extends Thread protected static final long PLAY_AGAIN_TIME = 3000L; protected static final String PROXY_REGISTRY = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; + protected static List toBeInstalledResouces; + protected static boolean readyToInstall; } diff --git a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java index 738d064..286ebc5 100644 --- a/src/main/java/com/threerings/getdown/net/HTTPDownloader.java +++ b/src/main/java/com/threerings/getdown/net/HTTPDownloader.java @@ -78,7 +78,7 @@ public class HTTPDownloader extends Downloader long currentSize = 0L; try { in = conn.getInputStream(); - out = new FileOutputStream(rsrc.getLocal()); + out = new FileOutputStream(rsrc.getLocalNew()); int read; // TODO: look to see if we have a download info file diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java index c957e59..efde120 100644 --- a/src/main/java/com/threerings/getdown/util/FileUtil.java +++ b/src/main/java/com/threerings/getdown/util/FileUtil.java @@ -57,6 +57,7 @@ public class FileUtil extends com.samskivert.util.FileUtil fin = new FileInputStream(source); fout = new FileOutputStream(dest); StreamUtil.copy(fin, fout); + fin.close(); // needed otherwise cannot delete source if (!source.delete()) { log.warning("Failed to delete " + source + " after brute force copy to " + dest + ".");