From 01825b6f88674cc8db890b876aa02ff1c050901b Mon Sep 17 00:00:00 2001 From: sergiorussia <22471371+sergiorussia@users.noreply.github.com> Date: Mon, 27 May 2019 23:02:57 +0300 Subject: [PATCH 1/8] fixed digesting with platform specifier for java, tracking and args configs --- .../threerings/getdown/tests/DigesterIT.java | 18 +-- core/src/it/resources/testapp/getdown.txt | 12 ++ .../threerings/getdown/data/Application.java | 104 ++++++++++-------- .../threerings/getdown/tools/Digester.java | 7 +- 4 files changed, 85 insertions(+), 56 deletions(-) diff --git a/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java b/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java index 52b4b5e..d2ddaf2 100644 --- a/core/src/it/java/com/threerings/getdown/tests/DigesterIT.java +++ b/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/core/src/it/resources/testapp/getdown.txt b/core/src/it/resources/testapp/getdown.txt index 3e0e538..ab0e473 100644 --- a/core/src/it/resources/testapp/getdown.txt +++ b/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/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 1412d8a..e4d0987 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -547,6 +547,36 @@ public class Application public Config init (boolean checkPlatform) throws IOException { + Config config = initConfig(checkPlatform); + initConfigJava(config); + initConfigTracking(config); + initConfigResources(config); + initConfigArgs(config); + + // 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"); + + _verifyTimeout = config.getInt("verify_timeout", 60); + return config; + } + + public Config initConfig (boolean checkPlatform) throws IOException { Config config = null; File cfgfile = _config; Config.ParseOpts opts = Config.createOpts(checkPlatform); @@ -591,7 +621,7 @@ public class Application // make sure there's a trailing slash if (!_appbase.endsWith("/")) { - _appbase = _appbase + "/"; + _appbase += "/"; } // extract our version information @@ -602,17 +632,15 @@ public class Application _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) { - if (latest.startsWith(_appbase)) { - latest = _appbase + latest.substring(_appbase.length()); - } else { - latest = SysProps.replaceDomain(latest); - } + latest = latest.startsWith(_appbase) + ? _appbase + latest.substring(_appbase.length()) + : SysProps.replaceDomain(latest); try { _latest = HostWhitelist.verify(new URL(latest)); } catch (MalformedURLException mue) { @@ -620,20 +648,12 @@ 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 _strictComments = config.getBoolean("strict_comments"); + return config; + } + public void initConfigJava (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); @@ -651,14 +671,13 @@ public class Application // 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)); + } + public void initConfigTracking (Config config) { // determine whether we have any tracking configuration _trackingURL = config.getString("tracking_url"); @@ -683,7 +702,9 @@ public class Application // Some app may need to generate google analytics code _trackingGAHash = config.getString("tracking_ga_hash"); + } + public void initConfigResources (Config config) throws IOException { // clear our arrays as we may be reinitializing _codes.clear(); _resources.clear(); @@ -709,10 +730,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); @@ -720,6 +741,19 @@ public class Application parseResources(config, auxgroup + ".nresource", Resource.NATIVE, rsrcs); _auxgroups.put(auxgroup, new AuxGroup(auxgroup, codes, rsrcs)); } + } + + private void initConfigArgs (Config config) throws IOException { + 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"); + } // transfer our JVM arguments (we include both "global" args and app_id-prefixed args) String[] jvmargs = config.getMultiValue("jvmarg"); @@ -741,28 +775,6 @@ public class Application // 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"); - - _verifyTimeout = config.getInt("verify_timeout", 60); - return config; } /** diff --git a/core/src/main/java/com/threerings/getdown/tools/Digester.java b/core/src/main/java/com/threerings/getdown/tools/Digester.java index b04a653..8e97f80 100644 --- a/core/src/main/java/com/threerings/getdown/tools/Digester.java +++ b/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; @@ -75,7 +76,8 @@ public class Digester // create our application and instruct it to parse its business Application app = new Application(new EnvConfig(appdir)); - app.init(false); + Config config = app.initConfig(false); + app.initConfigResources(config); List rsrcs = new ArrayList<>(); rsrcs.add(app.getConfigResource()); @@ -86,6 +88,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); } From 0d384966a6af9556c2838d3d5ceb401e494e5b5e Mon Sep 17 00:00:00 2001 From: sergiorussia <22471371+sergiorussia@users.noreply.github.com> Date: Mon, 27 May 2019 23:11:28 +0300 Subject: [PATCH 2/8] dropped obsolete comment --- .../com/threerings/getdown/data/Application.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index e4d0987..3ea1d15 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -621,7 +621,7 @@ public class Application // make sure there's a trailing slash if (!_appbase.endsWith("/")) { - _appbase += "/"; + _appbase = _appbase + "/"; } // extract our version information @@ -638,9 +638,11 @@ public class Application // check for a latest config URL String latest = config.getString("latest"); if (latest != null) { - latest = latest.startsWith(_appbase) - ? _appbase + latest.substring(_appbase.length()) - : SysProps.replaceDomain(latest); + if (latest.startsWith(_appbase)) { + latest = _appbase + latest.substring(_appbase.length()); + } else { + latest = SysProps.replaceDomain(latest); + } try { _latest = HostWhitelist.verify(new URL(latest)); } catch (MalformedURLException mue) { @@ -668,9 +670,6 @@ 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! _javaLocation = config.getString("java_location"); // used only in conjunction with java_location From c45615f0a028cc373788e47a58f9b7aea03f0b63 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 10:30:26 -0700 Subject: [PATCH 3/8] Add changelog, undo null tolerance. Null tolerance is a plague. I wish `String` did not admit nulls and we had to use Optional for potentially null strings, but Java will forever be cursed with the billion dollar mistake. --- CHANGELOG.md | 2 ++ .../java/com/threerings/getdown/data/Application.java | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2a029..238df75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * 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. + ## 1.8.4 - May 14, 2019 * Added `verify_timeout` config to allow customization of the default (60 second) timeout during diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index fa132f9..c0125e5 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -586,7 +586,7 @@ public class Application throw new RuntimeException("m.missing_appbase"); } - // check if we're overriding the domain in the appbase + // check if we're overriding the domain in the appbase, and sub envvars _appbase = processArg(SysProps.overrideAppbase(_appbase)); // make sure there's a trailing slash @@ -606,8 +606,9 @@ public class Application } // check for a latest config URL - String latest = processArg(config.getString("latest")); + String latest = config.getString("latest"); if (latest != null) { + latest = processArg(latest); if (latest.startsWith(_appbase)) { latest = _appbase + latest.substring(_appbase.length()); } else { @@ -1121,10 +1122,6 @@ public class Application /** Replaces the application directory and version in any argument. */ protected String processArg (String arg) { - if (arg == null) { - return null; - } - arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath()); arg = arg.replace("%VERSION%", String.valueOf(_version)); From 490420852e714387f2d9943bc6d5acf0b4b03edf Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 11:15:45 -0700 Subject: [PATCH 4/8] Bit of cleanup of config parsing. --- .../threerings/getdown/data/Application.java | 172 ++++++++++-------- .../threerings/getdown/tools/Digester.java | 8 +- 2 files changed, 100 insertions(+), 80 deletions(-) diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 3d08aaa..432c2b9 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -233,6 +233,44 @@ public class Application } } + /** + * 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; + } + /** 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 * getter and setter just to pretend like that's not the case. */ @@ -245,7 +283,6 @@ public class Application */ public Application (EnvConfig envc) { _envc = envc; - _config = new File(envc.appDir, CONFIG_FILE); } /** @@ -544,71 +581,22 @@ 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 = initConfig(checkPlatform); - initConfigJava(config); - initConfigTracking(config); - initConfigResources(config); - initConfigArgs(config); - - // 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"); - - _verifyTimeout = config.getInt("verify_timeout", 60); + Config config = readConfig(_envc, checkPlatform); + initBase(config); + initJava(config); + initTracking(config); + initResources(config); + initArgs(config); return config; } - public Config initConfig (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); - } - - // 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 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"); @@ -653,10 +641,26 @@ public class Application // determine whether we want strict comments _strictComments = config.getBoolean("strict_comments"); - return config; + + // determine whether we want to allow offline operation (defaults to false) + _allowOffline = config.getBoolean("allow_offline"); + + // 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())); + + _verifyTimeout = config.getInt("verify_timeout", 60); } - public void initConfigJava (Config config) { + /** + * 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); @@ -677,7 +681,10 @@ public class Application _javaLocalDir = getLocalPath(config.getString("java_local_dir", LaunchUtil.LOCAL_JAVA_DIR)); } - public void initConfigTracking (Config config) { + /** + * 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"); @@ -704,14 +711,14 @@ public class Application _trackingGAHash = config.getString("tracking_ga_hash"); } - public void initConfigResources (Config config) throws IOException { + /** + * 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 && @@ -743,7 +750,14 @@ public class Application } } - private void initConfigArgs (Config config) throws IOException { + /** + * 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) @@ -756,25 +770,30 @@ public class Application } // transfer our JVM arguments (we include both "global" args and app_id-prefixed args) - String[] jvmargs = config.getMultiValue("jvmarg"); - addAll(jvmargs, _jvmargs); + addAll(config.getMultiValue("jvmarg"), _jvmargs); if (appPrefix.length() > 0) { - jvmargs = config.getMultiValue(appPrefix + "jvmarg"); - addAll(jvmargs, _jvmargs); + 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); + + // 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"); + + // 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(); } /** @@ -1745,7 +1764,6 @@ public class Application } protected final EnvConfig _envc; - protected File _config; protected Digest _digest; protected long _version = -1; diff --git a/core/src/main/java/com/threerings/getdown/tools/Digester.java b/core/src/main/java/com/threerings/getdown/tools/Digester.java index 8e97f80..ae61b43 100644 --- a/core/src/main/java/com/threerings/getdown/tools/Digester.java +++ b/core/src/main/java/com/threerings/getdown/tools/Digester.java @@ -75,9 +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)); - Config config = app.initConfig(false); - app.initConfigResources(config); + 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()); From b57524d0d6eaa00a1cd5737d446fa015c8980849 Mon Sep 17 00:00:00 2001 From: sergiorussia <22471371+sergiorussia@users.noreply.github.com> Date: Tue, 28 May 2019 23:37:11 +0300 Subject: [PATCH 5/8] fixed exception logging --- .../getdown/tools/DigesterTask.java | 5 +- .../main/java/com/threerings/getdown/Log.java | 3 +- .../getdown/cache/GarbageCollector.java | 2 +- .../getdown/cache/ResourceCache.java | 2 +- .../threerings/getdown/data/Application.java | 38 ++++---- .../com/threerings/getdown/data/Digest.java | 7 +- .../threerings/getdown/data/EnvConfig.java | 2 +- .../com/threerings/getdown/data/Resource.java | 4 +- .../com/threerings/getdown/data/SysProps.java | 2 +- .../com/threerings/getdown/spi/ProxyAuth.java | 6 +- .../com/threerings/getdown/tools/Differ.java | 23 +++-- .../com/threerings/getdown/tools/JarDiff.java | 28 +++--- .../getdown/tools/JarDiffPatcher.java | 19 ++-- .../com/threerings/getdown/util/Base64.java | 26 +++--- .../com/threerings/getdown/util/Color.java | 8 +- .../com/threerings/getdown/util/Config.java | 12 +-- .../getdown/util/ConnectionUtil.java | 2 +- .../threerings/getdown/util/LaunchUtil.java | 23 +++-- .../threerings/getdown/util/MessageUtil.java | 6 +- .../getdown/util/ProgressObserver.java | 2 +- .../threerings/getdown/util/StreamUtil.java | 3 +- .../threerings/getdown/util/StringUtil.java | 14 +-- .../threerings/getdown/util/VersionUtil.java | 2 +- .../getdown/launcher/AbortPanel.java | 2 +- .../threerings/getdown/launcher/Getdown.java | 91 ++++++++++--------- .../getdown/launcher/GetdownApp.java | 4 +- .../getdown/launcher/ProxyPanel.java | 2 +- .../getdown/launcher/ProxyUtil.java | 18 ++-- .../getdown/launcher/RotatingBackgrounds.java | 4 +- .../getdown/launcher/StatusPanel.java | 5 +- 30 files changed, 179 insertions(+), 186 deletions(-) diff --git a/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java b/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java index 48cc8d4..76212ae 100644 --- a/ant/src/main/java/com/threerings/getdown/tools/DigesterTask.java +++ b/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/core/src/main/java/com/threerings/getdown/Log.java b/core/src/main/java/com/threerings/getdown/Log.java index d00a131..da98c90 100644 --- a/core/src/main/java/com/threerings/getdown/Log.java +++ b/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 { /** @@ -143,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/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java b/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java index ae18990..7e01e87 100644 --- a/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java +++ b/core/src/main/java/com/threerings/getdown/cache/GarbageCollector.java @@ -96,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/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java b/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java index 0210e9a..41f0c5f 100644 --- a/core/src/main/java/com/threerings/getdown/cache/ResourceCache.java +++ b/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/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 432c2b9..bcb601b 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -33,7 +33,7 @@ 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 @@ -212,10 +212,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); } /** @@ -277,7 +277,7 @@ public class Application public Proxy proxy = Proxy.NO_PROXY; /** - * 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. * */ @@ -406,8 +406,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); @@ -435,8 +434,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); @@ -609,7 +607,7 @@ public class Application // make sure there's a trailing slash if (!_appbase.endsWith("/")) { - _appbase = _appbase + "/"; + _appbase += "/"; } // extract our version information @@ -913,7 +911,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) @@ -924,7 +922,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 () @@ -935,7 +933,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 { @@ -1111,7 +1109,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 { @@ -1173,8 +1171,8 @@ public class Application } /** - * 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. * @@ -1265,7 +1263,7 @@ public class Application 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")) { + 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 @@ -1471,7 +1469,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)); } @@ -1642,7 +1640,7 @@ 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 + "'."); @@ -1679,9 +1677,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); } } diff --git a/core/src/main/java/com/threerings/getdown/data/Digest.java b/core/src/main/java/com/threerings/getdown/data/Digest.java index bc8d140..e310a52 100644 --- a/core/src/main/java/com/threerings/getdown/data/Digest.java +++ b/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/core/src/main/java/com/threerings/getdown/data/EnvConfig.java b/core/src/main/java/com/threerings/getdown/data/EnvConfig.java index 1d66ebb..a14b02c 100644 --- a/core/src/main/java/com/threerings/getdown/data/EnvConfig.java +++ b/core/src/main/java/com/threerings/getdown/data/EnvConfig.java @@ -19,7 +19,7 @@ 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); } diff --git a/core/src/main/java/com/threerings/getdown/data/Resource.java b/core/src/main/java/com/threerings/getdown/data/Resource.java index c7c380c..d1ccba3 100644 --- a/core/src/main/java/com/threerings/getdown/data/Resource.java +++ b/core/src/main/java/com/threerings/getdown/data/Resource.java @@ -23,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 @@ -35,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); diff --git a/core/src/main/java/com/threerings/getdown/data/SysProps.java b/core/src/main/java/com/threerings/getdown/data/SysProps.java index 0d96ecb..d89bf70 100644 --- a/core/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/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 () { diff --git a/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java b/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java index 22446ec..8c6d841 100644 --- a/core/src/main/java/com/threerings/getdown/spi/ProxyAuth.java +++ b/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/core/src/main/java/com/threerings/getdown/tools/Differ.java b/core/src/main/java/com/threerings/getdown/tools/Differ.java index 496f81c..4f8e50e 100644 --- a/core/src/main/java/com/threerings/getdown/tools/Differ.java +++ b/core/src/main/java/com/threerings/getdown/tools/Differ.java @@ -5,10 +5,17 @@ package com.threerings.getdown.tools; -import java.io.*; +import java.io.BufferedOutputStream; +import java.io.File; +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.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; @@ -31,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 @@ -53,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()); @@ -83,8 +90,8 @@ 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; @@ -200,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++; } diff --git a/core/src/main/java/com/threerings/getdown/tools/JarDiff.java b/core/src/main/java/com/threerings/getdown/tools/JarDiff.java index 107b74e..f0db8ac 100644 --- a/core/src/main/java/com/threerings/getdown/tools/JarDiff.java +++ b/core/src/main/java/com/threerings/getdown/tools/JarDiff.java @@ -67,15 +67,15 @@ 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 @@ -83,10 +83,10 @@ public class JarDiff implements JarDiffCodes 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 archive and determine which files are candidates for @@ -157,7 +157,7 @@ public class JarDiff implements JarDiffCodes // SECOND PASS: = - - // - - ArrayList deleted = new ArrayList<>(); + List deleted = new ArrayList<>(); for (ZipEntry oldEntry : oldArchive) { String oldName = oldEntry.getName(); if (!implicit.contains(oldName) && !moveSrc.contains(oldName) @@ -203,9 +203,9 @@ 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 (ZipOutputStream jos, List oldEntries, Map movedMap) @@ -286,7 +286,7 @@ public class JarDiff implements JarDiffCodes */ private static class ZipFile2 implements Iterable, Closeable { - private ZipFile _archive; + private final ZipFile _archive; private List _entries; private HashMap _nameToEntryMap; private HashMap> _crcToEntryMap; @@ -384,7 +384,7 @@ public class JarDiff implements JarDiffCodes public String hasSameContent (ZipFile2 file, ZipEntry entry) throws IOException { String thisName = null; - Long crcL = Long.valueOf(entry.getCrc()); + 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 @@ -419,7 +419,7 @@ public class JarDiff implements JarDiffCodes while (entries.hasMoreElements()) { 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); } diff --git a/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java b/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java index ac837a9..e55034b 100644 --- a/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java +++ b/core/src/main/java/com/threerings/getdown/tools/JarDiffPatcher.java @@ -215,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 @@ -223,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); @@ -233,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)) { @@ -245,12 +244,14 @@ 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; diff --git a/core/src/main/java/com/threerings/getdown/util/Base64.java b/core/src/main/java/com/threerings/getdown/util/Base64.java index 2a5db79..233f1e7 100644 --- a/core/src/main/java/com/threerings/getdown/util/Base64.java +++ b/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/core/src/main/java/com/threerings/getdown/util/Color.java b/core/src/main/java/com/threerings/getdown/util/Color.java index 047cead..416f77e 100644 --- a/core/src/main/java/com/threerings/getdown/util/Color.java +++ b/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/core/src/main/java/com/threerings/getdown/util/Config.java b/core/src/main/java/com/threerings/getdown/util/Config.java index 4fc5e16..5c0b65e 100644 --- a/core/src/main/java/com/threerings/getdown/util/Config.java +++ b/core/src/main/java/com/threerings/getdown/util/Config.java @@ -62,7 +62,7 @@ public class Config * * @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) @@ -83,7 +83,7 @@ public class Config 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); } @@ -97,7 +97,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(); @@ -108,7 +108,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 @@ -351,7 +351,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; @@ -371,7 +371,7 @@ 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); + return (osname.contains(os)) && (osarch.contains(arch)); } private final Map _data; diff --git a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java index 21b0569..a8e5361 100644 --- a/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java @@ -16,7 +16,7 @@ import com.threerings.getdown.data.SysProps; import static java.nio.charset.StandardCharsets.UTF_8; -public class ConnectionUtil +public final class ConnectionUtil { /** * Opens a connection to a URL, setting the authentication header if user info is present. diff --git a/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java index 08a8b26..cc51794 100644 --- a/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java +++ b/core/src/main/java/com/threerings/getdown/util/LaunchUtil.java @@ -17,19 +17,19 @@ 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 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 @@ -41,7 +41,7 @@ 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. @@ -177,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. @@ -232,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/core/src/main/java/com/threerings/getdown/util/MessageUtil.java b/core/src/main/java/com/threerings/getdown/util/MessageUtil.java index 28dbdcf..f2ee9a2 100644 --- a/core/src/main/java/com/threerings/getdown/util/MessageUtil.java +++ b/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/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java b/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java index ad4c560..ac3e0a3 100644 --- a/core/src/main/java/com/threerings/getdown/util/ProgressObserver.java +++ b/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/core/src/main/java/com/threerings/getdown/util/StreamUtil.java b/core/src/main/java/com/threerings/getdown/util/StreamUtil.java index 373cfff..5cb1a40 100644 --- a/core/src/main/java/com/threerings/getdown/util/StreamUtil.java +++ b/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/core/src/main/java/com/threerings/getdown/util/StringUtil.java b/core/src/main/java/com/threerings/getdown/util/StringUtil.java index 03d3c9c..86277c8 100644 --- a/core/src/main/java/com/threerings/getdown/util/StringUtil.java +++ b/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/core/src/main/java/com/threerings/getdown/util/VersionUtil.java b/core/src/main/java/com/threerings/getdown/util/VersionUtil.java index 49e4e6e..b2f2894 100644 --- a/core/src/main/java/com/threerings/getdown/util/VersionUtil.java +++ b/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/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java index dc1e54e..4bd9f90 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/AbortPanel.java +++ b/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/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java index 57021f4..0aa1522 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -22,7 +22,15 @@ import java.io.InputStreamReader; import java.io.PrintStream; import java.net.HttpURLConnection; 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; @@ -31,13 +39,24 @@ import javax.swing.JFrame; import javax.swing.JLayeredPane; 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.ConnectionUtil; +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; /** @@ -75,7 +94,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 + @@ -130,7 +149,7 @@ 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)); @@ -161,20 +180,7 @@ public abstract class Getdown extends Thread } 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); + fail(e); } } @@ -285,7 +291,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(); @@ -334,11 +340,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 @@ -423,22 +425,7 @@ public abstract class Getdown extends Thread } 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); - } - } - // Since we're dead, clear off the 'time remaining' label along with displaying the - // error message - fail(msg); + fail(e); _app.releaseLock(); } } @@ -525,7 +512,7 @@ public abstract class Getdown extends Thread // lastly regenerate the .jsa dump file that helps Java to start up faster String vmpath = LaunchUtil.getJVMBinaryPath(javaLocalDir, false); - String[] command = new String[] { vmpath, "-Xshare:dump" }; + String[] command = { vmpath, "-Xshare:dump" }; try { log.info("Regenerating classes.jsa for " + vmpath + "..."); Runtime.getRuntime().exec(command); @@ -732,7 +719,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) { } } @@ -848,8 +835,22 @@ 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 = e instanceof FileNotFoundException + ? MessageUtil.compose("m.missing_resource", MessageUtil.taint(msg), _ifc.installError) + : MessageUtil.compose("m.init_error", MessageUtil.taint(msg), _ifc.installError); + } + // Since we're dead, clear off the 'time remaining' label along with displaying the error message + 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) { diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index fde79f3..7de56e4 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -146,7 +146,7 @@ public class GetdownApp } if (_ifc.iconImages != null) { - ArrayList icons = new ArrayList<>(); + List icons = new ArrayList<>(); for (String path : _ifc.iconImages) { Image img = loadImage(path); if (img == null) { @@ -190,7 +190,7 @@ public class GetdownApp String[] cmdarray; if (LaunchUtil.isWindows()) { String osName = System.getProperty("os.name", ""); - if (osName.indexOf("9") != -1 || osName.indexOf("Me") != -1) { + if (osName.contains("9") || osName.contains("Me")) { cmdarray = new String[] { "command.com", "/c", "start", "\"" + url + "\"" }; } else { diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java index 2178273..40934f7 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java @@ -131,7 +131,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(); diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index a36b5fa..d72b8b9 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -19,20 +19,18 @@ import java.net.URLConnection; import java.util.Iterator; import java.util.ServiceLoader; -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.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 ca.beq.util.win32.registry.RegistryKey; +import ca.beq.util.win32.registry.RegistryValue; +import ca.beq.util.win32.registry.RootKey; import static com.threerings.getdown.Log.log; -public class ProxyUtil { +public final class ProxyUtil { public static boolean autoDetectProxy (Application app) { @@ -57,12 +55,12 @@ 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")) { + if ("ProxyServer".equals(value.getName())) { String strval = value.getStringValue(); - int cidx = strval.indexOf(":"); + int cidx = strval.indexOf(':'); if (cidx != -1) { rport = strval.substring(cidx+1); strval = strval.substring(0, cidx); diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java b/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java index d3aa2bd..d64e5f0 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/RotatingBackgrounds.java +++ b/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/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java index 99f44ca..197dc91 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/StatusPanel.java +++ b/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}. */ From 6af6081648724ebb7bc62448e44f2ef44159d810 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 15:56:23 -0700 Subject: [PATCH 6/8] Support PAC proxies. What a horrible wart lingering from the early days of the Internet. Note to future designers: please do not express configuration using a Turing complete language. Thanks jenriq for getting the ball rolling on this. Closes #196. --- .../threerings/getdown/data/Application.java | 3 +- launcher/pom.xml | 7 + .../getdown/launcher/ProxyUtil.java | 84 ++++++++- .../threerings/getdown/launcher/PacUtils.js | 168 ++++++++++++++++++ .../getdown/launcher/ProxyUtilTest.java | 137 ++++++++++++++ 5 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js create mode 100644 launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 432c2b9..aba0ece 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -822,8 +822,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)); } diff --git a/launcher/pom.xml b/launcher/pom.xml index 8b32172..3b14aba 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -38,6 +38,12 @@ 1.0 true + + junit + junit + 4.12 + test + @@ -189,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/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index a36b5fa..43535f0 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -8,17 +8,27 @@ 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; @@ -61,21 +71,35 @@ public class ProxyUtil { enabled = value.getStringValue().equals("1"); } 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 + for (String proxy : findPACProxiesForURL(acjs, app.getRemoteURL(""))) { + 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); } @@ -205,6 +229,54 @@ public class ProxyUtil { } } + public static class Resolver { + public String dnsResolve (String host) { + try { + return InetAddress.getByName(host).toString(); + } 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/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js new file mode 100644 index 0000000..96a644e --- /dev/null +++ b/launcher/src/main/resources/com/threerings/getdown/launcher/PacUtils.js @@ -0,0 +1,168 @@ +/** + * Extracted from Mozilla Firefox 3.0.4, \mozilla\netwerk\base\src\nsProxyAutoConfig.js + */ +function dnsDomainIs (host, domain) { + return (host.length >= domain.length && + host.substring(host.length - domain.length) == domain); +} + +function dnsDomainLevels (host) { + return host.split('.').length-1; +} + +function convert_addr (ipchars) { + var bytes = ipchars.split('.'); + var result = (((bytes[0] & 0xff) << 24) | + ((bytes[1] & 0xff) << 16) | + ((bytes[2] & 0xff) << 8) | + ( bytes[3] & 0xff )); + return result; +} + +function dnsResolve (host) { + return resolver.dnsResolve(host) +} + +function isInNet (addrOrHost, pattern, maskstr) { + var testRE = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + var test = testRE.exec(addrOrHost); + if (test == null) { + addrOrHost = dnsResolve(addrOrHost); + if (addrOrHost == null) { + return false; + } + } else if (test[1] > 255 || test[2] > 255 || test[3] > 255 || test[4] > 255) { + return false; // not an IP address + } + var host = convert_addr(addrOrHost); + var pat = convert_addr(pattern); + var mask = convert_addr(maskstr); + return ((host & mask) == (pat & mask)); +} + +function isPlainHostName (host) { + return (host.search('\\.') == -1); +} + +function isResolvable (host) { + var ip = dnsResolve(host); + return (ip != null); +} + +function localHostOrDomainIs (host, hostdom) { + return (host == hostdom) || (hostdom.lastIndexOf(host + '.', 0) == 0); +} + +function shExpMatch (url, pattern) { + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + pattern = pattern.replace(/\?/g, '.'); + var newRe = new RegExp('^'+pattern+'$'); + return newRe.test(url); +} + +var wdays = { + SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 +}; + +var months = { + JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11 +}; + +function weekdayRange () { + function getDay (weekday) { + if (weekday in wdays) { + return wdays[weekday]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + var wday; + if (argc < 1) { + return false; + } + if (arguments[argc - 1] == 'GMT') { + argc--; + wday = date.getUTCDay(); + } else { + wday = date.getDay(); + } + var wd1 = getDay(arguments[0]); + var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1; + return (wd1 == -1 || wd2 == -1) ? false : (wd1 <= wday && wday <= wd2); +} + +function dateRange () { + function getMonth (name) { + if (name in months) { + return months[name]; + } + return -1; + } + var date = new Date(); + var argc = arguments.length; + if (argc < 1) { + return false; + } + var isGMT = (arguments[argc - 1] == 'GMT'); + if (isGMT) { + argc--; + } + + // function will work even without explict handling of this case + if (argc == 1) { + var tmp = parseInt(arguments[0]); + if (isNaN(tmp)) { + return ((isGMT ? date.getUTCMonth() : date.getMonth()) == getMonth(arguments[0])); + } else if (tmp < 32) { + return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp); + } else { + return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) == tmp); + } + } + + var year = date.getFullYear(); + var date1, date2; + date1 = new Date(year, 0, 1, 0, 0, 0); + date2 = new Date(year, 11, 31, 23, 59, 59); + var adjustMonth = false; + for (var i = 0; i < (argc >> 1); i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date1.setMonth(mon); + } else if (tmp < 32) { + adjustMonth = (argc <= 2); + date1.setDate(tmp); + } else { + date1.setFullYear(tmp); + } + } + for (var i = (argc >> 1); i < argc; i++) { + var tmp = parseInt(arguments[i]); + if (isNaN(tmp)) { + var mon = getMonth(arguments[i]); + date2.setMonth(mon); + } else if(tmp < 32) { + date2.setDate(tmp); + } else { + date2.setFullYear(tmp); + } + } + if (adjustMonth) { + date1.setMonth(date.getMonth()); + date2.setMonth(date.getMonth()); + } + if (isGMT) { + var tmp = date; + tmp.setFullYear(date.getUTCFullYear()); + tmp.setMonth(date.getUTCMonth()); + tmp.setDate(date.getUTCDate()); + tmp.setHours(date.getUTCHours()); + tmp.setMinutes(date.getUTCMinutes()); + tmp.setSeconds(date.getUTCSeconds()); + date = tmp; + } + return ((date1 <= date) && (date <= date2)); +} diff --git a/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java new file mode 100644 index 0000000..e60ab90 --- /dev/null +++ b/launcher/src/test/java/com/threerings/getdown/launcher/ProxyUtilTest.java @@ -0,0 +1,137 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2018 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.launcher; + +import java.io.StringReader; +import java.net.URL; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class ProxyUtilTest { + + static String[] strs (String... strs) { return strs; } + + static String[] findPAC (String code, String url) throws Exception { + return ProxyUtil.findPACProxiesForURL(new StringReader(code), new URL(url)); + } + + static void testPAC (String code, String url, String... expectedProxies) throws Exception { + assertArrayEquals(expectedProxies, findPAC(code, url)); + } + + @Test public void testPACProxy () throws Exception { + String EXAMPLE0 = + "function FindProxyForURL(url, host) {\n" + + " if (shExpMatch(host, '*.example.com')) { return 'DIRECT'; }\n" + + " if (isInNet(host, '10.0.0.0', '255.255.248.0')) {\n" + + " return 'PROXY fastproxy.example.com:8080';\n" + + " }\n" + + " return 'PROXY proxy.example.com:8080; DIRECT';\n" + + "}\n"; + + testPAC(EXAMPLE0, "http://test.example.com/", "DIRECT"); + testPAC(EXAMPLE0, "http://10.0.1.1/", "PROXY fastproxy.example.com:8080"); + testPAC(EXAMPLE0, "http://chicken.gov/", "PROXY proxy.example.com:8080", "DIRECT"); + + String EXAMPLE1 = + "function FindProxyForURL(url, host) {" + + " if (isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) {" + + " return 'DIRECT';" + + " } else {" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';" + + " }" + + "}"; + testPAC(EXAMPLE1, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE1, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE1, "http://localhost/", "DIRECT"); + + String EXAMPLE2 = + " function FindProxyForURL(url, host) {\n" + + " if ((isPlainHostName(host) || dnsDomainIs(host, '.mozilla.org')) &&\n" + + " !localHostOrDomainIs(host, 'www.mozilla.org') &&\n" + + " !localHostOrDomainIs(host, 'merchant.mozilla.org')) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY w3proxy.mozilla.org:8080; DIRECT';\n" + + " }\n" + + " }"; + testPAC(EXAMPLE2, "http://test.example.com/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www.mozilla.org/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://www/", "PROXY w3proxy.mozilla.org:8080", "DIRECT"); + testPAC(EXAMPLE2, "http://foo.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE2, "http://localhost/", "DIRECT"); + + String EXAMPLE3A = + "function FindProxyForURL(url, host) {\n" + + " if (isResolvable(host)) return 'DIRECT';\n" + + " else return 'PROXY proxy.mydomain.com:8080';\n" + + "}"; + testPAC(EXAMPLE3A, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3A, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + String EXAMPLE3B = + "function FindProxyForURL(url, host) {\n" + + " if (isPlainHostName(host) ||\n" + + " dnsDomainIs(host, '.mydomain.com') ||\n" + + " isResolvable(host)) {\n" + + " return 'DIRECT';\n" + + " } else {\n" + + " return 'PROXY proxy.mydomain.com:8080';\n" + + " }\n" + + "}"; + testPAC(EXAMPLE3B, "http://plain/", "DIRECT"); + testPAC(EXAMPLE3B, "http://foo.mydomain.com/", "DIRECT"); + testPAC(EXAMPLE3B, "http://www.mozilla.org/", "DIRECT"); + testPAC(EXAMPLE3B, "http://doesnotexist.mozilla.org/", "PROXY proxy.mydomain.com:8080"); + + // example 4 + // function FindProxyForURL(url, host) { + // if (isInNet(host, '198.95.0.0', '255.255.0.0')) return 'DIRECT'; + // else return 'PROXY proxy.mydomain.com:8080'; + // } + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || + // dnsDomainIs(host, '.mydomain.com') || + // isInNet(host, '198.95.0.0', '255.255.0.0')) { + // return 'DIRECT'; + // } else { + // return 'PROXY proxy.mydomain.com:8080'; + // } + // } + + // example 5 + // function FindProxyForURL(url, host) { + // if (isPlainHostName(host) || dnsDomainIs(host, '.mydomain.com')) return 'DIRECT'; + // else if (shExpMatch(host, '*.com')) return 'PROXY proxy1.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else if (shExpMatch(host, '*.edu')) return 'PROXY proxy2.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // else return 'PROXY proxy3.mydomain.com:8080; ' + + // 'PROXY proxy4.mydomain.com:8080'; + // } + + // example 6 + // function FindProxyForURL(url, host) { + // if (url.substring(0, 5) == 'http:') { + // return 'PROXY http-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 4) == 'ftp:') { + // return 'PROXY ftp-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 7) == 'gopher:') { + // return 'PROXY gopher-proxy.mydomain.com:8080'; + // } + // else if (url.substring(0, 6) == 'https:' || + // url.substring(0, 6) == 'snews:') { + // return 'PROXY security-proxy.mydomain.com:8080'; + // } else { + // return 'DIRECT'; + // } + // } + } +} From e337ccb74344a7f86923a71075b0be2b7699a147 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 16:30:29 -0700 Subject: [PATCH 7/8] Added note about new PAC support. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 238df75..d5c9100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ * `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. + ## 1.8.4 - May 14, 2019 * Added `verify_timeout` config to allow customization of the default (60 second) timeout during From 0a4bfe65c4f08e8fbff4db6a88211d742258e913 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2019 17:21:59 -0700 Subject: [PATCH 8/8] Proxy handling now detects need for updated credentials. This revamps the way proxy handling is done to allow us to just proceed as normal, and then if we get HTTP errors that indicate that we need a proxy or need proxy credentials, we ask for them and then retry everything. We still try to auto-detect the need for a proxy on our very first invocation because on the first invocation, a failure to fetch a URL may well indicate that a proxy is needed, but that same assumption does not hold on subsequent invocations. In those later cases, it's probably just a transient network failure and it would be weird and annoying to pop up the "please provide proxy config" dialog in those cases. Thanks pb00068 for getting this ball rolling, even though once again I have nearly entirely rewritten the PR. I need to kick that habit. :) --- CHANGELOG.md | 8 + .../threerings/getdown/data/Application.java | 56 +---- .../com/threerings/getdown/data/SysProps.java | 10 + .../com/threerings/getdown/net/Connector.java | 182 ++++++++++++++ .../threerings/getdown/net/Downloader.java | 85 ++++++- .../getdown/net/HTTPDownloader.java | 115 --------- .../getdown/util/ConnectionUtil.java | 73 ------ .../threerings/getdown/launcher/Getdown.java | 231 ++++++++++-------- .../getdown/launcher/GetdownApp.java | 9 +- .../getdown/launcher/ProxyPanel.java | 24 +- .../getdown/launcher/ProxyUtil.java | 56 +++-- .../threerings/getdown/messages.properties | 3 + .../threerings/getdown/messages_de.properties | 3 + .../threerings/getdown/messages_it.properties | 6 +- 14 files changed, 481 insertions(+), 380 deletions(-) create mode 100644 core/src/main/java/com/threerings/getdown/net/Connector.java delete mode 100644 core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java delete mode 100644 core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c9100..c0b2310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ * 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. + ## 1.8.4 - May 14, 2019 * Added `verify_timeout` config to allow customization of the default (60 second) timeout during diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index 726d6bf..a1deca4 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -7,7 +7,6 @@ package com.threerings.getdown.data; import java.io.*; import java.lang.reflect.Method; -import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; @@ -23,8 +22,8 @@ import java.util.*; import java.util.concurrent.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.GZIPInputStream; +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; @@ -271,10 +270,10 @@ public class Application return config; } - /** 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 + /** 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 {@code getdown.txt} @@ -989,16 +988,8 @@ public class Application args.add("-Xdock:name=" + _dockName); } - // pass along our proxy settings - if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address(); - String proxyHost = proxyAddr.getHostString(); - int proxyPort = proxyAddr.getPort(); - args.add("-Dhttp.proxyHost=" + proxyHost); - args.add("-Dhttp.proxyPort=" + proxyPort); - args.add("-Dhttps.proxyHost=" + proxyHost); - args.add("-Dhttps.proxyPort=" + proxyPort); - } + // forward our proxy settings + conn.addProxyArgs(args); // add the marker indicating the app is running in getdown args.add("-D" + Properties.GETDOWN + "=true"); @@ -1258,10 +1249,10 @@ 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))) { + 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) { @@ -1485,8 +1476,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); } @@ -1628,8 +1618,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"); @@ -1643,26 +1632,7 @@ public class Application } 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; } diff --git a/core/src/main/java/com/threerings/getdown/data/SysProps.java b/core/src/main/java/com/threerings/getdown/data/SysProps.java index d89bf70..1de9cd4 100644 --- a/core/src/main/java/com/threerings/getdown/data/SysProps.java +++ b/core/src/main/java/com/threerings/getdown/data/SysProps.java @@ -101,6 +101,16 @@ public final 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/core/src/main/java/com/threerings/getdown/net/Connector.java b/core/src/main/java/com/threerings/getdown/net/Connector.java new file mode 100644 index 0000000..9e74787 --- /dev/null +++ b/core/src/main/java/com/threerings/getdown/net/Connector.java @@ -0,0 +1,182 @@ +// +// 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.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.threerings.getdown.data.SysProps; +import com.threerings.getdown.util.Base64; +import com.threerings.getdown.util.StreamUtil; + +/** + * Manages the process of making HTTP connections, using a proxy if necessary. Tracks and reports + * when proxy credentials are rejected. + */ +public class Connector { + + /** The default connector uses no proxy. */ + public static final Connector DEFAULT = new Connector(Proxy.NO_PROXY); + + /** Tracks the state of a connector. If it fails for proxy-related reasons, it may transition + * to a need_proxy or need_proxy_auth state. */ + public enum State { ACTIVE, NEED_PROXY, NEED_PROXY_AUTH } + + /** The proxy used by this connector. */ + public final Proxy proxy; + + /** The current state of this connector. */ + public State state = State.ACTIVE; + + public Connector (Proxy proxy) { + this.proxy = proxy; + } + + /** + * 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 URLConnection open (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 HttpURLConnection openHttp (URL url, int connectTimeout, int readTimeout) + throws IOException + { + return (HttpURLConnection)open(url, connectTimeout, readTimeout); + } + + /** + * Downloads {@code url} into {@code target}. + */ + public void download (URL url, File target) throws IOException { + URLConnection conn = open(url, 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 + conn.setUseCaches(false); + conn.setRequestProperty("Accept-Encoding", "gzip"); + checkConnectOK(conn, "Unable to download " + url); + try (InputStream fin = conn.getInputStream()) { + String encoding = conn.getContentEncoding(); + boolean gzip = "gzip".equalsIgnoreCase(encoding); + try (InputStream fin2 = (gzip ? new GZIPInputStream(fin) : fin)) { + try (FileOutputStream fout = new FileOutputStream(target)) { + StreamUtil.copy(fin2, fout); + } + } + } + } + + /** + * Fetches the data at {@code url} into a string. + */ + public String fetch (URL url) throws IOException { + URLConnection conn = open(url, 0, 0); + checkConnectOK(conn, "Unable to fetch " + url); + int size = conn.getContentLength(); + ByteArrayOutputStream out = new ByteArrayOutputStream(size > 0 ? size : 1024); + try (InputStream in = conn.getInputStream()) { + StreamUtil.copy(in, out); + } + return out.toString(UTF_8.toString()); + } + + /** + * Checks that {@code conn} returned an {@code OK} response code iff it is an HTTP connection. + * If the connection failed for proxy related reasons, this changes the state of this connector + * to reflect the needed proxy information. + */ + public void checkConnectOK (URLConnection conn, String errpre) throws IOException + { + // if it's not an HTTP connection, there's nothing to check + if (!(conn instanceof HttpURLConnection)) return; + + int code = ((HttpURLConnection)conn).getResponseCode(); + switch (code) { + case HttpURLConnection.HTTP_OK: + return; + case HttpURLConnection.HTTP_FORBIDDEN: + case HttpURLConnection.HTTP_USE_PROXY: + state = State.NEED_PROXY; + break; + case HttpURLConnection.HTTP_PROXY_AUTH: + state = State.NEED_PROXY_AUTH; + break; + } + throw new IOException(errpre + " [code=" + code + "]"); + } + + /** + * Adds appropriate proxy args from this connector's configuration to the supplied list of + * command line args that will be used to launch the app. + */ + public void addProxyArgs (List args) { + if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) { + InetSocketAddress proxyAddr = (InetSocketAddress) proxy.address(); + String proxyHost = proxyAddr.getHostString(); + int proxyPort = proxyAddr.getPort(); + args.add("-Dhttp.proxyHost=" + proxyHost); + args.add("-Dhttp.proxyPort=" + proxyPort); + args.add("-Dhttps.proxyHost=" + proxyHost); + args.add("-Dhttps.proxyPort=" + proxyPort); + } + } +} diff --git a/core/src/main/java/com/threerings/getdown/net/Downloader.java b/core/src/main/java/com/threerings/getdown/net/Downloader.java index 6033e2f..505958a 100644 --- a/core/src/main/java/com/threerings/getdown/net/Downloader.java +++ b/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. @@ -132,7 +143,22 @@ public abstract class Downloader /** * 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"); + } + // make sure we got a satisfactory response code + _conn.checkConnectOK(conn, "Unable to check up-to-date for " + rsrc.getRemote()); + return conn.getContentLength(); + + } 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 +229,62 @@ 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 + _conn.checkConnectOK(conn, "Unable to download resource " + rsrc.getRemote()); + + // 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/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java b/core/src/main/java/com/threerings/getdown/net/HTTPDownloader.java deleted file mode 100644 index a7a3287..0000000 --- a/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/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java b/core/src/main/java/com/threerings/getdown/util/ConnectionUtil.java deleted file mode 100644 index a8e5361..0000000 --- a/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 final 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/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java index 0aa1522..09d2d6e 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/Getdown.java @@ -39,17 +39,16 @@ import javax.swing.JFrame; import javax.swing.JLayeredPane; import com.samskivert.swing.util.SwingUtil; -import com.threerings.getdown.data.Application; import com.threerings.getdown.data.Application.UpdateInterface.Step; +import com.threerings.getdown.data.Application; 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.Connector; import com.threerings.getdown.net.Downloader; -import com.threerings.getdown.net.HTTPDownloader; import com.threerings.getdown.tools.Patcher; import com.threerings.getdown.util.Config; -import com.threerings.getdown.util.ConnectionUtil; import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.MessageUtil; @@ -62,12 +61,22 @@ 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. @@ -134,8 +143,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 @@ -156,59 +185,20 @@ public abstract class Getdown extends Thread 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); - fail(e); - } - } - - /** - * 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)) { + boolean tryNoProxy = SysProps.tryNoProxyFirst(); + if (!tryNoProxy && ProxyUtil.autoDetectProxy(_app)) { return true; } - // otherwise see if we actually need a proxy; first we have to initialize our application + // 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 log.info("Checking whether we need to use a proxy..."); try { @@ -217,14 +207,18 @@ public abstract class Getdown extends Thread // no worries } 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; } @@ -234,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. @@ -264,7 +277,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) { @@ -279,7 +292,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 " + @@ -424,9 +437,20 @@ public abstract class Getdown extends Thread throw new IOException("m.unable_to_repair"); } catch (Exception e) { - log.warning("getdown() failed.", e); - fail(e); - _app.releaseLock(); + // 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; + } } } @@ -607,7 +631,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"); } @@ -841,11 +865,12 @@ public abstract class Getdown extends Thread 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 = e instanceof FileNotFoundException - ? MessageUtil.compose("m.missing_resource", MessageUtil.taint(msg), _ifc.installError) - : MessageUtil.compose("m.init_error", MessageUtil.taint(msg), _ifc.installError); + 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 message + // since we're dead, clear off the 'time remaining' label along with displaying the error fail(msg); } @@ -931,14 +956,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); } } } @@ -1001,44 +1026,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); - } - } - - // now request our tracking URL and ensure that we get a non-error response - ucon.connect(); + protected void reportProgress (final URL url) { + Thread reporter = new Thread("Progress reporter") { + public void run () { try { - if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { - log.warning("Failed to report tracking event", - "url", _url, "rcode", ucon.getResponseCode()); + 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); + } } - } finally { - ucon.disconnect(); + + // 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(); + } + + } 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. */ diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java index 7de56e4..e4e3320 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/GetdownApp.java @@ -101,7 +101,7 @@ public class GetdownApp log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("---------------------------------------------"); - Getdown app = new Getdown(envc) { + Getdown getdown = new Getdown(envc) { @Override protected Container createContainer () { // create our user interface, and display it @@ -226,8 +226,7 @@ public class GetdownApp // 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() { + @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()) { @@ -247,7 +246,7 @@ public class GetdownApp protected JFrame _frame; }; - app.start(); - return app; + Getdown.run(getdown); + return getdown; } } diff --git a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java index 40934f7..5f18896 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyPanel.java +++ b/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 @@ -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/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java index 5616f16..209c7bb 100644 --- a/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java +++ b/launcher/src/main/java/com/threerings/getdown/launcher/ProxyUtil.java @@ -34,9 +34,9 @@ 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; @@ -121,32 +121,31 @@ public final 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."); @@ -214,9 +213,14 @@ public final 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; diff --git a/launcher/src/main/resources/com/threerings/getdown/messages.properties b/launcher/src/main/resources/com/threerings/getdown/messages.properties index 92e13ee..b113c8e 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages.properties @@ -17,6 +17,9 @@ m.configure_proxy = We were unable to connect to the application server to

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 \ diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties index b013f15..1c3af16 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_de.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_de.properties @@ -16,6 +16,9 @@ m.configure_proxy = Es konnte keine Verbindung zum Applikations-Server auf 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. diff --git a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties index 1328650..c2481ea 100644 --- a/launcher/src/main/resources/com/threerings/getdown/messages_it.properties +++ b/launcher/src/main/resources/com/threerings/getdown/messages_it.properties @@ -20,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.