1 // 2 // Getdown - application installer, patcher and launcher 3 // Copyright (C) 2004-2018 Getdown authors 4 // https://github.com/threerings/getdown/blob/master/LICENSE 5 6 package com.threerings.getdown.util; 7 8 import java.io.File; 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; 15 16 import javax.xml.bind.DatatypeConverter; 17 18 import java.security.MessageDigest; 19 20 import static com.threerings.getdown.Log.log; 21 22 /** 23 * Useful routines for launching Java applications from within other Java 24 * applications. 25 */ 26 public class LaunchUtil 27 { 28 /** The directory into which a local VM installation should be unpacked. */ 29 public static final String LOCAL_JAVA_DIR = "jre"; 30 31 /** 32 * Writes a <code>version.txt</code> file into the specified application directory and 33 * attempts to relaunch Getdown in that directory which will cause it to upgrade to the newly 34 * specified version and relaunch the application. 35 * 36 * @param appdir the directory in which the application is installed. 37 * @param getdownJarName the name of the getdown jar file in the application directory. This is 38 * probably <code>getdown-pro.jar</code> or <code>getdown-retro-pro.jar</code> if you are using 39 * the results of the standard build. 40 * @param newVersion the new version to which Getdown will update when it is executed. 41 * 42 * @return true if the relaunch succeeded, false if we were unable to relaunch due to being on 43 * Windows 9x where we cannot launch subprocesses without waiting around for them to exit, 44 * reading their stdout and stderr all the while. If true is returned, the application may exit 45 * after making this call as it will be upgraded and restarted. If false is returned, the 46 * application should tell the user that they must restart the application manually. 47 * 48 * @exception IOException thrown if we were unable to create the <code>version.txt</code> file 49 * in the supplied application directory. If the version.txt file cannot be created, restarting 50 * Getdown will not cause the application to be upgraded, so the application will have to 51 * resort to telling the user that it is in a bad way. 52 */ updateVersionAndRelaunch( File appdir, String getdownJarName, String newVersion)53 public static boolean updateVersionAndRelaunch ( 54 File appdir, String getdownJarName, String newVersion) 55 throws IOException 56 { 57 // create the file that instructs Getdown to upgrade 58 File vfile = new File(appdir, "version.txt"); 59 try (PrintStream ps = new PrintStream(new FileOutputStream(vfile))) { 60 ps.println(newVersion); 61 } 62 63 // make sure that we can find our getdown.jar file and can safely launch children 64 File pro = new File(appdir, getdownJarName); 65 if (mustMonitorChildren() || !pro.exists()) { 66 return false; 67 } 68 69 // do the deed 70 String[] args = new String[] { 71 getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath() 72 }; 73 log.info("Running " + StringUtil.join(args, "\n ")); 74 try { 75 Runtime.getRuntime().exec(args, null); 76 return true; 77 } catch (IOException ioe) { 78 log.warning("Failed to run getdown", ioe); 79 return false; 80 } 81 } 82 83 /** 84 * Reconstructs the path to the JVM used to launch this process. 85 */ getJVMPath(File appdir)86 public static String getJVMPath (File appdir) 87 { 88 return getJVMPath(appdir, false); 89 } 90 91 /** 92 * Reconstructs the path to the JVM used to launch this process. 93 * 94 * @param windebug if true we will use java.exe instead of javaw.exe on Windows. 95 */ getJVMPath(File appdir, boolean windebug)96 public static String getJVMPath (File appdir, boolean windebug) 97 { 98 // first look in our application directory for an installed VM 99 String vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR).getAbsolutePath(), windebug); 100 if (vmpath == null && isMacOS()) { 101 vmpath = checkJVMPath(new File(appdir, LOCAL_JAVA_DIR + "/Contents/Home").getAbsolutePath(), windebug); 102 } 103 104 // then fall back to the VM in which we're already running 105 if (vmpath == null) { 106 vmpath = checkJVMPath(System.getProperty("java.home"), windebug); 107 } 108 109 // then throw up our hands and hope for the best 110 if (vmpath == null) { 111 log.warning("Unable to find java [appdir=" + appdir + 112 ", java.home=" + System.getProperty("java.home") + "]!"); 113 vmpath = "java"; 114 } 115 116 // Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched 117 // from /usr/bin/java, and not if launched by directly referring to <java.home>/bin/java, 118 // even though the former is a symlink to the latter! To work around this, see if the 119 // desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead. 120 if (isMacOS()) { 121 try { 122 File localVM = new File("/usr/bin/java").getCanonicalFile(); 123 if (localVM.equals(new File(vmpath).getCanonicalFile())) { 124 vmpath = "/usr/bin/java"; 125 } 126 } catch (IOException ioe) { 127 log.warning("Failed to check Mac OS canonical VM path.", ioe); 128 } 129 } 130 131 return vmpath; 132 } 133 _getMD5FileChecksum(File file)134 private static String _getMD5FileChecksum (File file) { 135 // check md5 digest 136 String algo = "MD5"; 137 String checksum = ""; 138 try { 139 MessageDigest md = MessageDigest.getInstance(algo); 140 md.update(Files.readAllBytes(Paths.get(file.getAbsolutePath()))); 141 byte[] digest = md.digest(); 142 checksum = DatatypeConverter.printHexBinary(digest).toUpperCase(); 143 } catch (Exception e) { 144 System.out.println("Couldn't create "+algo+" digest of "+file.getPath()); 145 } 146 return checksum; 147 } 148 149 /** 150 * Upgrades Getdown by moving an installation managed copy of the Getdown jar file over the 151 * non-managed copy (which would be used to run Getdown itself). 152 * 153 * <p> If the upgrade fails for a variety of reasons, warnings are logged but no other actions 154 * are taken. There's not much else one can do other than try again next time around. 155 */ upgradeGetdown(File oldgd, File curgd, File newgd)156 public static void upgradeGetdown (File oldgd, File curgd, File newgd) 157 { 158 // we assume getdown's jar file size changes with every upgrade, this is not guaranteed, 159 // but in reality it will, and it allows us to avoid pointlessly upgrading getdown every 160 // time the client is updated which is unnecessarily flirting with danger 161 if (!newgd.exists()) 162 { 163 return; 164 } 165 166 if (newgd.length() == curgd.length()) { 167 if (_getMD5FileChecksum(newgd).equals(_getMD5FileChecksum(curgd))) 168 { 169 return; 170 } 171 } 172 173 log.info("Updating Getdown with " + newgd + "..."); 174 175 // clear out any old getdown 176 if (oldgd.exists()) { 177 FileUtil.deleteHarder(oldgd); 178 } 179 180 // now try updating using renames 181 if (!curgd.exists() || curgd.renameTo(oldgd)) { 182 if (newgd.renameTo(curgd)) { 183 FileUtil.deleteHarder(oldgd); // yay! 184 try { 185 // copy the moved file back to getdown-dop-new.jar so that we don't end up 186 // downloading another copy next time 187 FileUtil.copy(curgd, newgd); 188 } catch (IOException e) { 189 log.warning("Error copying updated Getdown back: " + e); 190 } 191 return; 192 } 193 194 log.warning("Unable to renameTo(" + oldgd + ")."); 195 // try to unfuck ourselves 196 if (!oldgd.renameTo(curgd)) { 197 log.warning("Oh God, why dost thee scorn me so."); 198 } 199 } 200 201 // that didn't work, let's try copying it 202 log.info("Attempting to upgrade by copying over " + curgd + "..."); 203 try { 204 FileUtil.copy(newgd, curgd); 205 } catch (IOException ioe) { 206 log.warning("Mayday! Brute force copy method also failed.", ioe); 207 } 208 } 209 210 /** 211 * Returns true if, on this operating system, we have to stick around and read the stderr from 212 * our children processes to prevent them from filling their output buffers and hanging. 213 */ mustMonitorChildren()214 public static boolean mustMonitorChildren () 215 { 216 String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); 217 return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1); 218 } 219 220 /** 221 * Returns true if we're running in a JVM that identifies its operating system as Windows. 222 */ isWindows()223 public static final boolean isWindows () { return _isWindows; } 224 225 /** 226 * Returns true if we're running in a JVM that identifies its operating system as MacOS. 227 */ isMacOS()228 public static final boolean isMacOS () { return _isMacOS; } 229 230 /** 231 * Returns true if we're running in a JVM that identifies its operating system as Linux. 232 */ isLinux()233 public static final boolean isLinux () { return _isLinux; } 234 235 /** 236 * Checks whether a Java Virtual Machine can be located in the supplied path. 237 */ checkJVMPath(String vmhome, boolean windebug)238 protected static String checkJVMPath (String vmhome, boolean windebug) 239 { 240 String vmbase = vmhome + File.separator + "bin" + File.separator; 241 String vmpath = vmbase + "java"; 242 if (new File(vmpath).exists()) { 243 return vmpath; 244 } 245 246 if (!windebug) { 247 vmpath = vmbase + "javaw.exe"; 248 if (new File(vmpath).exists()) { 249 return vmpath; 250 } 251 } 252 253 vmpath = vmbase + "java.exe"; 254 if (new File(vmpath).exists()) { 255 return vmpath; 256 } 257 258 return null; 259 } 260 261 /** Flag indicating that we're on Windows; initialized when this class is first loaded. */ 262 protected static boolean _isWindows; 263 /** Flag indicating that we're on MacOS; initialized when this class is first loaded. */ 264 protected static boolean _isMacOS; 265 /** Flag indicating that we're on Linux; initialized when this class is first loaded. */ 266 protected static boolean _isLinux; 267 268 static { 269 try { 270 String osname = System.getProperty("os.name"); 271 osname = (osname == null) ? "" : osname; 272 _isWindows = (osname.indexOf("Windows") != -1); 273 _isMacOS = (osname.indexOf("Mac OS") != -1 || 274 osname.indexOf("MacOS") != -1); 275 _isLinux = (osname.indexOf("Linux") != -1); 276 } catch (Exception e) { 277 // can't grab system properties; we'll just pretend we're not on any of these OSes 278 } 279 } 280 } 281