Consolidate & enhance "environment config" handling.

The new code is more careful about how it obtains config info, and issues
warnings when sources of info conflict. It does not abort in such circumstances
as Getdown's prime directive is to do the best it can to get the app updated
and launched. But the warnings will hopefully allow accidental conflicts to be
detected and avoided before being deployed to customers.

We also now allow bootstrap.properties to provide an appid; that matches what
was possible via system properties and command line arguments. We also avoid
"communicating" environment configuration through the Java system properties
mechanism. We now read from sysprops but do not write back to them.

Lastly, this includes some name tidying (like appdir -> appDir and
getAppdir -> getAppDir).
This commit is contained in:
Michael Bayne
2018-09-17 13:08:25 -07:00
parent ed22142488
commit 883954c3e5
11 changed files with 277 additions and 168 deletions
@@ -42,6 +42,14 @@ public class Log
*/ */
public void warning (Object message, Object... args) { doLog(2, message, args); } public void warning (Object message, Object... args) { doLog(2, message, args); }
/**
* Logs an error message.
*
* @param message the message to be logged.
* @param args a list of key/value pairs and an optional final Throwable.
*/
public void error (Object message, Object... args) { doLog(3, message, args); }
protected void doLog (int levIdx, Object message, Object[] args) { protected void doLog (int levIdx, Object message, Object[] args) {
if (_impl.isLoggable(LEVELS[levIdx])) { if (_impl.isLoggable(LEVELS[levIdx])) {
Throwable err = null; Throwable err = null;
@@ -48,7 +48,7 @@ public class ClassPaths
*/ */
public static ClassPath buildCachedClassPath (Application app) throws IOException public static ClassPath buildCachedClassPath (Application app) throws IOException
{ {
File cacheDir = new File(app.getAppdir(), CACHE_DIR); File cacheDir = new File(app.getAppDir(), CACHE_DIR);
// a negative value of code_cache_retention_days allows to clean up the cache forcefully // a negative value of code_cache_retention_days allows to clean up the cache forcefully
if (app.getCodeCacheRetentionDays() <= 0) { if (app.getCodeCacheRetentionDays() <= 0) {
runGarbageCollection(app, cacheDir); runGarbageCollection(app, cacheDir);
@@ -233,45 +233,21 @@ public class Application
} }
} }
/**
* Creates an application instance with no signers.
*
* @see #Application(File, String, List, String[], String[])
*/
public Application (File appdir, String appid)
{
this(appdir, appid, null, null, null);
}
/** /**
* Creates an application instance which records the location of the <code>getdown.txt</code> * Creates an application instance which records the location of the <code>getdown.txt</code>
* configuration file from the supplied application directory. * configuration file from the supplied application directory.
* *
* @param appid usually null but a string identifier if a secondary application is desired to
* be launched. That application will use {@code appid.class} and {@code appid.apparg} to
* configure itself but all other parameters will be the same as the primary application.
* @param signers a list of possible signers of this application. Used to verify the digest.
* @param jvmargs additional arguments to pass on to launched jvms.
* @param appargs additional arguments to pass on to launched application; these will be added
* after the args in the getdown.txt file.
*/ */
public Application (File appdir, String appid, List<Certificate> signers, public Application (EnvConfig envc) {
String[] jvmargs, String[] appargs) _envc = envc;
{ _config = getLocalPath(envc.appDir, CONFIG_FILE);
_appdir = appdir;
_appid = appid;
_signers = (signers == null) ? Collections.<Certificate>emptyList() : signers;
_config = getLocalPath(appdir, CONFIG_FILE);
_extraJvmArgs = (jvmargs == null) ? EMPTY_STRING_ARRAY : jvmargs;
_extraAppArgs = (appargs == null) ? EMPTY_STRING_ARRAY : appargs;
} }
/** /**
* Returns the configured application directory. * Returns the configured application directory.
*/ */
public File getAppdir() public File getAppDir () {
{ return _envc.appDir;
return _appdir;
} }
/** /**
@@ -550,7 +526,7 @@ public class Application
} }
// otherwise, issue a warning that we found no getdown file // otherwise, issue a warning that we found no getdown file
else { else {
log.info("Found no getdown.txt file", "appdir", _appdir); log.info("Found no getdown.txt file", "appdir", getAppDir());
} }
} catch (Exception e) { } catch (Exception e) {
log.warning("Failure reading config file", "file", config, e); log.warning("Failure reading config file", "file", config, e);
@@ -559,8 +535,8 @@ public class Application
// if we failed to read our config file, check for an appbase specified via a system // 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 // property; we can use that to bootstrap ourselves back into operation
if (config == null) { if (config == null) {
String appbase = SysProps.appBase(); String appbase = _envc.appBase;
log.info("Attempting to obtain 'appbase' from system property", "appbase", appbase); log.info("Using 'appbase' from bootstrap config", "appbase", appbase);
Map<String, Object> cdata = new HashMap<>(); Map<String, Object> cdata = new HashMap<>();
cdata.put("appbase", appbase); cdata.put("appbase", appbase);
config = new Config(cdata); config = new Config(cdata);
@@ -607,7 +583,7 @@ public class Application
} }
} }
String appPrefix = StringUtil.isBlank(_appid) ? "" : (_appid + "."); String appPrefix = _envc.appId == null ? "" : (_envc.appId + ".");
// determine our application class name (use app-specific class _if_ one is provided) // determine our application class name (use app-specific class _if_ one is provided)
_class = config.getString("class"); _class = config.getString("class");
@@ -708,9 +684,6 @@ public class Application
addAll(jvmargs, _jvmargs); addAll(jvmargs, _jvmargs);
} }
// Add the launch specific JVM arguments
addAll(_extraJvmArgs, _jvmargs);
// get the set of optimum JVM arguments // get the set of optimum JVM arguments
_optimumJvmArgs = config.getMultiValue("optimum_jvmarg"); _optimumJvmArgs = config.getMultiValue("optimum_jvmarg");
@@ -719,7 +692,7 @@ public class Application
addAll(appargs, _appargs); addAll(appargs, _appargs);
// add the launch specific application arguments // add the launch specific application arguments
addAll(_extraAppArgs, _appargs); _appargs.addAll(_envc.appArgs);
// look for custom arguments // look for custom arguments
fillAssignmentListFromPairs("extra.txt", _txtJvmArgs); fillAssignmentListFromPairs("extra.txt", _txtJvmArgs);
@@ -783,7 +756,7 @@ public class Application
*/ */
public File getLocalPath (String path) public File getLocalPath (String path)
{ {
return getLocalPath(_appdir, path); return getLocalPath(getAppDir(), path);
} }
/** /**
@@ -806,7 +779,7 @@ public class Application
// if we have an unpacked VM, check the 'release' file for its version // if we have an unpacked VM, check the 'release' file for its version
Resource vmjar = getJavaVMResource(); Resource vmjar = getJavaVMResource();
if (vmjar != null && vmjar.isMarkedValid()) { if (vmjar != null && vmjar.isMarkedValid()) {
File vmdir = new File(_appdir, LaunchUtil.LOCAL_JAVA_DIR); File vmdir = new File(getAppDir(), LaunchUtil.LOCAL_JAVA_DIR);
File relfile = new File(vmdir, "release"); File relfile = new File(vmdir, "release");
if (!relfile.exists()) { if (!relfile.exists()) {
log.warning("Unpacked JVM missing 'release' file. Assuming valid version."); log.warning("Unpacked JVM missing 'release' file. Assuming valid version.");
@@ -925,7 +898,7 @@ public class Application
ArrayList<String> args = new ArrayList<>(); ArrayList<String> args = new ArrayList<>();
// reconstruct the path to the JVM // reconstruct the path to the JVM
args.add(LaunchUtil.getJVMPath(_appdir, _windebug || optimum)); args.add(LaunchUtil.getJVMPath(getAppDir(), _windebug || optimum));
// check whether we're using -jar mode or -classpath mode // check whether we're using -jar mode or -classpath mode
boolean dashJarMode = MANIFEST_CLASS.equals(_class); boolean dashJarMode = MANIFEST_CLASS.equals(_class);
@@ -999,7 +972,7 @@ public class Application
String[] sargs = args.toArray(new String[args.size()]); String[] sargs = args.toArray(new String[args.size()]);
log.info("Running " + StringUtil.join(sargs, "\n ")); log.info("Running " + StringUtil.join(sargs, "\n "));
return Runtime.getRuntime().exec(sargs, envp, _appdir); return Runtime.getRuntime().exec(sargs, envp, getAppDir());
} }
/** /**
@@ -1095,7 +1068,7 @@ public class Application
/** Replaces the application directory and version in any argument. */ /** Replaces the application directory and version in any argument. */
protected String processArg (String arg) protected String processArg (String arg)
{ {
arg = arg.replace("%APPDIR%", _appdir.getAbsolutePath()); arg = arg.replace("%APPDIR%", getAppDir().getAbsolutePath());
arg = arg.replace("%VERSION%", String.valueOf(_version)); arg = arg.replace("%VERSION%", String.valueOf(_version));
// if this argument contains %ENV.FOO% replace those with the associated values looked up // if this argument contains %ENV.FOO% replace those with the associated values looked up
@@ -1135,7 +1108,7 @@ public class Application
// this will read in the contents of the digest file and validate itself // this will read in the contents of the digest file and validate itself
try { try {
_digest = new Digest(_appdir, _strictComments); _digest = new Digest(getAppDir(), _strictComments);
} catch (IOException ioe) { } catch (IOException ioe) {
log.info("Failed to load digest: " + ioe.getMessage() + ". Attempting recovery..."); log.info("Failed to load digest: " + ioe.getMessage() + ". Attempting recovery...");
} }
@@ -1149,7 +1122,7 @@ public class Application
try { try {
status.updateStatus("m.checking"); status.updateStatus("m.checking");
downloadDigestFiles(); downloadDigestFiles();
_digest = new Digest(_appdir, _strictComments); _digest = new Digest(getAppDir(), _strictComments);
if (!olddig.equals(_digest.getMetaDigest())) { if (!olddig.equals(_digest.getMetaDigest())) {
log.info("Unversioned digest changed. Revalidating..."); log.info("Unversioned digest changed. Revalidating...");
status.updateStatus("m.validating"); status.updateStatus("m.validating");
@@ -1167,7 +1140,7 @@ public class Application
if (_digest == null) { if (_digest == null) {
status.updateStatus("m.updating_metadata"); status.updateStatus("m.updating_metadata");
downloadDigestFiles(); downloadDigestFiles();
_digest = new Digest(_appdir, _strictComments); _digest = new Digest(getAppDir(), _strictComments);
} }
// now verify the contents of our main config file // now verify the contents of our main config file
@@ -1178,7 +1151,7 @@ public class Application
// caller because there's nothing we can do to automatically recover // caller because there's nothing we can do to automatically recover
downloadConfigFile(); downloadConfigFile();
downloadDigestFiles(); downloadDigestFiles();
_digest = new Digest(_appdir, _strictComments); _digest = new Digest(getAppDir(), _strictComments);
// revalidate everything if we end up downloading new metadata // revalidate everything if we end up downloading new metadata
clearValidationMarkers(); clearValidationMarkers();
// if the new copy validates, reinitialize ourselves; otherwise report baffling hoseage // if the new copy validates, reinitialize ourselves; otherwise report baffling hoseage
@@ -1509,8 +1482,8 @@ public class Application
File target = downloadFile(path); File target = downloadFile(path);
if (sigVersion > 0) { if (sigVersion > 0) {
if (_signers.isEmpty()) { if (_envc.certs.isEmpty()) {
log.info("No signers, not verifying file", "path", path); log.info("No signing certs, not verifying digest.txt", "path", path);
} else { } else {
File signatureFile = downloadFile(path + SIGNATURE_SUFFIX); File signatureFile = downloadFile(path + SIGNATURE_SUFFIX);
@@ -1523,7 +1496,7 @@ public class Application
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int length, validated = 0; int length, validated = 0;
for (Certificate cert : _signers) { for (Certificate cert : _envc.certs) {
try (FileInputStream dataInput = new FileInputStream(target)) { try (FileInputStream dataInput = new FileInputStream(target)) {
Signature sig = Signature.getInstance(Digest.sigAlgorithm(sigVersion)); Signature sig = Signature.getInstance(Digest.sigAlgorithm(sigVersion));
sig.initVerify(cert); sig.initVerify(cert);
@@ -1705,8 +1678,7 @@ public class Application
return new File(appdir, path); return new File(appdir, path);
} }
protected File _appdir; protected final EnvConfig _envc;
protected String _appid;
protected File _config; protected File _config;
protected Digest _digest; protected Digest _digest;
@@ -1749,15 +1721,10 @@ public class Application
protected List<String> _jvmargs = new ArrayList<>(); protected List<String> _jvmargs = new ArrayList<>();
protected List<String> _appargs = new ArrayList<>(); protected List<String> _appargs = new ArrayList<>();
protected String[] _extraJvmArgs;
protected String[] _extraAppArgs;
protected String[] _optimumJvmArgs; protected String[] _optimumJvmArgs;
protected List<String> _txtJvmArgs = new ArrayList<>(); protected List<String> _txtJvmArgs = new ArrayList<>();
protected List<Certificate> _signers;
/** If a warning has been issued about not being able to set modtimes. */ /** If a warning has been issued about not being able to set modtimes. */
protected boolean _warnedAboutSetLastModified; protected boolean _warnedAboutSetLastModified;
@@ -0,0 +1,207 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.data;
import java.io.File;
import java.io.FileInputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;
import com.threerings.getdown.util.StringUtil;
/** Configuration that comes from our "environment" (command line args, sys props, etc.). */
public final class EnvConfig {
/** Used to report problems or feedback by {@link #create}. */
public static final class Note {
public static enum Level { INFO, WARN, ERROR };
public 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); }
public final Level level;
public final String message;
public Note (Level level, String message) {
this.level = level;
this.message = message;
}
}
/**
* Creates an environment config, obtaining information (in order) from the following sources:
*
* <ul>
* <li> A {@code bootstrap.properties} file bundled with the jar. </li>
* <li> System properties supplied to the JVM. </li>
* <li> The supplied command line arguments ({@code argv}). </li>
* </ul>
*
* If a later source supplies a configuration already provided by a prior source, a warning
* message will be logged to indicate the conflict, and the prior source will be used.
*
* @param notes a list into which notes are added, to be logged after the logging system has
* been initialized (which cannot happen until the appdir is known). If any {@code ERROR} notes
* are included, the app should terminate after reporting them.
* @return an env config instance, or {@code null} if no appdir could be located via any
* configuration source.
*/
public static EnvConfig create (String[] argv, List<Note> notes) {
String appDir = null, appDirProv = null;
String appId = null, appIdProv = null;
String appBase = null, appBaseProv = null;
// start with bootstrap.properties config, if avaialble
try {
ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
if (bundle.containsKey("appdir")) {
appDir = bundle.getString("appdir");
appDir = appDir.replace(USER_HOME_KEY, System.getProperty("user.home"));
File appDirFile = new File(appDir);
if (!appDirFile.exists()) {
appDirFile.mkdirs();
}
appDirProv = "bootstrap.properties";
}
if (bundle.containsKey("appid")) {
appId = bundle.getString("appid");
appIdProv = "bootstrap.properties";
}
if (bundle.containsKey("appbase")) {
appBase = bundle.getString("appbase");
appBaseProv = "bootstrap.properties";
}
} catch (MissingResourceException e) {
// bootstrap.properties is optional; no need for a warning
}
// next seek config from system properties
String spropsAppDir = SysProps.appDir();
if (!StringUtil.isBlank(spropsAppDir)) {
if (appDir == null) {
appDir = spropsAppDir;
appDirProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appdir' system property, have appdir via '" +
appDirProv + "'"));
}
}
String spropsAppId = SysProps.appId();
if (!StringUtil.isBlank(spropsAppId)) {
if (appId == null) {
appId = spropsAppId;
appIdProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appid' system property, have appid via '" +
appIdProv + "'"));
}
}
String spropsAppBase = SysProps.appBase();
if (!StringUtil.isBlank(spropsAppBase)) {
if (appBase == null) {
appBase = spropsAppBase;
appBaseProv = "system property";
} else {
notes.add(Note.warn("Ignoring 'appbase' system property, have appbase via '" +
appBaseProv + "'"));
}
}
// finally obtain config from command line arguments
String argvAppDir = argv.length > 0 ? argv[0] : null;
if (!StringUtil.isBlank(argvAppDir)) {
if (appDir == null) {
appDir = argvAppDir;
appDirProv = "command line";
} else {
notes.add(Note.warn("Ignoring 'appdir' command line arg, have appdir via '" +
appDirProv + "'"));
}
}
String argvAppId = argv.length > 1 ? argv[1] : null;
if (!StringUtil.isBlank(spropsAppId)) {
if (appId == null) {
appId = argvAppId;
appIdProv = "command line";
} else {
notes.add(Note.warn("Ignoring 'appid' command line arg, have appid via '" +
appIdProv + "'"));
}
}
// ensure that we were able to fine an app dir
if (appDir == null) {
return null; // caller will report problem to user
}
// ensure that the appdir refers to a directory that exists
File appDirFile = new File(appDir);
if (!appDirFile.exists() || !appDirFile.isDirectory()) {
notes.add(Note.error("Invalid appdir '" + appDir + "'."));
return null;
}
notes.add(Note.info("Using appdir from " + appDirProv + ": " + appDir));
if (appId != null) notes.add(Note.info("Using appid from " + appIdProv + ": " + appId));
if (appBase != null) notes.add(Note.info("Using appbase from " + appBaseProv + ": " +
appBase));
// pass along anything after the first tow args as extra app args
List<String> appArgs = argv.length > 2 ?
Arrays.asList(argv).subList(2, argv.length-2) :
Collections.<String>emptyList();
// load X.509 certificate if it exists
File crtFile = new File(appDirFile, Digest.digestFile(Digest.VERSION) + ".crt");
List<Certificate> certs = new ArrayList<>();
if (crtFile.exists()) {
try (FileInputStream fis = new FileInputStream(crtFile)) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(fis);
certs.add(certificate);
} catch (Exception e) {
notes.add(Note.error("Certificate error: " + e.getMessage()));
}
}
return new EnvConfig(appDirFile, appId, appBase, certs, appArgs);
}
/** The directory in which the application and metadata is stored. */
public final File appDir;
/** Either {@code null} or an identifier for a secondary application that should be
* launched. That app will use {@code appid.class} and {@code appid.apparg} to configure
* itself but all other parameters will be the same as the primary app. */
public final String appId;
/** Either {@code null} or fallback {@code appbase} to use if one cannot be read from a
* {@code getdown.txt} file during startup. */
public final String appBase;
/** Zero or more signing certificates used to verify the digest file. */
public final List<Certificate> certs;
/** Additional arguments to pass on to launched application. These will be added after the
* args in the getdown.txt file. */
public final List<String> appArgs;
public EnvConfig (File appDir) {
this(appDir, null, null, Collections.<Certificate>emptyList(),
Collections.<String>emptyList());
}
private EnvConfig (File appDir, String appId, String appBase, List<Certificate> certs,
List<String> appArgs) {
this.appDir = appDir;
this.appId = appId;
this.appBase = appBase;
this.certs = certs;
this.appArgs = appArgs;
}
private static final String USER_HOME_KEY = "${user.home}";
}
@@ -28,8 +28,8 @@ public class SysProps
return System.getProperty("appid"); return System.getProperty("appid");
} }
/** Configures the appbase (in lieu of providing a skeleton getdown.txt, and as a last resort /** Configures the bootstrap appbase (used in lieu of providing a skeleton getdown.txt, and as
* fallback). Usage: {@code -Dappbase=URL}. */ * a last resort fallback). Usage: {@code -Dappbase=URL}. */
public static String appBase () { public static String appBase () {
return System.getProperty("appbase"); return System.getProperty("appbase");
} }
@@ -23,6 +23,7 @@ import java.security.MessageDigest;
import com.threerings.getdown.data.Application; import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest; import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.Resource; import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.FileUtil; import com.threerings.getdown.util.FileUtil;
import com.threerings.getdown.util.StreamUtil; import com.threerings.getdown.util.StreamUtil;
@@ -58,13 +59,13 @@ public class Differ
", overs=" + overs + "]."); ", overs=" + overs + "].");
} }
Application oapp = new Application(ovdir, null); Application oapp = new Application(new EnvConfig(ovdir));
oapp.init(false); oapp.init(false);
ArrayList<Resource> orsrcs = new ArrayList<>(); ArrayList<Resource> orsrcs = new ArrayList<>();
orsrcs.addAll(oapp.getCodeResources()); orsrcs.addAll(oapp.getCodeResources());
orsrcs.addAll(oapp.getResources()); orsrcs.addAll(oapp.getResources());
Application napp = new Application(nvdir, null); Application napp = new Application(new EnvConfig(nvdir));
napp.init(false); napp.init(false);
ArrayList<Resource> nrsrcs = new ArrayList<>(); ArrayList<Resource> nrsrcs = new ArrayList<>();
nrsrcs.addAll(napp.getCodeResources()); nrsrcs.addAll(napp.getCodeResources());
@@ -20,6 +20,7 @@ import java.util.List;
import com.threerings.getdown.data.Application; import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest; import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.Resource; import com.threerings.getdown.data.Resource;
import com.threerings.getdown.util.Base64; import com.threerings.getdown.util.Base64;
@@ -73,7 +74,7 @@ public class Digester
System.out.println("Generating digest file '" + target + "'..."); System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business // create our application and instruct it to parse its business
Application app = new Application(appdir, null); Application app = new Application(new EnvConfig(appdir));
app.init(false); app.init(false);
List<Resource> rsrcs = new ArrayList<>(); List<Resource> rsrcs = new ArrayList<>();
@@ -381,7 +381,7 @@ public class JarDiff implements JarDiffCodes
public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException { public String hasSameContent (JarFile2 file, JarEntry entry) throws IOException {
String thisName = null; String thisName = null;
Long crcL = new Long(entry.getCrc()); Long crcL = Long.valueOf(entry.getCrc());
// check if this jar contains files with the passed in entry's crc // check if this jar contains files with the passed in entry's crc
if (_crcToEntryMap.containsKey(crcL)) { if (_crcToEntryMap.containsKey(crcL)) {
// get the Linked List with files with the crc // get the Linked List with files with the crc
@@ -416,7 +416,7 @@ public class JarDiff implements JarDiffCodes
while (entries.hasMoreElements()) { while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement(); JarEntry entry = entries.nextElement();
long crc = entry.getCrc(); long crc = entry.getCrc();
Long crcL = new Long(crc); Long crcL = Long.valueOf(crc);
if (_debug) { if (_debug) {
System.out.println("\t" + entry.getName() + " CRC " + crc); System.out.println("\t" + entry.getName() + " CRC " + crc);
} }
@@ -28,7 +28,7 @@ public class ClassPathsTest
when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile); when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile); when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar)); when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
when(_application.getAppdir()).thenReturn(_appdir.getRoot()); when(_application.getAppDir()).thenReturn(_appdir.getRoot());
} }
@Test public void shouldBuildDefaultClassPath () throws IOException @Test public void shouldBuildDefaultClassPath () throws IOException
@@ -43,22 +43,11 @@ import ca.beq.util.win32.registry.RootKey;
import com.samskivert.swing.util.SwingUtil; import com.samskivert.swing.util.SwingUtil;
import com.threerings.getdown.data.Application.UpdateInterface.Step; import com.threerings.getdown.data.Application.UpdateInterface.Step;
import com.threerings.getdown.data.Application; import com.threerings.getdown.data.*;
import com.threerings.getdown.data.Build;
import com.threerings.getdown.data.Resource;
import com.threerings.getdown.data.SysProps;
import com.threerings.getdown.net.Downloader; import com.threerings.getdown.net.Downloader;
import com.threerings.getdown.net.HTTPDownloader; import com.threerings.getdown.net.HTTPDownloader;
import com.threerings.getdown.tools.Patcher; import com.threerings.getdown.tools.Patcher;
import com.threerings.getdown.util.Config; import com.threerings.getdown.util.*;
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; import static com.threerings.getdown.Log.log;
@@ -68,13 +57,7 @@ import static com.threerings.getdown.Log.log;
public abstract class Getdown extends Thread public abstract class Getdown extends Thread
implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
{ {
public Getdown (File appDir, String appId) public Getdown (EnvConfig envc)
{
this(appDir, appId, null, null, null);
}
public Getdown (File appDir, String appId, List<Certificate> signers,
String[] jvmargs, String[] appargs)
{ {
super("Getdown"); super("Getdown");
try { try {
@@ -94,7 +77,7 @@ public abstract class Getdown extends Thread
} catch (Exception e) { } catch (Exception e) {
// welcome to hell, where java can't cope with a classpath that contains jars that live // 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 // in a directory that contains a !, at least the same bug happens on all platforms
String dir = appDir.toString(); String dir = envc.appDir.toString();
if (dir.equals(".")) { if (dir.equals(".")) {
dir = System.getProperty("user.dir"); dir = System.getProperty("user.dir");
} }
@@ -103,7 +86,7 @@ public abstract class Getdown extends Thread
"contains the '!' character, this will trigger this error."; "contains the '!' character, this will trigger this error.";
fail(errmsg); fail(errmsg);
} }
_app = new Application(appDir, appId, signers, jvmargs, appargs); _app = new Application(envc);
_startup = System.currentTimeMillis(); _startup = System.currentTimeMillis();
} }
@@ -14,24 +14,18 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.WindowConstants; import javax.swing.WindowConstants;
import com.samskivert.swing.util.SwingUtil; import com.samskivert.swing.util.SwingUtil;
import com.threerings.getdown.data.Digest; import com.threerings.getdown.data.Digest;
import com.threerings.getdown.data.EnvConfig;
import com.threerings.getdown.data.SysProps; import com.threerings.getdown.data.SysProps;
import com.threerings.getdown.util.LaunchUtil; import com.threerings.getdown.util.LaunchUtil;
import com.threerings.getdown.util.StringUtil; import com.threerings.getdown.util.StringUtil;
@@ -42,11 +36,10 @@ import static com.threerings.getdown.Log.log;
*/ */
public class GetdownApp public class GetdownApp
{ {
/**
private static final String USER_HOME = "${user.home}"; * The main entry point of the Getdown launcher application.
*/
public static void main (String[] argv) public static void main (String[] argv) {
{
try { try {
start(argv); start(argv);
} catch (Exception e) { } catch (Exception e) {
@@ -61,58 +54,17 @@ public class GetdownApp
* @throws Exception if anything goes wrong starting Getdown. * @throws Exception if anything goes wrong starting Getdown.
*/ */
public static Getdown start (String[] argv) throws Exception { public static Getdown start (String[] argv) throws Exception {
int aidx = 0; List<EnvConfig.Note> notes = new ArrayList<>();
List<String> args = Arrays.asList(argv); EnvConfig envc = EnvConfig.create(argv, notes);
if (envc == null) {
// check for app dir in a sysprop and then via argv if (!notes.isEmpty()) for (EnvConfig.Note n : notes) System.err.println(n.message);
String adarg = SysProps.appDir(); else System.err.println("Usage: java -jar getdown.jar [app_dir] [app_id] [app args]");
if (StringUtil.isBlank(adarg)) {
loadBootstrapResource();
if (args.isEmpty() && SysProps.appDir() == null) {
System.err.println("Usage: java -jar getdown.jar app_dir [app_id] [app args]");
System.exit(-1); System.exit(-1);
} }
if (!args.isEmpty()) {
adarg = args.get(aidx++);
} else {
adarg = SysProps.appDir();
}
}
// check for an app identifier in a sysprop and then via argv
String appId = SysProps.appId();
if (StringUtil.isBlank(appId) && aidx < args.size()) {
appId = args.get(aidx++);
}
// pass along anything after that as app args
String[] appArgs = (aidx >= args.size()) ? null :
args.subList(aidx, args.size()).toArray(new String[0]);
// ensure a valid directory was supplied
File appDir = new File(adarg);
if (!appDir.exists() || !appDir.isDirectory()) {
log.warning("Invalid app_dir '" + adarg + "'.");
System.exit(-1);
}
// load X.509 certificate if it exists
File crtFile = new File(appDir, Digest.digestFile(Digest.VERSION) + ".crt");
List<Certificate> crts = new ArrayList<>();
if (crtFile.exists()) {
try (FileInputStream fis = new FileInputStream(crtFile)) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(fis);
crts.add(certificate);
} catch (Exception e) {
log.warning("Certificate error: " + e.getMessage());
System.exit(-1);
}
}
// pipe our output into a file in the application directory // pipe our output into a file in the application directory
if (!SysProps.noLogRedir()) { if (!SysProps.noLogRedir()) {
File logFile = new File(appDir, "launcher.log"); File logFile = new File(envc.appDir, "launcher.log");
try { try {
PrintStream logOut = new PrintStream( PrintStream logOut = new PrintStream(
new BufferedOutputStream(new FileOutputStream(logFile)), true); new BufferedOutputStream(new FileOutputStream(logFile)), true);
@@ -123,6 +75,17 @@ public class GetdownApp
} }
} }
// report any notes from reading our env config, and abort if necessary
boolean abort = false;
for (EnvConfig.Note note : notes) {
switch (note.level) {
case INFO: log.info(note.message); break;
case WARN: log.warning(note.message); break;
case ERROR: log.error(note.message); abort = true; break;
}
}
if (abort) System.exit(-1);
// record a few things for posterity // record a few things for posterity
log.info("------------------ VM Info ------------------"); log.info("------------------ VM Info ------------------");
log.info("-- OS Name: " + System.getProperty("os.name")); log.info("-- OS Name: " + System.getProperty("os.name"));
@@ -135,7 +98,7 @@ public class GetdownApp
log.info("-- Cur dir: " + System.getProperty("user.dir")); log.info("-- Cur dir: " + System.getProperty("user.dir"));
log.info("---------------------------------------------"); log.info("---------------------------------------------");
Getdown app = new Getdown(appDir, appId, crts, null, appArgs) { Getdown app = new Getdown(envc) {
@Override @Override
protected Container createContainer () { protected Container createContainer () {
// create our user interface, and display it // create our user interface, and display it
@@ -270,25 +233,4 @@ public class GetdownApp
app.start(); app.start();
return app; return app;
} }
private static void loadBootstrapResource() {
try {
ResourceBundle bundle = ResourceBundle.getBundle("bootstrap");
if (bundle.containsKey("appbase")) {
System.setProperty("appbase", bundle.getString("appbase"));
}
if (bundle.containsKey("appdir")) {
String appDir = bundle.getString("appdir");
appDir = appDir.replace(USER_HOME, System.getProperty("user.home"));
File appDirFile = new File(appDir);
if (!appDirFile.exists()) {
appDirFile.mkdirs();
}
System.setProperty("appdir", appDir);
}
log.info("bootstrap.properties found using:", "_appdir", System.getProperty("appdir"), "appbase", System.getProperty("appbase"));
} catch (MissingResourceException e) {
log.info("bootstrap.properties not found in resource bundle, starting without bootstrapping");
}
}
} }