JAL-3691 patch toUpper/toLower to use Locale.ROOT for 2.11.2 getdown - needs rebuild...
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / util / LaunchUtil.java
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      */
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      */
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      */
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
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(Locale.ROOT);
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      */
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      */
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      */
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      */
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      */
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      */
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 }