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