1faaa28a4ee0e59b3e69acaa0bfd536992893e8f
[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 jalview.util.LaunchUtils;
21
22 import static com.threerings.getdown.Log.log;
23
24 /**
25  * Useful routines for launching Java applications from within other Java
26  * applications.
27  */
28 public class LaunchUtil
29 {
30     /** The directory into which a local VM installation should be unpacked. */
31     public static final String LOCAL_JAVA_DIR = "jre";
32
33     /**
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.
37      *
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.
43      *
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.
49      *
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.
54      */
55     public static boolean updateVersionAndRelaunch (
56             File appdir, String getdownJarName, String newVersion)
57         throws IOException
58     {
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);
63         }
64
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()) {
68             return false;
69         }
70
71         // do the deed
72         String[] args = new String[] {
73             getJVMPath(appdir), "-jar", pro.toString(), appdir.getPath()
74         };
75         log.info("Running " + StringUtil.join(args, "\n  "));
76         try {
77             Runtime.getRuntime().exec(args, null);
78             return true;
79         } catch (IOException ioe) {
80             log.warning("Failed to run getdown", ioe);
81             return false;
82         }
83     }
84
85     /**
86      * Reconstructs the path to the JVM used to launch this process.
87      */
88     public static String getJVMPath (File appdir)
89     {
90         return getJVMPath(appdir, false);
91     }
92
93     private static String jvmPath = null;
94     /**
95      * Reconstructs the path to the JVM used to launch this process.
96      *
97      * @param windebug if true we will use java.exe instead of javaw.exe on Windows.
98      */
99     public static String getJVMPath (File appdir, boolean windebug)
100     {
101         if (jvmPath != null) {
102           return jvmPath;
103         }
104         
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();
109
110         String javaBin = LaunchUtils.findJavaBin(appDir, windebug, false);
111
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);
115         }
116
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") + "]!");
122         }
123
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.
128         if (isMacOS()) {
129             try {
130                 File localVM = new File("/usr/bin/java").getCanonicalFile();
131                 if (localVM.equals(new File(javaBin).getCanonicalFile())) {
132                     javaBin = "/usr/bin/java";
133                 }
134             } catch (IOException ioe) {
135                 log.warning("Failed to check Mac OS canonical VM path.", ioe);
136             }
137         }
138
139         jvmPath = javaBin;
140         return jvmPath;
141     }
142
143     private static String _getMD5FileChecksum (File file) {
144         // check md5 digest
145         String algo = "MD5";
146         String checksum = "";
147         try {
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());
154         }
155         return checksum;
156     }
157     
158     /**
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).
161      *
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.
164      */
165     public static void upgradeGetdown (File oldgd, File curgd, File newgd)
166     {
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
170         if (!newgd.exists())
171         {
172             return;
173         }
174         
175         if (newgd.length() == curgd.length()) {
176                 if (_getMD5FileChecksum(newgd).equals(_getMD5FileChecksum(curgd)))
177                 {
178                                 return;
179                 }
180         }
181
182         log.info("Updating Getdown with " + newgd + "...");
183
184         // clear out any old getdown
185         if (oldgd.exists()) {
186             FileUtil.deleteHarder(oldgd);
187         }
188
189         // now try updating using renames
190         if (!curgd.exists() || curgd.renameTo(oldgd)) {
191             if (newgd.renameTo(curgd)) {
192                 FileUtil.deleteHarder(oldgd); // yay!
193                 try {
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);
199                 }
200                 return;
201             }
202
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.");
207             }
208         }
209
210         // that didn't work, let's try copying it
211         log.info("Attempting to upgrade by copying over " + curgd + "...");
212         try {
213             FileUtil.copy(newgd, curgd);
214         } catch (IOException ioe) {
215             log.warning("Mayday! Brute force copy method also failed.", ioe);
216         }
217     }
218
219     /**
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.
222      */
223     public static boolean mustMonitorChildren ()
224     {
225         String osname = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
226         return (osname.indexOf("windows 98") != -1 || osname.indexOf("windows me") != -1);
227     }
228
229     /**
230      * Returns true if we're running in a JVM that identifies its operating system as Windows.
231      */
232     public static final boolean isWindows () { return _isWindows; }
233
234     /**
235      * Returns true if we're running in a JVM that identifies its operating system as MacOS.
236      */
237     public static final boolean isMacOS () { return _isMacOS; }
238
239     /**
240      * Returns true if we're running in a JVM that identifies its operating system as Linux.
241      */
242     public static final boolean isLinux () { return _isLinux; }
243
244     /**
245      * Check if a symlink (or file) points to a JVM
246      */
247     private static boolean checkJVMSymlink(String testBin)
248     {
249       File testBinFile = new File(testBin);
250       if (!testBinFile.exists())
251       {
252         return false;
253       }
254       File targetFile = null;
255       try
256       {
257         targetFile = testBinFile.getCanonicalFile();
258       } catch (IOException e)
259       {
260         return false;
261       }
262       if (targetFile != null && ("java".equals(targetFile.getName())
263             || "java.exe".equals(targetFile.getName())))
264       {
265         return true;
266       }
267       return false;
268     }
269
270     /**
271      * Checks whether a Java Virtual Machine can be located in the supplied path.
272      */
273     protected static String checkJVMPath (String vmhome, boolean windebug)
274     {
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)) {
279           return vmpath;
280         }
281         vmpath = vmbase + "Jalview";
282         if (checkJVMSymlink(vmpath)) {
283           return vmpath;
284         }
285         vmpath = vmbase + "java";
286         if (new File(vmpath).exists()) {
287             return vmpath;
288         }
289
290         if (!windebug) {
291             vmpath = vmbase + "javaw.exe";
292             if (new File(vmpath).exists()) {
293                 return vmpath;
294             }
295         }
296
297         vmpath = vmbase + "java.exe";
298         if (new File(vmpath).exists()) {
299             return vmpath;
300         }
301
302         return null;
303     }
304
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;
311
312     static {
313         try {
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
322         }
323     }
324 }