2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.util;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.io.PrintStream;
12 import java.nio.file.Files;
13 import java.nio.file.Paths;
14 import java.util.Locale;
16 import javax.xml.bind.DatatypeConverter;
18 import java.security.MessageDigest;
20 import jalview.util.LaunchUtils;
22 import static com.threerings.getdown.Log.log;
25 * Useful routines for launching Java applications from within other Java
28 public class LaunchUtil
30 /** The directory into which a local VM installation should be unpacked. */
31 public static final String LOCAL_JAVA_DIR = "jre";
34 * Writes a <code>version.txt</code> file into the specified application directory and
35 * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly
36 * specified version and relaunch the application.
38 * @param appdir the directory in which the application is installed.
39 * @param getdownJarName the name of the getdown jar file in the application directory. This is
40 * probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using
41 * the results of the standard build.
42 * @param newVersion the new version to which Getdown will update when it is executed.
44 * @return true if the relaunch succeeded, false if we were unable to relaunch due to being on
45 * Windows 9x where we cannot launch subprocesses without waiting around for them to exit,
46 * reading their stdout and stderr all the while. If true is returned, the application may exit
47 * after making this call as it will be upgraded and restarted. If false is returned, the
48 * application should tell the user that they must restart the application manually.
50 * @exception IOException thrown if we were unable to create the <code>version.txt</code> file
51 * in the supplied application directory. If the version.txt file cannot be created, restarting
52 * Getdown will not cause the application to be upgraded, so the application will have to
53 * resort to telling the user that it is in a bad way.
55 public static boolean updateVersionAndRelaunch (
56 File appdir, String getdownJarName, String newVersion)
59 // create the file that instructs Getdown to upgrade
60 File vfile = new File(appdir, "version.txt");
61 try (PrintStream ps = new PrintStream(new FileOutputStream(vfile))) {
62 ps.println(newVersion);
65 // make sure that we can find our getdown.jar file and can safely launch children
66 File pro = new File(appdir, getdownJarName);
67 if (mustMonitorChildren() || !pro.exists()) {
72 String[] args = new String[] {
73 getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
75 log.info("Running " + StringUtil.join(args, "\n "));
77 Runtime.getRuntime().exec(args, null);
79 } catch (IOException ioe) {
80 log.warning("Failed to run getdown", ioe);
86 * Reconstructs the path to the JVM used to launch this process.
88 public static String getJVMPath (File appdir)
90 return getJVMPath(appdir, false);
93 private static String jvmPath = null;
95 * Reconstructs the path to the JVM used to launch this process.
97 * @param windebug if true we will use java.exe instead of javaw.exe on Windows.
99 public static String getJVMPath (File appdir, boolean windebug)
101 if (jvmPath != null) {
105 // first look in our application directory for an installed VM
106 final String appDir = isMacOS() ?
107 (new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath()) + "/Contents/Home"
108 : new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath();
110 String javaBin = LaunchUtils.findJavaBin(appDir, windebug, false);
112 // then fall back to the VM in which we're already running
113 if (javaBin == null) {
114 javaBin = LaunchUtils.findJavaBin(System.getProperty("java.home"), windebug, false);
117 // then throw up our hands and hope for the best
118 if (javaBin == null) {
119 javaBin = LaunchUtils.findJavaBin(null, windebug, true);
120 log.warning("Unable to find java [appdir=" + appdir +
121 ", java.home=" + System.getProperty("java.home") + "]!");
124 // Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched
125 // from /usr/bin/java, and not if launched by directly referring to <java.home>/bin/java,
126 // even though the former is a symlink to the latter! To work around this, see if the
127 // desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead.
130 File localVM = new File("/usr/bin/java").getCanonicalFile();
131 if (localVM.equals(new File(javaBin).getCanonicalFile())) {
132 javaBin = "/usr/bin/java";
134 } catch (IOException ioe) {
135 log.warning("Failed to check Mac OS canonical VM path.", ioe);
143 private static String _getMD5FileChecksum (File file) {
146 String checksum = "";
148 MessageDigest md = MessageDigest.getInstance(algo);
149 md.update(Files.readAllBytes(Paths.get(file.getAbsolutePath())));
150 byte[] digest = md.digest();
151 checksum = DatatypeConverter.printHexBinary(digest).toUpperCase(Locale.ROOT);
152 } catch (Exception e) {
153 System.out.println("Couldn't create "+algo+" digest of "+file.getPath());
159 * Upgrades Getdown by moving an installation managed copy of the Getdown jar file over the
160 * non-managed copy (which would be used to run Getdown itself).
162 * <p> If the upgrade fails for a variety of reasons, warnings are logged but no other actions
163 * are taken. There's not much else one can do other than try again next time around.
165 public static void upgradeGetdown (File oldgd, File curgd, File newgd)
167 // we assume getdown's jar file size changes with every upgrade, this is not guaranteed,
168 // but in reality it will, and it allows us to avoid pointlessly upgrading getdown every
169 // time the client is updated which is unnecessarily flirting with danger
175 if (newgd.length() == curgd.length()) {
176 if (_getMD5FileChecksum(newgd).equals(_getMD5FileChecksum(curgd)))
182 log.info("Updating Getdown with " + newgd + "...");
184 // clear out any old getdown
185 if (oldgd.exists()) {
186 FileUtil.deleteHarder(oldgd);
189 // now try updating using renames
190 if (!curgd.exists() || curgd.renameTo(oldgd)) {
191 if (newgd.renameTo(curgd)) {
192 FileUtil.deleteHarder(oldgd); // yay!
194 // copy the moved file back to getdown-dop-new.jar so that we don't end up
195 // downloading another copy next time
196 FileUtil.copy(curgd, newgd);
197 } catch (IOException e) {
198 log.warning("Error copying updated Getdown back: " + e);
203 log.warning("Unable to renameTo(" + oldgd + ").");
204 // try to unfuck ourselves
205 if (!oldgd.renameTo(curgd)) {
206 log.warning("Oh God, why dost thee scorn me so.");
210 // that didn't work, let's try copying it
211 log.info("Attempting to upgrade by copying over " + curgd + "...");
213 FileUtil.copy(newgd, curgd);
214 } catch (IOException ioe) {
215 log.warning("Mayday! Brute force copy method also failed.", ioe);
220 * Returns true if, on this operating system, we have to stick around and read the stderr from
221 * our children processes to prevent them from filling their output buffers and hanging.
223 public static boolean mustMonitorChildren ()
225 String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
226 return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
230 * Returns true if we're running in a JVM that identifies its operating system as Windows.
232 public static final boolean isWindows () { return _isWindows; }
235 * Returns true if we're running in a JVM that identifies its operating system as MacOS.
237 public static final boolean isMacOS () { return _isMacOS; }
240 * Returns true if we're running in a JVM that identifies its operating system as Linux.
242 public static final boolean isLinux () { return _isLinux; }
245 * Check if a symlink (or file) points to a JVM
247 private static boolean checkJVMSymlink(String testBin)
249 File testBinFile = new File(testBin);
250 if (!testBinFile.exists())
254 File targetFile = null;
257 targetFile = testBinFile.getCanonicalFile();
258 } catch (IOException e)
262 if (targetFile != null && ("java".equals(targetFile.getName())
263 || "java.exe".equals(targetFile.getName())))
271 * Checks whether a Java Virtual Machine can be located in the supplied path.
273 protected static String checkJVMPath (String vmhome, boolean windebug)
275 String vmbase = vmhome + File.separator + "bin" + File.separator;
276 String appName = System.getProperty("channel.app_name", "Jalview");
277 String vmpath = vmbase + appName;
278 if (checkJVMSymlink(vmpath)) {
281 vmpath = vmbase + "Jalview";
282 if (checkJVMSymlink(vmpath)) {
285 vmpath = vmbase + "java";
286 if (new File(vmpath).exists()) {
291 vmpath = vmbase + "javaw.exe";
292 if (new File(vmpath).exists()) {
297 vmpath = vmbase + "java.exe";
298 if (new File(vmpath).exists()) {
305 /** Flag indicating that we're on Windows; initialized when this class is first loaded. */
306 protected static boolean _isWindows;
307 /** Flag indicating that we're on MacOS; initialized when this class is first loaded. */
308 protected static boolean _isMacOS;
309 /** Flag indicating that we're on Linux; initialized when this class is first loaded. */
310 protected static boolean _isLinux;
314 String osname = System.getProperty("os.name");
315 osname = (osname == null) ? "" : osname;
316 _isWindows = (osname.indexOf("Windows") != -1);
317 _isMacOS = (osname.indexOf("Mac OS") != -1 ||
318 osname.indexOf("MacOS") != -1);
319 _isLinux = (osname.indexOf("Linux") != -1);
320 } catch (Exception e) {
321 // can't grab system properties; we'll just pretend we're not on any of these OSes