From: Ben Soares Date: Tue, 18 Jun 2019 13:55:30 +0000 (+0100) Subject: JAL-3315 getdown_1_8_6 patch file. Needs careful applying. X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=c565b24f5a866bad10eda5050e6e942819d687ac;hp=d4e1d57420f851da08da04f2a82efd5f7a580664;p=jalview.git JAL-3315 getdown_1_8_6 patch file. Needs careful applying. --- diff --git a/getdown/src/getdown-diff-jv--1_8_6.patch b/getdown/src/getdown-diff-jv--1_8_6.patch new file mode 100644 index 0000000..7203f88 --- /dev/null +++ b/getdown/src/getdown-diff-jv--1_8_6.patch @@ -0,0 +1,7060 @@ +diff --git a/getdown/src/getdown/CHANGELOG.md b/getdown/src/getdown/CHANGELOG.md +index 098651eb1..ec7a923e1 100644 +--- a/getdown/src/getdown/CHANGELOG.md ++++ b/getdown/src/getdown/CHANGELOG.md +@@ -1,16 +1,73 @@ + # Getdown Releases + +-## 1.8.3 - Unreleased ++## 1.8.7 - Unreleased ++ ++* Reinstated env var support in `appbase` property. ++ ++* Fixed issue with `myIpAddress()` in PAC proxy support. ++ ++## 1.8.6 - June 4, 2019 ++ ++* Fixed issues with PAC proxy support: added `myIpAddress()`, fixed `dnsResolve()`, fixed crash ++ when detecting PAC proxy. ++ ++* Reverted env var support in `appbase` property. It's causing problems that need to be ++ investigated. ++ ++## 1.8.5 - May 29, 2019 ++ ++* Fixed issues with proxy information not getting properly passed through to app. ++ Via [#216](//github.com/threerings/getdown/pull/216). ++ ++* `appbase` and `latest` properties in `getdown.txt` now process env var subtitutions. ++ ++* Added support for [Proxy Auto-config](https://en.wikipedia.org/wiki/Proxy_auto-config) via PAC ++ files. ++ ++* Proxy handling can now recover from credentials going out of date. It will detect the error and ++ ask for updated credentials. ++ ++* Added `try_no_proxy` system property. This instructs Getdown to always first try to run without a ++ proxy, regardless of whether it has been configured to use a proxy in the past. And if it can run ++ without a proxy, it does so for that session, but retains the proxy config for future sessions in ++ which the proxy may again be needed. ++ ++* Added `revalidate_policy` config to control when Getdown revalidates resources (by hashing them ++ and comparing that hash to the values in `digest.txt`). The default, `after_update`, only ++ validates resources after the app is updated. A new mode, `always`, validates resources prior to ++ every application launch. ++ ++## 1.8.4 - May 14, 2019 ++ ++* Added `verify_timeout` config to allow customization of the default (60 second) timeout during ++ the resource verification process. Apparently in some pathological situations, this is needed. ++ Woe betide the users who have to stare at an unmoving progress bar for more than 60 seconds. ++ Via [#198](//github.com/threerings/getdown/pull/198) ++ and [901682d](//github.com/threerings/getdown/commit/901682d). ++ ++* Added `java_local_dir` config to allow custom location for Java if `java_location` is specified. ++ Via [#206](//github.com/threerings/getdown/pull/206). ++ ++* `messages_XX.properties` files are now all maintained in UTF-8 encoding and then converted to ++ escaped ISO-8859-1 during the build process. ++ ++* Resources and unpacked resources now support `.zip` files as well as `.jar` files. ++ Via [#210](//github.com/threerings/getdown/pull/210). ++ ++* Fixed issue when path to JVM contained spaces. Via [#214](//github.com/threerings/getdown/pull/214). ++ ++## 1.8.3 - Apr 10, 2019 + + * Added support for `nresource` resources which must be jar files that contain native libraries. + Prior to launching the application, these resources will be unpacked and their contents added to + the `java.library.path` system property. + + * When the app is updated to require a new version of the JVM, that JVM will be downloaded and used +- immediately during that app invocation (instead of one invocation later). Via PR#169. ++ immediately during that app invocation (instead of one invocation later). ++ Via [#169](//github.com/threerings/getdown/pull/169). + +-* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM. Via +- PR#170. ++* When a custom JVM is installed, old JVM files will be deleted prior to unpacking the new JVM. ++ Via [#170](//github.com/threerings/getdown/pull/170). + + * Number of concurrent downloads now defaults to num-cores minus one. Though downloads are I/O + bound rather than CPU bound, this still turns out to be a decent default. +@@ -23,6 +80,14 @@ + credentials supplied by the user. Otherwise they will be requested every time Getdown runs, which + is not a viable user experience. + ++* The Getdown window can be now closed by pressing the `ESC` key. ++ Via [#191](//github.com/threerings/getdown/pull/191). ++ ++* If no `appdir` is specified via the command line or system property, the current working ++ directory will be used as the `appdir`. Via [8d59367](//github.com/threerings/getdown/commit/8d59367) ++ ++* A basic Russian translation has been added. Thanks [@sergiorussia](//github.com/sergiorussia)! ++ + ## 1.8.2 - Nov 27, 2018 + + * Fixed a data corruption bug introduced at last minute into 1.8.1 release. Oops. +diff --git a/getdown/src/getdown/ant/.project-MOVED b/getdown/src/getdown/ant/.project-MOVED +deleted file mode 100644 +index 097cb89db..000000000 +--- a/getdown/src/getdown/ant/.project-MOVED ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- getdown-ant +- +- +- +- +- +- org.eclipse.jdt.core.javabuilder +- +- +- +- +- org.eclipse.m2e.core.maven2Builder +- +- +- +- +- +- org.eclipse.jdt.core.javanature +- org.eclipse.m2e.core.maven2Nature +- +- +diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs +deleted file mode 100644 +index e9441bb12..000000000 +--- a/getdown/src/getdown/ant/.settings/org.eclipse.core.resources.prefs ++++ /dev/null +@@ -1,3 +0,0 @@ +-eclipse.preferences.version=1 +-encoding//src/main/java=UTF-8 +-encoding/=UTF-8 +diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs +deleted file mode 100644 +index 54e56721d..000000000 +--- a/getdown/src/getdown/ant/.settings/org.eclipse.jdt.core.prefs ++++ /dev/null +@@ -1,6 +0,0 @@ +-eclipse.preferences.version=1 +-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +-org.eclipse.jdt.core.compiler.compliance=1.7 +-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +-org.eclipse.jdt.core.compiler.release=disabled +-org.eclipse.jdt.core.compiler.source=1.7 +diff --git a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs +deleted file mode 100644 +index f897a7f1c..000000000 +--- a/getdown/src/getdown/ant/.settings/org.eclipse.m2e.core.prefs ++++ /dev/null +@@ -1,4 +0,0 @@ +-activeProfiles= +-eclipse.preferences.version=1 +-resolveWorkspaceProjects=true +-version=1 +diff --git a/getdown/src/getdown/ant/pom.xml b/getdown/src/getdown/ant/pom.xml +index f8231aa2e..a72d95d87 100644 +--- a/getdown/src/getdown/ant/pom.xml ++++ b/getdown/src/getdown/ant/pom.xml +@@ -4,7 +4,7 @@ + + com.threerings.getdown + getdown +- 1.8.3-SNAPSHOT ++ 1.8.7-SNAPSHOT + + + getdown-ant +diff --git a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java +index 48cc8d426..76212ae89 100644 +--- a/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java ++++ b/getdown/src/getdown/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java +@@ -7,16 +7,13 @@ package com.threerings.getdown.tools; + + import java.io.File; + import java.io.IOException; +- + import java.security.GeneralSecurityException; + + import org.apache.tools.ant.BuildException; + import org.apache.tools.ant.Task; + +-import com.threerings.getdown.data.Digest; +- + /** +- * An ant task used to create a digest.txt for a Getdown ++ * An ant task used to create a {@code digest.txt} for a Getdown + * application deployment. + */ + public class DigesterTask extends Task +diff --git a/getdown/src/getdown/core/.project-MOVED b/getdown/src/getdown/core/.project-MOVED +deleted file mode 100644 +index 177252f5f..000000000 +--- a/getdown/src/getdown/core/.project-MOVED ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- getdown-core +- +- +- +- +- +- org.eclipse.jdt.core.javabuilder +- +- +- +- +- org.eclipse.m2e.core.maven2Builder +- +- +- +- +- +- org.eclipse.jdt.core.javanature +- org.eclipse.m2e.core.maven2Nature +- +- +diff --git a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs +deleted file mode 100644 +index 0a9bbb889..000000000 +--- a/getdown/src/getdown/core/.settings/org.eclipse.core.resources.prefs ++++ /dev/null +@@ -1,6 +0,0 @@ +-eclipse.preferences.version=1 +-encoding//src/it/java=UTF-8 +-encoding//src/main/java=UTF-8 +-encoding//src/test/java=UTF-8 +-encoding//src/test/resources=UTF-8 +-encoding/=UTF-8 +diff --git a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs +deleted file mode 100644 +index 54e56721d..000000000 +--- a/getdown/src/getdown/core/.settings/org.eclipse.jdt.core.prefs ++++ /dev/null +@@ -1,6 +0,0 @@ +-eclipse.preferences.version=1 +-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +-org.eclipse.jdt.core.compiler.compliance=1.7 +-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +-org.eclipse.jdt.core.compiler.release=disabled +-org.eclipse.jdt.core.compiler.source=1.7 +diff --git a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs +deleted file mode 100644 +index f897a7f1c..000000000 +--- a/getdown/src/getdown/core/.settings/org.eclipse.m2e.core.prefs ++++ /dev/null +@@ -1,4 +0,0 @@ +-activeProfiles= +-eclipse.preferences.version=1 +-resolveWorkspaceProjects=true +-version=1 +diff --git a/getdown/src/getdown/core/pom.xml b/getdown/src/getdown/core/pom.xml +index efec8b6ce..5e0f852ac 100644 +--- a/getdown/src/getdown/core/pom.xml ++++ b/getdown/src/getdown/core/pom.xml +@@ -4,7 +4,7 @@ + + com.threerings.getdown + getdown +- 1.8.3-SNAPSHOT ++ 1.8.7-SNAPSHOT + + + getdown-core +@@ -34,7 +34,7 @@ + Wildcards can be used (*.mycompany.com) and multiple values can be + separated by commas (app1.foo.com,app2.bar.com,app3.baz.com). --> + +- jalview.org,*.jalview.org ++ + + + +diff --git a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java +index 52b4b5ee3..d2ddaf272 100644 +--- a/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java ++++ b/getdown/src/getdown/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java +@@ -5,16 +5,16 @@ + + package com.threerings.getdown.tests; + +-import java.io.File; + import java.nio.charset.StandardCharsets; +-import java.nio.file.*; ++import java.nio.file.Files; ++import java.nio.file.Path; ++import java.nio.file.Paths; + import java.util.Arrays; + import java.util.List; + +-import org.junit.*; +-import static org.junit.Assert.*; +- + import com.threerings.getdown.tools.Digester; ++import org.junit.Test; ++import static org.junit.Assert.assertEquals; + + public class DigesterIT { + +@@ -32,23 +32,23 @@ public class DigesterIT { + Files.delete(digest2); + + assertEquals(Arrays.asList( +- "getdown.txt = 779c74fb4b251e18faf6e240a0667964", ++ "getdown.txt = 9c9b2494929c99d44ae51034d59e1a1b", + "testapp.jar = 404dafa55e78b25ec0e3a936357b1883", + "funny%test dir/some=file.txt = d8e8fca2dc0f896fd7cb4cb0031ba249", + "crazyhashfile#txt = f29d23fd5ab1781bd8d0760b3a516f16", + "foo.jar = 46ca4cc9079d9d019bb30cd21ebbc1ec", + "script.sh = f66e8ea25598e67e99c47d9b0b2a2cdf", +- "digest.txt = f5561d85e4d80cc85883963897e58ff6" ++ "digest.txt = 11f9ba349cf9edacac4d72a3158447e5" + ), digestLines); + + assertEquals(Arrays.asList( +- "getdown.txt = 4f0c657895c3c3a35fa55bf5951c64fa9b0694f8fc685af3f1d8635c639e066b", ++ "getdown.txt = 1efecfae2a189002a6658f17d162b1922c7bde978944949276dc038a0df2461f", + "testapp.jar = c9cb1906afbf48f8654b416c3f831046bd3752a76137e5bf0a9af2f790bf48e0", + "funny%test dir/some=file.txt = f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2", + "crazyhashfile#txt = 6816889f922de38f145db215a28ad7c5e1badf7354b5cdab225a27486789fa3b", + "foo.jar = ea188b872e0496debcbe00aaadccccb12a8aa9b025bb62c130cd3d9b8540b062", + "script.sh = cca1c5c7628d9bf7533f655a9cfa6573d64afb8375f81960d1d832dc5135c988", +- "digest2.txt = 70b442c9f56660561921da3368e1a206f05c379182fab3062750b7ddcf303407" ++ "digest2.txt = 41eacdabda8909bdbbf61e4f980867f4003c16a12f6770e6fc619b6af100e05b" + ), digest2Lines); + } + } +diff --git a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt +index 3e0e5381a..ab0e47383 100644 +--- a/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt ++++ b/getdown/src/getdown/core/src/it/resources/testapp/getdown.txt +@@ -13,6 +13,18 @@ apparg = %APPDIR% + # test the %env% mechanism + jvmarg = -Dusername=\%ENV.USER% + ++# test various java_*** configs, they are not interesting for digester ++java_local_dir = jre ++java_max_version = 1089999 ++java_min_version = [windows] 1080111 ++java_min_version = [!windows] 1080192 ++java_exact_version_required = [linux] true ++java_location = [linux-amd64] /files/java/java_linux_64.zip ++java_location = [linux-i386] /files/java/java_linux_32.zip ++java_location = [mac] /files/java/java_mac_64.zip ++java_location = [windows-amd64] /files/java/java_windows_64.zip ++java_location = [windows-x86] /files/java/java_windows_32.zip ++ + strict_comments = true + resource = funny%test dir/some=file.txt + resource = crazyhashfile#txt +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java +index 13b99564a..da98c9031 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/Log.java +@@ -15,7 +15,7 @@ import java.util.logging.*; + /** + * A placeholder class that contains a reference to the log object used by the Getdown code. + */ +-public class Log ++public final class Log + { + public static class Shim { + /** +@@ -69,6 +69,13 @@ public class Log + /** We dispatch our log messages through this logging shim. */ + public static final Shim log = new Shim(); + ++ /** ++ * Formats a message with key/value pairs. The pairs will be appended to the message as a ++ * comma separated list of {@code key=value} in square brackets. ++ * @param message the main log message. ++ * @param args the key/value pairs. Any trailing key with no value will be ignored. ++ * @return the formatted message, i.e. {@code Some log message [key=value, key=value]}. ++ */ + public static String format (Object message, Object... args) { + if (args.length < 2) return String.valueOf(message); + StringBuilder buf = new StringBuilder(String.valueOf(message)); +@@ -76,13 +83,13 @@ public class Log + buf.append(' '); + } + buf.append('['); +- for (int ii = 0; ii < args.length; ii += 2) { ++ for (int ii = 0, ll = args.length/2; ii < ll; ii += 1) { + if (ii > 0) { + buf.append(',').append(' '); + } +- buf.append(args[ii]).append('='); ++ buf.append(args[2*ii]).append('='); + try { +- buf.append(args[ii+1]); ++ buf.append(args[2*ii+1]); + } catch (Throwable t) { + buf.append(""); + } +@@ -136,6 +143,5 @@ public class Log + protected FieldPosition _fpos = new FieldPosition(SimpleDateFormat.DATE_FIELD); + } + +- protected static final String DATE_FORMAT = "{0,date} {0,time}"; + protected static final Level[] LEVELS = {Level.FINE, Level.INFO, Level.WARNING, Level.SEVERE}; + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java +index 67ea64575..7e01e87c5 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java +@@ -6,6 +6,8 @@ + package com.threerings.getdown.cache; + + import java.io.File; ++ ++import com.threerings.getdown.data.Resource; + import com.threerings.getdown.util.FileUtil; + + /** +@@ -55,9 +57,9 @@ public class GarbageCollector + if (subdirs != null) { + for (File dir : subdirs) { + if (dir.isDirectory()) { +- // Get all the native jars in the directory (there should only be one) ++ // Get all the native jars or zips in the directory (there should only be one) + for (File file : dir.listFiles()) { +- if (!file.getName().endsWith(".jar")) { ++ if (!Resource.isJar(file) && !Resource.isZip(file)) { + continue; + } + File cachedFile = getCachedFile(file); +@@ -94,6 +96,6 @@ public class GarbageCollector + private static File getCachedFile (File file) + { + return !isLastAccessedFile(file) ? file : new File( +- file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf("."))); ++ file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf('.'))); + } + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java +index 0210e9a86..41f0c5f6d 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java +@@ -69,7 +69,7 @@ public class ResourceCache + + private String getFileSuffix (File fileToCache) { + String fileName = fileToCache.getName(); +- int index = fileName.lastIndexOf("."); ++ int index = fileName.lastIndexOf('.'); + + return index > -1 ? fileName.substring(index) : ""; + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java +index 0de5c8ac8..a93122553 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Application.java +@@ -22,24 +22,17 @@ import java.util.*; + import java.util.concurrent.*; + import java.util.regex.Matcher; + import java.util.regex.Pattern; +-import java.util.zip.GZIPInputStream; +-import com.sun.management.OperatingSystemMXBean; +-import java.lang.management.ManagementFactory; +- +-import jalview.bin.MemorySetting; + ++import com.threerings.getdown.net.Connector; + import com.threerings.getdown.util.*; + // avoid ambiguity with java.util.Base64 which we can't use as it's 1.8+ + import com.threerings.getdown.util.Base64; + +-import com.threerings.getdown.data.EnvConfig; +-import com.threerings.getdown.data.EnvConfig.Note; +- + import static com.threerings.getdown.Log.log; + import static java.nio.charset.StandardCharsets.UTF_8; + + /** +- * Parses and provide access to the information contained in the getdown.txt ++ * Parses and provide access to the information contained in the {@code getdown.txt} + * configuration file. + */ + public class Application +@@ -218,10 +211,10 @@ public class Application + * Used by {@link #verifyMetadata} to communicate status in circumstances where it needs to + * take network actions. + */ +- public static interface StatusDisplay ++ public interface StatusDisplay + { + /** Requests that the specified status message be displayed. */ +- public void updateStatus (String message); ++ void updateStatus (String message); + } + + /** +@@ -239,19 +232,56 @@ public class Application + } + } + +- /** The proxy that should be used to do HTTP downloads. This must be configured prior to using +- * the application instance. Yes this is a public mutable field, no I'm not going to create a ++ /** ++ * Reads the {@code getdown.txt} config file into a {@code Config} object and returns it. ++ */ ++ public static Config readConfig (EnvConfig envc, boolean checkPlatform) throws IOException { ++ Config config = null; ++ File cfgfile = new File(envc.appDir, CONFIG_FILE); ++ Config.ParseOpts opts = Config.createOpts(checkPlatform); ++ try { ++ // if we have a configuration file, read the data from it ++ if (cfgfile.exists()) { ++ config = Config.parseConfig(cfgfile, opts); ++ } ++ // otherwise, try reading data from our backup config file; thanks to funny windows ++ // bullshit, we have to do this backup file fiddling in case we got screwed while ++ // updating getdown.txt during normal operation ++ else if ((cfgfile = new File(envc.appDir, Application.CONFIG_FILE + "_old")).exists()) { ++ config = Config.parseConfig(cfgfile, opts); ++ } ++ // otherwise, issue a warning that we found no getdown file ++ else { ++ log.info("Found no getdown.txt file", "appdir", envc.appDir); ++ } ++ } catch (Exception e) { ++ log.warning("Failure reading config file", "file", config, e); ++ } ++ ++ // if we failed to read our config file, check for an appbase specified via a system ++ // property; we can use that to bootstrap ourselves back into operation ++ if (config == null) { ++ log.info("Using 'appbase' from bootstrap config", "appbase", envc.appBase); ++ Map cdata = new HashMap<>(); ++ cdata.put("appbase", envc.appBase); ++ config = new Config(cdata); ++ } ++ ++ return config; ++ } ++ ++ /** A helper that is used to do HTTP downloads. This must be configured prior to using the ++ * application instance. Yes this is a public mutable field, no I'm not going to create a + * getter and setter just to pretend like that's not the case. */ +- public Proxy proxy = Proxy.NO_PROXY; ++ public Connector conn = Connector.DEFAULT; + + /** +- * Creates an application instance which records the location of the getdown.txt ++ * Creates an application instance which records the location of the {@code getdown.txt} + * configuration file from the supplied application directory. + * + */ + public Application (EnvConfig envc) { + _envc = envc; +- _config = getLocalPath(envc.appDir, CONFIG_FILE); + } + + /** +@@ -375,8 +405,7 @@ public class Application + */ + public List getActiveCodeResources () + { +- ArrayList codes = new ArrayList<>(); +- codes.addAll(getCodeResources()); ++ List codes = new ArrayList<>(getCodeResources()); + for (AuxGroup aux : getAuxGroups()) { + if (isAuxGroupActive(aux.name)) { + codes.addAll(aux.codes); +@@ -404,8 +433,7 @@ public class Application + */ + public List getActiveResources () + { +- ArrayList rsrcs = new ArrayList<>(); +- rsrcs.addAll(getResources()); ++ List rsrcs = new ArrayList<>(getResources()); + for (AuxGroup aux : getAuxGroups()) { + if (isAuxGroupActive(aux.name)) { + rsrcs.addAll(aux.rsrcs); +@@ -442,7 +470,15 @@ public class Application + } + + /** +- * Returns a resource for a zip file containing a Java VM that can be downloaded to use in ++ * @return directory into which a local VM installation should be unpacked. ++ */ ++ public File getJavaLocalDir () ++ { ++ return _javaLocalDir; ++ } ++ ++ /** ++ * @return a resource for a zip file containing a Java VM that can be downloaded to use in + * place of the installed VM (in the case where the VM that launched Getdown does not meet the + * application's version requirements) or null if no VM is available for this platform. + */ +@@ -452,21 +488,16 @@ public class Application + return null; + } + +- String extension = (_javaLocation.endsWith(".tgz"))?".tgz":".jar"; +- String vmfile = LaunchUtil.LOCAL_JAVA_DIR + extension; +- log.info("vmfile is '"+vmfile+"'"); +- System.out.println("vmfile is '"+vmfile+"'"); ++ // take extension from java location ++ String vmfileExt = _javaLocation.substring(_javaLocation.lastIndexOf('.')); ++ String vmfile = _javaLocalDir.getName() + vmfileExt; + try { + URL remote = new URL(createVAppBase(_targetVersion), encodePath(_javaLocation)); +- log.info("Attempting to fetch jvm at "+remote.toString()); +- System.out.println("Attempting to fetch jvm at "+remote.toString()); + return new Resource(vmfile, remote, getLocalPath(vmfile), + EnumSet.of(Resource.Attr.UNPACK, Resource.Attr.CLEAN)); + } catch (Exception e) { + log.warning("Failed to create VM resource", "vmfile", vmfile, "appbase", _appbase, + "tvers", _targetVersion, "javaloc", _javaLocation, "error", e); +- System.out.println("Failed to create VM resource: vmfile="+vmfile+", appbase="+_appbase+ +- ", tvers="+_targetVersion+", javaloc="+_javaLocation+", error="+e); + return null; + } + } +@@ -547,88 +578,52 @@ public class Application + * @exception IOException thrown if there is an error reading the file or an error encountered + * during its parsing. + */ +- public Config init (boolean checkPlatform) +- throws IOException ++ public Config init (boolean checkPlatform) throws IOException + { +- Config config = null; +- File cfgfile = _config; +- Config.ParseOpts opts = Config.createOpts(checkPlatform); +- try { +- // if we have a configuration file, read the data from it +- if (cfgfile.exists()) { +- config = Config.parseConfig(_config, opts); +- } +- // otherwise, try reading data from our backup config file; thanks to funny windows +- // bullshit, we have to do this backup file fiddling in case we got screwed while +- // updating getdown.txt during normal operation +- else if ((cfgfile = getLocalPath(CONFIG_FILE + "_old")).exists()) { +- config = Config.parseConfig(cfgfile, opts); +- } +- // otherwise, issue a warning that we found no getdown file +- else { +- log.info("Found no getdown.txt file", "appdir", getAppDir()); +- } +- } catch (Exception e) { +- log.warning("Failure reading config file", "file", _config, e); +- } +- +- // see if there's an override config from locator file +- Config locatorConfig = createLocatorConfig(opts); +- +- // merge the locator file config into config (or replace config with) +- if (locatorConfig != null) { +- if (config == null || locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_replace")) { +- config = locatorConfig; +- } else { +- config.mergeConfig(locatorConfig, locatorConfig.getBoolean(LOCATOR_FILE_EXTENSION+"_merge")); +- } +- } ++ Config config = readConfig(_envc, checkPlatform); ++ initBase(config); ++ initJava(config); ++ initTracking(config); ++ initResources(config); ++ initArgs(config); ++ return config; ++ } + +- // if we failed to read our config file, check for an appbase specified via a system +- // property; we can use that to bootstrap ourselves back into operation +- if (config == null) { +- String appbase = _envc.appBase; +- log.info("Using 'appbase' from bootstrap config", "appbase", appbase); +- Map cdata = new HashMap<>(); +- cdata.put("appbase", appbase); +- config = new Config(cdata); +- } ++ /** ++ * Reads the basic config info from {@code config} into this instance. This includes things ++ * like the appbase and version. ++ */ ++ public void initBase (Config config) throws IOException { ++ // first extract our version information ++ _version = config.getLong("version", -1L); + +- // first determine our application base, this way if anything goes wrong later in the ++ // determine our application base, this way if anything goes wrong later in the + // process, our caller can use the appbase to download a new configuration file + _appbase = config.getString("appbase"); +- +- // see if locatorConfig override +- if (locatorConfig != null && !StringUtil.isBlank(locatorConfig.getString("appbase"))) { +- _appbase = locatorConfig.getString("appbase"); +- } +- + if (_appbase == null) { + throw new RuntimeException("m.missing_appbase"); + } + +- // check if we're overriding the domain in the appbase +- _appbase = SysProps.overrideAppbase(_appbase); ++ // check if we're overriding the domain in the appbase, and sub envvars ++ _appbase = resolveEnvVars(SysProps.overrideAppbase(_appbase)); + + // make sure there's a trailing slash + if (!_appbase.endsWith("/")) { +- _appbase = _appbase + "/"; ++ _appbase += "/"; + } + +- // extract our version information +- _version = config.getLong("version", -1L); +- + // if we are a versioned deployment, create a versioned appbase + try { + _vappbase = createVAppBase(_version); + } catch (MalformedURLException mue) { + String err = MessageUtil.tcompose("m.invalid_appbase", _appbase); +- throw (IOException) new IOException(err).initCause(mue); ++ throw new IOException(err, mue); + } + + // check for a latest config URL + String latest = config.getString("latest"); + if (latest != null) { ++ latest = processArg(latest); + if (latest.startsWith(_appbase)) { + latest = _appbase + latest.substring(_appbase.length()); + } else { +@@ -641,20 +636,25 @@ public class Application + } + } + +- String appPrefix = _envc.appId == null ? "" : (_envc.appId + "."); +- +- // determine our application class name (use app-specific class _if_ one is provided) +- _class = config.getString("class"); +- if (appPrefix.length() > 0) { +- _class = config.getString(appPrefix + "class", _class); +- } +- if (_class == null) { +- throw new IOException("m.missing_class"); +- } +- +- // determine whether we want strict comments ++ // read some miscellaneous configurations + _strictComments = config.getBoolean("strict_comments"); ++ _allowOffline = config.getBoolean("allow_offline"); ++ _revalidatePolicy = config.getEnum( ++ "revalidate_policy", RevalidatePolicy.class, RevalidatePolicy.AFTER_UPDATE); ++ int tpSize = SysProps.threadPoolSize(); ++ _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", tpSize)); ++ _verifyTimeout = config.getInt("verify_timeout", 60); ++ ++ // whether to cache code resources and launch from cache ++ _useCodeCache = config.getBoolean("use_code_cache"); ++ _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7); ++ } + ++ /** ++ * Reads the JVM requirements from {@code config} into this instance. This includes things like ++ * the min and max java version, location of a locally installed JRE, etc. ++ */ ++ public void initJava (Config config) { + // check to see if we're using a custom java.version property and regex + _javaVersionProp = config.getString("java_version_prop", _javaVersionProp); + _javaVersionRegex = config.getString("java_version_regex", _javaVersionRegex); +@@ -669,14 +669,16 @@ public class Application + // check to see if we require a particular JVM version and have a supplied JVM + _javaExactVersionRequired = config.getBoolean("java_exact_version_required"); + +- // this is a little weird, but when we're run from the digester, we see a String[] which +- // contains java locations for all platforms which we can't grok, but the digester doesn't +- // need to know about that; when we're run in a real application there will be only one! +- Object javaloc = config.getRaw("java_location"); +- if (javaloc instanceof String) { +- _javaLocation = (String)javaloc; +- } ++ _javaLocation = config.getString("java_location"); ++ ++ // used only in conjunction with java_location ++ _javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR)); ++ } + ++ /** ++ * Reads the install tracking info from {@code config} into this instance. ++ */ ++ public void initTracking (Config config) { + // determine whether we have any tracking configuration + _trackingURL = config.getString("tracking_url"); + +@@ -701,14 +703,16 @@ public class Application + + // Some app may need to generate google analytics code + _trackingGAHash = config.getString("tracking_ga_hash"); ++ } + ++ /** ++ * Reads the app resource info from {@code config} into this instance. ++ */ ++ public void initResources (Config config) throws IOException { + // clear our arrays as we may be reinitializing + _codes.clear(); + _resources.clear(); + _auxgroups.clear(); +- _jvmargs.clear(); +- _appargs.clear(); +- _txtJvmArgs.clear(); + + // parse our code resources + if (config.getMultiValue("code") == null && +@@ -727,10 +731,10 @@ public class Application + + // parse our auxiliary resource groups + for (String auxgroup : config.getList("auxgroups")) { +- ArrayList codes = new ArrayList<>(); ++ List codes = new ArrayList<>(); + parseResources(config, auxgroup + ".code", Resource.NORMAL, codes); + parseResources(config, auxgroup + ".ucode", Resource.UNPACK, codes); +- ArrayList rsrcs = new ArrayList<>(); ++ List rsrcs = new ArrayList<>(); + parseResources(config, auxgroup + ".resource", Resource.NORMAL, rsrcs); + parseResources(config, auxgroup + ".xresource", Resource.EXEC, rsrcs); + parseResources(config, auxgroup + ".uresource", Resource.UNPACK, rsrcs); +@@ -738,87 +742,48 @@ public class Application + parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs); + _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs)); + } ++ } + +- // transfer our JVM arguments (we include both "global" args and app_id-prefixed args) +- String[] jvmargs = config.getMultiValue("jvmarg"); +- addAll(jvmargs, _jvmargs); ++ /** ++ * Reads the command line arg info from {@code config} into this instance. ++ */ ++ public void initArgs (Config config) throws IOException { ++ _jvmargs.clear(); ++ _appargs.clear(); ++ _txtJvmArgs.clear(); ++ ++ String appPrefix = _envc.appId == null ? "" : (_envc.appId + "."); ++ ++ // determine our application class name (use app-specific class _if_ one is provided) ++ _class = config.getString("class"); + if (appPrefix.length() > 0) { +- jvmargs = config.getMultiValue(appPrefix + "jvmarg"); +- addAll(jvmargs, _jvmargs); ++ _class = config.getString(appPrefix + "class", _class); ++ } ++ if (_class == null) { ++ throw new IOException("m.missing_class"); + } + +- // see if a percentage of physical memory option exists +- int jvmmempc = config.getInt("jvmmempc", -1); +- // app_id prefixed setting overrides ++ // transfer our JVM arguments (we include both "global" args and app_id-prefixed args) ++ addAll(config.getMultiValue("jvmarg"), _jvmargs); + if (appPrefix.length() > 0) { +- jvmmempc = config.getInt(appPrefix + "jvmmempc", jvmmempc); +- } +- if (0 <= jvmmempc && jvmmempc <= 100) { +- +- long maxMemLong = -1; +- +- try +- { +- maxMemLong = MemorySetting.memPercent(jvmmempc); +- } catch (Exception e) +- { +- e.printStackTrace(); +- } catch (Throwable t) +- { +- t.printStackTrace(); +- } +- +- if (maxMemLong > 0) +- { +- +- String[] maxMemHeapArg = new String[]{"-Xmx"+Long.toString(maxMemLong)}; +- // remove other max heap size arg +- ARG: for (int i = 0; i < _jvmargs.size(); i++) { +- if (_jvmargs.get(i) instanceof java.lang.String && _jvmargs.get(i).startsWith("-Xmx")) { +- _jvmargs.remove(i); +- } +- } +- addAll(maxMemHeapArg, _jvmargs); +- +- } +- +- } else if (jvmmempc != -1) { +- System.out.println("'jvmmempc' value must be in range 0 to 100 (read as '"+Integer.toString(jvmmempc)+"')"); ++ addAll(config.getMultiValue(appPrefix + "jvmarg"), _jvmargs); + } + + // get the set of optimum JVM arguments + _optimumJvmArgs = config.getMultiValue("optimum_jvmarg"); + + // transfer our application arguments +- String[] appargs = config.getMultiValue(appPrefix + "apparg"); +- addAll(appargs, _appargs); ++ addAll(config.getMultiValue(appPrefix + "apparg"), _appargs); + + // add the launch specific application arguments + _appargs.addAll(_envc.appArgs); +- ++ + // look for custom arguments + fillAssignmentListFromPairs("extra.txt", _txtJvmArgs); + +- // determine whether we want to allow offline operation (defaults to false) +- _allowOffline = config.getBoolean("allow_offline"); +- +- // look for a debug.txt file which causes us to run in java.exe on Windows so that we can +- // obtain a thread dump of the running JVM +- _windebug = getLocalPath("debug.txt").exists(); +- +- // whether to cache code resources and launch from cache +- _useCodeCache = config.getBoolean("use_code_cache"); +- _codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7); +- +- // maximum simultaneous downloads +- _maxConcDownloads = Math.max(1, config.getInt("max_concurrent_downloads", +- SysProps.threadPoolSize())); +- + // extract some info used to configure our child process on macOS + _dockName = config.getString("ui.name"); + _dockIconPath = config.getString("ui.mac_dock_icon", "../desktop.icns"); +- +- return config; + } + + /** +@@ -847,8 +812,7 @@ public class Application + * Returns a URL from which the specified path can be fetched. Our application base URL is + * properly versioned and combined with the supplied path. + */ +- public URL getRemoteURL (String path) +- throws MalformedURLException ++ public URL getRemoteURL (String path) throws MalformedURLException + { + return new URL(_vappbase, encodePath(path)); + } +@@ -858,7 +822,7 @@ public class Application + */ + public File getLocalPath (String path) + { +- return getLocalPath(getAppDir(), path); ++ return new File(getAppDir(), path); + } + + /** +@@ -881,8 +845,7 @@ public class Application + // if we have an unpacked VM, check the 'release' file for its version + Resource vmjar = getJavaVMResource(); + if (vmjar != null && vmjar.isMarkedValid()) { +- File vmdir = new File(getAppDir(), LaunchUtil.LOCAL_JAVA_DIR); +- File relfile = new File(vmdir, "release"); ++ File relfile = new File(_javaLocalDir, "release"); + if (!relfile.exists()) { + log.warning("Unpacked JVM missing 'release' file. Assuming valid version."); + return true; +@@ -939,7 +902,7 @@ public class Application + } + + /** +- * Attempts to redownload the getdown.txt file based on information parsed from a ++ * Attempts to redownload the {@code getdown.txt} file based on information parsed from a + * previous call to {@link #init}. + */ + public void attemptRecovery (StatusDisplay status) +@@ -950,7 +913,7 @@ public class Application + } + + /** +- * Downloads and replaces the getdown.txt and digest.txt files with ++ * Downloads and replaces the {@code getdown.txt} and {@code digest.txt} files with + * those for the target version of our application. + */ + public void updateMetadata () +@@ -961,7 +924,7 @@ public class Application + _vappbase = createVAppBase(_targetVersion); + } catch (MalformedURLException mue) { + String err = MessageUtil.tcompose("m.invalid_appbase", _appbase); +- throw (IOException) new IOException(err).initCause(mue); ++ throw new IOException(err, mue); + } + + try { +@@ -1000,7 +963,7 @@ public class Application + ArrayList args = new ArrayList<>(); + + // reconstruct the path to the JVM +- args.add(LaunchUtil.getJVMPath(getAppDir(), _windebug || optimum)); ++ args.add(LaunchUtil.getJVMBinaryPath(_javaLocalDir, SysProps.debug() || optimum)); + + // check whether we're using -jar mode or -classpath mode + boolean dashJarMode = MANIFEST_CLASS.equals(_class); +@@ -1018,14 +981,8 @@ public class Application + args.add("-Xdock:name=" + _dockName); + } + +- // pass along our proxy settings +- String proxyHost; +- if ((proxyHost = System.getProperty("http.proxyHost")) != null) { +- args.add("-Dhttp.proxyHost=" + proxyHost); +- args.add("-Dhttp.proxyPort=" + System.getProperty("http.proxyPort")); +- args.add("-Dhttps.proxyHost=" + proxyHost); +- args.add("-Dhttps.proxyPort=" + System.getProperty("http.proxyPort")); +- } ++ // forward our proxy settings ++ conn.addProxyArgs(args); + + // add the marker indicating the app is running in getdown + args.add("-D" + Properties.GETDOWN + "=true"); +@@ -1071,32 +1028,11 @@ public class Application + args.add(_class); + } + +- // almost finally check the startup file arguments +- for (File f : _startupFiles) { +- _appargs.add(f.getAbsolutePath()); +- break; // Only add one file to open +- } +- +- // check if one arg with recognised extension +- if ( _appargs.size() == 1 && _appargs.get(0) != null ) { +- String filename = _appargs.get(0); +- String ext = null; +- int j = filename.lastIndexOf('.'); +- if (j > -1) { +- ext = filename.substring(j+1); +- } +- if (LOCATOR_FILE_EXTENSION.equals(ext.toLowerCase())) { +- // this file extension should have been dealt with in Getdown class +- } else { +- _appargs.add(0, "-open"); +- } +- } +- + // finally add the application arguments + for (String string : _appargs) { + args.add(processArg(string)); + } +- ++ + String[] envp = createEnvironment(); + String[] sargs = args.toArray(new String[args.size()]); + log.info("Running " + StringUtil.join(sargs, "\n ")); +@@ -1156,7 +1092,7 @@ public class Application + for (String jvmarg : _jvmargs) { + if (jvmarg.startsWith("-D")) { + jvmarg = processArg(jvmarg.substring(2)); +- int eqidx = jvmarg.indexOf("="); ++ int eqidx = jvmarg.indexOf('='); + if (eqidx == -1) { + log.warning("Bogus system property: '" + jvmarg + "'?"); + } else { +@@ -1194,32 +1130,36 @@ public class Application + } + } + +- /** Replaces the application directory and version in any argument. */ ++ /** Replaces the application directory, version and env vars in any argument. */ + protected String processArg (String arg) + { + arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath()); + arg = arg.replace("%VERSION%", String.valueOf(_version)); ++ arg = resolveEnvVars(arg); ++ return arg; ++ } + +- // if this argument contains %ENV.FOO% replace those with the associated values looked up +- // from the environment +- if (arg.contains(ENV_VAR_PREFIX)) { ++ /** Resolves env var substitutions in {@code text}. */ ++ protected String resolveEnvVars (String text) { ++ // if the text contains %ENV.FOO% replace it with FOO looked up in the environment ++ if (text.contains(ENV_VAR_PREFIX)) { + StringBuffer sb = new StringBuffer(); +- Matcher matcher = ENV_VAR_PATTERN.matcher(arg); ++ Matcher matcher = ENV_VAR_PATTERN.matcher(text); + while (matcher.find()) { + String varName = matcher.group(1), varValue = System.getenv(varName); + String repValue = varValue == null ? "MISSING:"+varName : varValue; + matcher.appendReplacement(sb, Matcher.quoteReplacement(repValue)); + } + matcher.appendTail(sb); +- arg = sb.toString(); ++ return sb.toString(); ++ } else { ++ return text; + } +- +- return arg; + } + + /** +- * Loads the digest.txt file and verifies the contents of both that file and the +- * getdown.text file. Then it loads the version.txt and decides ++ * Loads the {@code digest.txt} file and verifies the contents of both that file and the ++ * {@code getdown.text} file. Then it loads the {@code version.txt} and decides + * whether or not the application needs to be updated or whether we can proceed to verification + * and execution. + * +@@ -1306,11 +1246,11 @@ public class Application + } + + if (_latest != null) { +- try (InputStream in = ConnectionUtil.open(proxy, _latest, 0, 0).getInputStream(); +- InputStreamReader reader = new InputStreamReader(in, UTF_8); +- BufferedReader bin = new BufferedReader(reader)) { +- for (String[] pair : Config.parsePairs(bin, Config.createOpts(false))) { +- if (pair[0].equals("version")) { ++ try { ++ List vdata = Config.parsePairs( ++ new StringReader(conn.fetch(_latest)), Config.createOpts(false)); ++ for (String[] pair : vdata) { ++ if ("version".equals(pair[0])) { + _targetVersion = Math.max(Long.parseLong(pair[1]), _targetVersion); + if (fileVersion != -1 && _targetVersion > fileVersion) { + // replace the file with the newest version +@@ -1404,7 +1344,10 @@ public class Application + while (completed[0] < rsrcs.size()) { + // we should be getting progress completion updates WAY more often than one every + // minute, so if things freeze up for 60 seconds, abandon ship +- Runnable action = actions.poll(60, TimeUnit.SECONDS); ++ Runnable action = actions.poll(_verifyTimeout, TimeUnit.SECONDS); ++ if (action == null) { ++ throw new IllegalStateException("m.verify_timeout"); ++ } + action.run(); + } + +@@ -1415,14 +1358,14 @@ public class Application + unpacked.addAll(unpackedAsync); + + long complete = System.currentTimeMillis(); +- log.info("Verified resources", "count", rsrcs.size(), "size", (totalSize/1024) + "k", +- "duration", (complete-start) + "ms"); ++ log.info("Verified resources", "count", rsrcs.size(), "alreadyValid", alreadyValid[0], ++ "size", (totalSize/1024) + "k", "duration", (complete-start) + "ms"); + } + + private void verifyResource (Resource rsrc, ProgressObserver obs, int[] alreadyValid, + Set unpacked, + Set toInstall, Set toDownload) { +- if (rsrc.isMarkedValid()) { ++ if (_revalidatePolicy != RevalidatePolicy.ALWAYS && rsrc.isMarkedValid()) { + if (alreadyValid != null) { + alreadyValid[0]++; + } +@@ -1513,7 +1456,7 @@ public class Application + protected URL createVAppBase (long version) + throws MalformedURLException + { +- String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version); ++ String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", String.valueOf(version)); + return HostWhitelist.verify(new URL(url)); + } + +@@ -1530,8 +1473,7 @@ public class Application + /** + * Downloads a new copy of CONFIG_FILE. + */ +- protected void downloadConfigFile () +- throws IOException ++ protected void downloadConfigFile () throws IOException + { + downloadControlFile(CONFIG_FILE, 0); + } +@@ -1673,8 +1615,7 @@ public class Application + * Download a path to a temporary file, returning a {@link File} instance with the path + * contents. + */ +- protected File downloadFile (String path) +- throws IOException ++ protected File downloadFile (String path) throws IOException + { + File target = getLocalPath(path + "_new"); + +@@ -1684,30 +1625,11 @@ public class Application + } catch (Exception e) { + log.warning("Requested to download invalid control file", + "appbase", _vappbase, "path", path, "error", e); +- throw (IOException) new IOException("Invalid path '" + path + "'.").initCause(e); ++ throw new IOException("Invalid path '" + path + "'.", e); + } + + log.info("Attempting to refetch '" + path + "' from '" + targetURL + "'."); +- +- // stream the URL into our temporary file +- URLConnection uconn = ConnectionUtil.open(proxy, targetURL, 0, 0); +- // we have to tell Java not to use caches here, otherwise it will cache any request for +- // same URL for the lifetime of this JVM (based on the URL string, not the URL object); +- // if the getdown.txt file, for example, changes in the meanwhile, we would never hear +- // about it; turning off caches is not a performance concern, because when Getdown asks +- // to download a file, it expects it to come over the wire, not from a cache +- uconn.setUseCaches(false); +- uconn.setRequestProperty("Accept-Encoding", "gzip"); +- try (InputStream fin = uconn.getInputStream()) { +- String encoding = uconn.getContentEncoding(); +- boolean gzip = "gzip".equalsIgnoreCase(encoding); +- try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) { +- try (FileOutputStream fout = new FileOutputStream(target)) { +- StreamUtil.copy(fin2, fout); +- } +- } +- } +- ++ conn.download(targetURL, target); // stream the URL into our temporary file + return target; + } + +@@ -1721,9 +1643,7 @@ public class Application + /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */ + protected static void addAll (String[] values, List target) { + if (values != null) { +- for (String value : values) { +- target.add(value); +- } ++ Collections.addAll(target, values); + } + } + +@@ -1805,96 +1725,7 @@ public class Application + } + } + +- protected File getLocalPath (File appdir, String path) +- { +- return new File(appdir, path); +- } +- +- public static void setStartupFilesFromParameterString(String p) { +- // multiple files *might* be passed in as space separated quoted filenames +- String q = "\""; +- if (!StringUtil.isBlank(p)) { +- String[] filenames; +- // split quoted params or treat as single string array +- if (p.startsWith(q) && p.endsWith(q)) { +- // this fails if, e.g. +- // p=q("stupidfilename\" " "otherfilename") +- // let's hope no-one ever ends a filename with '" ' +- filenames = p.substring(q.length(),p.length()-q.length()).split(q+" "+q); +- } else { +- // single unquoted filename +- filenames = new String[]{p}; +- } +- +- // check for locator file. Only allow one locator file to be double clicked (if multiple files opened, ignore locator files) +- String locatorFilename = filenames.length >= 1 ? filenames[0] : null; +- if ( +- !StringUtil.isBlank(locatorFilename) +- && locatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION) +- ) { +- setLocatorFile(locatorFilename); +- // remove the locator filename from the filenames array +- String[] otherFilenames = new String[filenames.length - 1]; +- System.arraycopy(filenames, 1, otherFilenames, 0, otherFilenames.length); +- filenames = otherFilenames; +- } +- +- for (int i = 0; i < filenames.length; i++) { +- String filename = filenames[i]; +- // skip any other locator files in a multiple file list +- if (! filename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION)) { +- addStartupFile(filename); +- } +- } +- } +- } +- +- public static void setLocatorFile(String filename) { +- _locatorFile = new File(filename); +- } +- +- public static void addStartupFile(String filename) { +- _startupFiles.add(new File(filename)); +- } +- +- private Config createLocatorConfig(Config.ParseOpts opts) { +- if (_locatorFile == null) { +- return null; +- } +- +- Config locatorConfig = null; +- +- try { +- Config tmpConfig = null; +- if (_locatorFile.exists()) { +- tmpConfig = Config.parseConfig(_locatorFile, opts); +- } else { +- log.warning("Given locator file does not exist", "file", _locatorFile); +- } +- +- // appbase is sanitised in HostWhitelist +- Map tmpData = new HashMap<>(); +- for (Map.Entry entry : tmpConfig.getData().entrySet()) { +- String key = entry.getKey(); +- Object value = entry.getValue(); +- String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key; +- if (Config.allowedReplaceKeys.contains(mkey) || Config.allowedMergeKeys.contains(mkey)) { +- tmpData.put(key, value); +- } +- } +- locatorConfig = new Config(tmpData); +- +- } catch (Exception e) { +- log.warning("Failure reading locator file", "file", _locatorFile, e); +- } +- +- log.info("Returning locatorConfig", locatorConfig); +- +- return locatorConfig; +- } +- + protected final EnvConfig _envc; +- protected File _config; + protected Digest _digest; + + protected long _version = -1; +@@ -1906,7 +1737,6 @@ public class Application + protected String _dockName; + protected String _dockIconPath; + protected boolean _strictComments; +- protected boolean _windebug; + protected boolean _allowOffline; + protected int _maxConcDownloads; + +@@ -1924,10 +1754,14 @@ public class Application + protected long _javaMinVersion, _javaMaxVersion; + protected boolean _javaExactVersionRequired; + protected String _javaLocation; ++ protected File _javaLocalDir; + + protected List _codes = new ArrayList<>(); + protected List _resources = new ArrayList<>(); + ++ protected int _verifyTimeout = 60; ++ ++ protected RevalidatePolicy _revalidatePolicy = RevalidatePolicy.AFTER_UPDATE; + protected boolean _useCodeCache; + protected int _codeCacheRetentionDays; + +@@ -1941,9 +1775,6 @@ public class Application + + protected List _txtJvmArgs = new ArrayList<>(); + +- /** If a warning has been issued about not being able to set modtimes. */ +- protected boolean _warnedAboutSetLastModified; +- + /** Locks gettingdown.lock in the app dir. Held the entire time updating is going on.*/ + protected FileLock _lock; + +@@ -1956,8 +1787,6 @@ public class Application + + protected static final String ENV_VAR_PREFIX = "%ENV."; + protected static final Pattern ENV_VAR_PATTERN = Pattern.compile("%ENV\\.(.*?)%"); +- +- protected static File _locatorFile; +- protected static List _startupFiles = new ArrayList<>(); +- public static final String LOCATOR_FILE_EXTENSION = "jvl"; ++ ++ protected static enum RevalidatePolicy { ALWAYS, AFTER_UPDATE } + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java +index bc8d14052..e310a52a2 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Digest.java +@@ -21,7 +21,7 @@ import static com.threerings.getdown.Log.log; + import static java.nio.charset.StandardCharsets.UTF_8; + + /** +- * Manages the digest.txt file and the computing and processing of digests for an ++ * Manages the {@code digest.txt} file and the computing and processing of digests for an + * application. + */ + public class Digest +@@ -72,8 +72,7 @@ public class Digest + digests.put(rsrc, rsrc.computeDigest(fversion, md, null)); + completed.add(rsrc); + } catch (Throwable t) { +- completed.add(new IOException("Error computing digest for: " + rsrc). +- initCause(t)); ++ completed.add(new IOException("Error computing digest for: " + rsrc, t)); + } + } + }); +@@ -88,7 +87,7 @@ public class Digest + if (done instanceof IOException) { + throw (IOException)done; + } else if (done instanceof Resource) { +- pending.remove((Resource)done); ++ pending.remove(done); + } else { + throw new AssertionError("What is this? " + done); + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java +index 57b8d8493..a14b02c63 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/EnvConfig.java +@@ -7,22 +7,19 @@ package com.threerings.getdown.data; + + import java.io.File; + import java.io.FileInputStream; +-import java.net.MalformedURLException; +-import java.net.URL; + import java.security.cert.Certificate; + import java.security.cert.CertificateFactory; + import java.security.cert.X509Certificate; + import java.util.*; + + import com.threerings.getdown.util.StringUtil; +-import com.threerings.getdown.data.Application; + + /** Configuration that comes from our "environment" (command line args, sys props, etc.). */ + public final class EnvConfig { + + /** Used to report problems or feedback by {@link #create}. */ + public static final class Note { +- public static enum Level { INFO, WARN, ERROR }; ++ public enum Level { INFO, WARN, ERROR } + public static Note info (String msg) { return new Note(Level.INFO, msg); } + public static Note warn (String msg) { return new Note(Level.WARN, msg); } + public static Note error (String msg) { return new Note(Level.ERROR, msg); } +@@ -141,23 +138,11 @@ public final class EnvConfig { + appIdProv + "'")); + } + } +- +- int skipArgs = 2; +- // Look for locator file, pass to Application and remove from appArgs +- String argvLocatorFilename = argv.length > 2 ? argv[2] : null; +- if ( +- !StringUtil.isBlank(argvLocatorFilename) +- && argvLocatorFilename.toLowerCase().endsWith("."+Application.LOCATOR_FILE_EXTENSION) +- ) { +- notes.add(Note.info("locatorFilename in args: '"+argv[2]+"'")); +- Application.setLocatorFile(argvLocatorFilename); +- +- skipArgs++; +- } + +- // ensure that we were able to find an app dir ++ // if no appdir was provided, default to the current working directory + if (appDir == null) { +- return null; // caller will report problem to user ++ appDir = System.getProperty("user.dir"); ++ appDirProv = "default (cwd)"; + } + + notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir)); +@@ -187,9 +172,9 @@ public final class EnvConfig { + return null; + } + +- // pass along anything after the first two (or three) args as extra app args +- List appArgs = argv.length > skipArgs ? +- Arrays.asList(argv).subList(skipArgs, argv.length) : ++ // pass along anything after the first two args as extra app args ++ List appArgs = argv.length > 2 ? ++ Arrays.asList(argv).subList(2, argv.length) : + Collections.emptyList(); + + // load X.509 certificate if it exists +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java +index b0a1dc920..57e9275be 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java +@@ -10,7 +10,7 @@ import java.io.IOException; + import java.util.LinkedHashSet; + import java.util.List; + import java.util.concurrent.TimeUnit; +-import java.util.jar.JarFile; ++import java.util.zip.ZipFile; + + import com.threerings.getdown.cache.GarbageCollector; + import com.threerings.getdown.cache.ResourceCache; +@@ -112,7 +112,7 @@ public class PathBuilder + + if (!unpackedIndicator.exists()) { + try { +- FileUtil.unpackJar(new JarFile(cachedFile), cachedParent, false); ++ FileUtil.unpackJar(new ZipFile(cachedFile), cachedParent, false); + unpackedIndicator.createNewFile(); + } catch (IOException ioe) { + log.warning("Failed to unpack native jar", +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java +index adc2d4f21..d1ccba3ba 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java +@@ -7,25 +7,14 @@ package com.threerings.getdown.data; + + import java.io.*; + import java.net.URL; +-import java.nio.file.Files; +-import java.nio.file.Paths; + import java.security.MessageDigest; +-import java.util.Collections; +-import java.util.Comparator; +-import java.util.EnumSet; +-import java.util.List; +-import java.util.Locale; +-import java.util.jar.JarEntry; +-import java.util.jar.JarFile; +- +-import org.apache.commons.compress.archivers.ArchiveInputStream; +-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +-import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; ++import java.util.*; ++import java.util.zip.ZipEntry; ++import java.util.zip.ZipFile; + + import com.threerings.getdown.util.FileUtil; + import com.threerings.getdown.util.ProgressObserver; + import com.threerings.getdown.util.StringUtil; +- + import static com.threerings.getdown.Log.log; + + /** +@@ -34,7 +23,7 @@ import static com.threerings.getdown.Log.log; + public class Resource implements Comparable + { + /** Defines special attributes for resources. */ +- public static enum Attr { ++ public enum Attr { + /** Indicates that the resource should be unpacked. */ + UNPACK, + /** If present, when unpacking a resource, any directories created by the newly +@@ -46,7 +35,7 @@ public class Resource implements Comparable + PRELOAD, + /** Indicates that the resource is a jar containing native libs. */ + NATIVE +- }; ++ } + + public static final EnumSet NORMAL = EnumSet.noneOf(Attr.class); + public static final EnumSet UNPACK = EnumSet.of(Attr.UNPACK); +@@ -66,29 +55,30 @@ public class Resource implements Comparable + byte[] buffer = new byte[DIGEST_BUFFER_SIZE]; + int read; + +- boolean isJar = isJar(target.getPath()); +- boolean isPacked200Jar = isPacked200Jar(target.getPath()); ++ boolean isZip = isJar(target) || isZip(target); // jar is a zip too ++ boolean isPacked200Jar = isPacked200Jar(target); + + // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic + // manner to properly correlate jardiff patched jars with their unpatched originals +- if (isJar || isPacked200Jar){ ++ if (isPacked200Jar || isZip){ + File tmpJarFile = null; +- JarFile jar = null; ++ ZipFile zip = null; + try { +- // if this is a compressed jar file, uncompress it to compute the jar file digest ++ // if this is a compressed zip file, uncompress it to compute the zip file digest + if (isPacked200Jar){ + tmpJarFile = new File(target.getPath() + ".tmp"); ++ tmpJarFile.deleteOnExit(); + FileUtil.unpackPacked200Jar(target, tmpJarFile); +- jar = new JarFile(tmpJarFile); ++ zip = new ZipFile(tmpJarFile); + } else{ +- jar = new JarFile(target); ++ zip = new ZipFile(target); + } + +- List entries = Collections.list(jar.entries()); ++ List entries = Collections.list(zip.entries()); + Collections.sort(entries, ENTRY_COMP); + + int eidx = 0; +- for (JarEntry entry : entries) { ++ for (ZipEntry entry : entries) { + // old versions of the digest code skipped metadata + if (version < 2) { + if (entry.getName().startsWith("META-INF")) { +@@ -97,7 +87,7 @@ public class Resource implements Comparable + } + } + +- try (InputStream in = jar.getInputStream(entry)) { ++ try (InputStream in = zip.getInputStream(entry)) { + while ((read = in.read(buffer)) != -1) { + md.update(buffer, 0, read); + } +@@ -107,11 +97,11 @@ public class Resource implements Comparable + } + + } finally { +- if (jar != null) { ++ if (zip != null) { + try { +- jar.close(); ++ zip.close(); + } catch (IOException ioe) { +- log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe); ++ log.warning("Error closing", "path", target, "zip", zip, "error", ioe); + } + } + if (tmpJarFile != null) { +@@ -132,6 +122,34 @@ public class Resource implements Comparable + return StringUtil.hexlate(md.digest()); + } + ++ /** ++ * Returns whether {@code file} is a {@code zip} file. ++ */ ++ public static boolean isZip (File file) ++ { ++ String path = file.getName(); ++ return path.endsWith(".zip") || path.endsWith(".zip_new"); ++ } ++ ++ /** ++ * Returns whether {@code file} is a {@code jar} file. ++ */ ++ public static boolean isJar (File file) ++ { ++ String path = file.getName(); ++ return path.endsWith(".jar") || path.endsWith(".jar_new"); ++ } ++ ++ /** ++ * Returns whether {@code file} is a {@code jar.pack} file. ++ */ ++ public static boolean isPacked200Jar (File file) ++ { ++ String path = file.getName(); ++ return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") || ++ path.endsWith(".jar.pack.gz") || path.endsWith(".jar.pack.gz_new"); ++ } ++ + /** + * Creates a resource with the supplied remote URL and local path. + */ +@@ -141,17 +159,13 @@ public class Resource implements Comparable + _remote = remote; + _local = local; + _localNew = new File(local.toString() + "_new"); +- String lpath = _local.getPath(); +- _marker = new File(lpath + "v"); ++ _marker = new File(_local.getPath() + "v"); + + _attrs = attrs; +- _isTgz = isTgz(lpath); +- _isJar = isJar(lpath); +- _isPacked200Jar = isPacked200Jar(lpath); ++ _isZip = isJar(local) || isZip(local); ++ _isPacked200Jar = isPacked200Jar(local); + boolean unpack = attrs.contains(Attr.UNPACK); +- if (unpack && _isJar) { +- _unpacked = _local.getParentFile(); +- } else if(unpack && _isTgz) { ++ if (unpack && _isZip) { + _unpacked = _local.getParentFile(); + } else if(unpack && _isPacked200Jar) { + String dotJar = ".jar", lname = _local.getName(); +@@ -307,20 +321,13 @@ public class Resource implements Comparable + public void unpack () throws IOException + { + // sanity check +- if (!_isJar && !_isPacked200Jar && !_isTgz) { +- throw new IOException("Requested to unpack non-jar/tgz file '" + _local + "'."); ++ if (!_isZip && !_isPacked200Jar) { ++ throw new IOException("Requested to unpack non-jar file '" + _local + "'."); + } +- if (_isJar) { +- try (JarFile jar = new JarFile(_local)) { ++ if (_isZip) { ++ try (ZipFile jar = new ZipFile(_local)) { + FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN)); + } +- } else if (_isTgz) { +- try (InputStream fi = Files.newInputStream(_local.toPath()); +- InputStream bi = new BufferedInputStream(fi); +- InputStream gzi = new GzipCompressorInputStream(bi); +- TarArchiveInputStream tgz = new TarArchiveInputStream(gzi)) { +- FileUtil.unpackTgz(tgz, _unpacked, _attrs.contains(Attr.CLEAN)); +- } + } else { + FileUtil.unpackPacked200Jar(_local, _unpacked); + } +@@ -382,31 +389,15 @@ public class Resource implements Comparable + } + } + +- protected static boolean isJar (String path) +- { +- return path.endsWith(".jar") || path.endsWith(".jar_new"); +- } +- +- protected static boolean isTgz (String path) +- { +- return path.endsWith(".tgz") || path.endsWith(".tgz_new"); +- } +- +- protected static boolean isPacked200Jar (String path) +- { +- return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") || +- path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new"); +- } +- + protected String _path; + protected URL _remote; + protected File _local, _localNew, _marker, _unpacked; + protected EnumSet _attrs; +- protected boolean _isJar, _isPacked200Jar, _isTgz; ++ protected boolean _isZip, _isPacked200Jar; + + /** Used to sort the entries in a jar file. */ +- protected static final Comparator ENTRY_COMP = new Comparator() { +- @Override public int compare (JarEntry e1, JarEntry e2) { ++ protected static final Comparator ENTRY_COMP = new Comparator() { ++ @Override public int compare (ZipEntry e1, ZipEntry e2) { + return e1.getName().compareTo(e2.getName()); + } + }; +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java +index 0d96ecb71..b36d40021 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/SysProps.java +@@ -16,7 +16,7 @@ import com.threerings.getdown.util.VersionUtil; + * accessor so that it's easy to see all of the secret system property arguments that Getdown makes + * use of. + */ +-public class SysProps ++public final class SysProps + { + /** Configures the appdir (in lieu of passing it in argv). Usage: {@code -Dappdir=foo}. */ + public static String appDir () { +@@ -40,6 +40,14 @@ public class SysProps + return System.getProperty("no_log_redir") != null; + } + ++ /** Used to debug Getdown's launching of an app. When set, it disables redirection of stdout ++ * and stderr into a log file, and on Windows it uses {@code java.exe} to launch the app so ++ * that its console output can be observed. ++ * Usage: {@code -Ddebug}. */ ++ public static boolean debug () { ++ return System.getProperty("debug") != null; ++ } ++ + /** Overrides the domain on {@code appbase}. Usage: {@code -Dappbase_domain=foo}. */ + public static String appbaseDomain () { + return System.getProperty("appbase_domain"); +@@ -101,6 +109,16 @@ public class SysProps + return Boolean.getBoolean("direct"); + } + ++ /** If true, Getdown will always try to connect without proxy settings even it a proxy is set ++ * in {@code proxy.txt}. If direct access is possible it will not clear {@code proxy.txt}, it ++ * will preserve the settings. This is to support cases where a user uses a workstation in two ++ * different networks, one with proxy the other one without. They should not be asked for ++ * proxy settings again each time they switch back to the proxy network. ++ * Usage: {@code -Dtry_no_proxy}. */ ++ public static boolean tryNoProxyFirst () { ++ return Boolean.getBoolean("try_no_proxy"); ++ } ++ + /** Specifies the connection timeout (in seconds) to use when downloading control files from + * the server. This is chiefly useful when you are running in versionless mode and want Getdown + * to more quickly timeout its startup update check if the server with which it is +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java +index 6033e2f6e..2298d6099 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/Downloader.java +@@ -6,7 +6,13 @@ + package com.threerings.getdown.net; + + import java.io.File; ++import java.io.FileOutputStream; + import java.io.IOException; ++import java.io.InputStream; ++import java.net.HttpURLConnection; ++import java.net.URLConnection; ++import java.nio.channels.Channels; ++import java.nio.channels.ReadableByteChannel; + + import java.util.Collection; + import java.util.HashMap; +@@ -26,8 +32,13 @@ import static com.threerings.getdown.Log.log; + * implementors must take care to only execute thread-safe code or simply pass a message to the AWT + * thread, for example. + */ +-public abstract class Downloader ++public class Downloader + { ++ public Downloader (Connector conn) ++ { ++ _conn = conn; ++ } ++ + /** + * Start the downloading process. + * @param resources the resources to download. +@@ -129,10 +140,31 @@ public abstract class Downloader + */ + protected void downloadFailed (Resource rsrc, Exception cause) {} + ++ /** ++ * Called when a to-be-downloaded resource returns a 404 not found. ++ */ ++ protected void resourceMissing (Resource rsrc) {} ++ + /** + * Performs the protocol-specific portion of checking download size. + */ +- protected abstract long checkSize (Resource rsrc) throws IOException; ++ protected long checkSize (Resource rsrc) throws IOException { ++ URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0); ++ try { ++ // if we're accessing our data via HTTP, we only need a HEAD request ++ if (conn instanceof HttpURLConnection) { ++ ((HttpURLConnection)conn).setRequestMethod("HEAD"); ++ } ++ // if we get a satisfactory response code, return a size; ignore errors as we'll report ++ // those when we actually attempt to download the resource ++ int code = _conn.checkConnectStatus(conn); ++ return code == HttpURLConnection.HTTP_OK ? conn.getContentLength() : 0; ++ ++ } finally { ++ // let it be known that we're done with this connection ++ conn.getInputStream().close(); ++ } ++ } + + /** + * Periodically called by the protocol-specific downloaders to update their progress. This +@@ -203,13 +235,68 @@ public abstract class Downloader + * protocol-specific code. This method should periodically check whether {@code _state} is set + * to aborted and abort any in-progress download if so. + */ +- protected abstract void download (Resource rsrc) throws IOException; ++ protected void download (Resource rsrc) throws IOException { ++ URLConnection conn = _conn.open(rsrc.getRemote(), 0, 0); ++ // make sure we got a satisfactory response code ++ int code = _conn.checkConnectStatus(conn); ++ if (code == HttpURLConnection.HTTP_NOT_FOUND) { ++ resourceMissing(rsrc); ++ } else if (code != HttpURLConnection.HTTP_OK) { ++ throw new IOException( ++ "Resource returned HTTP error " + rsrc.getRemote() + " [code=" + code + "]"); ++ } ++ ++ // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a ++ // system property ++ if (true) { ++ // download the resource from the specified URL ++ long actualSize = conn.getContentLength(); ++ log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize); ++ long currentSize = 0L; ++ byte[] buffer = new byte[4*4096]; ++ try (InputStream in = conn.getInputStream(); ++ FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) { ++ ++ // 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 ++ int read; ++ while ((read = in.read(buffer)) != -1) { ++ // abort the download if the downloader is aborted ++ if (_state == State.ABORTED) { ++ break; ++ } ++ // write it out to our local copy ++ out.write(buffer, 0, read); ++ // note that we've downloaded some data ++ currentSize += read; ++ reportProgress(rsrc, currentSize, actualSize); ++ } ++ } ++ ++ } else { ++ log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown"); ++ File localNew = rsrc.getLocalNew(); ++ try (ReadableByteChannel rbc = Channels.newChannel(conn.getInputStream()); ++ FileOutputStream fos = new FileOutputStream(localNew)) { ++ // TODO: more work is needed here, transferFrom can fail to transfer the entire ++ // file, in which case it's not clear what we're supposed to do.. call it again? ++ // will it repeatedly fail? ++ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); ++ reportProgress(rsrc, localNew.length(), localNew.length()); ++ } ++ } ++ } ++ ++ protected final Connector _conn; + + /** The reported sizes of our resources. */ +- protected Map _sizes = new HashMap<>(); ++ protected final Map _sizes = new HashMap<>(); + + /** The bytes downloaded for each resource. */ +- protected Map _downloaded = new HashMap<>(); ++ protected final Map _downloaded = new HashMap<>(); + + /** The time at which the file transfer began. */ + protected long _start; +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java +deleted file mode 100644 +index a7a3287a9..000000000 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java ++++ /dev/null +@@ -1,115 +0,0 @@ +-// +-// Getdown - application installer, patcher and launcher +-// Copyright (C) 2004-2018 Getdown authors +-// https://github.com/threerings/getdown/blob/master/LICENSE +- +-package com.threerings.getdown.net; +- +-import java.io.File; +-import java.io.FileOutputStream; +-import java.io.IOException; +-import java.io.InputStream; +-import java.net.HttpURLConnection; +-import java.net.Proxy; +-import java.net.URL; +-import java.net.URLConnection; +-import java.nio.channels.Channels; +-import java.nio.channels.ReadableByteChannel; +- +-import com.threerings.getdown.data.Resource; +-import com.threerings.getdown.util.ConnectionUtil; +- +-import static com.threerings.getdown.Log.log; +- +-/** +- * Implements downloading files over HTTP +- */ +-public class HTTPDownloader extends Downloader +-{ +- public HTTPDownloader (Proxy proxy) +- { +- _proxy = proxy; +- } +- +- @Override protected long checkSize (Resource rsrc) throws IOException +- { +- URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0); +- try { +- // if we're accessing our data via HTTP, we only need a HEAD request +- if (conn instanceof HttpURLConnection) { +- HttpURLConnection hcon = (HttpURLConnection)conn; +- hcon.setRequestMethod("HEAD"); +- hcon.connect(); +- // make sure we got a satisfactory response code +- if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) { +- throw new IOException("Unable to check up-to-date for " + +- rsrc.getRemote() + ": " + hcon.getResponseCode()); +- } +- } +- return conn.getContentLength(); +- +- } finally { +- // let it be known that we're done with this connection +- conn.getInputStream().close(); +- } +- } +- +- @Override protected void download (Resource rsrc) throws IOException +- { +- // TODO: make FileChannel download impl (below) robust and allow apps to opt-into it via a +- // system property +- if (true) { +- // download the resource from the specified URL +- URLConnection conn = ConnectionUtil.open(_proxy, rsrc.getRemote(), 0, 0); +- conn.connect(); +- +- // make sure we got a satisfactory response code +- if (conn instanceof HttpURLConnection) { +- HttpURLConnection hcon = (HttpURLConnection)conn; +- if (hcon.getResponseCode() != HttpURLConnection.HTTP_OK) { +- throw new IOException("Unable to download resource " + rsrc.getRemote() + ": " + +- hcon.getResponseCode()); +- } +- } +- long actualSize = conn.getContentLength(); +- log.info("Downloading resource", "url", rsrc.getRemote(), "size", actualSize); +- long currentSize = 0L; +- byte[] buffer = new byte[4*4096]; +- try (InputStream in = conn.getInputStream(); +- FileOutputStream out = new FileOutputStream(rsrc.getLocalNew())) { +- +- // 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 +- int read; +- while ((read = in.read(buffer)) != -1) { +- // abort the download if the downloader is aborted +- if (_state == State.ABORTED) { +- break; +- } +- // write it out to our local copy +- out.write(buffer, 0, read); +- // note that we've downloaded some data +- currentSize += read; +- reportProgress(rsrc, currentSize, actualSize); +- } +- } +- +- } else { +- log.info("Downloading resource", "url", rsrc.getRemote(), "size", "unknown"); +- File localNew = rsrc.getLocalNew(); +- try (ReadableByteChannel rbc = Channels.newChannel(rsrc.getRemote().openStream()); +- FileOutputStream fos = new FileOutputStream(localNew)) { +- // TODO: more work is needed here, transferFrom can fail to transfer the entire +- // file, in which case it's not clear what we're supposed to do.. call it again? +- // will it repeatedly fail? +- fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); +- reportProgress(rsrc, localNew.length(), localNew.length()); +- } +- } +- } +- +- protected final Proxy _proxy; +-} +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java +index 22446ec0a..8c6d84160 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java +@@ -11,7 +11,7 @@ package com.threerings.getdown.spi; + public interface ProxyAuth + { + /** Credentials for a proxy server. */ +- public static class Credentials { ++ class Credentials { + public final String username; + public final String password; + public Credentials (String username, String password) { +@@ -23,10 +23,10 @@ public interface ProxyAuth + /** + * Loads the credentials for the app installed in {@code appDir}. + */ +- public Credentials loadCredentials (String appDir); ++ Credentials loadCredentials (String appDir); + + /** + * Encrypts and saves the credentials for the app installed in {@code appDir}. + */ +- public void saveCredentials (String appDir, String username, String password); ++ void saveCredentials (String appDir, String username, String password); + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java +index c2e740b6e..4f8e50ebd 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Differ.java +@@ -11,15 +11,14 @@ import java.io.FileInputStream; + import java.io.FileOutputStream; + import java.io.IOException; + import java.io.InputStream; +- ++import java.io.OutputStream; ++import java.security.MessageDigest; + import java.util.ArrayList; + import java.util.Enumeration; +-import java.util.jar.JarEntry; +-import java.util.jar.JarFile; +-import java.util.jar.JarOutputStream; ++import java.util.List; + import java.util.zip.ZipEntry; +- +-import java.security.MessageDigest; ++import java.util.zip.ZipFile; ++import java.util.zip.ZipOutputStream; + + import com.threerings.getdown.data.Application; + import com.threerings.getdown.data.Digest; +@@ -39,8 +38,8 @@ public class Differ + /** + * Creates a single patch file that contains the differences between + * the two specified application directories. The patch file will be +- * created in the nvdir directory with name +- * patchV.dat where V is the old application version. ++ * created in the {@code nvdir} directory with name ++ * {@code patchV.dat} where V is the old application version. + */ + public void createDiff (File nvdir, File ovdir, boolean verbose) + throws IOException +@@ -61,13 +60,13 @@ public class Differ + + Application oapp = new Application(new EnvConfig(ovdir)); + oapp.init(false); +- ArrayList orsrcs = new ArrayList<>(); ++ List orsrcs = new ArrayList<>(); + orsrcs.addAll(oapp.getCodeResources()); + orsrcs.addAll(oapp.getResources()); + + Application napp = new Application(new EnvConfig(nvdir)); + napp.init(false); +- ArrayList nrsrcs = new ArrayList<>(); ++ List nrsrcs = new ArrayList<>(); + nrsrcs.addAll(napp.getCodeResources()); + nrsrcs.addAll(napp.getResources()); + +@@ -91,15 +90,15 @@ public class Differ + } + } + +- protected void createPatch (File patch, ArrayList orsrcs, +- ArrayList nrsrcs, boolean verbose) ++ protected void createPatch (File patch, List orsrcs, ++ List nrsrcs, boolean verbose) + throws IOException + { + int version = Digest.VERSION; + MessageDigest md = Digest.getMessageDigest(version); + try (FileOutputStream fos = new FileOutputStream(patch); + BufferedOutputStream buffered = new BufferedOutputStream(fos); +- JarOutputStream jout = new JarOutputStream(buffered)) { ++ ZipOutputStream jout = new ZipOutputStream(buffered)) { + + // for each file in the new application, it either already exists + // in the old application, or it is new +@@ -172,13 +171,13 @@ public class Differ + throws IOException + { + File temp = File.createTempFile("differ", "jar"); +- try (JarFile jar = new JarFile(target); ++ try (ZipFile jar = new ZipFile(target); + FileOutputStream tempFos = new FileOutputStream(temp); + BufferedOutputStream tempBos = new BufferedOutputStream(tempFos); +- JarOutputStream jout = new JarOutputStream(tempBos)) { ++ ZipOutputStream jout = new ZipOutputStream(tempBos)) { + byte[] buffer = new byte[4096]; +- for (Enumeration< JarEntry > iter = jar.entries(); iter.hasMoreElements();) { +- JarEntry entry = iter.nextElement(); ++ for (Enumeration iter = jar.entries(); iter.hasMoreElements();) { ++ ZipEntry entry = iter.nextElement(); + entry.setCompressedSize(-1); + jout.putNextEntry(entry); + try (InputStream in = jar.getInputStream(entry)) { +@@ -193,8 +192,7 @@ public class Differ + return temp; + } + +- protected void jarDiff (File ofile, File nfile, JarOutputStream jout) +- throws IOException ++ protected void jarDiff (File ofile, File nfile, ZipOutputStream jout) throws IOException + { + JarDiff.createPatch(ofile.getPath(), nfile.getPath(), jout, false); + } +@@ -209,7 +207,7 @@ public class Differ + Differ differ = new Differ(); + boolean verbose = false; + int aidx = 0; +- if (args[0].equals("-verbose")) { ++ if ("-verbose".equals(args[0])) { + verbose = true; + aidx++; + } +@@ -222,11 +220,10 @@ public class Differ + } + } + +- protected static void pipe (File file, JarOutputStream jout) +- throws IOException ++ protected static void pipe (File file, OutputStream out) throws IOException + { + try (FileInputStream fin = new FileInputStream(file)) { +- StreamUtil.copy(fin, jout); ++ StreamUtil.copy(fin, out); + } + } + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java +index b04a6539b..ae61b4333 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Digester.java +@@ -23,6 +23,7 @@ import com.threerings.getdown.data.Digest; + import com.threerings.getdown.data.EnvConfig; + import com.threerings.getdown.data.Resource; + import com.threerings.getdown.util.Base64; ++import com.threerings.getdown.util.Config; + + import static java.nio.charset.StandardCharsets.UTF_8; + +@@ -74,8 +75,11 @@ public class Digester + System.out.println("Generating digest file '" + target + "'..."); + + // create our application and instruct it to parse its business +- Application app = new Application(new EnvConfig(appdir)); +- app.init(false); ++ EnvConfig envc = new EnvConfig(appdir); ++ Application app = new Application(envc); ++ Config config = Application.readConfig(envc, false); ++ app.initBase(config); ++ app.initResources(config); + + List rsrcs = new ArrayList<>(); + rsrcs.add(app.getConfigResource()); +@@ -86,6 +90,9 @@ public class Digester + rsrcs.addAll(ag.rsrcs); + } + ++ // reinit app just to verify that getdown.txt has valid format ++ app.init(true); ++ + // now generate the digest file + Digest.createDigest(version, rsrcs, target); + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java +index 1cea0eacd..f0db8ac03 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiff.java +@@ -41,15 +41,24 @@ + + package com.threerings.getdown.tools; + +-import java.io.*; ++import java.io.Closeable; ++import java.io.File; ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.OutputStream; ++import java.io.StringWriter; ++import java.io.Writer; + import java.util.*; +-import java.util.jar.*; ++import java.util.zip.ZipEntry; ++import java.util.zip.ZipFile; ++import java.util.zip.ZipOutputStream; + + import static java.nio.charset.StandardCharsets.UTF_8; + + /** +- * JarDiff is able to create a jar file containing the delta between two jar files (old and new). +- * The delta jar file can then be applied to the old jar file to reconstruct the new jar file. ++ * JarDiff is able to create a zip file containing the delta between two jar or zip files (old ++ * and new). The delta file can then be applied to the old archive file to reconstruct the new ++ * archive file. + * + *

Refer to the JNLP spec for details on how this is done. + * +@@ -58,39 +67,37 @@ import static java.nio.charset.StandardCharsets.UTF_8; + public class JarDiff implements JarDiffCodes + { + private static final int DEFAULT_READ_SIZE = 2048; +- private static byte[] newBytes = new byte[DEFAULT_READ_SIZE]; +- private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; ++ private static final byte[] newBytes = new byte[DEFAULT_READ_SIZE]; ++ private static final byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; + + // The JARDiff.java is the stand-alone jardiff.jar tool. Thus, we do not depend on Globals.java + // and other stuff here. Instead, we use an explicit _debug flag. + private static boolean _debug; + + /** +- * Creates a patch from the two passed in files, writing the result to os. ++ * Creates a patch from the two passed in files, writing the result to {@code os}. + */ + public static void createPatch (String oldPath, String newPath, + OutputStream os, boolean minimal) throws IOException + { +- try (JarFile2 oldJar = new JarFile2(oldPath); +- JarFile2 newJar = new JarFile2(newPath)) { ++ try (ZipFile2 oldArchive = new ZipFile2(oldPath); ++ ZipFile2 newArchive = new ZipFile2(newPath)) { + +- HashMap moved = new HashMap<>(); +- HashSet implicit = new HashSet<>(); +- HashSet moveSrc = new HashSet<>(); +- HashSet newEntries = new HashSet<>(); ++ Map moved = new HashMap<>(); ++ Set implicit = new HashSet<>(); ++ Set moveSrc = new HashSet<>(); ++ Set newEntries = new HashSet<>(); + + // FIRST PASS +- // Go through the entries in new jar and +- // determine which files are candidates for implicit moves +- // ( files that has the same filename and same content in old.jar +- // and new.jar ) +- // and for files that cannot be implicitly moved, we will either +- // find out whether it is moved or new (modified) +- for (JarEntry newEntry : newJar) { ++ // Go through the entries in new archive and determine which files are candidates for ++ // implicit moves (files that have the same filename and same content in old and new) ++ // and for files that cannot be implicitly moved, we will either find out whether it is ++ // moved or new (modified) ++ for (ZipEntry newEntry : newArchive) { + String newname = newEntry.getName(); + + // Return best match of contents, will return a name match if possible +- String oldname = oldJar.getBestMatch(newJar, newEntry); ++ String oldname = oldArchive.getBestMatch(newArchive, newEntry); + if (oldname == null) { + // New or modified entry + if (_debug) { +@@ -101,7 +108,7 @@ public class JarDiff implements JarDiffCodes + // Content already exist - need to do a move + + // Should do implicit move? Yes, if names are the same, and +- // no move command already exist from oldJar ++ // no move command already exist from oldArchive + if (oldname.equals(newname) && !moveSrc.contains(oldname)) { + if (_debug) { + System.out.println(newname + " added to implicit set!"); +@@ -117,12 +124,9 @@ public class JarDiff implements JarDiffCodes + // JarDiffPatcher also. + if (!minimal && (implicit.contains(oldname) || + moveSrc.contains(oldname) )) { +- + // generate non-minimal jardiff + // for backward compatibility +- + if (_debug) { +- + System.out.println("NEW: "+ newname); + } + newEntries.add(newname); +@@ -153,8 +157,8 @@ public class JarDiff implements JarDiffCodes + + // SECOND PASS: = - - + // - +- ArrayList deleted = new ArrayList<>(); +- for (JarEntry oldEntry : oldJar) { ++ List deleted = new ArrayList<>(); ++ for (ZipEntry oldEntry : oldArchive) { + String oldName = oldEntry.getName(); + if (!implicit.contains(oldName) && !moveSrc.contains(oldName) + && !newEntries.contains(oldName)) { +@@ -180,7 +184,7 @@ public class JarDiff implements JarDiffCodes + } + } + +- JarOutputStream jos = new JarOutputStream(os); ++ ZipOutputStream jos = new ZipOutputStream(os); + + // Write out all the MOVEs and REMOVEs + createIndex(jos, deleted, moved); +@@ -190,7 +194,7 @@ public class JarDiff implements JarDiffCodes + if (_debug) { + System.out.println("New File: " + newName); + } +- writeEntry(jos, newJar.getEntryByName(newName), newJar); ++ writeEntry(jos, newArchive.getEntryByName(newName), newArchive); + } + + jos.finish(); +@@ -199,11 +203,11 @@ public class JarDiff implements JarDiffCodes + } + + /** +- * Writes the index file out to jos. +- * oldEntries gives the names of the files that were removed, +- * movedMap maps from the new name to the old name. ++ * Writes the index file out to {@code jos}. ++ * {@code oldEntries} gives the names of the files that were removed, ++ * {@code movedMap} maps from the new name to the old name. + */ +- private static void createIndex (JarOutputStream jos, List oldEntries, ++ private static void createIndex (ZipOutputStream jos, List oldEntries, + Map movedMap) + throws IOException + { +@@ -220,17 +224,17 @@ public class JarDiff implements JarDiffCodes + } + + // And those that have moved +- for (String newName : movedMap.keySet()) { +- String oldName = movedMap.get(newName); ++ for (Map.Entry entry : movedMap.entrySet()) { ++ String oldName = entry.getValue(); + writer.write(MOVE_COMMAND); + writer.write(" "); + writeEscapedString(writer, oldName); + writer.write(" "); +- writeEscapedString(writer, newName); ++ writeEscapedString(writer, entry.getKey()); + writer.write("\r\n"); + } + +- jos.putNextEntry(new JarEntry(INDEX_NAME)); ++ jos.putNextEntry(new ZipEntry(INDEX_NAME)); + byte[] bytes = writer.toString().getBytes(UTF_8); + jos.write(bytes, 0, bytes.length); + } +@@ -264,10 +268,10 @@ public class JarDiff implements JarDiffCodes + return writer; + } + +- private static void writeEntry (JarOutputStream jos, JarEntry entry, JarFile2 file) ++ private static void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile2 file) + throws IOException + { +- try (InputStream data = file.getJarFile().getInputStream(entry)) { ++ try (InputStream data = file.getArchive().getInputStream(entry)) { + jos.putNextEntry(entry); + int size = data.read(newBytes); + while (size != -1) { +@@ -278,31 +282,31 @@ public class JarDiff implements JarDiffCodes + } + + /** +- * JarFile2 wraps a JarFile providing some convenience methods. ++ * ZipFile2 wraps a ZipFile providing some convenience methods. + */ +- private static class JarFile2 implements Iterable, Closeable ++ private static class ZipFile2 implements Iterable, Closeable + { +- private JarFile _jar; +- private List _entries; +- private HashMap _nameToEntryMap; +- private HashMap> _crcToEntryMap; ++ private final ZipFile _archive; ++ private List _entries; ++ private HashMap _nameToEntryMap; ++ private HashMap> _crcToEntryMap; + +- public JarFile2 (String path) throws IOException { +- _jar = new JarFile(new File(path)); ++ public ZipFile2 (String path) throws IOException { ++ _archive = new ZipFile(new File(path)); + index(); + } + +- public JarFile getJarFile () { +- return _jar; ++ public ZipFile getArchive () { ++ return _archive; + } + +- // from interface Iterable ++ // from interface Iterable + @Override +- public Iterator iterator () { ++ public Iterator iterator () { + return _entries.iterator(); + } + +- public JarEntry getEntryByName (String name) { ++ public ZipEntry getEntryByName (String name) { + return _nameToEntryMap.get(name); + } + +@@ -350,7 +354,7 @@ public class JarDiff implements JarDiffCodes + return retVal; + } + +- public String getBestMatch (JarFile2 file, JarEntry entry) throws IOException { ++ public String getBestMatch (ZipFile2 file, ZipEntry entry) throws IOException { + // check for same name and same content, return name if found + if (contains(file, entry)) { + return (entry.getName()); +@@ -360,11 +364,10 @@ public class JarDiff implements JarDiffCodes + return (hasSameContent(file,entry)); + } + +- public boolean contains (JarFile2 f, JarEntry e) throws IOException { +- +- JarEntry thisEntry = getEntryByName(e.getName()); ++ public boolean contains (ZipFile2 f, ZipEntry e) throws IOException { ++ ZipEntry thisEntry = getEntryByName(e.getName()); + +- // Look up name in 'this' Jar2File - if not exist return false ++ // Look up name in 'this' ZipFile2 - if not exist return false + if (thisEntry == null) + return false; + +@@ -373,26 +376,26 @@ public class JarDiff implements JarDiffCodes + return false; + + // Check contents - if no match - return false +- try (InputStream oldIS = getJarFile().getInputStream(thisEntry); +- InputStream newIS = f.getJarFile().getInputStream(e)) { ++ try (InputStream oldIS = getArchive().getInputStream(thisEntry); ++ InputStream newIS = f.getArchive().getInputStream(e)) { + return !differs(oldIS, newIS); + } + } + +- public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException { ++ public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException { + String thisName = null; +- Long crcL = Long.valueOf(entry.getCrc()); +- // check if this jar contains files with the passed in entry's crc ++ Long crcL = entry.getCrc(); ++ // check if this archive contains files with the passed in entry's crc + if (_crcToEntryMap.containsKey(crcL)) { + // get the Linked List with files with the crc +- LinkedList ll = _crcToEntryMap.get(crcL); ++ LinkedList ll = _crcToEntryMap.get(crcL); + // go through the list and check for content match +- ListIterator li = ll.listIterator(0); ++ ListIterator li = ll.listIterator(0); + while (li.hasNext()) { +- JarEntry thisEntry = li.next(); ++ ZipEntry thisEntry = li.next(); + // check for content match +- try (InputStream oldIS = getJarFile().getInputStream(thisEntry); +- InputStream newIS = file.getJarFile().getInputStream(entry)) { ++ try (InputStream oldIS = getArchive().getInputStream(thisEntry); ++ InputStream newIS = file.getArchive().getInputStream(entry)) { + if (!differs(oldIS, newIS)) { + thisName = thisEntry.getName(); + return thisName; +@@ -404,19 +407,19 @@ public class JarDiff implements JarDiffCodes + } + + private void index () throws IOException { +- Enumeration entries = _jar.entries(); ++ Enumeration entries = _archive.entries(); + + _nameToEntryMap = new HashMap<>(); + _crcToEntryMap = new HashMap<>(); + _entries = new ArrayList<>(); + if (_debug) { +- System.out.println("indexing: " + _jar.getName()); ++ System.out.println("indexing: " + _archive.getName()); + } + if (entries != null) { + while (entries.hasMoreElements()) { +- JarEntry entry = entries.nextElement(); ++ ZipEntry entry = entries.nextElement(); + long crc = entry.getCrc(); +- Long crcL = Long.valueOf(crc); ++ Long crcL = crc; + if (_debug) { + System.out.println("\t" + entry.getName() + " CRC " + crc); + } +@@ -427,13 +430,13 @@ public class JarDiff implements JarDiffCodes + // generate the CRC to entries map + if (_crcToEntryMap.containsKey(crcL)) { + // key exist, add the entry to the correcponding linked list +- LinkedList ll = _crcToEntryMap.get(crcL); ++ LinkedList ll = _crcToEntryMap.get(crcL); + ll.add(entry); + _crcToEntryMap.put(crcL, ll); + + } else { + // create a new entry in the hashmap for the new key +- LinkedList ll = new LinkedList(); ++ LinkedList ll = new LinkedList<>(); + ll.add(entry); + _crcToEntryMap.put(crcL, ll); + } +@@ -443,7 +446,7 @@ public class JarDiff implements JarDiffCodes + + @Override + public void close() throws IOException { +- _jar.close(); ++ _archive.close(); + } + } + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java +index b5a0a1763..e55034bca 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java +@@ -16,27 +16,25 @@ import java.util.ArrayList; + import java.util.Enumeration; + import java.util.HashMap; + import java.util.HashSet; +-import java.util.Iterator; + import java.util.List; + import java.util.Map; + import java.util.Set; +- +-import java.util.jar.JarEntry; +-import java.util.jar.JarFile; + import java.util.jar.JarOutputStream; ++import java.util.zip.ZipEntry; ++import java.util.zip.ZipFile; ++import java.util.zip.ZipOutputStream; + + import com.threerings.getdown.util.ProgressObserver; +- + import static java.nio.charset.StandardCharsets.UTF_8; + + /** +- * Applies a jardiff patch to a jar file. ++ * Applies a jardiff patch to a jar/zip file. + */ + public class JarDiffPatcher implements JarDiffCodes + { + /** +- * Patches the specified jar file using the supplied patch file and writing +- * the new jar file to the supplied target. ++ * Patches the specified jar file using the supplied patch file and writing the new jar file to ++ * the supplied target. + * + * @param jarPath the path to the original jar file. + * @param diffPath the path to the jardiff patch file. +@@ -49,11 +47,9 @@ public class JarDiffPatcher implements JarDiffCodes + throws IOException + { + File oldFile = new File(jarPath), diffFile = new File(diffPath); +- +- try (JarFile oldJar = new JarFile(oldFile); +- JarFile jarDiff = new JarFile(diffFile); +- JarOutputStream jos = new JarOutputStream(new FileOutputStream(target))) { +- ++ try (ZipFile oldJar = new ZipFile(oldFile); ++ ZipFile jarDiff = new ZipFile(diffFile); ++ ZipOutputStream jos = makeOutputStream(oldFile, target)) { + Set ignoreSet = new HashSet<>(); + Map renameMap = new HashMap<>(); + determineNameMapping(jarDiff, ignoreSet, renameMap); +@@ -63,7 +59,7 @@ public class JarDiffPatcher implements JarDiffCodes + + // Files to implicit move + Set oldjarNames = new HashSet<>(); +- Enumeration oldEntries = oldJar.entries(); ++ Enumeration oldEntries = oldJar.entries(); + if (oldEntries != null) { + while (oldEntries.hasMoreElements()) { + oldjarNames.add(oldEntries.nextElement().getName()); +@@ -83,10 +79,10 @@ public class JarDiffPatcher implements JarDiffCodes + size -= ignoreSet.size(); + + // Add content from JARDiff +- Enumeration entries = jarDiff.entries(); ++ Enumeration entries = jarDiff.entries(); + if (entries != null) { + while (entries.hasMoreElements()) { +- JarEntry entry = entries.nextElement(); ++ ZipEntry entry = entries.nextElement(); + if (!INDEX_NAME.equals(entry.getName())) { + updateObserver(observer, currentEntry, size); + currentEntry++; +@@ -114,15 +110,15 @@ public class JarDiffPatcher implements JarDiffCodes + // Apply move command + String oldName = renameMap.get(newName); + +- // Get source JarEntry +- JarEntry oldEntry = oldJar.getJarEntry(oldName); ++ // Get source ZipEntry ++ ZipEntry oldEntry = oldJar.getEntry(oldName); + if (oldEntry == null) { + String moveCmd = MOVE_COMMAND + oldName + " " + newName; + throw new IOException("error.badmove: " + moveCmd); + } + +- // Create dest JarEntry +- JarEntry newEntry = new JarEntry(newName); ++ // Create dest ZipEntry ++ ZipEntry newEntry = new ZipEntry(newName); + newEntry.setTime(oldEntry.getTime()); + newEntry.setSize(oldEntry.getSize()); + newEntry.setCompressedSize(oldEntry.getCompressedSize()); +@@ -149,19 +145,15 @@ public class JarDiffPatcher implements JarDiffCodes + } + + // implicit move +- Iterator iEntries = oldjarNames.iterator(); +- if (iEntries != null) { +- while (iEntries.hasNext()) { +- String name = iEntries.next(); +- JarEntry entry = oldJar.getJarEntry(name); +- if (entry == null) { +- // names originally retrieved from the JAR, so this should never happen +- throw new AssertionError("JAR entry not found: " + name); +- } +- updateObserver(observer, currentEntry, size); +- currentEntry++; +- writeEntry(jos, entry, oldJar); ++ for (String name : oldjarNames) { ++ ZipEntry entry = oldJar.getEntry(name); ++ if (entry == null) { ++ // names originally retrieved from the archive, so this should never happen ++ throw new AssertionError("Archive entry not found: " + name); + } ++ updateObserver(observer, currentEntry, size); ++ currentEntry++; ++ writeEntry(jos, entry, oldJar); + } + updateObserver(observer, currentEntry, size); + } +@@ -175,7 +167,7 @@ public class JarDiffPatcher implements JarDiffCodes + } + + protected void determineNameMapping ( +- JarFile jarDiff, Set ignoreSet, Map renameMap) ++ ZipFile jarDiff, Set ignoreSet, Map renameMap) + throws IOException + { + InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME)); +@@ -223,7 +215,7 @@ public class JarDiffPatcher implements JarDiffCodes + { + int index = 0; + int length = path.length(); +- ArrayList sub = new ArrayList<>(); ++ List sub = new ArrayList<>(); + + while (index < length) { + while (index < length && Character.isWhitespace +@@ -231,9 +223,8 @@ public class JarDiffPatcher implements JarDiffCodes + index++; + } + if (index < length) { +- int start = index; +- int last = start; +- String subString = null; ++ int last = index; ++ StringBuilder subString = null; + + while (index < length) { + char aChar = path.charAt(index); +@@ -241,9 +232,9 @@ public class JarDiffPatcher implements JarDiffCodes + path.charAt(index + 1) == ' ') { + + if (subString == null) { +- subString = path.substring(last, index); ++ subString = new StringBuilder(path.substring(last, index)); + } else { +- subString += path.substring(last, index); ++ subString.append(path, last, index); + } + last = ++index; + } else if (Character.isWhitespace(aChar)) { +@@ -253,18 +244,20 @@ public class JarDiffPatcher implements JarDiffCodes + } + if (last != index) { + if (subString == null) { +- subString = path.substring(last, index); ++ subString = new StringBuilder(path.substring(last, index)); + } else { +- subString += path.substring(last, index); ++ subString.append(path, last, index); + } + } +- sub.add(subString); ++ if (subString != null) { ++ sub.add(subString.toString()); ++ } + } + } + return sub; + } + +- protected void writeEntry (JarOutputStream jos, JarEntry entry, JarFile file) ++ protected void writeEntry (ZipOutputStream jos, ZipEntry entry, ZipFile file) + throws IOException + { + try (InputStream data = file.getInputStream(entry)) { +@@ -272,10 +265,10 @@ public class JarDiffPatcher implements JarDiffCodes + } + } + +- protected void writeEntry (JarOutputStream jos, JarEntry entry, InputStream data) ++ protected void writeEntry (ZipOutputStream jos, ZipEntry entry, InputStream data) + throws IOException + { +- jos.putNextEntry(new JarEntry(entry.getName())); ++ jos.putNextEntry(new ZipEntry(entry.getName())); + + // Read the entry + int size = data.read(newBytes); +@@ -285,6 +278,15 @@ public class JarDiffPatcher implements JarDiffCodes + } + } + ++ protected static ZipOutputStream makeOutputStream (File source, File target) ++ throws IOException ++ { ++ FileOutputStream out = new FileOutputStream(target); ++ if (source.getName().endsWith(".jar")) return new JarOutputStream(out); ++ else if (source.getName().endsWith(".zip")) return new ZipOutputStream(out); ++ else throw new AssertionError("Unsupported source file '" + source + "'. Not a .jar or .zip?"); ++ } ++ + protected static final int DEFAULT_READ_SIZE = 2048; + + protected static byte[] newBytes = new byte[DEFAULT_READ_SIZE]; +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java +index 4ead59bb3..5c8baaaab 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/tools/Patcher.java +@@ -9,16 +9,13 @@ import java.io.File; + import java.io.FileOutputStream; + import java.io.IOException; + import java.io.InputStream; +- + import java.util.Enumeration; +-import java.util.jar.JarEntry; +-import java.util.jar.JarFile; + import java.util.zip.ZipEntry; ++import java.util.zip.ZipFile; + + import com.threerings.getdown.util.FileUtil; + import com.threerings.getdown.util.ProgressObserver; + import com.threerings.getdown.util.StreamUtil; +- + import static com.threerings.getdown.Log.log; + + /** +@@ -55,10 +52,10 @@ public class Patcher + _obs = obs; + _plength = patch.length(); + +- try (JarFile file = new JarFile(patch)) { +- Enumeration entries = file.entries(); // old skool! ++ try (ZipFile file = new ZipFile(patch)) { ++ Enumeration entries = file.entries(); + while (entries.hasMoreElements()) { +- JarEntry entry = entries.nextElement(); ++ ZipEntry entry = entries.nextElement(); + String path = entry.getName(); + long elength = entry.getCompressedSize(); + +@@ -96,7 +93,7 @@ public class Patcher + return path.substring(0, path.length() - suffix.length()); + } + +- protected void createFile (JarFile file, ZipEntry entry, File target) ++ protected void createFile (ZipFile file, ZipEntry entry, File target) + { + // create our copy buffer if necessary + if (_buffer == null) { +@@ -124,8 +121,7 @@ public class Patcher + } + } + +- protected void patchFile (JarFile file, ZipEntry entry, +- File appdir, String path) ++ protected void patchFile (ZipFile file, ZipEntry entry, File appdir, String path) + { + File target = new File(appdir, path); + File patch = new File(appdir, entry.getName()); +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java +index 2a5db79ec..233f1e739 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Base64.java +@@ -24,7 +24,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII; + * href="http://www.ietf.org/rfc/rfc2045.txt">2045 and 3548. + */ +-public class Base64 { ++public final class Base64 { + /** + * Default values for encoder/decoder flags. + */ +@@ -68,7 +68,7 @@ public class Base64 { + // shared code + // -------------------------------------------------------- + +- /* package */ static abstract class Coder { ++ /* package */ abstract static class Coder { + public byte[] output; + public int op; + +@@ -178,7 +178,7 @@ public class Base64 { + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ +- private static final int DECODE[] = { ++ private static final int[] DECODE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, +@@ -201,7 +201,7 @@ public class Base64 { + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ +- private static final int DECODE_WEBSAFE[] = { ++ private static final int[] DECODE_WEBSAFE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, +@@ -236,7 +236,7 @@ public class Base64 { + private int state; // state number (0 to 6) + private int value; + +- final private int[] alphabet; ++ private final int[] alphabet; + + public Decoder(int flags, byte[] output) { + this.output = output; +@@ -541,7 +541,7 @@ public class Base64 { + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ +- private static final byte ENCODE[] = { ++ private static final byte[] ENCODE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', +@@ -552,21 +552,21 @@ public class Base64 { + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ +- private static final byte ENCODE_WEBSAFE[] = { ++ private static final byte[] ENCODE_WEBSAFE = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + +- final private byte[] tail; ++ private final byte[] tail; + /* package */ int tailLen; + private int count; + +- final public boolean do_padding; +- final public boolean do_newline; +- final public boolean do_cr; +- final private byte[] alphabet; ++ public final boolean do_padding; ++ public final boolean do_newline; ++ public final boolean do_cr; ++ private final byte[] alphabet; + + public Encoder(int flags, byte[] output) { + this.output = output; +@@ -618,7 +618,7 @@ public class Base64 { + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; +- }; ++ } + break; + + case 2: +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java +index 047cead76..416f77e58 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Color.java +@@ -8,11 +8,11 @@ package com.threerings.getdown.util; + /** + * Utilities for handling ARGB colors. + */ +-public class Color ++public final class Color + { +- public final static int CLEAR = 0x00000000; +- public final static int WHITE = 0xFFFFFFFF; +- public final static int BLACK = 0xFF000000; ++ public static final int CLEAR = 0x00000000; ++ public static final int WHITE = 0xFFFFFFFF; ++ public static final int BLACK = 0xFF000000; + + public static float brightness (int argb) { + // TODO: we're ignoring alpha here... +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java +index 6ad2b4fd9..75357e882 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/Config.java +@@ -14,7 +14,6 @@ import java.net.MalformedURLException; + import java.net.URL; + import java.nio.charset.StandardCharsets; + import java.util.ArrayList; +-import java.util.Arrays; + import java.util.HashMap; + import java.util.List; + import java.util.Locale; +@@ -58,33 +57,17 @@ public class Config + } + + /** +- * Parses a configuration file containing key/value pairs. The file must be in the UTF-8 +- * encoding. +- * ++ * Parses configuration text containing key/value pairs. + * @param opts options that influence the parsing. See {@link #createOpts}. +- * +- * @return a list of String[] instances containing the key/value pairs in the ++ * @return a list of {@code String[]} instances containing the key/value pairs in the + * order they were parsed from the file. + */ +- public static List parsePairs (File source, ParseOpts opts) +- throws IOException +- { +- // annoyingly FileReader does not allow encoding to be specified (uses platform default) +- try (FileInputStream fis = new FileInputStream(source); +- InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) { +- return parsePairs(input, opts); +- } +- } +- +- /** +- * See {@link #parsePairs(File,ParseOpts)}. +- */ + public static List parsePairs (Reader source, ParseOpts opts) throws IOException + { + List pairs = new ArrayList<>(); + for (String line : FileUtil.readLines(source)) { + // nix comments +- int cidx = line.indexOf("#"); ++ int cidx = line.indexOf('#'); + if (opts.strictComments ? cidx == 0 : cidx != -1) { + line = line.substring(0, cidx); + } +@@ -98,7 +81,7 @@ public class Config + // parse our key/value pair + String[] pair = new String[2]; + // if we're biasing toward key, put all the extra = in the key rather than the value +- int eidx = opts.biasToKey ? line.lastIndexOf("=") : line.indexOf("="); ++ int eidx = opts.biasToKey ? line.lastIndexOf('=') : line.indexOf('='); + if (eidx != -1) { + pair[0] = line.substring(0, eidx).trim(); + pair[1] = line.substring(eidx+1).trim(); +@@ -109,7 +92,7 @@ public class Config + + // if the pair has an os qualifier, we need to process it + if (pair[1].startsWith("[")) { +- int qidx = pair[1].indexOf("]"); ++ int qidx = pair[1].indexOf(']'); + if (qidx == -1) { + log.warning("Bogus platform specifier", "key", pair[0], "value", pair[1]); + continue; // omit the pair entirely +@@ -132,6 +115,21 @@ public class Config + return pairs; + } + ++ /** ++ * Parses configuration file containing key/value pairs. ++ * @param source the file containing the config text. Must be in the UTF-8 encoding. ++ * @param opts options that influence the parsing. See {@link #createOpts}. ++ */ ++ public static List parsePairs (File source, ParseOpts opts) ++ throws IOException ++ { ++ // annoyingly FileReader does not allow encoding to be specified (uses platform default) ++ try (FileInputStream fis = new FileInputStream(source); ++ InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) { ++ return parsePairs(input, opts); ++ } ++ } ++ + /** + * Takes a comma-separated String of four integers and returns a rectangle using those ints as + * the its x, y, width, and height. +@@ -167,13 +165,10 @@ public class Config + } + + /** +- * Parses a configuration file containing key/value pairs. The file must be in the UTF-8 +- * encoding. +- * +- * @return a map from keys to values, where a value will be an array of strings if more than +- * one key/value pair in the config file was associated with the same key. ++ * Parses the data for a config instance from the supplied {@code source} reader. ++ * @return a map that can be used to create a {@link #Config}. + */ +- public static Config parseConfig (File source, ParseOpts opts) ++ public static Map parseData (Reader source, ParseOpts opts) + throws IOException + { + Map data = new HashMap<>(); +@@ -196,15 +191,34 @@ public class Config + } + } + +- // special magic for the getdown.txt config: if the parsed data contains 'strict_comments = +- // true' then we reparse the file with strict comments (i.e. # is only assumed to start a +- // comment in column 0) +- if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) { +- opts.strictComments = true; +- return parseConfig(source, opts); +- } ++ return data; ++ } ++ ++ /** ++ * Parses a configuration file containing key/value pairs. The file must be in the UTF-8 ++ * encoding. ++ * ++ * @return a map from keys to values, where a value will be an array of strings if more than ++ * one key/value pair in the config file was associated with the same key. ++ */ ++ public static Config parseConfig (File source, ParseOpts opts) ++ throws IOException ++ { ++ // annoyingly FileReader does not allow encoding to be specified (uses platform default) ++ try (FileInputStream fis = new FileInputStream(source); ++ InputStreamReader input = new InputStreamReader(fis, StandardCharsets.UTF_8)) { ++ Map data = parseData(input, opts); ++ ++ // special magic for the getdown.txt config: if the parsed data contains ++ // 'strict_comments = true' then we reparse the file with strict comments (i.e. # is ++ // only assumed to start a comment in column 0) ++ if (!opts.strictComments && Boolean.parseBoolean((String)data.get("strict_comments"))) { ++ opts.strictComments = true; ++ return parseConfig(source, opts); ++ } + +- return new Config(data); ++ return new Config(data); ++ } + } + + public Config (Map data) { +@@ -248,6 +262,20 @@ public class Config + return Boolean.parseBoolean(getString(name)); + } + ++ /** ++ * Returns the specified config value as an enum value. The string value of the config is ++ * converted to all upper case and then turned into an enum via {@link Enum#valueOf}. ++ */ ++ public > T getEnum (String name, Class eclass, T defval) { ++ String value = getString(name, defval.toString()); ++ try { ++ return Enum.valueOf(eclass, value.toUpperCase()); ++ } catch (Exception e) { ++ log.warning("Invalid value for '" + name + "' config: '" + value + "'."); ++ return defval; ++ } ++ } ++ + /** + * Massages a single string into an array and leaves existing array values as is. Simplifies + * access to parameters that are expected to be arrays. +@@ -255,9 +283,6 @@ public class Config + public String[] getMultiValue (String name) + { + Object value = _data.get(name); +- if (value == null) { +- return new String[] {}; +- } + if (value instanceof String) { + return new String[] { (String)value }; + } else { +@@ -355,7 +380,7 @@ public class Config + protected static boolean checkQualifiers (String quals, String osname, String osarch) + { + if (quals.startsWith("!")) { +- if (quals.indexOf(",") != -1) { // sanity check ++ if (quals.contains(",")) { // sanity check + log.warning("Multiple qualifiers cannot be used when one of the qualifiers " + + "is negative", "quals", quals); + return false; +@@ -375,103 +400,8 @@ public class Config + { + String[] bits = qual.trim().toLowerCase(Locale.ROOT).split("-"); + String os = bits[0], arch = (bits.length > 1) ? bits[1] : ""; +- return (osname.indexOf(os) != -1) && (osarch.indexOf(arch) != -1); +- } +- +- public void mergeConfig(Config newValues, boolean merge) { +- +- for (Map.Entry entry : newValues.getData().entrySet()) { +- +- String key = entry.getKey(); +- Object nvalue = entry.getValue(); +- +- String mkey = key.indexOf('.') > -1 ? key.substring(key.indexOf('.') + 1) : key; +- if (merge && allowedMergeKeys.contains(mkey)) { +- +- // merge multi values +- +- Object value = _data.get(key); +- +- if (value == null) { +- _data.put(key, nvalue); +- } else if (value instanceof String) { +- if (nvalue instanceof String) { +- +- // value is String, nvalue is String +- _data.put(key, new String[] { (String)value, (String)nvalue }); +- +- } else if (nvalue instanceof String[]) { +- +- // value is String, nvalue is String[] +- String[] nvalues = (String[])nvalue; +- String[] newvalues = new String[nvalues.length+1]; +- newvalues[0] = (String)value; +- System.arraycopy(nvalues, 0, newvalues, 1, nvalues.length); +- _data.put(key, newvalues); +- +- } +- } else if (value instanceof String[]) { +- if (nvalue instanceof String) { +- +- // value is String[], nvalue is String +- String[] values = (String[])value; +- String[] newvalues = new String[values.length+1]; +- System.arraycopy(values, 0, newvalues, 0, values.length); +- newvalues[values.length] = (String)nvalue; +- _data.put(key, newvalues); +- +- } else if (nvalue instanceof String[]) { +- +- // value is String[], nvalue is String[] +- String[] values = (String[])value; +- String[] nvalues = (String[])nvalue; +- String[] newvalues = new String[values.length + nvalues.length]; +- System.arraycopy(values, 0, newvalues, 0, values.length); +- System.arraycopy(nvalues, 0, newvalues, values.length, newvalues.length); +- _data.put(key, newvalues); +- +- } +- } +- +- } else if (allowedReplaceKeys.contains(mkey)){ +- +- // replace value +- _data.put(key, nvalue); +- +- } else { +- log.warning("Not merging key '"+key+"' into config"); +- } +- +- } +- +- } +- +- public String toString() { +- StringBuilder sb = new StringBuilder(); +- for (Map.Entry entry : getData().entrySet()) { +- String key = entry.getKey(); +- Object val = entry.getValue(); +- sb.append(key); +- sb.append("="); +- if (val instanceof String) { +- sb.append((String)val); +- } else if (val instanceof String[]) { +- sb.append(Arrays.toString((String[])val)); +- } else { +- sb.append("Value not String or String[]"); +- } +- sb.append("\n"); +- } +- return sb.toString(); +- } +- +- public Map getData() { +- return _data; ++ return (osname.contains(os)) && (osarch.contains(arch)); + } + + private final Map _data; +- +- public static final List allowedReplaceKeys = Arrays.asList("appbase","apparg","jvmarg"); // these are the ones we might use +- public static final List allowedMergeKeys = Arrays.asList("apparg","jvmarg"); // these are the ones we might use +- //private final List allowedMergeKeys = Arrays.asList("apparg","jvmarg","resource","code","java_location"); // (not exhaustive list here) + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java +deleted file mode 100644 +index 21b056932..000000000 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java ++++ /dev/null +@@ -1,73 +0,0 @@ +-// +-// Getdown - application installer, patcher and launcher +-// Copyright (C) 2004-2018 Getdown authors +-// https://github.com/threerings/getdown/blob/master/LICENSE +- +-package com.threerings.getdown.util; +- +-import java.io.IOException; +-import java.net.HttpURLConnection; +-import java.net.Proxy; +-import java.net.URL; +-import java.net.URLConnection; +-import java.net.URLDecoder; +- +-import com.threerings.getdown.data.SysProps; +- +-import static java.nio.charset.StandardCharsets.UTF_8; +- +-public class ConnectionUtil +-{ +- /** +- * Opens a connection to a URL, setting the authentication header if user info is present. +- * @param proxy the proxy via which to perform HTTP connections. +- * @param url the URL to which to open a connection. +- * @param connectTimeout if {@code > 0} then a timeout, in seconds, to use when opening the +- * connection. If {@code 0} is supplied, the connection timeout specified via system properties +- * will be used instead. +- * @param readTimeout if {@code > 0} then a timeout, in seconds, to use while reading data from +- * the connection. If {@code 0} is supplied, the read timeout specified via system properties +- * will be used instead. +- */ +- public static URLConnection open (Proxy proxy, URL url, int connectTimeout, int readTimeout) +- throws IOException +- { +- URLConnection conn = url.openConnection(proxy); +- +- // configure a connect timeout, if requested +- int ctimeout = connectTimeout > 0 ? connectTimeout : SysProps.connectTimeout(); +- if (ctimeout > 0) { +- conn.setConnectTimeout(ctimeout * 1000); +- } +- +- // configure a read timeout, if requested +- int rtimeout = readTimeout > 0 ? readTimeout : SysProps.readTimeout(); +- if (rtimeout > 0) { +- conn.setReadTimeout(rtimeout * 1000); +- } +- +- // If URL has a username:password@ before hostname, use HTTP basic auth +- String userInfo = url.getUserInfo(); +- if (userInfo != null) { +- // Remove any percent-encoding in the username/password +- userInfo = URLDecoder.decode(userInfo, "UTF-8"); +- // Now base64 encode the auth info and make it a single line +- String encoded = Base64.encodeToString(userInfo.getBytes(UTF_8), Base64.DEFAULT). +- replaceAll("\\n","").replaceAll("\\r", ""); +- conn.setRequestProperty("Authorization", "Basic " + encoded); +- } +- +- return conn; +- } +- +- /** +- * Opens a connection to a http or https URL, setting the authentication header if user info is +- * present. Throws a class cast exception if the connection returned is not the right type. See +- * {@link #open} for parameter documentation. +- */ +- public static HttpURLConnection openHttp ( +- Proxy proxy, URL url, int connectTimeout, int readTimeout) throws IOException +- { +- return (HttpURLConnection)open(proxy, url, connectTimeout, readTimeout); +- } +-} +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java +index 67d033086..d7de78bbd 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/FileUtil.java +@@ -6,25 +6,17 @@ + package com.threerings.getdown.util; + + import java.io.*; +-import java.nio.file.Files; +-import java.nio.file.Paths; + import java.util.*; + import java.util.jar.*; +-import java.util.zip.GZIPInputStream; ++import java.util.zip.*; + +-import org.apache.commons.compress.archivers.ArchiveEntry; +-import org.apache.commons.compress.archivers.ArchiveInputStream; +-import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +- +-import com.threerings.getdown.util.StreamUtil; + import com.threerings.getdown.Log; + import static com.threerings.getdown.Log.log; + + /** + * File related utilities. + */ +-public class FileUtil ++public final class FileUtil + { + /** + * Gets the specified source file to the specified destination file by hook or crook. Windows +@@ -122,13 +114,13 @@ public class FileUtil + * @param cleanExistingDirs if true, all files in all directories contained in {@code jar} will + * be deleted prior to unpacking the jar. + */ +- public static void unpackJar (JarFile jar, File target, boolean cleanExistingDirs) ++ public static void unpackJar (ZipFile jar, File target, boolean cleanExistingDirs) + throws IOException + { + if (cleanExistingDirs) { +- Enumeration entries = jar.entries(); ++ Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { +- JarEntry entry = (JarEntry)entries.nextElement(); ++ ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + File efile = new File(target, entry.getName()); + if (efile.exists()) { +@@ -141,9 +133,9 @@ public class FileUtil + } + } + +- Enumeration entries = jar.entries(); ++ Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { +- JarEntry entry = (JarEntry)entries.nextElement(); ++ ZipEntry entry = entries.nextElement(); + File efile = new File(target, entry.getName()); + + // if we're unpacking a normal jar file, it will have special path +@@ -164,7 +156,7 @@ public class FileUtil + } + + try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile)); +- InputStream jin = jar.getInputStream(entry)) { ++ InputStream jin = jar.getInputStream(entry)) { + StreamUtil.copy(jin, fout); + } catch (Exception e) { + throw new IOException( +@@ -174,82 +166,13 @@ public class FileUtil + } + + /** +- * Unpacks the specified tgz file into the specified target directory. +- * @param cleanExistingDirs if true, all files in all directories contained in {@code tgz} will +- * be deleted prior to unpacking the tgz. +- */ +- public static void unpackTgz (TarArchiveInputStream tgz, File target, boolean cleanExistingDirs) +- throws IOException +- { +- TarArchiveEntry entry; +- while ((entry = tgz.getNextTarEntry()) != null) { +- // sanitize the entry name +- String entryName = entry.getName(); +- if (entryName.startsWith(File.separator)) +- { +- entryName = entryName.substring(File.separator.length()); +- } +- File efile = new File(target, entryName); +- +- // if we're unpacking a normal tgz file, it will have special path +- // entries that allow us to create our directories first +- if (entry.isDirectory()) { +- +- if (cleanExistingDirs) { +- if (efile.exists()) { +- for (File f : efile.listFiles()) { +- if (!f.isDirectory()) +- f.delete(); +- } +- } +- } +- +- if (!efile.exists() && !efile.mkdir()) { +- log.warning("Failed to create tgz entry path", "tgz", tgz, "entry", entry); +- } +- continue; +- } +- +- // but some do not, so we want to ensure that our directories exist +- // prior to getting down and funky +- File parent = new File(efile.getParent()); +- if (!parent.exists() && !parent.mkdirs()) { +- log.warning("Failed to create tgz entry parent", "tgz", tgz, "parent", parent); +- continue; +- } +- +- if (entry.isLink()) +- { +- System.out.println("Creating hard link "+efile.getName()+" -> "+entry.getLinkName()); +- Files.createLink(efile.toPath(), Paths.get(entry.getLinkName())); +- continue; +- } +- +- if (entry.isSymbolicLink()) +- { +- System.out.println("Creating symbolic link "+efile.getName()+" -> "+entry.getLinkName()); +- Files.createSymbolicLink(efile.toPath(), Paths.get(entry.getLinkName())); +- continue; +- } +- +- try (BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(efile)); +- InputStream tin = tgz;) { +- StreamUtil.copy(tin, fout); +- } catch (Exception e) { +- throw new IOException( +- Log.format("Failure unpacking", "tgz", tgz, "entry", efile), e); +- } +- } +- } +- +- /** +- * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. If {@code +- * packedJar} has a {@code .gz} extension, it will be gunzipped first. ++ * Unpacks a pack200 packed jar file from {@code packedJar} into {@code target}. ++ * If {@code packedJar} has a {@code .gz} extension, it will be gunzipped first. + */ + public static void unpackPacked200Jar (File packedJar, File target) throws IOException + { + try (InputStream packJarIn = new FileInputStream(packedJar); +- JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) { ++ JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(target))) { + boolean gz = (packedJar.getName().endsWith(".gz") || + packedJar.getName().endsWith(".gz_new")); + try (InputStream packJarIn2 = (gz ? new GZIPInputStream(packJarIn) : packJarIn)) { +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java +index f2f7ef39f..c05992abd 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java +@@ -25,8 +25,6 @@ public final class HostWhitelist + */ + public static URL verify (URL url) throws MalformedURLException + { +- +- + return verify(Build.hostWhitelist(), url); + } + +@@ -43,12 +41,6 @@ public final class HostWhitelist + } + + String urlHost = url.getHost(); +- String protocol = url.getProtocol(); +- +- if (ALLOW_LOCATOR_FILE_PROTOCOL && protocol.equals("file") && urlHost.equals("")) { +- return url; +- } +- + for (String host : hosts) { + String regex = host.replace(".", "\\.").replace("*", ".*"); + if (urlHost.matches(regex)) { +@@ -59,5 +51,4 @@ public final class HostWhitelist + throw new MalformedURLException( + "The host for the specified URL (" + url + ") is not in the host whitelist: " + hosts); + } +- private static boolean ALLOW_LOCATOR_FILE_PROTOCOL = true; + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java +index f2cd5736e..cc51794ef 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java +@@ -17,21 +17,23 @@ import static com.threerings.getdown.Log.log; + * Useful routines for launching Java applications from within other Java + * applications. + */ +-public class LaunchUtil ++public final class LaunchUtil + { +- /** The directory into which a local VM installation should be unpacked. */ +- public static final String LOCAL_JAVA_DIR = "jre"; ++ /** The default directory into which a local VM installation should be unpacked. */ ++ public static final String LOCAL_JAVA_DIR = "java_vm"; + + /** +- * Writes a version.txt file into the specified application directory and ++ * Writes a {@code version.txt} file into the specified application directory and + * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly + * specified version and relaunch the application. + * + * @param appdir the directory in which the application is installed. + * @param getdownJarName the name of the getdown jar file in the application directory. This is +- * probably getdown-pro.jar or getdown-retro-pro.jar if you are using ++ * probably {@code getdown-pro.jar} or {@code getdown-retro-pro.jar} if you are using + * the results of the standard build. + * @param newVersion the new version to which Getdown will update when it is executed. ++ * @param javaLocalDir the name of the directory (inside {@code appdir}) that contains a ++ * locally installed JRE. Defaults to {@link #LOCAL_JAVA_DIR} if null is passed. + * + * @return true if the relaunch succeeded, false if we were unable to relaunch due to being on + * Windows 9x where we cannot launch subprocesses without waiting around for them to exit, +@@ -39,13 +41,13 @@ public class LaunchUtil + * after making this call as it will be upgraded and restarted. If false is returned, the + * application should tell the user that they must restart the application manually. + * +- * @exception IOException thrown if we were unable to create the version.txt file ++ * @exception IOException thrown if we were unable to create the {@code version.txt} file + * in the supplied application directory. If the version.txt file cannot be created, restarting + * Getdown will not cause the application to be upgraded, so the application will have to + * resort to telling the user that it is in a bad way. + */ + public static boolean updateVersionAndRelaunch ( +- File appdir, String getdownJarName, String newVersion) ++ File appdir, String getdownJarName, String newVersion, String javaLocalDir) + throws IOException + { + // create the file that instructs Getdown to upgrade +@@ -61,9 +63,9 @@ public class LaunchUtil + } + + // do the deed +- String[] args = new String[] { +- getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath() +- }; ++ String javaDir = StringUtil.isBlank(javaLocalDir) ? LOCAL_JAVA_DIR : javaLocalDir; ++ String javaBin = getJVMBinaryPath(new File(appdir, javaDir), false); ++ String[] args = { javaBin, "-jar", pro.toString(), appdir.getPath() }; + log.info("Running " + StringUtil.join(args, "\n ")); + try { + Runtime.getRuntime().exec(args, null); +@@ -75,25 +77,15 @@ public class LaunchUtil + } + + /** +- * Reconstructs the path to the JVM used to launch this process. +- */ +- public static String getJVMPath (File appdir) +- { +- return getJVMPath(appdir, false); +- } +- +- /** +- * Reconstructs the path to the JVM used to launch this process. +- * ++ * Resolves a path to a JVM binary. ++ * @param javaLocalDir JRE location within appdir. + * @param windebug if true we will use java.exe instead of javaw.exe on Windows. ++ * @return the path to the JVM binary used to launch this process. + */ +- public static String getJVMPath (File appdir, boolean windebug) ++ public static String getJVMBinaryPath (File javaLocalDir, boolean windebug) + { + // first look in our application directory for an installed VM +- String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug); +- if (vmpath == null && isMacOS()) { +- vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR + "/Contents/Home").getAbsolutePath(), windebug); +- } ++ String vmpath = checkJVMPath(javaLocalDir.getAbsolutePath(), windebug); + + // then fall back to the VM in which we're already running + if (vmpath == null) { +@@ -102,7 +94,7 @@ public class LaunchUtil + + // then throw up our hands and hope for the best + if (vmpath == null) { +- log.warning("Unable to find java [appdir=" + appdir + ++ log.warning("Unable to find java [local=" + javaLocalDir + + ", java.home=" + System.getProperty("java.home") + "]!"); + vmpath = "java"; + } +@@ -153,7 +145,7 @@ public class LaunchUtil + if (newgd.renameTo(curgd)) { + FileUtil.deleteHarder(oldgd); // yay! + try { +- // copy the moved file back to getdown-dop-new.jar so that we don't end up ++ // copy the moved file back to newgd so that we don't end up + // downloading another copy next time + FileUtil.copy(curgd, newgd); + } catch (IOException e) { +@@ -185,23 +177,23 @@ public class LaunchUtil + public static boolean mustMonitorChildren () + { + String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); +- return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1); ++ return (osname.contains("windows 98") || osname.contains("windows me")); + } + + /** + * Returns true if we're running in a JVM that identifies its operating system as Windows. + */ +- public static final boolean isWindows () { return _isWindows; } ++ public static boolean isWindows () { return _isWindows; } + + /** + * Returns true if we're running in a JVM that identifies its operating system as MacOS. + */ +- public static final boolean isMacOS () { return _isMacOS; } ++ public static boolean isMacOS () { return _isMacOS; } + + /** + * Returns true if we're running in a JVM that identifies its operating system as Linux. + */ +- public static final boolean isLinux () { return _isLinux; } ++ public static boolean isLinux () { return _isLinux; } + + /** + * Checks whether a Java Virtual Machine can be located in the supplied path. +@@ -240,10 +232,9 @@ public class LaunchUtil + try { + String osname = System.getProperty("os.name"); + osname = (osname == null) ? "" : osname; +- _isWindows = (osname.indexOf("Windows") != -1); +- _isMacOS = (osname.indexOf("Mac OS") != -1 || +- osname.indexOf("MacOS") != -1); +- _isLinux = (osname.indexOf("Linux") != -1); ++ _isWindows = (osname.contains("Windows")); ++ _isMacOS = (osname.contains("Mac OS") || osname.contains("MacOS")); ++ _isLinux = (osname.contains("Linux")); + } catch (Exception e) { + // can't grab system properties; we'll just pretend we're not on any of these OSes + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java +index 28dbdcff5..f2ee9a265 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/MessageUtil.java +@@ -5,7 +5,7 @@ + + package com.threerings.getdown.util; + +-public class MessageUtil { ++public final class MessageUtil { + + /** + * Returns whether or not the provided string is tainted. See {@link #taint}. Null strings +@@ -101,11 +101,11 @@ public class MessageUtil { + } + + /** +- * Used to escape single quotes so that they are not interpreted by MessageFormat. ++ * Used to escape single quotes so that they are not interpreted by {@code MessageFormat}. + * As we assume all single quotes are to be escaped, we cannot use the characters + * { and } in our translation strings, but this is a small price to + * pay to have to differentiate between messages that will and won't eventually be parsed by a +- * MessageFormat instance. ++ * {@code MessageFormat} instance. + */ + public static String escape (String message) + { +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java +index ad4c560a4..ac3e0a3de 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java +@@ -14,5 +14,5 @@ public interface ProgressObserver + * Informs the observer that we have completed the specified + * percentage of the process. + */ +- public void progress (int percent); ++ void progress (int percent); + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java +index 373cfff00..5cb1a405b 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StreamUtil.java +@@ -11,11 +11,10 @@ import java.io.InputStream; + import java.io.OutputStream; + import java.io.Reader; + import java.io.Writer; +-import java.nio.charset.Charset; + + import static com.threerings.getdown.Log.log; + +-public class StreamUtil { ++public final class StreamUtil { + /** + * Convenient close for a stream. Use in a finally clause and love life. + */ +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java +index 03d3c9ccd..86277c881 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/StringUtil.java +@@ -7,14 +7,14 @@ package com.threerings.getdown.util; + + import java.util.StringTokenizer; + +-public class StringUtil { ++public final class StringUtil { + + /** + * @return true if the specified string could be a valid URL (contains no illegal characters) + */ + public static boolean couldBeValidUrl (String url) + { +- return url.matches("[A-Za-z0-9\\-\\._~:/\\?#\\[\\]@!$&'\\(\\)\\*\\+,;=%]+"); ++ return url.matches("[A-Za-z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); + } + + /** +@@ -84,7 +84,7 @@ public class StringUtil { + source = source.replace(",,", "%COMMA%"); + + // count up the number of tokens +- while ((tpos = source.indexOf(",", tpos+1)) != -1) { ++ while ((tpos = source.indexOf(',', tpos+1)) != -1) { + tcount++; + } + +@@ -92,7 +92,7 @@ public class StringUtil { + tpos = -1; tcount = 0; + + // do the split +- while ((tpos = source.indexOf(",", tpos+1)) != -1) { ++ while ((tpos = source.indexOf(',', tpos+1)) != -1) { + tokens[tcount] = source.substring(tstart, tpos); + tokens[tcount] = tokens[tcount].trim().replace("%COMMA%", ","); + if (intern) { +@@ -119,7 +119,7 @@ public class StringUtil { + + /** + * Generates a string from the supplied bytes that is the HEX encoded representation of those +- * bytes. Returns the empty string for a null or empty byte array. ++ * bytes. Returns the empty string for a {@code null} or empty byte array. + * + * @param bytes the bytes for which we want a string representation. + * @param count the number of bytes to stop at (which will be coerced into being {@code <=} the +@@ -185,7 +185,7 @@ public class StringUtil { + } + + /** +- * Helper function for the various join methods. ++ * Helper function for the various {@code join} methods. + */ + protected static String join (Object[] values, String separator, boolean escape) + { +@@ -201,6 +201,6 @@ public class StringUtil { + return buf.toString(); + } + +- /** Used by {@link #hexlate} and {@link #unhexlate}. */ ++ /** Used by {@link #hexlate}. */ + protected static final String XLATE = "0123456789abcdef"; + } +diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java +index 49e4e6e0e..b2f289415 100644 +--- a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java ++++ b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/util/VersionUtil.java +@@ -22,7 +22,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; + /** + * Version related utilities. + */ +-public class VersionUtil ++public final class VersionUtil + { + /** + * Reads a version number from a file. +diff --git a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java b/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java +deleted file mode 100644 +index 8af09da6d..000000000 +--- a/getdown/src/getdown/core/src/main/java/jalview/bin/MemorySetting.java ++++ /dev/null +@@ -1,51 +0,0 @@ +-package jalview.bin; +- +-import java.lang.management.ManagementFactory; +-import java.lang.management.OperatingSystemMXBean; +- +-public class MemorySetting +-{ +- public static final long leaveFreeMinMemory = 536870912; // 0.5 GB +- +- public static final long applicationMinMemory = 536870912; // 0.5 GB +- +- protected static long getPhysicalMemory() +- { +- final OperatingSystemMXBean o = ManagementFactory +- .getOperatingSystemMXBean(); +- +- try +- { +- if (o instanceof com.sun.management.OperatingSystemMXBean) +- { +- final com.sun.management.OperatingSystemMXBean osb = (com.sun.management.OperatingSystemMXBean) o; +- return osb.getTotalPhysicalMemorySize(); +- } +- } catch (NoClassDefFoundError e) +- { +- // com.sun.management.OperatingSystemMXBean doesn't exist in this JVM +- System.err.println("No com.sun.management.OperatingSystemMXBean"); +- } +- +- // We didn't get a com.sun.management.OperatingSystemMXBean. +- return -1; +- } +- +- public static long memPercent(int percent) +- { +- long memPercent = -1; +- +- long physicalMem = getPhysicalMemory(); +- if (physicalMem > applicationMinMemory) +- { +- // try and set at least applicationMinMemory and thereafter ensure +- // leaveFreeMinMemory is left for the OS +- memPercent = Math.max(applicationMinMemory, +- physicalMem - Math.max(physicalMem * (100 - percent) / 100, +- leaveFreeMinMemory)); +- } +- +- return memPercent; +- } +- +-} +diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java +index d5a393745..c7fcf7271 100644 +--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java ++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/GarbageCollectorTest.java +@@ -7,23 +7,36 @@ package com.threerings.getdown.cache; + + import java.io.File; + import java.io.IOException; ++import java.util.Arrays; ++import java.util.Collection; + import java.util.concurrent.TimeUnit; + + import org.junit.*; + import org.junit.rules.TemporaryFolder; +- ++import org.junit.runner.RunWith; ++import org.junit.runners.Parameterized; + import static org.junit.Assert.*; + import static org.junit.Assume.assumeTrue; + + /** + * Validates that cache garbage is collected and deleted correctly. + */ ++@RunWith(Parameterized.class) + public class GarbageCollectorTest + { ++ @Parameterized.Parameters(name = "{0}") ++ public static Collection data() { ++ return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }}); ++ } ++ ++ @Parameterized.Parameter ++ public String extension; ++ + @Before public void setupFiles () throws IOException + { +- _cachedFile = _folder.newFile("abc123.jar"); +- _lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX); ++ _cachedFile = _folder.newFile("abc123" + extension); ++ _lastAccessedFile = _folder.newFile( ++ "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX); + } + + @Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached () +diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java +index 860c72a37..6acffda32 100644 +--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java ++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/cache/ResourceCacheTest.java +@@ -7,26 +7,38 @@ package com.threerings.getdown.cache; + + import java.io.File; + import java.io.IOException; ++import java.util.Arrays; ++import java.util.Collection; + import java.util.concurrent.TimeUnit; + + import org.junit.*; + import org.junit.rules.TemporaryFolder; +- ++import org.junit.runner.RunWith; ++import org.junit.runners.Parameterized; + import static org.junit.Assert.*; + + /** + * Asserts the correct functionality of the {@link ResourceCache}. + */ ++@RunWith(Parameterized.class) + public class ResourceCacheTest + { ++ @Parameterized.Parameters(name = "{0}") ++ public static Collection data() { ++ return Arrays.asList(new Object[][] {{ ".jar" }, { ".zip" }}); ++ } ++ ++ @Parameterized.Parameter ++ public String extension; ++ + @Before public void setupCache () throws IOException { +- _fileToCache = _folder.newFile("filetocache.jar"); ++ _fileToCache = _folder.newFile("filetocache" + extension); + _cache = new ResourceCache(_folder.newFolder(".cache")); + } + + @Test public void shouldCacheFile () throws IOException + { +- assertEquals("abc123.jar", cacheFile().getName()); ++ assertEquals("abc123" + extension, cacheFile().getName()); + } + + private File cacheFile() throws IOException +@@ -36,7 +48,7 @@ public class ResourceCacheTest + + @Test public void shouldTrackFileUsage () throws IOException + { +- String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX; ++ String name = "abc123" + extension + ResourceCache.LAST_ACCESSED_FILE_SUFFIX; + File lastAccessedFile = new File(cacheFile().getParentFile(), name); + assertTrue(lastAccessedFile.exists()); + } +diff --git a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java +index 61786518c..04a73d38b 100644 +--- a/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java ++++ b/getdown/src/getdown/core/src/test/java/com/threerings/getdown/data/EnvConfigTest.java +@@ -19,13 +19,13 @@ public class EnvConfigTest { + static String TESTID = "testid"; + static String TESTBASE = "https://test.com/test"; + +- private void debugNotes(List notes) { ++ private void debugNotes (List notes) { + for (EnvConfig.Note note : notes) { + System.out.println(note.message); + } + } + +- private void checkNoNotes (List notes) { ++ static void checkNoNotes (List notes) { + StringBuilder msg = new StringBuilder(); + for (EnvConfig.Note note : notes) { + if (note.level != EnvConfig.Note.Level.INFO) { +diff --git a/getdown/src/getdown/launcher/.project-MOVED b/getdown/src/getdown/launcher/.project-MOVED +deleted file mode 100644 +index d77a6e8db..000000000 +--- a/getdown/src/getdown/launcher/.project-MOVED ++++ /dev/null +@@ -1,23 +0,0 @@ +- +- +- getdown-launcher +- +- +- +- +- +- org.eclipse.jdt.core.javabuilder +- +- +- +- +- org.eclipse.m2e.core.maven2Builder +- +- +- +- +- +- org.eclipse.jdt.core.javanature +- org.eclipse.m2e.core.maven2Nature +- +- +diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs +deleted file mode 100644 +index abdea9ac0..000000000 +--- a/getdown/src/getdown/launcher/.settings/org.eclipse.core.resources.prefs ++++ /dev/null +@@ -1,4 +0,0 @@ +-eclipse.preferences.version=1 +-encoding//src/main/java=UTF-8 +-encoding//src/main/resources=UTF-8 +-encoding/=UTF-8 +diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs +deleted file mode 100644 +index 54e56721d..000000000 +--- a/getdown/src/getdown/launcher/.settings/org.eclipse.jdt.core.prefs ++++ /dev/null +@@ -1,6 +0,0 @@ +-eclipse.preferences.version=1 +-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +-org.eclipse.jdt.core.compiler.compliance=1.7 +-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +-org.eclipse.jdt.core.compiler.release=disabled +-org.eclipse.jdt.core.compiler.source=1.7 +diff --git a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs b/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs +deleted file mode 100644 +index f897a7f1c..000000000 +--- a/getdown/src/getdown/launcher/.settings/org.eclipse.m2e.core.prefs ++++ /dev/null +@@ -1,4 +0,0 @@ +-activeProfiles= +-eclipse.preferences.version=1 +-resolveWorkspaceProjects=true +-version=1 +diff --git a/getdown/src/getdown/launcher/pom.xml b/getdown/src/getdown/launcher/pom.xml +index e77061a3b..f55234977 100644 +--- a/getdown/src/getdown/launcher/pom.xml ++++ b/getdown/src/getdown/launcher/pom.xml +@@ -4,7 +4,7 @@ + + com.threerings.getdown + getdown +- 1.8.3-SNAPSHOT ++ 1.8.7-SNAPSHOT + + + getdown-launcher +@@ -24,11 +24,13 @@ + com.threerings.getdown + getdown-core + ${project.version} ++ true + + + com.samskivert + samskivert + 1.2 ++ true + + + jregistrykey +@@ -36,22 +38,47 @@ + 1.0 + true + ++ ++ junit ++ junit ++ 4.12 ++ test ++ + + + + +- ++ + + org.apache.maven.plugins + maven-jar-plugin +@@ -130,40 +157,6 @@ + + + +- +- +- org.apache.maven.plugins +- maven-shade-plugin +- 3.2.1 +- +- +- +- +- +- package +- +- shade +- +- +- +- +- +- +- +- +- + + + +@@ -202,6 +195,7 @@ + ${java.home}/jmods/java.base.jmod + ${java.home}/jmods/java.desktop.jmod + ${java.home}/jmods/java.logging.jmod ++ ${java.home}/jmods/java.scripting.jmod + ${java.home}/jmods/jdk.jsobject.jmod + + +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java +index dc1e54e46..4bd9f90b0 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java +@@ -72,7 +72,7 @@ public final class AbortPanel extends JFrame + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); +- if (cmd.equals("ok")) { ++ if ("ok".equals(cmd)) { + System.exit(0); + } else { + setVisible(false); +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +index 5750fce66..1cc6922c8 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +@@ -9,51 +9,73 @@ import java.awt.BorderLayout; + import java.awt.Container; + import java.awt.Dimension; + import java.awt.EventQueue; +-import java.awt.Graphics; + import java.awt.GraphicsEnvironment; + import java.awt.Image; + import java.awt.event.ActionEvent; + import java.awt.image.BufferedImage; + import java.io.BufferedReader; + import java.io.File; +-import java.io.FileInputStream; + import java.io.FileNotFoundException; +-import java.io.FileReader; + import java.io.IOException; + import java.io.InputStream; + import java.io.InputStreamReader; + import java.io.PrintStream; + import java.net.HttpURLConnection; +-import java.net.MalformedURLException; + import java.net.URL; +-import java.util.*; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Locale; ++import java.util.ResourceBundle; ++import java.util.Set; ++import java.util.concurrent.TimeUnit; + + import javax.imageio.ImageIO; + import javax.swing.AbstractAction; + import javax.swing.JButton; + import javax.swing.JFrame; + import javax.swing.JLayeredPane; +-import javax.swing.JPanel; + + import com.samskivert.swing.util.SwingUtil; +-import com.threerings.getdown.data.*; ++import com.threerings.getdown.data.Application; + import com.threerings.getdown.data.Application.UpdateInterface.Step; ++import com.threerings.getdown.data.Build; ++import com.threerings.getdown.data.EnvConfig; ++import com.threerings.getdown.data.Resource; ++import com.threerings.getdown.data.SysProps; + import com.threerings.getdown.net.Downloader; +-import com.threerings.getdown.net.HTTPDownloader; + import com.threerings.getdown.tools.Patcher; +-import com.threerings.getdown.util.*; +- ++import com.threerings.getdown.util.Config; ++import com.threerings.getdown.util.FileUtil; ++import com.threerings.getdown.util.LaunchUtil; ++import com.threerings.getdown.util.MessageUtil; ++import com.threerings.getdown.util.ProgressAggregator; ++import com.threerings.getdown.util.ProgressObserver; ++import com.threerings.getdown.util.StringUtil; ++import com.threerings.getdown.util.VersionUtil; + import static com.threerings.getdown.Log.log; + + /** + * Manages the main control for the Getdown application updater and deployment system. + */ +-public abstract class Getdown extends Thread ++public abstract class Getdown + implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader + { ++ /** ++ * Starts a thread to run Getdown and ultimately (hopefully) launch the target app. ++ */ ++ public static void run (final Getdown getdown) { ++ new Thread("Getdown") { ++ @Override public void run () { ++ getdown.run(); ++ } ++ }.start(); ++ } ++ + public Getdown (EnvConfig envc) + { +- super("Getdown"); + try { + // 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. +@@ -80,7 +102,7 @@ public abstract class Getdown extends Thread + // welcome to hell, where java can't cope with a classpath that contains jars that live + // in a directory that contains a !, at least the same bug happens on all platforms + String dir = envc.appDir.toString(); +- if (dir.equals(".")) { ++ if (".".equals(dir)) { + dir = System.getProperty("user.dir"); + } + String errmsg = "The directory in which this application is installed:\n" + dir + +@@ -107,7 +129,7 @@ public abstract class Getdown extends Thread + { + if (SysProps.noInstall()) { + log.info("Skipping install due to 'no_install' sysprop."); +- } else if (_readyToInstall) { ++ } else if (isUpdateAvailable()) { + log.info("Installing " + _toInstallResources.size() + " downloaded resources:"); + for (Resource resource : _toInstallResources) { + resource.install(true); +@@ -120,8 +142,28 @@ public abstract class Getdown extends Thread + } + } + +- @Override +- public void run () ++ /** ++ * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher. ++ */ ++ public void configProxy (String host, String port, String username, String password) ++ { ++ log.info("User configured proxy", "host", host, "port", port); ++ ProxyUtil.configProxy(_app, host, port, username, password); ++ ++ // clear out our UI ++ disposeContainer(); ++ _container = null; ++ ++ // fire up a new thread ++ run(this); ++ } ++ ++ /** ++ * The main entry point of Getdown: does some sanity checks and preparation, then delegates the ++ * actual getting down to {@link #getdown}. This is not called directly, but rather via the ++ * static {@code run} method as Getdown does its main work on a separate thread. ++ */ ++ protected void run () + { + // if we have no messages, just bail because we're hosed; the error message will be + // displayed to the user already +@@ -135,95 +177,48 @@ public abstract class Getdown extends Thread + File instdir = _app.getLocalPath(""); + if (!instdir.canWrite()) { + String path = instdir.getPath(); +- if (path.equals(".")) { ++ if (".".equals(path)) { + path = System.getProperty("user.dir"); + } + fail(MessageUtil.tcompose("m.readonly_error", path)); + return; + } + +- try { +- _dead = false; +- // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and +- // run the app anyway because we're prepared to cope with not being able to update +- if (detectProxy() || _app.allowOffline()) { +- getdown(); +- } else if (_silent) { +- log.warning("Need a proxy, but we don't want to bother anyone. Exiting."); +- } else { +- // create a panel they can use to configure the proxy settings +- _container = createContainer(); +- // allow them to close the window to abort the proxy configuration +- _dead = true; +- configureContainer(); +- ProxyPanel panel = new ProxyPanel(this, _msgs); +- // set up any existing configured proxy +- String[] hostPort = ProxyUtil.loadProxy(_app); +- panel.setProxy(hostPort[0], hostPort[1]); +- _container.add(panel, BorderLayout.CENTER); +- showContainer(); +- } +- +- } catch (Exception e) { +- log.warning("run() failed.", e); +- String msg = e.getMessage(); +- if (msg == null) { +- msg = MessageUtil.compose("m.unknown_error", _ifc.installError); +- } else if (!msg.startsWith("m.")) { +- // try to do something sensible based on the type of error +- if (e instanceof FileNotFoundException) { +- msg = MessageUtil.compose( +- "m.missing_resource", MessageUtil.taint(msg), _ifc.installError); +- } else { +- msg = MessageUtil.compose( +- "m.init_error", MessageUtil.taint(msg), _ifc.installError); +- } +- } +- fail(msg); +- } +- } +- +- /** +- * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher. +- */ +- public void configProxy (String host, String port, String username, String password) +- { +- log.info("User configured proxy", "host", host, "port", port); +- +- if (!StringUtil.isBlank(host)) { +- ProxyUtil.configProxy(_app, host, port, username, password); +- } +- +- // clear out our UI +- disposeContainer(); +- _container = null; +- +- // fire up a new thread +- new Thread(this).start(); ++ _dead = false; ++ // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and ++ // run the app anyway because we're prepared to cope with not being able to update ++ if (detectProxy() || _app.allowOffline()) getdown(); ++ else requestProxyInfo(false); + } + + protected boolean detectProxy () { +- if (ProxyUtil.autoDetectProxy(_app)) { +- return true; +- } +- +- // otherwise see if we actually need a proxy; first we have to initialize our application +- // to get some sort of interface configuration and the appbase URL ++ // first we have to initialize our application to get the appbase URL, etc. + log.info("Checking whether we need to use a proxy..."); + try { + readConfig(true); + } catch (IOException ioe) { + // no worries + } ++ ++ boolean tryNoProxy = SysProps.tryNoProxyFirst(); ++ if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) { ++ return true; ++ } ++ ++ // see if we actually need a proxy + updateStatus("m.detecting_proxy"); +- if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) { +- return false; ++ URL configURL = _app.getConfigResource().getRemote(); ++ if (!ProxyUtil.canLoadWithoutProxy(configURL, tryNoProxy ? 2 : 5)) { ++ // if we didn't auto-detect proxy first thing, do auto-detect now ++ return tryNoProxy ? ProxyUtil.autoDetectProxy(_app) : false; + } + +- // we got through, so we appear not to require a proxy; make a blank proxy config so that +- // we don't go through this whole detection process again next time + log.info("No proxy appears to be needed."); +- ProxyUtil.saveProxy(_app, null, null); ++ if (!tryNoProxy) { ++ // we got through, so we appear not to require a proxy; make a blank proxy config so ++ // that we don't go through this whole detection process again next time ++ ProxyUtil.saveProxy(_app, null, null); ++ } + return true; + } + +@@ -233,6 +228,25 @@ public abstract class Getdown extends Thread + _ifc = new Application.UpdateInterface(config); + } + ++ protected void requestProxyInfo (boolean reinitAuth) { ++ if (_silent) { ++ log.warning("Need a proxy, but we don't want to bother anyone. Exiting."); ++ return; ++ } ++ ++ // create a panel they can use to configure the proxy settings ++ _container = createContainer(); ++ // allow them to close the window to abort the proxy configuration ++ _dead = true; ++ configureContainer(); ++ ProxyPanel panel = new ProxyPanel(this, _msgs, reinitAuth); ++ // set up any existing configured proxy ++ String[] hostPort = ProxyUtil.loadProxy(_app); ++ panel.setProxy(hostPort[0], hostPort[1]); ++ _container.add(panel, BorderLayout.CENTER); ++ showContainer(); ++ } ++ + /** + * Downloads and installs (without verifying) any resources that are marked with a + * {@code PRELOAD} attribute. +@@ -247,13 +261,15 @@ public abstract class Getdown extends Thread + } + } + +- try { +- download(predownloads); +- for (Resource rsrc : predownloads) { +- rsrc.install(false); // install but don't validate yet ++ if (!predownloads.isEmpty()) { ++ try { ++ download(predownloads); ++ for (Resource rsrc : predownloads) { ++ rsrc.install(false); // install but don't validate yet ++ } ++ } catch (IOException ioe) { ++ log.warning("Failed to predownload resources. Continuing...", ioe); + } +- } catch (IOException ioe) { +- log.warning("Failed to predownload resources. Continuing...", ioe); + } + } + +@@ -263,7 +279,7 @@ public abstract class Getdown extends Thread + protected void getdown () + { + try { +- // first parses our application deployment file ++ // first parse our application deployment file + try { + readConfig(true); + } catch (IOException ioe) { +@@ -278,7 +294,7 @@ public abstract class Getdown extends Thread + throw new MultipleGetdownRunning(); + } + +- // Update the config modtime so a sleeping getdown will notice the change. ++ // update the config modtime so a sleeping getdown will notice the change + File config = _app.getLocalPath(Application.CONFIG_FILE); + if (!config.setLastModified(System.currentTimeMillis())) { + log.warning("Unable to set modtime on config file, will be unable to check for " + +@@ -290,7 +306,7 @@ public abstract class Getdown extends Thread + // Store the config modtime before waiting the delay amount of time + long lastConfigModtime = config.lastModified(); + log.info("Waiting " + _delay + " minutes before beginning actual work."); +- Thread.sleep(_delay * 60 * 1000); ++ TimeUnit.MINUTES.sleep(_delay); + if (lastConfigModtime < config.lastModified()) { + log.warning("getdown.txt was modified while getdown was waiting."); + throw new MultipleGetdownRunning(); +@@ -339,11 +355,7 @@ public abstract class Getdown extends Thread + + if (toDownload.size() > 0) { + // we have resources to download, also note them as to-be-installed +- for (Resource r : toDownload) { +- if (!_toInstallResources.contains(r)) { +- _toInstallResources.add(r); +- } +- } ++ _toInstallResources.addAll(toDownload); + + try { + // if any of our resources have already been marked valid this is not a +@@ -427,24 +439,20 @@ public abstract class Getdown extends Thread + throw new IOException("m.unable_to_repair"); + + } catch (Exception e) { +- log.warning("getdown() failed.", e); +- String msg = e.getMessage(); +- if (msg == null) { +- msg = MessageUtil.compose("m.unknown_error", _ifc.installError); +- } else if (!msg.startsWith("m.")) { +- // try to do something sensible based on the type of error +- if (e instanceof FileNotFoundException) { +- msg = MessageUtil.compose( +- "m.missing_resource", MessageUtil.taint(msg), _ifc.installError); +- } else { +- msg = MessageUtil.compose( +- "m.init_error", MessageUtil.taint(msg), _ifc.installError); +- } ++ // if we failed due to proxy errors, ask for proxy info ++ switch (_app.conn.state) { ++ case NEED_PROXY: ++ requestProxyInfo(false); ++ break; ++ case NEED_PROXY_AUTH: ++ requestProxyInfo(true); ++ break; ++ default: ++ log.warning("getdown() failed.", e); ++ fail(e); ++ _app.releaseLock(); ++ break; + } +- // Since we're dead, clear off the 'time remaining' label along with displaying the +- // error message +- fail(msg); +- _app.releaseLock(); + } + } + +@@ -499,6 +507,18 @@ public abstract class Getdown extends Thread + throw new IOException("m.java_download_failed"); + } + ++ // on Windows, if the local JVM is in use, we will not be able to replace it with an ++ // updated JVM; we detect this by attempting to rename the java.dll to its same name, which ++ // will fail on Windows for in use files; hackery! ++ File javaLocalDir = _app.getJavaLocalDir(); ++ File javaDll = new File(javaLocalDir, "bin" + File.separator + "java.dll"); ++ if (javaDll.exists()) { ++ if (!javaDll.renameTo(javaDll)) { ++ log.info("Cannot update local Java VM as it is in use."); ++ return; ++ } ++ } ++ + reportTrackingEvent("jvm_start", -1); + + updateStatus("m.downloading_java"); +@@ -507,21 +527,24 @@ public abstract class Getdown extends Thread + download(list); + + reportTrackingEvent("jvm_unpack", -1); +- + updateStatus("m.unpacking_java"); +- vmjar.install(true); ++ try { ++ vmjar.install(true); ++ } catch (IOException ioe) { ++ throw new IOException("m.java_unpack_failed", ioe); ++ } + + // these only run on non-Windows platforms, so we use Unix file separators +- String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/"; +- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java")); +- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper")); +- FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper")); ++ FileUtil.makeExecutable(new File(javaLocalDir, "bin/java")); ++ FileUtil.makeExecutable(new File(javaLocalDir, "lib/jspawnhelper")); ++ FileUtil.makeExecutable(new File(javaLocalDir, "lib/amd64/jspawnhelper")); + + // lastly regenerate the .jsa dump file that helps Java to start up faster +- String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath("")); ++ String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false); ++ String[] command = { vmpath, "-Xshare:dump" }; + try { + log.info("Regenerating classes.jsa for " + vmpath + "..."); +- Runtime.getRuntime().exec(vmpath + " -Xshare:dump"); ++ Runtime.getRuntime().exec(command); + } catch (Exception e) { + log.warning("Failed to regenerate .jsa dump file", "error", e); + } +@@ -578,6 +601,8 @@ public abstract class Getdown extends Thread + int ii = 0; for (Resource prsrc : list) { + ProgressObserver pobs = pragg.startElement(ii++); + try { ++ // if this patch file failed to download, skip it ++ if (!prsrc.getLocalNew().exists()) continue; + // install the patch file (renaming them from _new) + prsrc.install(false); + // now apply the patch +@@ -613,7 +638,7 @@ public abstract class Getdown extends Thread + // create our user interface + createInterfaceAsync(false); + +- Downloader dl = new HTTPDownloader(_app.proxy) { ++ Downloader dl = new Downloader(_app.conn) { + @Override protected void resolvingDownloads () { + updateStatus("m.resolving"); + } +@@ -640,6 +665,10 @@ public abstract class Getdown extends Thread + log.warning("Download failed", "rsrc", rsrc, e); + } + ++ @Override protected void resourceMissing (Resource rsrc) { ++ log.warning("Resource missing (got 404)", "rsrc", rsrc); ++ } ++ + /** The last percentage at which we checked for another getdown running, or -1 for not + * having checked at all. */ + protected int _lastCheck = -1; +@@ -725,7 +754,7 @@ public abstract class Getdown extends Thread + long minshow = _ifc.minShowSeconds * 1000L; + if (_container != null && uptime < minshow) { + try { +- Thread.sleep(minshow - uptime); ++ TimeUnit.MILLISECONDS.sleep(minshow - uptime); + } catch (Exception e) { + } + } +@@ -750,10 +779,9 @@ public abstract class Getdown extends Thread + if (_silent || (_container != null && !reinit)) { + return; + } +-/* ++ + EventQueue.invokeLater(new Runnable() { + public void run () { +-*/ + if (_container == null || reinit) { + if (_container == null) { + _container = createContainer(); +@@ -762,42 +790,6 @@ public abstract class Getdown extends Thread + } + configureContainer(); + _layers = new JLayeredPane(); +- +- +- +- // added in the instant display of a splashscreen +- try { +- readConfig(false); +- Graphics g = _container.getGraphics(); +- String imageFile = _ifc.backgroundImage; +- BufferedImage bgImage = loadImage(_ifc.backgroundImage); +- int bwidth = bgImage.getWidth(); +- int bheight = bgImage.getHeight(); +- +- instantSplashPane = new JPanel() { +- @Override +- protected void paintComponent(Graphics g) +- { +- super.paintComponent(g); +- // attempt to draw a background image... +- if (bgImage != null) { +- g.drawImage(bgImage, 0, 0, this); +- } +- } +- }; +- +- instantSplashPane.setSize(bwidth,bheight); +- instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight)); +- +- _layers.add(instantSplashPane, Integer.valueOf(0)); +- +- _container.setPreferredSize(new Dimension(bwidth,bheight)); +- } catch (Exception e) { +- log.warning("Failed to set instant background image", "bg", _ifc.backgroundImage); +- } +- +- +- + _container.add(_layers, BorderLayout.CENTER); + _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) { + @Override public void actionPerformed (ActionEvent event) { +@@ -807,14 +799,12 @@ public abstract class Getdown extends Thread + _patchNotes.setFont(StatusPanel.FONT); + _layers.add(_patchNotes); + _status = new StatusPanel(_msgs); +- _layers.add(_status, Integer.valueOf(10)); ++ _layers.add(_status); + initInterface(); + } + showContainer(); +-/* + } + }); +-*/ + } + + /** +@@ -845,12 +835,13 @@ public abstract class Getdown extends Thread + + protected RotatingBackgrounds getBackground () + { +- if (_ifc.rotatingBackgrounds != null && _ifc.rotatingBackgrounds.size() > 0) { ++ if (_ifc.rotatingBackgrounds != null) { + if (_ifc.backgroundImage != null) { + log.warning("ui.background_image and ui.rotating_background were both specified. " + +- "The background image is being used."); ++ "The rotating images are being used."); + } +- return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this); ++ return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, ++ Getdown.this); + } else if (_ifc.backgroundImage != null) { + return new RotatingBackgrounds(loadImage(_ifc.backgroundImage)); + } else { +@@ -879,8 +870,23 @@ public abstract class Getdown extends Thread + } + } + ++ private void fail (Exception e) { ++ String msg = e.getMessage(); ++ if (msg == null) { ++ msg = MessageUtil.compose("m.unknown_error", _ifc.installError); ++ } else if (!msg.startsWith("m.")) { ++ // try to do something sensible based on the type of error ++ msg = MessageUtil.taint(msg); ++ msg = e instanceof FileNotFoundException ? ++ MessageUtil.compose("m.missing_resource", msg, _ifc.installError) : ++ MessageUtil.compose("m.init_error", msg, _ifc.installError); ++ } ++ // since we're dead, clear off the 'time remaining' label along with displaying the error ++ fail(msg); ++ } ++ + /** +- * Update the status to indicate getdown has failed for the reason in message. ++ * Update the status to indicate getdown has failed for the reason in {@code message}. + */ + protected void fail (String message) + { +@@ -961,14 +967,14 @@ public abstract class Getdown extends Thread + do { + URL url = _app.getTrackingProgressURL(++_reportedProgress); + if (url != null) { +- new ProgressReporter(url).start(); ++ reportProgress(url); + } + } while (_reportedProgress <= progress); + + } else { + URL url = _app.getTrackingURL(event); + if (url != null) { +- new ProgressReporter(url).start(); ++ reportProgress(url); + } + } + } +@@ -1031,44 +1037,40 @@ public abstract class Getdown extends Thread + } + + /** Used to fetch a progress report URL. */ +- protected class ProgressReporter extends Thread +- { +- public ProgressReporter (URL url) { +- setDaemon(true); +- _url = url; +- } +- +- @Override +- public void run () { +- try { +- HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0); +- +- // if we have a tracking cookie configured, configure the request with it +- if (_app.getTrackingCookieName() != null && +- _app.getTrackingCookieProperty() != null) { +- String val = System.getProperty(_app.getTrackingCookieProperty()); +- if (val != null) { +- ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val); ++ protected void reportProgress (final URL url) { ++ Thread reporter = new Thread("Progress reporter") { ++ public void run () { ++ try { ++ HttpURLConnection ucon = _app.conn.openHttp(url, 0, 0); ++ ++ // if we have a tracking cookie configured, configure the request with it ++ if (_app.getTrackingCookieName() != null && ++ _app.getTrackingCookieProperty() != null) { ++ String val = System.getProperty(_app.getTrackingCookieProperty()); ++ if (val != null) { ++ ucon.setRequestProperty( ++ "Cookie", _app.getTrackingCookieName() + "=" + val); ++ } + } +- } + +- // now request our tracking URL and ensure that we get a non-error response +- ucon.connect(); +- try { +- if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { +- log.warning("Failed to report tracking event", +- "url", _url, "rcode", ucon.getResponseCode()); ++ // now request our tracking URL and ensure that we get a non-error response ++ ucon.connect(); ++ try { ++ if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { ++ log.warning("Failed to report tracking event", ++ "url", url, "rcode", ucon.getResponseCode()); ++ } ++ } finally { ++ ucon.disconnect(); + } +- } finally { +- ucon.disconnect(); +- } + +- } catch (IOException ioe) { +- log.warning("Failed to report tracking event", "url", _url, "error", ioe); ++ } catch (IOException ioe) { ++ log.warning("Failed to report tracking event", "url", url, "error", ioe); ++ } + } +- } +- +- protected URL _url; ++ }; ++ reporter.setDaemon(true); ++ reporter.start(); + } + + /** Used to pass progress on to our user interface. */ +@@ -1084,7 +1086,6 @@ public abstract class Getdown extends Thread + protected ResourceBundle _msgs; + protected Container _container; + protected JLayeredPane _layers; +- protected JPanel instantSplashPane; + protected StatusPanel _status; + protected JButton _patchNotes; + protected AbortPanel _abort; +@@ -1112,5 +1113,4 @@ public abstract class Getdown extends Thread + + protected static final int MAX_LOOPS = 5; + protected static final long FALLBACK_CHECK_TIME = 1000L; +- + } +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +index 2d1908904..3859f6a09 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +@@ -28,258 +28,225 @@ import javax.swing.KeyStroke; + import javax.swing.WindowConstants; + + import com.samskivert.swing.util.SwingUtil; +-import com.threerings.getdown.data.Application; + import com.threerings.getdown.data.EnvConfig; + import com.threerings.getdown.data.SysProps; + import com.threerings.getdown.util.LaunchUtil; + import com.threerings.getdown.util.StringUtil; + import static com.threerings.getdown.Log.log; +-import jalview.bin.StartupNotificationListener; + + /** + * The main application entry point for Getdown. + */ + public class GetdownApp + { +- public static String startupFilesParameterString = ""; +- /** +- * The main entry point of the Getdown launcher application. +- */ +- public static void main (String[] argv) { +- try { +- start(argv); +- } catch (Exception e) { +- log.warning("main() failed.", e); +- } +- } +- +- /** +- * Runs Getdown as an application, using the arguments supplie as {@code argv}. +- * @return the {@code Getdown} instance that is running. {@link Getdown#start} will have been +- * called on it. +- * @throws Exception if anything goes wrong starting Getdown. +- */ +- public static Getdown start (String[] argv) throws Exception { +- List notes = new ArrayList<>(); +- EnvConfig envc = EnvConfig.create(argv, notes); +- if (envc == null) { +- if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message); +- else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]"); +- System.exit(-1); +- } +- +- // pipe our output into a file in the application directory +- if (!SysProps.noLogRedir()) { +- File logFile = new File(envc.appDir, "launcher.log"); +- try { +- PrintStream logOut = new PrintStream( +- new BufferedOutputStream(new FileOutputStream(logFile)), true); +- System.setOut(logOut); +- System.setErr(logOut); +- } catch (IOException ioe) { +- log.warning("Unable to redirect output to '" + logFile + "': " + ioe); +- } +- } +- +- // report any notes from reading our env config, and abort if necessary +- boolean abort = false; +- for (EnvConfig.Note note : notes) { +- switch (note.level) { +- case INFO: log.info(note.message); break; +- case WARN: log.warning(note.message); break; +- case ERROR: log.error(note.message); abort = true; break; +- } ++ /** ++ * The main entry point of the Getdown launcher application. ++ */ ++ public static void main (String[] argv) { ++ try { ++ start(argv); ++ } catch (Exception e) { ++ log.warning("main() failed.", e); ++ } + } +- if (abort) System.exit(-1); + +- try +- { +- jalview.bin.StartupNotificationListener.setListener(); +- } catch (Exception e) +- { +- e.printStackTrace(); +- } catch (NoClassDefFoundError e) +- { +- log.warning("Starting without install4j classes"); +- } catch (Throwable t) +- { +- t.printStackTrace(); +- } +- +- // record a few things for posterity +- log.info("------------------ VM Info ------------------"); +- log.info("-- OS Name: " + System.getProperty("os.name")); +- log.info("-- OS Arch: " + System.getProperty("os.arch")); +- log.info("-- OS Vers: " + System.getProperty("os.version")); +- log.info("-- Java Vers: " + System.getProperty("java.version")); +- log.info("-- Java Home: " + System.getProperty("java.home")); +- log.info("-- User Name: " + System.getProperty("user.name")); +- log.info("-- User Home: " + System.getProperty("user.home")); +- log.info("-- Cur dir: " + System.getProperty("user.dir")); +- log.info("-- startupFilesParameterString: " + startupFilesParameterString); +- log.info("---------------------------------------------"); ++ /** ++ * Runs Getdown as an application, using the arguments supplie as {@code argv}. ++ * @return the {@code Getdown} instance that is running. {@link Getdown#run} will have been ++ * called on it. ++ * @throws Exception if anything goes wrong starting Getdown. ++ */ ++ public static Getdown start (String[] argv) throws Exception { ++ List notes = new ArrayList<>(); ++ EnvConfig envc = EnvConfig.create(argv, notes); ++ if (envc == null) { ++ if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message); ++ else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]"); ++ System.exit(-1); ++ } + +- Getdown app = new Getdown(envc) { +- @Override +- protected Container createContainer () { +- // create our user interface, and display it +- if (_frame == null) { +- _frame = new JFrame(""); +- _frame.addWindowListener(new WindowAdapter() { +- @Override +- public void windowClosing (WindowEvent evt) { +- handleWindowClose(); +- } +- }); +- // handle close on ESC +- String cancelId = "Cancel"; // $NON-NLS-1$ +- _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( +- KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId); +- _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() { +- public void actionPerformed (ActionEvent e) { +- handleWindowClose(); ++ // pipe our output into a file in the application directory ++ if (!SysProps.noLogRedir() && !SysProps.debug()) { ++ File logFile = new File(envc.appDir, "launcher.log"); ++ try { ++ PrintStream logOut = new PrintStream( ++ new BufferedOutputStream(new FileOutputStream(logFile)), true); ++ System.setOut(logOut); ++ System.setErr(logOut); ++ } catch (IOException ioe) { ++ log.warning("Unable to redirect output to '" + logFile + "': " + ioe); + } +- }); +- // this cannot be called in configureContainer as it is only allowed before the +- // frame has been displayed for the first time +- _frame.setUndecorated(_ifc.hideDecorations); +- _frame.setResizable(false); +- } else { +- _frame.getContentPane().removeAll(); + } +- _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); +- return _frame.getContentPane(); +- } +- +- @Override +- protected void configureContainer () { +- if (_frame == null) return; +- +- _frame.setTitle(_ifc.name); + +- try { +- _frame.setBackground(new Color(_ifc.background, true)); +- } catch (Exception e) { +- log.warning("Failed to set background", "bg", _ifc.background, e); ++ // report any notes from reading our env config, and abort if necessary ++ boolean abort = false; ++ for (EnvConfig.Note note : notes) { ++ switch (note.level) { ++ case INFO: log.info(note.message); break; ++ case WARN: log.warning(note.message); break; ++ case ERROR: log.error(note.message); abort = true; break; ++ } + } ++ if (abort) System.exit(-1); ++ ++ // record a few things for posterity ++ log.info("------------------ VM Info ------------------"); ++ log.info("-- OS Name: " + System.getProperty("os.name")); ++ log.info("-- OS Arch: " + System.getProperty("os.arch")); ++ log.info("-- OS Vers: " + System.getProperty("os.version")); ++ log.info("-- Java Vers: " + System.getProperty("java.version")); ++ log.info("-- Java Home: " + System.getProperty("java.home")); ++ log.info("-- User Name: " + System.getProperty("user.name")); ++ log.info("-- User Home: " + System.getProperty("user.home")); ++ log.info("-- Cur dir: " + System.getProperty("user.dir")); ++ log.info("---------------------------------------------"); ++ ++ Getdown getdown = new Getdown(envc) { ++ @Override ++ protected Container createContainer () { ++ // create our user interface, and display it ++ if (_frame == null) { ++ _frame = new JFrame(""); ++ _frame.addWindowListener(new WindowAdapter() { ++ @Override ++ public void windowClosing (WindowEvent evt) { ++ handleWindowClose(); ++ } ++ }); ++ // handle close on ESC ++ String cancelId = "Cancel"; // $NON-NLS-1$ ++ _frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( ++ KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), cancelId); ++ _frame.getRootPane().getActionMap().put(cancelId, new AbstractAction() { ++ public void actionPerformed (ActionEvent e) { ++ handleWindowClose(); ++ } ++ }); ++ // this cannot be called in configureContainer as it is only allowed before the ++ // frame has been displayed for the first time ++ _frame.setUndecorated(_ifc.hideDecorations); ++ _frame.setResizable(false); ++ } else { ++ _frame.getContentPane().removeAll(); ++ } ++ _frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); ++ return _frame.getContentPane(); ++ } + +- if (_ifc.iconImages != null && _ifc.iconImages.size() > 0) { +- ArrayList icons = new ArrayList<>(); +- for (String path : _ifc.iconImages) { +- Image img = loadImage(path); +- if (img == null) { +- log.warning("Error loading icon image", "path", path); +- } else { +- icons.add(img); ++ @Override ++ protected void configureContainer () { ++ if (_frame == null) return; ++ ++ _frame.setTitle(_ifc.name); ++ ++ try { ++ _frame.setBackground(new Color(_ifc.background, true)); ++ } catch (Exception e) { ++ log.warning("Failed to set background", "bg", _ifc.background, e); ++ } ++ ++ if (_ifc.iconImages != null) { ++ List icons = new ArrayList<>(); ++ for (String path : _ifc.iconImages) { ++ Image img = loadImage(path); ++ if (img == null) { ++ log.warning("Error loading icon image", "path", path); ++ } else { ++ icons.add(img); ++ } ++ } ++ if (icons.isEmpty()) { ++ log.warning("Failed to load any icons", "iconImages", _ifc.iconImages); ++ } else { ++ _frame.setIconImages(icons); ++ } ++ } + } +- } +- if (icons.isEmpty()) { +- log.warning("Failed to load any icons", "iconImages", _ifc.iconImages); +- } else { +- _frame.setIconImages(icons); +- } +- } +- } + +- @Override +- protected void showContainer () { +- if (_frame != null) { +- _frame.pack(); +- SwingUtil.centerWindow(_frame); +- _frame.setVisible(true); +- } +- } ++ @Override ++ protected void showContainer () { ++ if (_frame != null) { ++ _frame.pack(); ++ SwingUtil.centerWindow(_frame); ++ _frame.setVisible(true); ++ } ++ } + +- @Override +- protected void disposeContainer () { +- if (_frame != null) { +- _frame.dispose(); +- _frame = null; +- } +- } ++ @Override ++ protected void disposeContainer () { ++ if (_frame != null) { ++ _frame.dispose(); ++ _frame = null; ++ } ++ } + +- @Override +- protected void showDocument (String url) { +- if (!StringUtil.couldBeValidUrl(url)) { +- // command injection would be possible if we allowed e.g. spaces and double quotes +- log.warning("Invalid document URL.", "url", url); +- return; +- } +- String[] cmdarray; +- if (LaunchUtil.isWindows()) { +- String osName = System.getProperty("os.name", ""); +- if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { +- cmdarray = new String[] { +- "command.com", "/c", "start", "\"" + url + "\"" }; +- } else { +- cmdarray = new String[] { +- "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" }; +- } +- } else if (LaunchUtil.isMacOS()) { +- cmdarray = new String[] { "open", url }; +- } else { // Linux, Solaris, etc. +- cmdarray = new String[] { "firefox", url }; +- } +- try { +- Runtime.getRuntime().exec(cmdarray); +- } catch (Exception e) { +- log.warning("Failed to open browser.", "cmdarray", cmdarray, e); +- } +- } ++ @Override ++ protected void showDocument (String url) { ++ if (!StringUtil.couldBeValidUrl(url)) { ++ // command injection would be possible if we allowed e.g. spaces and double quotes ++ log.warning("Invalid document URL.", "url", url); ++ return; ++ } ++ String[] cmdarray; ++ if (LaunchUtil.isWindows()) { ++ String osName = System.getProperty("os.name", ""); ++ if (osName.contains("9") || osName.contains("Me")) { ++ cmdarray = new String[] { ++ "command.com", "/c", "start", "\"" + url + "\"" }; ++ } else { ++ cmdarray = new String[] { ++ "cmd.exe", "/c", "start", "\"\"", "\"" + url + "\"" }; ++ } ++ } else if (LaunchUtil.isMacOS()) { ++ cmdarray = new String[] { "open", url }; ++ } else { // Linux, Solaris, etc. ++ cmdarray = new String[] { "firefox", url }; ++ } ++ try { ++ Runtime.getRuntime().exec(cmdarray); ++ } catch (Exception e) { ++ log.warning("Failed to open browser.", "cmdarray", cmdarray, e); ++ } ++ } + +- @Override +- protected void exit (int exitCode) { +- // if we're running the app in the same JVM, don't call System.exit, but do +- // make double sure that the download window is closed. +- if (invokeDirect()) { +- disposeContainer(); +- } else { +- System.exit(exitCode); +- } +- } ++ @Override ++ protected void exit (int exitCode) { ++ // if we're running the app in the same JVM, don't call System.exit, but do ++ // make double sure that the download window is closed. ++ if (invokeDirect()) { ++ disposeContainer(); ++ } else { ++ System.exit(exitCode); ++ } ++ } + +- @Override +- protected void fail (String message) { +- super.fail(message); +- // super.fail causes the UI to be created (if needed) on the next UI tick, so we +- // want to wait until that happens before we attempt to redecorate the window +- EventQueue.invokeLater(new Runnable() { +- @Override +- public void run() { +- // if the frame was set to be undecorated, make window decoration available +- // to allow the user to close the window +- if (_frame != null && _frame.isUndecorated()) { +- _frame.dispose(); +- Color bg = _frame.getBackground(); +- if (bg != null && bg.getAlpha() < 255) { +- // decorated windows do not allow alpha backgrounds +- _frame.setBackground( +- new Color(bg.getRed(), bg.getGreen(), bg.getBlue())); +- } +- _frame.setUndecorated(false); +- showContainer(); ++ @Override ++ protected void fail (String message) { ++ super.fail(message); ++ // super.fail causes the UI to be created (if needed) on the next UI tick, so we ++ // want to wait until that happens before we attempt to redecorate the window ++ EventQueue.invokeLater(new Runnable() { ++ @Override public void run () { ++ // if the frame was set to be undecorated, make window decoration available ++ // to allow the user to close the window ++ if (_frame != null && _frame.isUndecorated()) { ++ _frame.dispose(); ++ Color bg = _frame.getBackground(); ++ if (bg != null && bg.getAlpha() < 255) { ++ // decorated windows do not allow alpha backgrounds ++ _frame.setBackground( ++ new Color(bg.getRed(), bg.getGreen(), bg.getBlue())); ++ } ++ _frame.setUndecorated(false); ++ showContainer(); ++ } ++ } ++ }); + } +- } +- }); +- } + +- protected JFrame _frame; +- }; +- +- String startupFile = getStartupFilesParameterString(); +- if (!StringUtil.isBlank(startupFile)) { +- Application.setStartupFilesFromParameterString(startupFile); ++ protected JFrame _frame; ++ }; ++ Getdown.run(getdown); ++ return getdown; + } +- +- app.start(); +- return app; +- } +- +- public static void setStartupFilesParameterString(String parameters) { +- startupFilesParameterString = parameters; +- } +- +- public static String getStartupFilesParameterString() { +- return startupFilesParameterString; +- } + } +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +index 217827364..5f18896c4 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +@@ -35,14 +35,16 @@ import static com.threerings.getdown.Log.log; + */ + public final class ProxyPanel extends JPanel implements ActionListener + { +- public ProxyPanel (Getdown getdown, ResourceBundle msgs) ++ public ProxyPanel (Getdown getdown, ResourceBundle msgs, boolean updateAuth) + { + _getdown = getdown; + _msgs = msgs; ++ _updateAuth = updateAuth; + + setLayout(new VGroupLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); +- add(new SaneLabelField(get("m.configure_proxy"))); ++ String title = get(updateAuth ? "m.update_proxy_auth" : "m.configure_proxy"); ++ add(new SaneLabelField(title)); + add(new Spacer(5, 5)); + + JPanel row = new JPanel(new GridLayout()); +@@ -61,19 +63,20 @@ public final class ProxyPanel extends JPanel implements ActionListener + row.add(new SaneLabelField(get("m.proxy_auth_required")), BorderLayout.WEST); + _useAuth = new JCheckBox(); + row.add(_useAuth); ++ _useAuth.setSelected(updateAuth); + add(row); + + row = new JPanel(new GridLayout()); + row.add(new SaneLabelField(get("m.proxy_username")), BorderLayout.WEST); + _username = new SaneTextField(); +- _username.setEnabled(false); ++ _username.setEnabled(updateAuth); + row.add(_username); + add(row); + + row = new JPanel(new GridLayout()); + row.add(new SaneLabelField(get("m.proxy_password")), BorderLayout.WEST); + _password = new SanePasswordField(); +- _password.setEnabled(false); ++ _password.setEnabled(updateAuth); + row.add(_password); + add(row); + +@@ -112,7 +115,13 @@ public final class ProxyPanel extends JPanel implements ActionListener + public void addNotify () + { + super.addNotify(); +- _host.requestFocusInWindow(); ++ if (_updateAuth) { ++ // we are asking the user to update the credentials for an existing proxy ++ // configuration, so focus that instead of the proxy host config ++ _username.requestFocusInWindow(); ++ } else { ++ _host.requestFocusInWindow(); ++ } + } + + // documentation inherited +@@ -131,7 +140,7 @@ public final class ProxyPanel extends JPanel implements ActionListener + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); +- if (cmd.equals("ok")) { ++ if ("ok".equals(cmd)) { + String user = null, pass = null; + if (_useAuth.isSelected()) { + user = _username.getText(); +@@ -184,8 +193,9 @@ public final class ProxyPanel extends JPanel implements ActionListener + return dim; + } + +- protected Getdown _getdown; +- protected ResourceBundle _msgs; ++ protected final Getdown _getdown; ++ protected final ResourceBundle _msgs; ++ protected final boolean _updateAuth; + + protected JTextField _host; + protected JTextField _port; +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +index a36b5fa67..8962d35b9 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +@@ -8,31 +8,41 @@ package com.threerings.getdown.launcher; + import java.io.File; + import java.io.FileOutputStream; + import java.io.IOException; ++import java.io.InputStreamReader; + import java.io.PrintStream; ++import java.io.Reader; + import java.net.Authenticator; + import java.net.HttpURLConnection; ++import java.net.InetAddress; + import java.net.InetSocketAddress; + import java.net.PasswordAuthentication; + import java.net.Proxy; + import java.net.URL; + import java.net.URLConnection; ++import java.net.UnknownHostException; + import java.util.Iterator; + import java.util.ServiceLoader; + ++import javax.script.Bindings; ++import javax.script.Invocable; ++import javax.script.ScriptContext; ++import javax.script.ScriptEngine; ++import javax.script.ScriptEngineManager; ++ + import ca.beq.util.win32.registry.RegistryKey; + import ca.beq.util.win32.registry.RegistryValue; + import ca.beq.util.win32.registry.RootKey; + + import com.threerings.getdown.data.Application; ++import com.threerings.getdown.net.Connector; + import com.threerings.getdown.spi.ProxyAuth; + import com.threerings.getdown.util.Config; +-import com.threerings.getdown.util.ConnectionUtil; + import com.threerings.getdown.util.LaunchUtil; + import com.threerings.getdown.util.StringUtil; + + import static com.threerings.getdown.Log.log; + +-public class ProxyUtil { ++public final class ProxyUtil { + + public static boolean autoDetectProxy (Application app) + { +@@ -57,25 +67,40 @@ public class ProxyUtil { + RegistryKey r = new RegistryKey(RootKey.HKEY_CURRENT_USER, PROXY_REGISTRY); + for (Iterator iter = r.values(); iter.hasNext(); ) { + RegistryValue value = (RegistryValue)iter.next(); +- if (value.getName().equals("ProxyEnable")) { +- enabled = value.getStringValue().equals("1"); ++ if ("ProxyEnable".equals(value.getName())) { ++ enabled = "1".equals(value.getStringValue()); + } + if (value.getName().equals("ProxyServer")) { +- String strval = value.getStringValue(); +- int cidx = strval.indexOf(":"); +- if (cidx != -1) { +- rport = strval.substring(cidx+1); +- strval = strval.substring(0, cidx); ++ String[] hostPort = splitHostPort(value.getStringValue()); ++ rhost = hostPort[0]; ++ rport = hostPort[1]; ++ } ++ if (value.getName().equals("AutoConfigURL")) { ++ String acurl = value.getStringValue(); ++ Reader acjs = new InputStreamReader(new URL(acurl).openStream()); ++ // technically we should be returning all this info and trying each proxy ++ // in succession, but that's complexity we'll leave for another day ++ URL configURL = app.getConfigResource().getRemote(); ++ for (String proxy : findPACProxiesForURL(acjs, configURL)) { ++ if (proxy.startsWith("PROXY ")) { ++ String[] hostPort = splitHostPort(proxy.substring(6)); ++ rhost = hostPort[0]; ++ rport = hostPort[1]; ++ // TODO: is this valid? Does AutoConfigURL imply proxy enabled? ++ enabled = true; ++ break; ++ } + } +- rhost = strval; + } + } ++ + if (enabled) { + host = rhost; + port = rport; + } else { + log.info("Detected no proxy settings in the registry."); + } ++ + } catch (Throwable t) { + log.info("Failed to find proxy settings in Windows registry", "error", t); + } +@@ -97,32 +122,31 @@ public class ProxyUtil { + return true; + } + +- public static boolean canLoadWithoutProxy (URL rurl) ++ public static boolean canLoadWithoutProxy (URL rurl, int timeoutSeconds) + { +- log.info("Testing whether proxy is needed, via: " + rurl); ++ log.info("Attempting to fetch without proxy: " + rurl); + try { +- // try to make a HEAD request for this URL (use short connect and read timeouts) +- URLConnection conn = ConnectionUtil.open(Proxy.NO_PROXY, rurl, 5, 5); +- if (conn instanceof HttpURLConnection) { +- HttpURLConnection hcon = (HttpURLConnection)conn; +- try { +- hcon.setRequestMethod("HEAD"); +- hcon.connect(); +- // make sure we got a satisfactory response code +- int rcode = hcon.getResponseCode(); +- if (rcode == HttpURLConnection.HTTP_PROXY_AUTH || +- rcode == HttpURLConnection.HTTP_FORBIDDEN) { +- log.warning("Got an 'HTTP credentials needed' response", "code", rcode); +- } else { +- return true; +- } +- } finally { +- hcon.disconnect(); +- } +- } else { +- // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy ++ URLConnection conn = Connector.DEFAULT.open(rurl, timeoutSeconds, timeoutSeconds); ++ // if the appbase is not an HTTP/S URL (like file:), then we don't need a proxy ++ if (!(conn instanceof HttpURLConnection)) { + return true; + } ++ // otherwise, try to make a HEAD request for this URL ++ HttpURLConnection hcon = (HttpURLConnection)conn; ++ try { ++ hcon.setRequestMethod("HEAD"); ++ hcon.connect(); ++ // make sure we got a satisfactory response code ++ int rcode = hcon.getResponseCode(); ++ if (rcode == HttpURLConnection.HTTP_PROXY_AUTH || ++ rcode == HttpURLConnection.HTTP_FORBIDDEN) { ++ log.warning("Got an 'HTTP credentials needed' response", "code", rcode); ++ } else { ++ return true; ++ } ++ } finally { ++ hcon.disconnect(); ++ } + } catch (IOException ioe) { + log.info("Failed to HEAD " + rurl + ": " + ioe); + log.info("We probably need a proxy, but auto-detection failed."); +@@ -190,9 +214,14 @@ public class ProxyUtil { + } + boolean haveCreds = !StringUtil.isBlank(username) && !StringUtil.isBlank(password); + +- int pport = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port); +- log.info("Using proxy", "host", host, "port", pport, "haveCreds", haveCreds); +- app.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pport)); ++ if (StringUtil.isBlank(host)) { ++ log.info("Using no proxy"); ++ app.conn = new Connector(Proxy.NO_PROXY); ++ } else { ++ int pp = StringUtil.isBlank(port) ? 80 : Integer.valueOf(port); ++ log.info("Using proxy", "host", host, "port", pp, "haveCreds", haveCreds); ++ app.conn = new Connector(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, pp))); ++ } + + if (haveCreds) { + final String fuser = username; +@@ -205,6 +234,61 @@ public class ProxyUtil { + } + } + ++ public static class Resolver { ++ public String dnsResolve (String host) { ++ try { ++ return InetAddress.getByName(host).getHostAddress(); ++ } catch (UnknownHostException uhe) { ++ return null; ++ } ++ } ++ public String myIpAddress () { ++ try { ++ return InetAddress.getLocalHost().getHostAddress(); ++ } catch (UnknownHostException uhe) { ++ return null; ++ } ++ } ++ } ++ ++ public static String[] findPACProxiesForURL (Reader pac, URL url) { ++ ScriptEngineManager manager = new ScriptEngineManager(); ++ ScriptEngine engine = manager.getEngineByName("javascript"); ++ Bindings globals = engine.createBindings(); ++ globals.put("resolver", new Resolver()); ++ engine.setBindings(globals, ScriptContext.GLOBAL_SCOPE); ++ try { ++ URL utils = ProxyUtil.class.getResource("PacUtils.js"); ++ if (utils == null) { ++ log.error("Unable to load PacUtils.js"); ++ return new String[0]; ++ } ++ engine.eval(new InputStreamReader(utils.openStream())); ++ Object res = engine.eval(pac); ++ if (engine instanceof Invocable) { ++ Object[] args = new Object[] { url.toString(), url.getHost() }; ++ res = ((Invocable) engine).invokeFunction("FindProxyForURL", args); ++ } ++ String[] proxies = res.toString().split(";"); ++ for (int ii = 0; ii < proxies.length; ii += 1) { ++ proxies[ii] = proxies[ii].trim(); ++ } ++ return proxies; ++ } catch (Exception e) { ++ log.warning("Failed to resolve PAC proxy", e); ++ } ++ return new String[0]; ++ } ++ ++ private static String[] splitHostPort (String hostPort) { ++ int cidx = hostPort.indexOf(":"); ++ if (cidx == -1) { ++ return new String[] { hostPort, null}; ++ } else { ++ return new String[] { hostPort.substring(0, cidx), hostPort.substring(cidx+1) }; ++ } ++ } ++ + protected static final String PROXY_REGISTRY = + "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; + } +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java +index d3aa2bd25..d64e5f02d 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java +@@ -14,7 +14,7 @@ public final class RotatingBackgrounds + { + public interface ImageLoader { + /** Loads and returns the image with the supplied path. */ +- public Image loadImage (String path); ++ Image loadImage (String path); + } + + /** +@@ -35,7 +35,7 @@ public final class RotatingBackgrounds + } + + /** +- * Create a sequence of images to be rotated through from backgrounds. ++ * Create a sequence of images to be rotated through from {@code backgrounds}. + * + * Each String in backgrounds should be the path to the image, a semicolon, and the minimum + * amount of time to display the image in seconds. Each image will be active for an equal +diff --git a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java +index 99f44ca51..197dc9170 100644 +--- a/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java ++++ b/getdown/src/getdown/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java +@@ -26,12 +26,9 @@ import com.samskivert.swing.Label; + import com.samskivert.swing.LabelStyleConstants; + import com.samskivert.swing.util.SwingUtil; + import com.samskivert.util.Throttle; +- + import com.threerings.getdown.data.Application.UpdateInterface; + import com.threerings.getdown.util.MessageUtil; + import com.threerings.getdown.util.Rectangle; +-import com.threerings.getdown.util.StringUtil; +- + import static com.threerings.getdown.Log.log; + + /** +@@ -344,7 +341,7 @@ public final class StatusPanel extends JComponent + { + String msg = get(key); + if (msg != null) return MessageFormat.format(MessageUtil.escape(msg), (Object[])args); +- return key + String.valueOf(Arrays.asList(args)); ++ return key + Arrays.asList(args); + } + + /** Used by {@link #setStatus}, and {@link #setProgress}. */ +diff --git a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java b/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java +deleted file mode 100644 +index 5c6c7c393..000000000 +--- a/getdown/src/getdown/launcher/src/main/java/jalview/bin/StartupNotificationListener.java ++++ /dev/null +@@ -1,29 +0,0 @@ +-package jalview.bin; +- +-import com.threerings.getdown.launcher.GetdownApp; +-import static com.threerings.getdown.Log.log; +- +-public class StartupNotificationListener { +- +- public static void setListener() { +- +- +- try { +- com.install4j.api.launcher.StartupNotification.registerStartupListener( +- new com.install4j.api.launcher.StartupNotification.Listener() { +- @Override +- public void startupPerformed(String parameters) { +- log.info("StartupNotification.Listener.startupPerformed: '"+parameters+"'"); +- GetdownApp.setStartupFilesParameterString(parameters); +- } +- } +- ); +- } catch (Exception e) { +- e.printStackTrace(); +- } catch (NoClassDefFoundError t) { +- log.warning("Starting without install4j classes"); +- } +- +- } +- +-} +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties +index 19b2999e1..7a33ca0e4 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages.properties +@@ -12,11 +12,14 @@ m.abort_cancel = Continue installation + m.detecting_proxy = Trying to auto-detect proxy settings + + m.configure_proxy = We were unable to connect to the application server to download data. \ +-

Please make sure that no virus scanner or firewall is blocking network communicaton with \ ++

Please make sure that no virus scanner or firewall is blocking network communication with \ + the server. \ +

Your computer may access the Internet through a proxy and we were unable to automatically \ + detect your proxy settings. If you know your proxy settings, you can enter them below. + ++m.update_proxy_auth = The stored proxy user/password is wrong or obsolete. \ ++

Please provide an updated user/password combination. ++ + m.proxy_extra = If you are sure that you don't use a proxy then \ + perhaps there is a temporary Internet outage that is preventing us from \ + communicating with the servers. In this case, you can cancel and try \ +@@ -41,10 +44,7 @@ m.checking = Checking for update + m.validating = Validating + m.patching = Patching + m.launching = Launching +- + m.patch_notes = Patch Notes +-m.play_again = Play Again +- + m.complete = {0}% complete + m.remain = {0} remaining + +@@ -99,7 +99,7 @@ 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. + +-m.applet_stopped = Getdown's applet was told to stop working. ++m.verify_timeout = Verifying resources took too long. + + # application/digest errors + m.missing_appbase = The configuration file is missing the 'appbase'. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties +index 8e3683594..db35593b2 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_de.properties +@@ -1,13 +1,9 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown German translation messages + + m.abort_title = Installation abbrechen? +-m.abort_confirm = Bist du sicher, dass du die Installation abbrechen \ +-m\u00f6chtest? \ +- Du kannst sp\u00e4ter fortfahren, indem du die Anwendung erneut \ +-ausf\u00fchrst. ++m.abort_confirm = Bist du sicher, dass du die Installation abbrechen möchtest? \ ++ Du kannst später fortfahren, indem du die Anwendung erneut ausführst. + m.abort_ok = Beenden + m.abort_cancel = Installation fortsetzen + +@@ -17,9 +13,12 @@ m.configure_proxy = Es konnte keine Verbindung zum Applikations-Server auf +

Bitte kontrollieren Sie die Proxyeinstellungen und stellen Sie sicher, dass keine lokal oder \ + im Netzwerk betriebene Sicherheitsanwendung (Virenscanner, Firewall, etc.) die Kommunikation \ + mit dem Server blockiert.
\ +- Wenn kein Proxy verwendet werden soll, l\u00f6schen Sie bitte alle Eintr\u00e4ge in den unten \ ++ Wenn kein Proxy verwendet werden soll, löschen Sie bitte alle Einträge in den unten \ + stehenden Feldern und klicken sie auf OK. + ++m.update_proxy_auth = Gespeicherte Proxy User/Passwort Kombination ungültig oder obsolet. \ ++

Bitte geben Sie die korrekte User/Passwort Kombination ein. ++ + m.proxy_extra = Sollten Sie keine Proxyeinstellungen gesetzt haben wenden Sie sich bitte \ + an Ihren Administrator. + +@@ -46,71 +45,68 @@ m.launching = Starte + m.patch_notes = Patchnotes + + m.complete = {0}% abgeschlossen +-m.remain = {0} \u00fcbrig ++m.remain = {0} übrig + + m.updating_metadata = Lade Steuerungsdateien herunter + +-m.init_failed = Unsere Konfigurationsdatei fehlt oder ist besch\u00e4digt. \ +-Versuche, eine neue Kopie herunterzuladen... ++m.init_failed = Unsere Konfigurationsdatei fehlt oder ist beschädigt. \ ++ Versuche, eine neue Kopie herunterzuladen... + +-m.java_download_failed = Wir konnten die notwendige Javaversion f\u00fcr deinen \ +-Computer nicht automatisch herunterladen. \n\n \ +-Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \ +-Anwendung erneut starten. ++m.java_download_failed = Wir konnten die notwendige Javaversion für deinen \ ++ Computer nicht automatisch herunterladen. \n\n \ ++ Bitte auf www.java.com die aktuelle Javaversion herunterladen und dann die \ ++ Anwendung erneut starten. + + m.java_unpack_failed = Wir konnten die aktualisierte Javaversion nicht \ +-entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \ +-Festplatte frei sind und versuche dann die Anwendung erneut zu \ +-starten.\n\n\ \ +-Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \ +-Javaversion herunterladen und installieren und dann erneut versuchen. ++ entpacken. Bitte stelle sicher, dass wenigstens 100MB Platz auf der \ ++ Festplatte frei sind und versuche dann die Anwendung erneut zu \ ++ starten.\n\n\ \ ++ Falls das das Problem nicht beseitigt, bitte auf www.java.com die aktuelle \ ++ Javaversion herunterladen und installieren und dann erneut versuchen. + + m.unable_to_repair = Wir konnten die notwendigen Dateien nach 5 Versuchen \ +-nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \ +-aber wenn dies erneut fehlschl\u00e4gt, musst du die Anwendung deinstallieren \ +-und erneut installieren. ++ nicht herunterladen. Du kannst versuchen, die Anwendung erneut zu starten, \ ++ aber wenn dies erneut fehlschlägt, musst du die Anwendung deinstallieren \ ++ und erneut installieren. + + m.unknown_error = Die Anwendung konnte wegen eines unbekannten Fehlers \ +-nicht gestartet werden. Bitte auf \n{0} weiterlesen. ++ nicht gestartet werden. Bitte auf \n{0} weiterlesen. + + m.init_error = Die Anwendung konnte wegen folgendem Fehler nicht gestartet \ +-werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \ +-solchen Problemen vorzugehen ist. ++ werden:\n{0}\n\n Bitte auf \n{1} weiterlesen, um zu erfahren, wie bei \ ++ solchen Problemen vorzugehen ist. + +-m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist: \ +- \n{0}\nist nicht schreibberechtigt. Bitte in ein Verzeichnis mit \ +-Schreibzugriff installieren. ++m.readonly_error = Das Verzeichnis, in dem die Anwendung installiert ist:\n{0}\n \ ++ ist nicht schreibberechtigt. Bitte in ein Verzeichnis mit Schreibzugriff installieren. + + m.missing_resource = Die Anwendung konnte nicht gestartet werden, da die \ +-folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \ +-weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist. ++ folgende Quelle nicht gefunden wurde:\n{0}\n\n\ Bitte auf \n{1} \ ++ weiterlesen, um zu erfahren, wie bei solchen Problemen vorzugehen ist. + + m.insufficient_permissions_error = Du hast die digitale Signatur dieser \ +-Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \ +-musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \ +-deinen Browser beenden, neu starten und erneut die Anwendung von dieser \ +-Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \ +-digitale Signatur akzeptieren, um der Anwendung die n\u00f6tigen Rechte zu \ +-geben, die sie braucht, um zu laufen. ++ Anwendung nicht akzeptiert. Falls du diese Anwendung benutzen willst, \ ++ musst du ihre digitale Signatur akzeptieren. \n\Um das zu tun, musst du \ ++ deinen Browser beenden, neu starten und erneut die Anwendung von dieser \ ++ Webseite aus starten. Wenn die Sicherheitsabfrage erscheint, bitte die \ ++ digitale Signatur akzeptieren, um der Anwendung die nötigen Rechte zu \ ++ geben, die sie braucht, um zu laufen. + + m.corrupt_digest_signature_error = Wir konnten die digitale Signatur \ +-dieser Anwendung nicht \u00fcberpr\u00fcfen.\nBitte \u00fcberpr\u00fcfe, ob du die Anwendung \ +-von der richtigen Webseite aus startest. ++ dieser Anwendung nicht überprüfen.\nBitte überprüfe, ob du die Anwendung \ ++ von der richtigen Webseite aus startest. + + m.default_install_error = der Support-Webseite + +-m.another_getdown_running = Diese Installationsanwendung l\u00e4uft in mehreren \ +-Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \ +-Vorgang erledigen lassen. +- +-m.applet_stopped = Die Anwendung wurde beendet. ++m.another_getdown_running = Diese Installationsanwendung läuft in mehreren \ ++ Instanzen. Diese Instanz wird sich beenden und eine andere Instanz den \ ++ Vorgang erledigen lassen. + ++m.verify_timeout = Timeout beim Verifizieren der Resourcen. + + # application/digest errors + m.missing_appbase = In der Konfigurationsdatei fehlt die 'appbase'. + m.invalid_version = In der Konfigurationsdatei steht die falsche Version. + m.invalid_appbase = In der Konfigurationsdatei steht die falsche 'appbase'. + m.missing_class = In der Konfigurationsdatei fehlt die Anwendungsklasse. +-m.missing_code = Die Konfigurationsdatei enth\u00e4lt keine Codequellen. +-m.invalid_digest_file = Die Hashwertedatei ist ung\u00fcltig. +- ++m.missing_code = Die Konfigurationsdatei enthält keine Codequellen. ++m.invalid_digest_file = Die Hashwertedatei ist ungültig. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties +index 609b02524..46cd64ac9 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_es.properties +@@ -1,36 +1,34 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown Spanish translation messages + +-m.abort_title = \u00bfCancelar la instalaci\u00f3n? +-m.abort_confirm = \u00bfEst\u00e1s seguro de querer cancelar la instalaci\u00f3n? \ +- Puedes continuarla despu\u00e9s si corres de nuevo la aplicaci\u00f3n. ++m.abort_title = ¿Cancelar la instalación? ++m.abort_confirm = ¿Estás seguro de querer cancelar la instalación? \ ++ Puedes continuarla después si corres de nuevo la aplicación. + m.abort_ok = Cancelar +-m.abort_cancel = Continuar la instalaci\u00f3n ++m.abort_cancel = Continuar la instalación + +-m.detecting_proxy = Detectando autom\u00e1ticamente la configuraci\u00f3n proxy ++m.detecting_proxy = Detectando automáticamente la configuración proxy + + m.configure_proxy = No ha sido posible conectar con nuestros servidores para \ + descargar los datos del juego. \ +

  • Si el cortafuegos de Windows o Norton Internet Security tiene instrucciones \ +- de bloquear javaw.exe no podemos descargar el juego. Necesitar\u00e1s \ ++ de bloquear javaw.exe no podemos descargar el juego. Necesitarás \ + permitir que javaw.exe tenga acceso al Internet. Puedes intentar \ + correr el juego de nuevo, pero es posible que debas dar permisos a javaw.exe en la \ +- configuraci\u00f3n de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ).
\ ++ configuración de tu cortafuegos ( Inicio -> Panel de control -> Firewall de Windows ). \ +

Es posible que tu computadora tenga acceso al Internet por medio de un proxy por lo que \ +- no ha sido posible detectar autom\u00e1ticamente tu configuraci\u00f3n. Si conoces tu \ +- configuraci\u00f3n proxy, puedes anotarla abajo. ++ no ha sido posible detectar automáticamente tu configuración. Si conoces tu \ ++ configuración proxy, puedes anotarla abajo. + +-m.proxy_extra = Si est\u00e1s seguro de que no tienes un proxy entonces \ +- tal vez exista un falla temporal en el Internet que est\u00e1 evitando que podamos \ ++m.proxy_extra = Si estás seguro de que no tienes un proxy entonces \ ++ tal vez exista un falla temporal en el Internet que está evitando que podamos \ + comunicarnos con los servidores. En este caso, puedes cancelar e intentar \ +- instalarla de nuevo m\u00e1s tarde. ++ instalarla de nuevo más tarde. + + m.proxy_host = IP proxy + m.proxy_port = Puerto proxy + m.proxy_username = Nombre de usuario +-m.proxy_password = Contrase\u00f1a ++m.proxy_password = Contraseña + m.proxy_auth_required = Autenticacion requerida + m.proxy_ok = OK + m.proxy_cancel = Cancelar +@@ -54,62 +52,59 @@ m.remain = {0} restante + + m.updating_metadata = Descargando los archivos de control + +-m.init_failed = Un archivo de configuraci\u00f3n est\u00e1 faltante o est\u00e1 corrupto. Intentando \ ++m.init_failed = Un archivo de configuración está faltante o está corrupto. Intentando \ + descargar una nueva copia... + +-m.java_download_failed = No ha sido posible descargar autom\u00e1ticamente la \ +- versi\u00f3n de Java necesaria para tu computadora.\n\n\ +- Por favor ve a www.java.com y descarga la \u00faltima versi\u00f3n de \ +- Java, despu\u00e9s intenta correr de nuevo la aplicaci\u00f3n. ++m.java_download_failed = No ha sido posible descargar automáticamente la \ ++ versión de Java necesaria para tu computadora.\n\n\ ++ Por favor ve a www.java.com y descarga la última versión de \ ++ Java, después intenta correr de nuevo la aplicación. + +-m.java_unpack_failed = No ha sido posible desempacar una versi\u00f3n actualizada de \ +- Java. Por favor aseg\u00farate de tener al menos 100 MB de espacio libre en tu \ +- disco duro e intenta correr de nuevo la aplicaci\u00f3n.\n\n\ ++m.java_unpack_failed = No ha sido posible desempacar una versión actualizada de \ ++ Java. Por favor asegúrate de tener al menos 100 MB de espacio libre en tu \ ++ disco duro e intenta correr de nuevo la aplicación.\n\n\ + Si eso no soluciona el problema, ve a www.java.com y descarga e \ +- instala la \u00faltima versi\u00f3n de Java e intenta de nuevo. ++ instala la última versión de Java e intenta de nuevo. + +-m.unable_to_repair = No ha sido posible descargar los archivos necesarios despu\u00e9s de \ +- cinco intentos. Puedes intentar correr de nuevo la aplicaci\u00f3n, pero si falla \ +- de nuevo podr\u00edas necesitar desinstalar y reinstalar. ++m.unable_to_repair = No ha sido posible descargar los archivos necesarios después de \ ++ cinco intentos. Puedes intentar correr de nuevo la aplicación, pero si falla \ ++ de nuevo podrías necesitar desinstalar y reinstalar. + +-m.unknown_error = La aplicaci\u00f3n no ha podido iniciar debido a un extra\u00f1o \ +- error del que no se pudo recobrar. Por favor visita\n{0} para ver informaci\u00f3n acerca \ ++m.unknown_error = La aplicación no ha podido iniciar debido a un extraño \ ++ error del que no se pudo recobrar. Por favor visita\n{0} para ver información acerca \ + de como recuperarla. +-m.init_error = La aplicaci\u00f3n no ha podido iniciar debido al siguiente \ ++m.init_error = La aplicación no ha podido iniciar debido al siguiente \ + error:\n{0}\n\nPor favor visita\n{1} para \ +- ver informaci\u00f3n acerca de como manejar ese tipo de problemas. ++ ver información acerca de como manejar ese tipo de problemas. + +-m.readonly_error = El directorio en el que esta aplicaci\u00f3n est\u00e1 instalada: \ +- \n{0}\nes solo lectura. Por favor instala la aplicaci\u00f3n en un directorio en el cual \ ++m.readonly_error = El directorio en el que esta aplicación está instalada: \ ++ \n{0}\nes solo lectura. Por favor instala la aplicación en un directorio en el cual \ + tengas acceso de escritura. + +-m.missing_resource = La aplicaci\u00f3n no ha podido iniciar debido a un recurso \ +- faltante:\n{0}\n\nPor favor visita\n{1} para informaci\u00f3n acerca de como solucionar \ ++m.missing_resource = La aplicación no ha podido iniciar debido a un recurso \ ++ faltante:\n{0}\n\nPor favor visita\n{1} para información acerca de como solucionar \ + estos problemas. + + m.insufficient_permissions_error = No aceptaste la firma digital de \ +- esta aplicaci\u00f3n. Si quieres correr la aplicaci\u00f3n, necesitas aceptar \ ++ esta aplicación. Si quieres correr la aplicación, necesitas aceptar \ + su firma digital.\n\nPara hacerlo, necesitas cerrar tu navegador, \ +- reiniciarlo, y regresar a esta p\u00e1gina web para reiniciar la aplicaci\u00f3n. Cuando se muestre \ +- el di\u00e1logo de seguridad, haz clic en el bot\u00f3n para aceptar la firmar digital \ +- y otorgar a esta aplicaci\u00f3n los privilegios que necesita para correr. ++ reiniciarlo, y regresar a esta página web para reiniciar la aplicación. Cuando se muestre \ ++ el diálogo de seguridad, haz clic en el botón para aceptar la firmar digital \ ++ y otorgar a esta aplicación los privilegios que necesita para correr. + + m.corrupt_digest_signature_error = No pudimos verificar la firma digital \ +- de la aplicaci\u00f3n.\nPor favor revisa que est\u00e9s lanzando la aplicaci\u00f3n desde\nel \ ++ de la aplicación.\nPor favor revisa que estés lanzando la aplicación desde\nel \ + sitio web correcto. + +-m.default_install_error = la secci\u00f3n de asistencia de este sitio web +- +-m.another_getdown_running = Est\u00e1n corriendo m\u00faltiples instancias de \ +- este instalador. Este se detendr\u00e1 para permitir que otra contin\u00fae. ++m.default_install_error = la sección de asistencia de este sitio web + +-m.applet_stopped = Se le dijo al applet de Getdown que dejara de trabajar. ++m.another_getdown_running = Están corriendo múltiples instancias de \ ++ este instalador. Este se detendrá para permitir que otra continúe. + + # application/digest errors +-m.missing_appbase = Al archivo de configuraci\u00f3n le falta el 'appbase'. +-m.invalid_version = El archivo de configuraci\u00f3n especifica una versi\u00f3n no v\u00e1lida. +-m.invalid_appbase = El archivo de configuraci\u00f3n especifica un 'appbase' no v\u00e1lido. +-m.missing_class = Al archivo de configuraci\u00f3n le falta la clase de aplicaci\u00f3n. +-m.missing_code = El archivo de configuraci\u00f3n especifica que no hay recursos de c\u00f3digo. +-m.invalid_digest_file = El archivo digest no es v\u00e1lido. +- ++m.missing_appbase = Al archivo de configuración le falta el 'appbase'. ++m.invalid_version = El archivo de configuración especifica una versión no válida. ++m.invalid_appbase = El archivo de configuración especifica un 'appbase' no válido. ++m.missing_class = Al archivo de configuración le falta la clase de aplicación. ++m.missing_code = El archivo de configuración especifica que no hay recursos de código. ++m.invalid_digest_file = El archivo digest no es válido. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties +index 3666204e2..5eb8ec9cd 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_fr.properties +@@ -1,29 +1,27 @@ + # +-# $Id: messages.properties 485 2012-03-08 22:05:30Z ray.j.greenwell $ +-# +-# Getdown translation messages ++# Getdown French translation messages + + m.abort_title = Annuler l'installation? +-m.abort_confirm =\u00cates-vous s\u00fbr de vouloir annuler l'installation? \ +- Vous pourrez reprendre l'installation en ex\u00e9cutant l'application de nouveau. ++m.abort_confirm =Êtes-vous sûr de vouloir annuler l'installation? \ ++ Vous pourrez reprendre l'installation en exécutant l'application de nouveau. + m.abort_ok = Quitter + m.abort_cancel = Continuer l'installation + +-m.detecting_proxy = D\u00e9tection automatique des r\u00e9glages proxy ++m.detecting_proxy = Détection automatique des réglages proxy + + m.configure_proxy =Connexion au serveur impossible. \ +-

  • Veuillez v\u00e9rifier que javaw.exe n'est bloqu\u00e9 \ ++
    • Veuillez vérifier que javaw.exe n'est bloqué \ + par aucun pare-feu ou antivirus. \ + Vous pouvez vous rendre sur la configuration du pare-feu windows via \ +- (D\u00e9marrer -> Panneau de Configuration -> Pare-feu Windows ).
    \ +-

    Il est \u00e9galement possible que vous soyez derri\u00e8re un proxy que l'application \ +- est incapable de d\u00e9tecter automatiquement. \ +- Si tel est le cas, veuillez saisir les r\u00e9glages proxy ci-dessous. ++ (Démarrer -> Panneau de Configuration -> Pare-feu Windows ).

\ ++

Il est également possible que vous soyez derrière un proxy que l'application \ ++ est incapable de détecter automatiquement. \ ++ Si tel est le cas, veuillez saisir les réglages proxy ci-dessous. + +-m.proxy_extra =Si vous \u00eates certain de ne pas utiliser de proxy, il est \ +- possible qu'une interruption temporaire de la connexion internet emp\u00fbche la \ ++m.proxy_extra =Si vous êtes certain de ne pas utiliser de proxy, il est \ ++ possible qu'une interruption temporaire de la connexion internet empûche la \ + communication avec les serveurs. Dans ce cas, vous pouvez relancer \ +- l'installation ult\u00e9rieurement. ++ l'installation ultérieurement. + + m.proxy_host = Proxy IP + m.proxy_port = Proxy port +@@ -33,79 +31,77 @@ m.proxy_auth_required = Identification requise + m.proxy_ok = OK + m.proxy_cancel = Annuler + +-m.downloading_java = T\u00e9l\u00e9chargement en cours de la Machine Virtuelle Java +-m.unpacking_java = D\u00e9compression en cours de la Machine Virtuelle Java ++m.downloading_java = Téléchargement en cours de la Machine Virtuelle Java ++m.unpacking_java = Décompression en cours de la Machine Virtuelle Java + +-m.resolving = R\u00e9solution des t\u00e9l\u00e9chargements en cours +-m.downloading = T\u00e9l\u00e9chargement des donn\u00e9es en cours +-m.failure = \u00c9chec du t\u00e9l\u00e9chargement: {0} ++m.resolving = Résolution des téléchargements en cours ++m.downloading = Téléchargement des données en cours ++m.failure = Échec du téléchargement: {0} + +-m.checking = V\u00e9rification de la mise-\u00e0-jour en cours ++m.checking = Vérification de la mise-à-jour en cours + m.validating = Validation en cours + m.patching = Modification en cours + m.launching = Lancement en cours + +-m.patch_notes = Notes de mise-\u00e0-jour ++m.patch_notes = Notes de mise-à-jour + +-m.complete = Complet \u00e0 {0}% ++m.complete = Complet à {0}% + m.remain = {0} restant + +-m.updating_metadata = T\u00e9l\u00e9chargement des fichiers de contr\u00f4les en cours ++m.updating_metadata = Téléchargement des fichiers de contrôles en cours + +-m.init_failed = Notre fichier de configuration est perdu ou corrompu. T\u00e9l\u00e9chargement \ ++m.init_failed = Notre fichier de configuration est perdu ou corrompu. Téléchargement \ + d'une nouvelle copie en cours ... + +-m.java_download_failed = Impossible de t\u00e9l\u00e9charger automatiquement la \ +- version de Java n\u00e9cessaire.\n\n\ +- Veuillez vous rendre sur www.java.com et t\u00e9l\u00e9charger et installer la version \ +- la plus r\u00e9cente de Java, avant d'ex\u00e9cuter l'application \u00e0 nouveau. ++m.java_download_failed = Impossible de télécharger automatiquement la \ ++ version de Java nécessaire.\n\n\ ++ Veuillez vous rendre sur www.java.com et télécharger et installer la version \ ++ la plus récente de Java, avant d'exécuter l'application à nouveau. + +-m.java_unpack_failed = Impossible de d\u00e9compresser la version de \ +- Java n\u00e9cessaire. Veuillez v\u00e9rifier que vous avez au moins 100 MB d'espace libre \ +- sur votre disque dur puis tenter d'ex\u00e9cuter l'application \u00e0 nouveau.\n\n\ +- Si le probl\u00e8me persiste, rendez vous www.java.com et t\u00e9l\u00e9chargez et \ +- installez la version plus r\u00e9cente de Java puis essayez de nouveau. ++m.java_unpack_failed = Impossible de décompresser la version de \ ++ Java nécessaire. Veuillez vérifier que vous avez au moins 100 MB d'espace libre \ ++ sur votre disque dur puis tenter d'exécuter l'application à nouveau.\n\n\ ++ Si le problème persiste, rendez vous www.java.com et téléchargez et \ ++ installez la version plus récente de Java puis essayez de nouveau. + +-m.unable_to_repair = Impossible de t\u00e9l\u00e9charger les fichiers n\u00e9cessaires apr\u00e8s \ +- cinq tentatives. Vous pouvez tenter d'ex\u00e9cuter l'application \u00e0 nouveau, mais il est \ +- possible qu'une d\u00e9sinstallation / r\u00e9installation soit n\u00e9cessaire. ++m.unable_to_repair = Impossible de télécharger les fichiers nécessaires après \ ++ cinq tentatives. Vous pouvez tenter d'exécuter l'application à nouveau, mais il est \ ++ possible qu'une désinstallation / réinstallation soit nécessaire. + +-m.unknown_error = Une erreur inconnue a fait \u00e9chouer le lancement de l'application. \ ++m.unknown_error = Une erreur inconnue a fait échouer le lancement de l'application. \ + Veuillez visiter\n{0} pour plus d'informations. +-m.init_error = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause de l'erreur \ ++m.init_error = Le lancement de l'application a échoué à cause de l'erreur \ + suivante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations. + +-m.readonly_error = Le r\u00e9pertoire d'installation de cette application: \ +- \n{0}\nest en lecture seule. Veuillez installer l'application dans un r\u00e9pertoire avec \ +- un acc\u00e8s en \u00e9criture. ++m.readonly_error = Le répertoire d'installation de cette application: \ ++ \n{0}\nest en lecture seule. Veuillez installer l'application dans un répertoire avec \ ++ un accès en écriture. + +-m.missing_resource = Le lancement de l'application a \u00e9chou\u00e9 \u00e0 cause d'une \ ++m.missing_resource = Le lancement de l'application a échoué à cause d'une \ + ressource manquante:\n{0}\n\nVeuillez visiter\n{1} pour plus d'informations. + + m.insufficient_permissions_error = Vous n'avez pas accepter la signature \ +- num\u00e9rique de cette application. Si vous souhaitez ex\u00e9cuter cette application, vous \ +- devez accepter sa signature num\u00e9rique.\n\nAfin de le faire, vous devez quitter votre \ +- navigateur, le red\u00e9marrer, retourner \u00e0 cette page puis relancer l'application. \ +- Une fois la bo\u00eete de dialogue de s\u00e9curit\u00e9 affich\u00e9e, cliquez sur le bouton \ +- pour accepter la signature num\u00e9rique et accorder les permissions n\u00e9cessaires au bon \ ++ numérique de cette application. Si vous souhaitez exécuter cette application, vous \ ++ devez accepter sa signature numérique.\n\nAfin de le faire, vous devez quitter votre \ ++ navigateur, le redémarrer, retourner à cette page puis relancer l'application. \ ++ Une fois la boîte de dialogue de sécurité affichée, cliquez sur le bouton \ ++ pour accepter la signature numérique et accorder les permissions nécessaires au bon \ + fonctionnement de l'application. + +-m.corrupt_digest_signature_error = Nous ne pouvons pas v\u00e9rifier la signature num\u00e9rique \ +- de l'application.\nVeuillez v\u00e9rifier que vous lancez l'application \ndepuis \ ++m.corrupt_digest_signature_error = Nous ne pouvons pas vérifier la signature numérique \ ++ de l'application.\nVeuillez vérifier que vous lancez l'application \ndepuis \ + la bonne adresse internet. + + m.default_install_error = la section de support du site + + m.another_getdown_running = Plusieurs instances d'installation de cette \ +- application sont d\u00e9j\u00e0 en cours d'ex\u00e9cution. Cette instance va s'arr\u00eater \ ++ application sont déjà en cours d'exécution. Cette instance va s'arrêter \ + afin de permettre aux autres d'aboutir. + +-m.applet_stopped = L'appelet Getdown a \u00e9t\u00e9 stopp\u00e9e. +- + # application/digest errors + m.missing_appbase = Le fichier de configuration ne contient pas 'appbase'. +-m.invalid_version = Le fichier de configuration sp\u00e9cifie une version invalide. +-m.invalid_appbase = Le fichier de configuration sp\u00e9cifie un 'appbase' invalide. ++m.invalid_version = Le fichier de configuration spécifie une version invalide. ++m.invalid_appbase = Le fichier de configuration spécifie un 'appbase' invalide. + m.missing_class = Le fichier de configuration ne contient pas la classe de l'application. +-m.missing_code = Le fichier de configuration ne sp\u00e9cifie aucune ressource de code. ++m.missing_code = Le fichier de configuration ne spécifie aucune ressource de code. + m.invalid_digest_file = Le fichier digest est invalide. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties +index 33b3260ce..aea9e9017 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_it.properties +@@ -1,7 +1,5 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown Italian translation messages + + m.abort_title = Annullare l'installazione? + m.abort_confirm = Sei sicuro di voler annullare l'installazione? \ +@@ -22,7 +20,11 @@ m.configure_proxy = Impossibile collegarsi al server per \ + questo potrebbe non essere stato riconosciuto automaticamente. Se conosci le \ + tue impostazioni del proxy, puoi inserirle di seguito. + +-m.proxy_extra = Se sei sicuro di non usare proxy \ ++m.update_proxy_auth = Combinazione User/Password salvata per il proxy non è valida \ ++ oppure obsoleta. \ ++

Perfavore inserire una combinazione User/Password valida. ++ ++m.proxy_extra = Se sei sicuro di non usare proxy \ + potrebbe essere un problema di internet o di collegamento con il server. \ + In questo caso puoi annullare e ripetere l'installazione più tardi. + +@@ -47,8 +49,6 @@ m.patching = Applico le patch + m.launching = Avvio + + m.patch_notes = Note delle Patch +-m.play_again = Avvia Nuovamente +- + m.complete = {0}% completato + m.remain = {0} rimasto + +@@ -103,8 +103,6 @@ m.default_install_error = la sezione di supporto del sito + m.another_getdown_running = E' già in esecuzione un'istanza del programma. \ + Questa verrà chiusa. + +-m.applet_stopped = L'applet di Getdown è stata interrotta. +- + # application/digest errors + m.missing_appbase = Il tag "appbase" è mancante. + m.invalid_version = Il file di configurazione non contiene una versione valida (tag "version"). +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties +index c344c16e0..f3538d0ac 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ja.properties +@@ -1,107 +1,105 @@ + # +-# $Id$ +-# +-# Getdown translation messages +- +-m.abort_title = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f +-m.abort_confirm = \u672c\u5f53\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u4e2d\u6b62\u3057\u307e\u3059\u304b\uff1f \ +- \u5f8c\u3067\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u305f\u969b\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u518d\u958b\u3067\u304d\u307e\u3059\u3002 +-m.abort_ok = \u4e2d\u6b62 +-m.abort_cancel = \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u7d9a\u884c +- +-m.detecting_proxy = \u81ea\u52d5\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u5b9f\u884c\u4e2d +- +-m.configure_proxy = \u30b5\u30fc\u30d0\u306b\u63a5\u7d9a\u3067\u304d\u306a\u3044\u305f\u3081\u3001\u30b2\u30fc\u30e0\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306b \ +- \u5931\u6557\u3057\u307e\u3057\u305f\u3002 \ +-

  • \u30a6\u30a3\u30f3\u30c9\u30a6\u30ba\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u307e\u305f\u306f\u30ce\u30fc\u30c8\u30f3\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u304c \ +- javaw.exe\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b\u3088\u3046\u8a2d\u5b9a\u3057\u3066\u3042\u308b\u5834\u5408\u306f\u3001\u30b2\u30fc\u30e0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3002 \u8a2d\u5b9a\u3092 \ +- javaw.exe\u7d4c\u7531\u3067\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u5909\u66f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u30b2\u30fc\u30e0\u3092\u518d\u8d77\u52d5 \ +- \u3057\u305f\u5f8c\u3001\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\u306e\u8a2d\u5b9a\u304b\u3089javaw.exe \u3092\u524a\u9664 \ +- \u3057\u3066\u304f\u3060\u3055\u3044\uff08\u30b9\u30bf\u30fc\u30c8\u2192\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u2192\u30d5\u30a1\u30a4\u30a2\u30a6\u30a9\u30fc\u30eb\uff09\u3002
\ +-

\u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u81ea\u52d5\u691c\u51fa\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306f \ +- \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u3078\u30a2\u30af\u30bb\u30b9\u3057\u3066\u3044\u307e\u3059\u3002 \u30d7\u30ed\u30ad\u30b7\u8a2d\u5b9a\u306e\u8a73\u7d30\u304c \ +- \u308f\u304b\u3063\u3066\u3044\u308b\u5834\u5408\u306f\u3001\u4e0b\u306b\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 +- +-m.proxy_extra = \u30d7\u30ed\u30ad\u30b7\u3092\u4f7f\u7528\u3057\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u4e00\u6642\u7684\u306a \ +- \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u306e\u4e0d\u5177\u5408\u306b\u3088\u308a\u3001\u30b5\u30fc\u30d0\u3068\u4ea4\u4fe1\u3067\u304d\u306a\u3044\u72b6\u614b\u306b\u3042\u308b \ +- \u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002 \u305d\u306e\u5834\u5408\u306f\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u3066\u3001 \ +- \u5f8c\u307b\u3069\u6539\u3081\u3066\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 +- +-m.proxy_host = \u30d7\u30ed\u30ad\u30b7IP +-m.proxy_port = \u30d7\u30ed\u30ad\u30b7\u30dd\u30fc\u30c8 ++# Getdown Japanese translation messages ++ ++m.abort_title = インストールを中止しますか? ++m.abort_confirm = 本当にインストールを中止しますか? \ ++ 後でアプリケーションを起動した際にインストールを再開できます。 ++m.abort_ok = 中止 ++m.abort_cancel = インストールの続行 ++ ++m.detecting_proxy = 自動プロキシ設定実行中 ++ ++m.configure_proxy = サーバに接続できないため、ゲームのダウンロードに \ ++ 失敗しました。 \ ++

  • ウィンドウズファイアウォールまたはノートンインターネットセキュリティが \ ++ javaw.exeをブロックするよう設定してある場合は、ゲームをダウンロードできません。 設定を \ ++ javaw.exe経由でインターネットにアクセスできるように変更してください。 ゲームを再起動 \ ++ した後、ファイアウォールの設定からjavaw.exe を削除 \ ++ してください(スタート→コントロールパネル→ファイアウォール)。
\ ++

プロキシ設定の自動検出ができません。お使いのコンピューターは \ ++ プロキシを使用してインターネットへアクセスしています。 プロキシ設定の詳細が \ ++ わかっている場合は、下に入力してください。 ++ ++m.proxy_extra = プロキシを使用していない場合は、一時的な \ ++ インターネットの不具合により、サーバと交信できない状態にある \ ++ 可能性があります。 その場合はインストールをキャンセルして、 \ ++ 後ほど改めて実行してください。 ++ ++m.proxy_host = プロキシIP ++m.proxy_port = プロキシポート + m.proxy_username = Username + m.proxy_password = Password + m.proxy_auth_required = Authentication required +-m.proxy_ok = OK +-m.proxy_cancel = \u30ad\u30e3\u30f3\u30bb\u30eb ++m.proxy_ok = OK ++m.proxy_cancel = キャンセル + +-m.downloading_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d +-m.unpacking_java = Java\u30d0\u30fc\u30c1\u30e3\u30eb\u30de\u30b7\u30f3\u306e\u89e3\u51cd\u4e2d ++m.downloading_java = Javaバーチャルマシンのダウンロード中 ++m.unpacking_java = Javaバーチャルマシンの解凍中 + +-m.resolving = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u306e\u8a2d\u5b9a\u4e2d +-m.downloading = \u30c7\u30fc\u30bf\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d +-m.failure = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u5931\u6557\uff1a {0} ++m.resolving = ダウンロードの設定中 ++m.downloading = データのダウンロード中 ++m.failure = ダウンロード失敗: {0} + +-m.checking = \u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d +-m.validating = \u8a8d\u8a3c\u4e2d +-m.patching = \u4fee\u6b63\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u5b9f\u884c\u4e2d +-m.launching = \u5b9f\u884c\u4e2d ++m.checking = アップデートの確認中 ++m.validating = 認証中 ++m.patching = 修正プログラムの実行中 ++m.launching = 実行中 + +-m.complete = {0}\uff05\u5b8c\u4e86 +-m.remain = \u3000\u6b8b\u308a{0} ++m.complete = {0}%完了 ++m.remain =  残り{0} + +-m.updating_metadata = \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d ++m.updating_metadata = コントロールファイルのダウンロード中 + +-m.init_failed = \u74b0\u5883\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u307e\u305f\u306f\u58ca\u308c\u3066\u3044\u307e\u3059\u3002 \u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \ +- \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d\u2026 ++m.init_failed = 環境設定ファイルが存在しないか、または壊れています。 新バージョンを \ ++ ダウンロード中… + +-m.java_download_failed = \u304a\u4f7f\u3044\u306e\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u30fc\u306b\u3001Java\u30d7\u30ed\u30b0\u30e9\u30e0\u306e\u6700\u65b0 \ +- \u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u81ea\u52d5\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\n \ +- www.java.com \u304b\u3089\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u3001 \ +- \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.java_download_failed = お使いのコンピューターに、Javaプログラムの最新 \ ++ バージョンを自動インストールできませんでした。\n\n \ ++ www.java.com から最新バージョンを手動でダウンロードして、 \ ++ 再度アプリケーションを起動してください。 + +-m.java_unpack_failed = Java\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u89e3\u51cd \ +- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u30cf\u30fc\u30c9\u30c9\u30e9\u30a4\u30d6\u306e\u30e1\u30e2\u30ea\u304c100MB\u4ee5\u4e0a\u3042\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304b\u3089 \ +- \u518d\u5ea6\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n \ +- \u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001www.java.com \u304b\u3089Java\u306e\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u3092 \ +- \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3066\u304b\u3089\u3001\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002 ++m.java_unpack_failed = Javaのアップデートバージョンが解凍 \ ++ できませんでした。 ハードドライブのメモリが100MB以上あることを確認してから \ ++ 再度アプリケーションを起動してください。\n\n \ ++ 問題が解決しない場合は、www.java.com からJavaの最新バージョンを \ ++ ダウンロードしてから、再度お試しください。 + +-m.unable_to_repair = 5\u56de\u8a66\u884c\u3057\u307e\u3057\u305f\u304c\u3001\u5fc5\u8981\u306a\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9 \ +- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u5f8c\u307b\u3069\u6539\u3081\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \ +- \u518d\u5ea6\u5931\u6557\u3057\u305f\u5834\u5408\u306f\u3001\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u5f8c\u306b\u518d\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.unable_to_repair = 5回試行しましたが、必要なファイルをダウンロード \ ++ できませんでした。 後ほど改めてアプリケーションを実行してください。 \ ++ 再度失敗した場合は、アンインストール後に再インストールしてください。 + +-m.unknown_error = \u539f\u56e0\u4e0d\u660e\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c \ +- \u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u89e3\u6c7a\u65b9\u6cd5\u3092\n{0}\u3067 \ +- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 +-m.init_error = \u6b21\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \ +- \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067 \ +- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.unknown_error = 原因不明のエラーにより、アプリケーションが \ ++ 実行できませんでした。 解決方法を\n{0}で \ ++ 確認してください。 ++m.init_error = 次のエラーによりアプリケーションを実行できません \ ++ でした。\n{0}\n\n対処方法を\n{1}で \ ++ 確認してください。 + +-m.readonly_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u305f\u30d5\u30a9\u30eb\u30c0\u306f \ +- \n{0}\n\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002 \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u308b\u30d5\u30a9\u30eb\u30c0\u306b \ +- \u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.readonly_error = このアプリケーションがインストールされたフォルダは \ ++ \n{0}\n読み取り専用に設定されています。 アプリケーションを書き込みができるフォルダに \ ++ インストールしてください。 + +-m.missing_resource = \u30ea\u30bd\u30fc\u30b9\u4e0d\u660e\u306e\u305f\u3081\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093 \ +- \u3067\u3057\u305f\u3002\n{0}\n\n\u5bfe\u51e6\u65b9\u6cd5\u3092\n{1}\u3067\u78ba\u8a8d \ +- \u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.missing_resource = リソース不明のためアプリケーションを実行できません \ ++ でした。\n{0}\n\n対処方法を\n{1}で確認 \ ++ してください。 + +-m.insufficient_permissions_error = \u3053\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u62d2\u5426 \ +- \u3055\u308c\u307e\u3057\u305f\u3002 \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3059\u308b\u5834\u5408\u306f\u3001\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u306e\u627f\u8a8d\u304c \ +- \u5fc5\u8981\u3067\u3059\u3002\n\n\u627f\u8a8d\u306b\u306f\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304b\u3089\u518d\u5ea6\u958b\u304d\u3001 \ +- \u672c\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3092\u518d\u8868\u793a\u3057\u3066\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044 \u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u306e \ +- \u8b66\u544a\u304c\u8868\u793a\u3055\u308c\u305f\u6642\u306f\u3001\u5b9f\u884c\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u3092\u627f\u8a8d\u3057\u3001 \ +- \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.insufficient_permissions_error = このアプリケーションのデジタル署名が拒否 \ ++ されました。 アプリケーションを実行する場合は、デジタル署名の承認が \ ++ 必要です。\n\n承認には、ブラウザを閉じてから再度開き、 \ ++ 本ホームページを再表示してアプリケーションを再度実行してください セキュリティの \ ++ 警告が表示された時は、実行をクリックしてデジタル署名を承認し、 \ ++ アプリケーションを実行してください。 + +-m.corrupt_digest_signature_error = \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30c7\u30b8\u30bf\u30eb\u7f72\u540d\u304c\u8a8d\u8a3c \ +- \u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n\u6307\u5b9a\u30db\u30fc\u30e0\u30da\u30fc\u30b8\u304b\u3089\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u304b\n \ +- \u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 ++m.corrupt_digest_signature_error = アプリケーションのデジタル署名が認証 \ ++ できませんでした。\n指定ホームページからアプリケーションを実行しているか\n \ ++ 確認してください。 + +-m.default_install_error = \u30db\u30fc\u30e0\u30da\u30fc\u30b8\u3067\u306e\u30b5\u30dd\u30fc\u30c8\u8868\u793a ++m.default_install_error = ホームページでのサポート表示 + + # application/digest errors +-m.missing_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306eappbase\u304c\u4e0d\u660e\u3067\u3059\u3002 +-m.invalid_version = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u52b9\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 +-m.invalid_appbase = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u306aappbase\u3092\u6307\u5b9a\u3057\u3066\u3044\u307e\u3059\u3002 +-m.missing_class = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u306e\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30af\u30e9\u30b9\u304c\u4e0d\u660e\u3067\u3059\u3002 +-m.missing_code = \u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u30b3\u30fc\u30c9\u30ea\u30bd\u30fc\u30b9\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 +-m.invalid_digest_file = \u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u304c\u7121\u52b9\u3067\u3059\u3002 ++m.missing_appbase = 設定ファイルのappbaseが不明です。 ++m.invalid_version = 設定ファイルは無効なバージョンを指定しています。 ++m.invalid_appbase = 設定ファイルが無効なappbaseを指定しています。 ++m.missing_class = 設定ファイルのアプリケーションクラスが不明です。 ++m.missing_code = 設定ファイルでコードリソースが指定されていません。 ++m.invalid_digest_file = ダイジェストファイルが無効です。 +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties +index 3f8a47f35..05700d363 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_ko.properties +@@ -1,102 +1,96 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown Korean translation messages + +-m.abort_title = \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? +-m.abort_confirm = \uC815\uB9D0\uB85C \uC124\uCE58\uB97C \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? \ +- \uB098\uC911\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uC5EC \uC124\uCE58\uB97C \uC7AC\uAC1C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. +-m.abort_ok = \uC911\uC9C0 +-m.abort_cancel = \uACC4\uC18D\uD558\uC5EC \uC124\uCE58 ++m.abort_title = 설치를 취소하시겠습니까? ++m.abort_confirm = 정말로 설치를 취소하시겠습니까? \ ++ 나중에 어플리케이션을 실행하여 설치를 재개하여 주십시오. ++m.abort_ok = 중지 ++m.abort_cancel = 계속하여 설치 + +-m.detecting_proxy = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4 ++m.detecting_proxy = 자동 프록시를 설정을 시도 + +-m.configure_proxy = \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uBC1B\uAE30 \uC704\uD55C \uC11C\uBC84 \uC811\uC18D\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.\ +-

  • \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD \uB610\uB294 \uB178\uD134 \uC778\uD130\uB137 \uC2DC\uD050\uB9AC\uD2F0\uAC00 javaw.exe\uC774 \uC124\uC815\uC5D0\uC11C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC744 \uACBD\uC6B0, \ +- \uAC8C\uC784 \uB370\uC774\uD130\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \ +- javaw.exe\uAC00 \uC778\uD130\uB137 \uC5F0\uACB0\uC744 \uD560 \uC218 \uC788\uB3C4\uB85D \uC124\uC815\uC744 \uBCC0\uACBD\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \ +- \uAC8C\uC784\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uD6C4, \uBC29\uD654\uBCBD \uC124\uC815\uC5D0\uC11C javaw.exe\uB97C \uC0AD\uC81C\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \ +- ( \uC2DC\uC791 -> \uC81C\uC5B4\uD310 -> \uC708\uB3C4\uC6B0 \uBC29\uD654\uBCBD )
\ +-

\uCEF4\uD4E8\uD130\uAC00 \uD504\uB85D\uC2DC \uC11C\uBC84\uB97C \uD1B5\uD574 \uC778\uD130\uB137\uC5D0 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB2E4\uBA74, \uD504\uB85D\uC2DC \uC124\uC815\uC758 \uC790\uB3D9 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC73C\uBBC0\uB85C, \ +- \uC0AC\uC6A9\uD558\uB294 \uD504\uB85D\uC2DC \uC124\uC815\uC744 \uC54C\uACE0 \uC788\uC744 \uACBD\uC6B0 \uC544\uB798\uC5D0 \uC785\uB825\uD558\uC5EC \uC8FC\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4. ++m.configure_proxy = 게임 데이터를 받기 위한 서버 접속에 실패하였습니다.\ ++

  • 윈도우 방화벽 또는 노턴 인터넷 시큐리티가 javaw.exe이 설정에서 차단되어 있을 경우, \ ++ 게임 데이터를 다운로드 할 수 없습니다. \ ++ javaw.exe가 인터넷 연결을 할 수 있도록 설정을 변경하여 주십시오. \ ++ 게임을 다시 실행한 후, 방화벽 설정에서 javaw.exe를 삭제하여 주십시오. \ ++ ( 시작 -> 제어판 -> 윈도우 방화벽 )
\ ++

컴퓨터가 프록시 서버를 통해 인터넷에 연결되어 있다면, 프록시 설정의 자동 구성을 사용할 수 없으므로, \ ++ 사용하는 프록시 설정을 알고 있을 경우 아래에 입력하여 주시길 바랍니다. + +-m.proxy_extra = \uC790\uB3D9 \uD504\uB85D\uC2DC\uB97C \uC124\uC815\uC744 \uC2DC\uB3C4 ++m.proxy_extra = 자동 프록시를 설정을 시도 + +-m.proxy_host = \uD504\uB85D\uC2DC IP +-m.proxy_port = \uD504\uB85D\uC2DC \uD3EC\uD2B8 ++m.proxy_host = 프록시 IP ++m.proxy_port = 프록시 포트 + m.proxy_username = Username + m.proxy_password = Password + m.proxy_auth_required = Authentication required + m.proxy_ok = OK +-m.proxy_cancel = \uCDE8\uC18C +- +-m.downloading_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uB2E4\uC6B4\uB85C\uB4DC \uC911 +-m.unpacking_java = \uC790\uBC14 \uAC00\uC0C1 \uBA38\uC2E0(JVM) \uC555\uCD95\uC744 \uD574\uC81C\uD558\uB294 \uC911 +- +-m.resolving = \uB2E4\uC6B4\uB85C\uB4DC \uBD84\uC11D \uC911 +-m.downloading = \uB370\uC774\uD130 \uB2E4\uC6B4\uB85C\uB4DC \uC911 +-m.failure = \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: {0} ++m.proxy_cancel = 취소 + +-m.checking = \uC5C5\uB370\uC774\uD2B8 \uCCB4\uD06C +-m.validating = \uC720\uD6A8\uC131 \uAC80\uC0AC \uC911 +-m.patching = \uD328\uCE58 \uC911 +-m.launching = \uC2E4\uD589 \uC911 ++m.downloading_java = 자바 가상 머신(JVM) 다운로드 중 ++m.unpacking_java = 자바 가상 머신(JVM) 압축을 해제하는 중 + +-m.patch_notes = \uD328\uCE58 \uB178\uD2B8 +-m.play_again = \uB2E4\uC2DC \uC2E4\uD589 ++m.resolving = 다운로드 분석 중 ++m.downloading = 데이터 다운로드 중 ++m.failure = 다운로드 실패: {0} + +-m.complete = {0}% \uC644\uB8CC +-m.remain = {0} \uB0A8\uC74C ++m.checking = 업데이트 체크 ++m.validating = 유효성 검사 중 ++m.patching = 패치 중 ++m.launching = 실행 중 + +-m.updating_metadata = \uCEE8\uD2B8\uB864 \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911 ++m.patch_notes = 패치 노트 ++m.complete = {0}% 완료 ++m.remain = {0} 남음 + +-m.init_failed = \uC124\uC815 \uD30C\uC77C\uC774 \uB204\uB77D\uB418\uC5C8\uAC70\uB098 \uBCC0\uD615\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \ +- \uC0C8\uB85C\uC6B4 \uBCF5\uC0AC\uBCF8\uC744 \uB2E4\uC6B4\uB85C\uB4DC \uC911\uC785\uB2C8\uB2E4... ++m.updating_metadata = 컨트롤 파일을 다운로드 중 + +-m.java_download_failed = \uC774 \uCEF4\uD4E8\uD130\uC5D0 \uD544\uC694\uD55C \uC0C8\uB85C\uC6B4 \uBC84\uC804\uC758 \uC790\uBC14\uB97C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\n\ +- \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \ +- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624. ++m.init_failed = 설정 파일이 누락되었거나 변형되었습니다. \ ++ 새로운 복사본을 다운로드 중입니다... + +-m.java_unpack_failed = \uC5C5\uB370\uC774\uD2B8\uB41C \uBC84\uC804\uC758 \uC790\uBC14\uC758 \uC555\uCD95\uC744 \uD480 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \ +- \uD558\uB4DC\uB4DC\uB77C\uC774\uBE0C\uC5D0 \uCD5C\uC18C\uD55C 100MB\uC758 \uC6A9\uB7C9\uC744 \uD655\uBCF4\uD55C \uC774\uD6C4, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624.\n\n\ +- \uB9CC\uC57D \uBB38\uC81C\uAC00 \uD574\uACB0\uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uC790\uBC14 \uC6F9\uC0AC\uC774\uD2B8(www.java.com)\uB85C \uAC00\uC11C \uCD5C\uC2E0\uC758 \uC790\uBC14\uB97C \uB2E4\uC6B4\uB85C\uB4DC \uBC1B\uC73C\uC2E0 \uD6C4, \ +- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC2ED\uC2DC\uC624. ++m.java_download_failed = 이 컴퓨터에 필요한 새로운 버전의 자바를 자동으로 다운로드할 수 없습니다.\n\n\ ++ 자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \ ++ 어플리케이션을 다시 실행해 주십시오. + +-m.unable_to_repair = \uB2E4\uC12F\uBC88\uC758 \uC2DC\uB3C4\uC5D0\uB3C4 \uD544\uC694\uD55C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \ +- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD574\uBCF4\uC2DC\uACE0, \uADF8\uB798\uB3C4 \uB2E4\uC6B4\uB85C\uB4DC\uC5D0 \uC2E4\uD328\uD55C\uB2E4\uBA74, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC81C\uAC70\uD55C \uD6C4, \uB2E4\uC2DC \uC2E4\uD589\uD574\uBCF4\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. ++m.java_unpack_failed = 업데이트된 버전의 자바의 압축을 풀 수 없습니다. \ ++ 하드드라이브에 최소한 100MB의 용량을 확보한 이후, 어플리케이션을 다시 실행해 주십시오.\n\n\ ++ 만약 문제가 해결되지 않는다면, 자바 웹사이트(www.java.com)로 가서 최신의 자바를 다운로드 받으신 후, \ ++ 어플리케이션을 다시 실행해 주십시오. + +-m.unknown_error = \uBCF5\uAD6C\uB420 \uC218 \uC5C6\uB294 \uC624\uB958\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \ +- \n{0}\uC5D0 \uB300\uD55C \uBCF5\uAD6C \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4. ++m.unable_to_repair = 다섯번의 시도에도 필요한 파일을 다운로드하지 못했습니다. \ ++ 어플리케이션을 다시 시작해보시고, 그래도 다운로드에 실패한다면, 어플리케이션을 제거한 후, 다시 실행해보시기 바랍니다. + +-m.init_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC544\uB798\uC640 \uAC19\uC740 \uC5D0\uB7EC\uB85C \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC5D0\uB7EC:\ +- \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4. ++m.unknown_error = 복구될 수 없는 오류로 인하여 어플리케이션의 실행이 중단되었습니다. \ ++ \n{0}에 대한 복구 방법을 찾기 위해서 방문하시길 바랍니다. + +-m.readonly_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC124\uCE58\uB41C \uB514\uB809\uD1A0\uB9AC: \ +- \n{0}\n\uAC00 \uC77D\uAE30 \uC804\uC6A9\uC785\uB2C8\uB2E4. \uC77D\uAE30 \uAD8C\uD55C\uC774 \uC2B9\uC778\uB41C \uB809\uD1A0\uB9AC\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC124\uCE58\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4. ++m.init_error = 어플리케이션이 아래와 같은 에러로 실행이 중단되었습니다. 에러:\ ++ \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다. + +-m.missing_resource = \uB9AC\uC18C\uC2A4\uC758 \uC190\uC2E4\uB85C \uC778\uD558\uC5EC \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. : \ +- \n{0}\n\n{1}\uC5D0 \uB300\uD55C \uBB38\uC81C \uD574\uACB0 \uBC29\uBC95\uC744 \uCC3E\uAE30 \uC704\uD574\uC11C \uBC29\uBB38\uD558\uC2DC\uAE38 \uBC14\uB78D\uB2C8\uB2E4. ++m.readonly_error = 어플리케이션이 설치된 디렉토리: \ ++ \n{0}\n가 읽기 전용입니다. 읽기 권한이 승인된 렉토리에 어플리케이션을 설치하시길 바랍니다. + +-m.insufficient_permissions_error = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \ +- \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC2E4\uD589\uD558\uAE30 \uC704\uD574\uC11C \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD558\uC5EC \uC8FC\uC2ED\uC2DC\uC624. \ +- \n\n\uADF8\uB9AC\uACE0 \uB098\uC11C \uC6F9 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uACE0 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC5EC \uC6F9\uD398\uC774\uC9C0\uB85C \uB3CC\uC544\uC640 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC744 \uC7AC\uC2DC\uC791\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. \ +- \uBCF4\uC548\uC5D0 \uB300\uD55C \uB300\uD654\uC0C1\uC790\uAC00 \uBCF4\uC774\uBA74, \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC5D0 \uB300\uD55C \uD655\uC778 \uBC84\uD2BC\uC744 \uD074\uB9AD\uD558\uACE0, \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uAE30 \uC704\uD55C \ +- \uAD8C\uD55C\uC744 \uBD80\uC5EC\uD574\uC8FC\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. ++m.missing_resource = 리소스의 손실로 인하여 어플리케이션의 실행이 중단되었습니다. : \ ++ \n{0}\n\n{1}에 대한 문제 해결 방법을 찾기 위해서 방문하시길 바랍니다. + +-m.corrupt_digest_signature_error = \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC758 \uB514\uC9C0\uD0C8 \uC11C\uBA85\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n \ +- \uC62C\uBC14\uB978 \uC6F9\uC0AC\uC774\uD2B8\uC5D0\uC11C \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158\uC774 \uC2E4\uD589\uB418\uACE0 \uC788\uB294 \uC9C0 \uD655\uC778\uBC14\uB78D\uB2C8\uB2E4. ++m.insufficient_permissions_error = 이 어플리케이션의 디지탈 서명을 확인하지 않았습니다. \ ++ 어플리케이션을 실행하기 위해서 디지탈 서명을 확인하여 주십시오. \ ++ \n\n그리고 나서 웹 브라우저를 닫고 다시 시작하여 웹페이지로 돌아와 어플리케이션을 재시작해주시기 바랍니다. \ ++ 보안에 대한 대화상자가 보이면, 디지탈 서명에 대한 확인 버튼을 클릭하고, 어플리케이션이 실행되기 위한 \ ++ 권한을 부여해주시기 바랍니다. + +-m.default_install_error = \uC6F9\uC0AC\uC774\uD2B8\uC758 \uC9C0\uC6D0 \uBA54\uB274(support section)\uB97C \uD655\uC778\uD558\uC2DC\uAE30 \uBC14\uB78D\uB2C8\uB2E4. ++m.corrupt_digest_signature_error = 어플리케이션의 디지탈 서명을 확인할 수 없습니다.\n \ ++ 올바른 웹사이트에서 어플리케이션이 실행되고 있는 지 확인바랍니다. + +-m.another_getdown_running = \uC774 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uC778\uC2A4\uD1A8\uB7EC\uC758 \uB2E4\uC911 \uC778\uC2A4\uD134\uC2A4\uAC00 \uC2E4\uD589\uC911\uC785\uB2C8\uB2E4. \ +- \uD558\uB098\uAC00 \uC644\uB8CC\uB420 \uB54C\uAE4C\uC9C0 \uC911\uB2E8\uB429\uB2C8\uB2E4. ++m.default_install_error = 웹사이트의 지원 메뉴(support section)를 확인하시기 바랍니다. + +-m.applet_stopped = Getdown \uC560\uD50C\uB9BF \uC2E4\uD589\uC774 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. ++m.another_getdown_running = 이 어플리케이션 인스톨러의 다중 인스턴스가 실행중입니다. \ ++ 하나가 완료될 때까지 중단됩니다. + + # application/digest errors +-m.missing_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 'appbase' \uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. +-m.invalid_version = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C \uBC84\uC804\uC774 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. +-m.invalid_appbase = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC798\uBABB\uB41C 'appbase'\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. +-m.missing_class = \uC124\uC815 \uD30C\uC77C\uC5D0 \uC5B4\uD50C\uB9AC\uCF00\uC774\uC158 \uD074\uB798\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. +-m.missing_code = \uC124\uC815 \uD30C\uC77C\uC5D0 \uB9AC\uC18C\uC2A4\uC5D0 \uB300\uD55C \uCF54\uB4DC\uAC00 \uBA85\uC2DC\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. +-m.invalid_digest_file = \uB2E4\uC774\uC81C\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC798\uBABB\uB418\uC5C8\uC2B5\uB2C8\uB2E4. ++m.missing_appbase = 설정 파일에 'appbase' 가 없습니다. ++m.invalid_version = 설정 파일에 잘못된 버전이 명시되어 있습니다. ++m.invalid_appbase = 설정 파일에 잘못된 'appbase'가 명시되어 있습니다. ++m.missing_class = 설정 파일에 어플리케이션 클래스가 없습니다. ++m.missing_code = 설정 파일에 리소스에 대한 코드가 명시되어 있지 않습니다. ++m.invalid_digest_file = 다이제스트 파일이 잘못되었습니다. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties +index 47db91c90..e59ed20b2 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_pt.properties +@@ -1,118 +1,112 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown Portuguese translation messages + +-m.abort_title = Cancelar a instala\u00E7\u00E3o? +-m.abort_confirm = Tem certeza que deseja cancelar a instala\u00E7\u00E3o? \ +- Voc\u00EA pode continuar a instala\u00E7\u00E3o mais tarde, \ +- basta executar a aplica\u00E7\u00E3o novamente. ++m.abort_title = Cancelar a instalação? ++m.abort_confirm = Tem certeza que deseja cancelar a instalação? \ ++ Você pode continuar a instalação mais tarde, \ ++ basta executar a aplicação novamente. + m.abort_ok = Sair +-m.abort_cancel = Continuar a instala\u00E7\u00E3o ++m.abort_cancel = Continuar a instalação + +-m.detecting_proxy = Tentando detectar automaticamente as configura\u00E7\u00F5es de proxy ++m.detecting_proxy = Tentando detectar automaticamente as configurações de proxy + +-m.configure_proxy = N\u00E3o foi poss\u00EDvel conectar aos nossos servidores para \ ++m.configure_proxy = Não foi possível conectar aos nossos servidores para \ + fazer o download dos dados. \ +-

  • Se o Firewall do Windows ou o Norton Internet Security est\u00E1 configurado \ +- para bloquear o programa javaw.exe n\u00E3o ser\u00E1 poss\u00EDvel realizar \ +- o download. Voc\u00EA ter\u00E1 que permitir que o programa javaw.exe acesse \ +- a internet. Voc\u00EA pode tentar executar o programa novamente, mas voc\u00EA precisa \ +- remover o programa javaw.exe das configura\u00E7\u00F5es do firewall (Iniciar -> Painel \ ++
    • Se o Firewall do Windows ou o Norton Internet Security está configurado \ ++ para bloquear o programa javaw.exe não será possível realizar \ ++ o download. Você terá que permitir que o programa javaw.exe acesse \ ++ a internet. Você pode tentar executar o programa novamente, mas você precisa \ ++ remover o programa javaw.exe das configurações do firewall (Iniciar -> Painel \ + de controle -> Firewall do Windows).
    \ +-

    Seu computador pode estar acessando a internet atrav\u00E9s de um proxy e n\u00E3o foi \ +- capaz de detectar automaticamente as configura\u00E7\u00F5es de proxy. \ +- Voc\u00EA pode informar esses dados abaixo. ++

    Seu computador pode estar acessando a internet através de um proxy e não foi \ ++ capaz de detectar automaticamente as configurações de proxy. \ ++ Você pode informar esses dados abaixo. + +-m.proxy_extra = Se voc\u00EA tem certeza que n\u00E3o usa um proxy, ent\u00E3o pode ser \ +- que exista um problema tempor\u00E1rio que est\u00E1 impedindo a comunica\u00E7\u00E3o \ +- com os nossos servidores. Neste caso voc\u00EA pode cancelar e tentar instalar novamente \ ++m.proxy_extra = Se você tem certeza que não usa um proxy, então pode ser \ ++ que exista um problema temporário que está impedindo a comunicação \ ++ com os nossos servidores. Neste caso você pode cancelar e tentar instalar novamente \ + mais tarde. + + m.proxy_host = IP do Proxy + m.proxy_port = Porta do Proxy +-m.proxy_username = Nome de usu\u00e1rio ++m.proxy_username = Nome de usuário + m.proxy_password = Senha +-m.proxy_auth_required = Autentifica\u00e7\u00e3o requerida ++m.proxy_auth_required = Autentificação requerida + m.proxy_ok = OK + m.proxy_cancel = Cancelar + +-m.downloading_java = Fazendo o download da m\u00E1quina virtual Java +-m.unpacking_java = Descompactando a m\u00E1quina virtual Java ++m.downloading_java = Fazendo o download da máquina virtual Java ++m.unpacking_java = Descompactando a máquina virtual Java + + m.resolving = Resolvendo downloads + m.downloading = Transferindo dados + m.failure = Download falhou: {0} + +-m.checking = Verificando atualiza\u00E7\u00F5es ++m.checking = Verificando atualizações + m.validating = Validando + m.patching = Atualizando + m.launching = Executando + + m.patch_notes = Corrigir notas +-m.play_again = Jogar de novo +- + m.complete = {0}% completo + m.remain = {0} Permanecer + + m.updating_metadata = Transferindo arquivos de controle + +-m.init_failed = Nosso arquivo de configura\u00E7\u00E3o est\u00E1 ausente ou corrompido. Tente \ +- baixar uma nova c\u00F3pia... ++m.init_failed = Nosso arquivo de configuração está ausente ou corrompido. Tente \ ++ baixar uma nova cópia... + +-m.java_download_failed = N\u00E3o conseguimos baixar automaticamente a\ +- vers\u00E3o necess\u00E1ria do Java para o seu computador.\n\n\ +- Por favor, acesse www.java.com, baixe e instale a \u00FAltima vers\u00E3o do \ ++m.java_download_failed = Não conseguimos baixar automaticamente a\ ++ versão necessária do Java para o seu computador.\n\n\ ++ Por favor, acesse www.java.com, baixe e instale a última versão do \ + Java, em seguida, tente executar o aplicativo novamente. + +-m.java_unpack_failed = N\u00E3o conseguimos descompactar uma vers\u00E3o atualizada do \ +- Java. Por favor, certifique-se de ter pelo menos 100 MB de espa\u00E7o livre em seu \ +- disco r\u00EDgido e tente executar o aplicativo novamente. \n\n\ +- Se isso n\u00E3o resolver o problema, acesse www.java.com,baixe e \ +- instale a \u00FAltima vers\u00E3o do Java e tente novamente. ++m.java_unpack_failed = Não conseguimos descompactar uma versão atualizada do \ ++ Java. Por favor, certifique-se de ter pelo menos 100 MB de espaço livre em seu \ ++ disco rígido e tente executar o aplicativo novamente. \n\n\ ++ Se isso não resolver o problema, acesse www.java.com,baixe e \ ++ instale a última versão do Java e tente novamente. + +-m.unable_to_repair = N\u00E3o conseguimos baixar os arquivos necess\u00E1rios depois de \ +- cinco tentativas. Voc\u00EA pode tentar executar o aplicativo novamente, mas se ele \ +- falhar pode ser necess\u00E1rio desinstalar e reinstalar. ++m.unable_to_repair = Não conseguimos baixar os arquivos necessários depois de \ ++ cinco tentativas. Você pode tentar executar o aplicativo novamente, mas se ele \ ++ falhar pode ser necessário desinstalar e reinstalar. + +-m.unknown_error = A aplica\u00E7\u00E3o falhou ao iniciar devido a algum erro estranho \ +- do qual n\u00E3o conseguimos recuperar. Por favor, visite \n{0} para obter \ +- informa\u00E7\u00F5es sobre como recuperar. +-m.init_error = A aplica\u00E7\u00E3o falhou ao iniciar devido ao seguinte \ ++m.unknown_error = A aplicação falhou ao iniciar devido a algum erro estranho \ ++ do qual não conseguimos recuperar. Por favor, visite \n{0} para obter \ ++ informações sobre como recuperar. ++m.init_error = A aplicação falhou ao iniciar devido ao seguinte \ + erro:\n{0}\n\nPor favor visite \n{1} para \ +- informa\u00E7\u00F5es sobre como lidar com esse problema. ++ informações sobre como lidar com esse problema. + +-m.readonly_error =O diret\u00F3rio no qual este aplicativo est\u00E1 instalado: \ +- \n{0}\n \u00E9 somente leitura. Por favor, instale o aplicativo em um diret\u00F3rio onde \ +- voc\u00EA tem acesso de grava\u00E7\u00E3o. ++m.readonly_error =O diretório no qual este aplicativo está instalado: \ ++ \n{0}\n é somente leitura. Por favor, instale o aplicativo em um diretório onde \ ++ você tem acesso de gravação. + +-m.missing_resource = A aplica\u00E7\u00E3o falhou ao iniciar devido a uma falta \ +- de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informa\u00E7\u00F5es sobre \ ++m.missing_resource = A aplicação falhou ao iniciar devido a uma falta \ ++ de recurso:\n{0}\n\n Por favor, visite\n{1} para obter informações sobre \ + como lidar com tal problema. + +-m.insufficient_permissions_error = Voc\u00EA n\u00E3o aceitou a assinatura digital \ +- do aplicativo. Se voc\u00EA quiser executar o aplicativo, voc\u00EA ter\u00E1 que aceitar \ +- a assinatura digital. \n\nPara fazer isso, voc\u00EA ter\u00E1 que sair do seu navegador, \ +- reinici\u00E1-lo, e retornar a esta p\u00E1gina web para executar a aplica\u00E7\u00E3o. \ +- Quando o di\u00E1logo de seguran\u00E7a aparecer, clique no bot\u00E3o para aceitar a \ +- assinatura digital e conceder a este aplicativo os privil\u00E9gios necess\u00E1rios \ ++m.insufficient_permissions_error = Você não aceitou a assinatura digital \ ++ do aplicativo. Se você quiser executar o aplicativo, você terá que aceitar \ ++ a assinatura digital. \n\nPara fazer isso, você terá que sair do seu navegador, \ ++ reiniciá-lo, e retornar a esta página web para executar a aplicação. \ ++ Quando o diálogo de segurança aparecer, clique no botão para aceitar a \ ++ assinatura digital e conceder a este aplicativo os privilégios necessários \ + para executar. + +-m.corrupt_digest_signature_error = N\u00E3o conseguimos verificar a assinatura digital \ +- do aplicativo.\nPor favor, verifique se voc\u00EA est\u00E1 utilizando o aplicativo \nde um \ ++m.corrupt_digest_signature_error = Não conseguimos verificar a assinatura digital \ ++ do aplicativo.\nPor favor, verifique se você está utilizando o aplicativo \nde um \ + site correto. + +-m.default_install_error = a se\u00E7\u00E3o de suporte do site +- +-m.another_getdown_running = V\u00E1rias inst\u00E2ncias desta aplica\u00E7\u00E3o \ +- est\u00E3o em execu\u00E7\u00E3o. Esta ir\u00E1 parar e deixar outra completar suas atividades. ++m.default_install_error = a seção de suporte do site + +-m.applet_stopped = Foi solicitado ao miniaplicativo GetDow que parasse de trabalhar. ++m.another_getdown_running = Várias instâncias desta aplicação \ ++ estão em execução. Esta irá parar e deixar outra completar suas atividades. + + # application/digest errors +-m.missing_appbase = O arquivo de configura\u00E7\u00E3o n\u00E3o possui o 'AppBase'. +-m.invalid_version = O arquivo de configura\u00E7\u00E3o especifica uma vers\u00E3o inv\u00E1lida. +-m.invalid_appbase = O arquivo de configura\u00E7\u00E3o especifica um 'AppBase' inv\u00E1lido. +-m.missing_class = O arquivo de configura\u00E7\u00E3o n\u00E3o possui a classe de aplicativo. +-m.missing_code = O arquivo de configura\u00E7\u00E3o n\u00E3o especifica um recurso de c\u00F3digo. +-m.invalid_digest_file = O arquivo digest \u00E9 inv\u00E1lido. ++m.missing_appbase = O arquivo de configuração não possui o 'AppBase'. ++m.invalid_version = O arquivo de configuração especifica uma versão inválida. ++m.invalid_appbase = O arquivo de configuração especifica um 'AppBase' inválido. ++m.missing_class = O arquivo de configuração não possui a classe de aplicativo. ++m.missing_code = O arquivo de configuração não especifica um recurso de código. ++m.invalid_digest_file = O arquivo digest é inválido. +diff --git a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties +index 2c275437b..fa74fb293 100644 +--- a/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties ++++ b/getdown/src/getdown/launcher/src/main/resources/com/threerings/getdown/messages_zh.properties +@@ -1,61 +1,57 @@ + # +-# $Id$ +-# +-# Getdown translation messages ++# Getdown Chinese translation messages + +-m.detecting_proxy = \u641c\u5bfb\u4ee3\u7406\u670d\u52a1\u5668 ++m.detecting_proxy = 搜寻代理服务器 + +-m.configure_proxy = \u6211\u4eec\u65e0\u6cd5\u8fde\u63a5\u5230\u670d\u52a1\u5668\u4e0b\u8f7d\u6e38\u620f\u6570\u636e\u3002\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e \ +- \u60a8\u7684\u8ba1\u7b97\u673a\u662f\u901a\u8fc7\u4ee3\u7406\u670d\u52a1\u5668\u8fde\u63a5\u4e92\u8054\u7f51\u7684\uff0c\u5e76\u4e14\u6211\u4eec\u65e0\u6cd5\u81ea\u52a8\u83b7\u5f97\u4ee3\u7406\u670d\u52a1\u5668\u7684 \ +- \u8bbe\u7f6e\u3002\u5982\u679c\u60a8\u77e5\u9053\u60a8\u4ee3\u7406\u670d\u52a1\u5668\u7684\u8bbe\u7f6e\uff0c\u60a8\u53ef\u4ee5\u5728\u4e0b\u9762\u8f93\u5165\u3002 ++m.configure_proxy = 我们无法连接到服务器下载游戏数据。这可能是由于 \ ++ 您的计算机是通过代理服务器连接互联网的,并且我们无法自动获得代理服务器的 \ ++ 设置。如果您知道您代理服务器的设置,您可以在下面输入。 + +-m.proxy_extra = \u5982\u679c\u60a8\u786e\u5b9a\u60a8\u6ca1\u6709\u4f7f\u7528\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8fd9\u53ef\u80fd\u662f\u7531\u4e8e\u6682\u65f6\u65e0\u6cd5 \ +- \u8fde\u63a5\u5230\u4e92\u8054\u7f51\uff0c\u5bfc\u81f4\u65e0\u6cd5\u548c\u670d\u52a1\u5668\u901a\u8baf\u3002\u8fd9\u79cd\u60c5\u51b5\uff0c\u60a8\u53ef\u4ee5\u53d6\u6d88\uff0c\u7a0d\u5019\u518d\u91cd\u65b0\u5b89\u88c5\u3002

    \ +- \u5982\u679c\u60a8\u65e0\u6cd5\u786e\u5b9a\u60a8\u662f\u5426\u4f7f\u7528\u4e86\u4ee3\u7406\u670d\u52a1\u5668\uff0c\u8bf7\u8bbf\u95ee\u6211\u4eec\u7f51\u7ad9\u4e2d\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c \ +- \u4e86\u89e3\u5982\u4f55\u68c0\u6d4b\u60a8\u7684\u4ee3\u7406\u670d\u52a1\u5668\u8bbe\u7f6e\u3002 ++m.proxy_extra = 如果您确定您没有使用代理服务器,这可能是由于暂时无法 \ ++ 连接到互联网,导致无法和服务器通讯。这种情况,您可以取消,稍候再重新安装。

    \ ++ 如果您无法确定您是否使用了代理服务器,请访问我们网站中的技术支持部份, \ ++ 了解如何检测您的代理服务器设置。 + +-m.proxy_host = \u4ee3\u7406\u670d\u52a1\u5668\u7684IP\u5730\u5740 +-m.proxy_port = \u4ee3\u7406\u670d\u52a1\u5668\u7684\u7aef\u53e3\u53f7 ++m.proxy_host = 代理服务器的IP地址 ++m.proxy_port = 代理服务器的端口号 + m.proxy_username = Username + m.proxy_password = Password + m.proxy_auth_required = Authentication required +-m.proxy_ok = \u786e\u5b9a +-m.proxy_cancel = \u53d6\u6d88 +- +-m.resolving = \u5206\u6790\u9700\u4e0b\u8f7d\u5185\u5bb9 +-m.downloading = \u4e0b\u8f7d\u6570\u636e +-m.failure = \u4e0b\u8f7d\u5931\u8d25: {0} +- +-m.checking = \u68c0\u67e5\u66f4\u65b0\u5185\u5bb9 +-m.validating = \u786e\u8ba4 +-m.patching = \u5347\u7ea7 +-m.launching = \u542f\u52a8 ++m.proxy_ok = 确定 ++m.proxy_cancel = 取消 + +-m.complete = {0}% \u5b8c\u6210 +-m.remain = {0} \u5269\u4f59\u65f6\u95f4 ++m.resolving = 分析需下载内容 ++m.downloading = 下载数据 ++m.failure = 下载失败: {0} + +-m.updating_metadata = \u4e0b\u8f7d\u63a7\u5236\u6587\u4ef6 ++m.checking = 检查更新内容 ++m.validating = 确认 ++m.patching = 升级 ++m.launching = 启动 + +-m.init_failed = \u65e0\u6cd5\u627e\u5230\u914d\u7f6e\u6587\u4ef6\u6216\u5df2\u635f\u574f\u3002\u5c1d\u8bd5\u91cd\u65b0\u4e0b\u8f7d... ++m.complete = {0}% 完成 ++m.remain = {0} 剩余时间 + +-m.unable_to_repair = \u7ecf\u8fc75\u6b21\u5c1d\u8bd5\uff0c\u4f9d\u7136\u65e0\u6cd5\u4e0b\u8f7d\u6240\u9700\u7684\u6587\u4ef6\u3002\ +-\u60a8\u53ef\u4ee5\u91cd\u65b0\u8fd0\u884c\u7a0b\u5e8f\uff0c\u4f46\u662f\u5982\u679c\u4f9d\u7136\u5931\u8d25\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u53cd\u5b89\u88c5\u5e76\u91cd\u65b0\u5b89\u88c5\u3002 ++m.updating_metadata = 下载控制文件 + ++m.init_failed = 无法找到配置文件或已损坏。尝试重新下载... + +-m.unknown_error = \u7531\u4e8e\u4e00\u4e9b\u65e0\u6cd5\u56de\u590d\u7684\u4e25\u91cd\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\u3002\ +-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u89e3\u51b3\u95ee\u9898\u3002 ++m.unable_to_repair = 经过5次尝试,依然无法下载所需的文件。\ ++您可以重新运行程序,但是如果依然失败,您可能需要反安装并重新安装。 + +-m.init_error = \u7531\u4e8e\u4e0b\u5217\u9519\u8bef\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \ +-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u9519\u8bef\u3002 ++m.unknown_error = 由于一些无法回复的严重错误,程序启动失败。\ ++请访问我们的网站的技术支持部份,了解如何解决问题。 + ++m.init_error = 由于下列错误,程序启动失败:\n{0}\n\n \ ++请访问我们的网站的技术支持部份,了解如何处理这些错误。 + +-m.missing_resource = \u7531\u4e8e\u65e0\u6cd5\u627e\u5230\u4e0b\u5217\u8d44\u6e90\uff0c\u7a0b\u5e8f\u542f\u52a8\u5931\u8d25\uff1a\n{0}\n\n \ +-\u8bf7\u8bbf\u95ee\u6211\u4eec\u7684\u7f51\u7ad9\u7684\u6280\u672f\u652f\u6301\u90e8\u4efd\uff0c\u4e86\u89e3\u5982\u4f55\u5904\u7406\u8fd9\u4e9b\u95ee\u9898\u3002 ++m.missing_resource = 由于无法找到下列资源,程序启动失败:\n{0}\n\n \ ++请访问我们的网站的技术支持部份,了解如何处理这些问题。 + + # application/digest errors +-m.missing_appbase = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230 'appbase'\u3002 +-m.invalid_version = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684\u7248\u672c\u3002 +-m.invalid_appbase = \u914d\u7f6e\u6587\u4ef6\u6307\u5b9a\u4e86\u65e0\u6548\u7684 'appbase'\u3002 +-m.missing_class = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u7a0b\u5e8f\u6587\u4ef6\u3002 +-m.missing_code = \u914d\u7f6e\u6587\u4ef6\u4e2d\u65e0\u6cd5\u627e\u5230\u6307\u5b9a\u7684\u8d44\u6e90\u3002 +-m.invalid_digest_file = \u65e0\u6548\u7684\u914d\u7f6e\u6587\u4ef6\u3002 ++m.missing_appbase = 配置文件中无法找到 'appbase'。 ++m.invalid_version = 配置文件指定了无效的版本。 ++m.invalid_appbase = 配置文件指定了无效的 'appbase'。 ++m.missing_class = 配置文件中无法找到程序文件。 ++m.missing_code = 配置文件中无法找到指定的资源。 ++m.invalid_digest_file = 无效的配置文件。 +diff --git a/getdown/src/getdown/lib/commons-compress-1.18.jar b/getdown/src/getdown/lib/commons-compress-1.18.jar +deleted file mode 100644 +index e401046b5..000000000 +Binary files a/getdown/src/getdown/lib/commons-compress-1.18.jar and /dev/null differ +diff --git a/getdown/src/getdown/mvn_cmd b/getdown/src/getdown/mvn_cmd +deleted file mode 100644 +index 0ce786ff8..000000000 +--- a/getdown/src/getdown/mvn_cmd ++++ /dev/null +@@ -1 +0,0 @@ +-mvn clean package -Dgetdown.host.whitelist="jalview.org,*.jalview.org" && cp launcher/target/getdown-launcher-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-launcher.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../getdown/lib/getdown-core-1.8.3-SNAPSHOT.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j8lib/getdown-core.jar && cp core/target/getdown-core-1.8.3-SNAPSHOT.jar ../../../j11lib/getdown-core.jar +diff --git a/getdown/src/getdown/pom.xml b/getdown/src/getdown/pom.xml +index 78d67b0a5..ae1370dde 100644 +--- a/getdown/src/getdown/pom.xml ++++ b/getdown/src/getdown/pom.xml +@@ -10,7 +10,7 @@ + com.threerings.getdown + getdown + pom +- 1.8.3-SNAPSHOT ++ 1.8.7-SNAPSHOT + + getdown + An application installer and updater. +@@ -35,6 +35,10 @@ + + + ++ ++ UTF-8 ++ ++ + + scm:git:git://github.com/threerings/getdown.git + scm:git:git@github.com:threerings/getdown.git +@@ -47,28 +51,6 @@ + ant + + +- +- +- ej-technologies +- https://maven.ej-technologies.com/repository +- +- +- +- +- +- +- org.apache.commons +- commons-compress +- 1.18 +- +- +- com.install4j +- install4j-runtime +- 7.0.11 +- provided +- +- +- + + + +@@ -83,6 +65,20 @@ + aa555c46fc37d0 + + ++ ++ ++ org.apache.maven.plugins ++ maven-javadoc-plugin ++ 3.1.0 ++ ++ true ++ public ++ ++ -Xdoclint:all ++ -Xdoclint:-missing ++ ++ ++ + + + +@@ -91,10 +87,10 @@ + + org.apache.maven.plugins + maven-compiler-plugin +- 3.8.1 ++ 3.7.0 + +- 1.8 +- 1.8 ++ 1.7 ++ 1.7 + true + true + true +@@ -110,20 +106,12 @@ + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 +- +- UTF-8 +- + + + + org.apache.maven.plugins +- maven-javadoc-plugin +- 3.0.0-M1 +- +- true +- public +- -Xdoclint:all -Xdoclint:-missing +- ++ maven-gpg-plugin ++ 1.6 + + + +@@ -181,7 +169,6 @@ + + org.apache.maven.plugins + maven-gpg-plugin +- 1.6 + + + sign-artifacts