JAL-3247 Code changes to getdown. Double click open of .jvp files in Windows and...
[jalview.git] / getdown / src / getdown / launcher / src / main / java / com / threerings / getdown / launcher / Getdown.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.launcher;
7
8 import java.awt.BorderLayout;
9 import java.awt.Container;
10 import java.awt.Dimension;
11 import java.awt.EventQueue;
12 import java.awt.GraphicsEnvironment;
13 import java.awt.Image;
14 import java.awt.event.ActionEvent;
15 import java.awt.image.BufferedImage;
16 import java.io.BufferedReader;
17 import java.io.File;
18 import java.io.FileNotFoundException;
19 import java.io.FileReader;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.io.PrintStream;
24 import java.net.HttpURLConnection;
25 import java.net.MalformedURLException;
26 import java.net.URL;
27 import java.util.*;
28
29 import javax.imageio.ImageIO;
30 import javax.swing.AbstractAction;
31 import javax.swing.JButton;
32 import javax.swing.JFrame;
33 import javax.swing.JLayeredPane;
34
35 import com.samskivert.swing.util.SwingUtil;
36 import com.threerings.getdown.data.*;
37 import com.threerings.getdown.data.Application.UpdateInterface.Step;
38 import com.threerings.getdown.net.Downloader;
39 import com.threerings.getdown.net.HTTPDownloader;
40 import com.threerings.getdown.tools.Patcher;
41 import com.threerings.getdown.util.*;
42
43 import static com.threerings.getdown.Log.log;
44
45 /**
46  * Manages the main control for the Getdown application updater and deployment system.
47  */
48 public abstract class Getdown extends Thread
49     implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
50 {
51     public Getdown (EnvConfig envc)
52     {
53         super("Getdown");
54         try {
55             // If the silent property exists, install without bringing up any gui. If it equals
56             // launch, start the application after installing. Otherwise, just install and exit.
57             _silent = SysProps.silent();
58             if (_silent) {
59                 _launchInSilent = SysProps.launchInSilent();
60                 _noUpdate = SysProps.noUpdate();
61             }
62             // If we're running in a headless environment and have not otherwise customized
63             // silence, operate without a UI and do launch the app.
64             if (!_silent && GraphicsEnvironment.isHeadless()) {
65                 log.info("Running in headless JVM, will attempt to operate without UI.");
66                 _silent = true;
67                 _launchInSilent = true;
68             }
69             _delay = SysProps.startDelay();
70         } catch (SecurityException se) {
71             // don't freak out, just assume non-silent and no delay; we're probably already
72             // recovering from a security failure
73         }
74         try {
75             _msgs = ResourceBundle.getBundle("com.threerings.getdown.messages");
76         } catch (Exception e) {
77             // welcome to hell, where java can't cope with a classpath that contains jars that live
78             // in a directory that contains a !, at least the same bug happens on all platforms
79             String dir = envc.appDir.toString();
80             if (dir.equals(".")) {
81                 dir = System.getProperty("user.dir");
82             }
83             String errmsg = "The directory in which this application is installed:\n" + dir +
84                 "\nis invalid (" + e.getMessage() + "). If the full path to the app directory " +
85                 "contains the '!' character, this will trigger this error.";
86             fail(errmsg);
87         }
88         _app = new Application(envc);
89         _startup = System.currentTimeMillis();
90     }
91
92     /**
93      * Returns true if there are pending new resources, waiting to be installed.
94      */
95     public boolean isUpdateAvailable ()
96     {
97         return _readyToInstall && !_toInstallResources.isEmpty();
98     }
99
100     /**
101      * Installs the currently pending new resources.
102      */
103     public void install () throws IOException
104     {
105         if (SysProps.noInstall()) {
106             log.info("Skipping install due to 'no_install' sysprop.");
107         } else if (_readyToInstall) {
108             log.info("Installing " + _toInstallResources.size() + " downloaded resources:");
109             for (Resource resource : _toInstallResources) {
110                 resource.install(true);
111             }
112             _toInstallResources.clear();
113             _readyToInstall = false;
114             log.info("Install completed.");
115         } else {
116             log.info("Nothing to install.");
117         }
118     }
119
120     @Override
121     public void run ()
122     {
123         // if we have no messages, just bail because we're hosed; the error message will be
124         // displayed to the user already
125         if (_msgs == null) {
126             return;
127         }
128
129         log.info("Getdown starting", "version", Build.version(), "built", Build.time());
130
131         // determine whether or not we can write to our install directory
132         File instdir = _app.getLocalPath("");
133         if (!instdir.canWrite()) {
134             String path = instdir.getPath();
135             if (path.equals(".")) {
136                 path = System.getProperty("user.dir");
137             }
138             fail(MessageUtil.tcompose("m.readonly_error", path));
139             return;
140         }
141
142         try {
143             _dead = false;
144             // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
145             // run the app anyway because we're prepared to cope with not being able to update
146             if (detectProxy() || _app.allowOffline()) {
147                 getdown();
148             } else if (_silent) {
149                 log.warning("Need a proxy, but we don't want to bother anyone.  Exiting.");
150             } else {
151                 // create a panel they can use to configure the proxy settings
152                 _container = createContainer();
153                 // allow them to close the window to abort the proxy configuration
154                 _dead = true;
155                 configureContainer();
156                 ProxyPanel panel = new ProxyPanel(this, _msgs);
157                 // set up any existing configured proxy
158                 String[] hostPort = ProxyUtil.loadProxy(_app);
159                 panel.setProxy(hostPort[0], hostPort[1]);
160                 _container.add(panel, BorderLayout.CENTER);
161                 showContainer();
162             }
163
164         } catch (Exception e) {
165             log.warning("run() failed.", e);
166             String msg = e.getMessage();
167             if (msg == null) {
168                 msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
169             } else if (!msg.startsWith("m.")) {
170                 // try to do something sensible based on the type of error
171                 if (e instanceof FileNotFoundException) {
172                     msg = MessageUtil.compose(
173                         "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
174                 } else {
175                     msg = MessageUtil.compose(
176                         "m.init_error", MessageUtil.taint(msg), _ifc.installError);
177                 }
178             }
179             fail(msg);
180         }
181     }
182
183     /**
184      * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
185      */
186     public void configProxy (String host, String port, String username, String password)
187     {
188         log.info("User configured proxy", "host", host, "port", port);
189
190         if (!StringUtil.isBlank(host)) {
191             ProxyUtil.configProxy(_app, host, port, username, password);
192         }
193
194         // clear out our UI
195         disposeContainer();
196         _container = null;
197
198         // fire up a new thread
199         new Thread(this).start();
200     }
201
202     protected boolean detectProxy () {
203         if (ProxyUtil.autoDetectProxy(_app)) {
204             return true;
205         }
206
207         // otherwise see if we actually need a proxy; first we have to initialize our application
208         // to get some sort of interface configuration and the appbase URL
209         log.info("Checking whether we need to use a proxy...");
210         try {
211             readConfig(true);
212         } catch (IOException ioe) {
213             // no worries
214         }
215         updateStatus("m.detecting_proxy");
216         if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
217             return false;
218         }
219
220         // we got through, so we appear not to require a proxy; make a blank proxy config so that
221         // we don't go through this whole detection process again next time
222         log.info("No proxy appears to be needed.");
223         ProxyUtil.saveProxy(_app, null, null);
224         return true;
225     }
226
227     protected void readConfig (boolean preloads) throws IOException {
228         Config config = _app.init(true);
229         if (preloads) doPredownloads(_app.getResources());
230         _ifc = new Application.UpdateInterface(config);
231     }
232
233     /**
234      * Downloads and installs (without verifying) any resources that are marked with a
235      * {@code PRELOAD} attribute.
236      * @param resources the full set of resources from the application (the predownloads will be
237      * extracted from it).
238      */
239     protected void doPredownloads (Collection<Resource> resources) {
240         List<Resource> predownloads = new ArrayList<>();
241         for (Resource rsrc : resources) {
242             if (rsrc.shouldPredownload() && !rsrc.getLocal().exists()) {
243                 predownloads.add(rsrc);
244             }
245         }
246
247         try {
248             download(predownloads);
249             for (Resource rsrc : predownloads) {
250                 rsrc.install(false); // install but don't validate yet
251             }
252         } catch (IOException ioe) {
253             log.warning("Failed to predownload resources. Continuing...", ioe);
254         }
255     }
256
257     /**
258      * Does the actual application validation, update and launching business.
259      */
260     protected void getdown ()
261     {
262         try {
263             // first parses our application deployment file
264             try {
265                 readConfig(true);
266             } catch (IOException ioe) {
267                 log.warning("Failed to initialize: " + ioe);
268                 _app.attemptRecovery(this);
269                 // and re-initalize
270                 readConfig(true);
271                 // and force our UI to be recreated with the updated info
272                 createInterfaceAsync(true);
273             }
274             if (!_noUpdate && !_app.lockForUpdates()) {
275                 throw new MultipleGetdownRunning();
276             }
277
278             // Update the config modtime so a sleeping getdown will notice the change.
279             File config = _app.getLocalPath(Application.CONFIG_FILE);
280             if (!config.setLastModified(System.currentTimeMillis())) {
281                 log.warning("Unable to set modtime on config file, will be unable to check for " +
282                             "another instance of getdown running while this one waits.");
283             }
284             if (_delay > 0) {
285                 // don't hold the lock while waiting, let another getdown proceed if it starts.
286                 _app.releaseLock();
287                 // Store the config modtime before waiting the delay amount of time
288                 long lastConfigModtime = config.lastModified();
289                 log.info("Waiting " + _delay + " minutes before beginning actual work.");
290                 Thread.sleep(_delay * 60 * 1000);
291                 if (lastConfigModtime < config.lastModified()) {
292                     log.warning("getdown.txt was modified while getdown was waiting.");
293                     throw new MultipleGetdownRunning();
294                 }
295             }
296
297             // if no_update was specified, directly start the app without updating
298             if (_noUpdate) {
299                 log.info("Launching without update!");
300                 launch();
301                 return;
302             }
303
304             // we create this tracking counter here so that we properly note the first time through
305             // the update process whether we previously had validated resources (which means this
306             // is not a first time install); we may, in the course of updating, wipe out our
307             // validation markers and revalidate which would make us think we were doing a fresh
308             // install if we didn't specifically remember that we had validated resources the first
309             // time through
310             int[] alreadyValid = new int[1];
311
312             // we'll keep track of all the resources we unpack
313             Set<Resource> unpacked = new HashSet<>();
314
315             _toInstallResources = new HashSet<>();
316             _readyToInstall = false;
317
318             // setStep(Step.START);
319             for (int ii = 0; ii < MAX_LOOPS; ii++) {
320                 // make sure we have the desired version and that the metadata files are valid...
321                 setStep(Step.VERIFY_METADATA);
322                 setStatusAsync("m.validating", -1, -1L, false);
323                 if (_app.verifyMetadata(this)) {
324                     log.info("Application requires update.");
325                     update();
326                     // loop back again and reverify the metadata
327                     continue;
328                 }
329
330                 // now verify (and download) our resources...
331                 setStep(Step.VERIFY_RESOURCES);
332                 setStatusAsync("m.validating", -1, -1L, false);
333                 Set<Resource> toDownload = new HashSet<>();
334                 _app.verifyResources(_progobs, alreadyValid, unpacked,
335                                      _toInstallResources, toDownload);
336
337                 if (toDownload.size() > 0) {
338                     // we have resources to download, also note them as to-be-installed
339                     for (Resource r : toDownload) {
340                         if (!_toInstallResources.contains(r)) {
341                             _toInstallResources.add(r);
342                         }
343                     }
344
345                     try {
346                         // if any of our resources have already been marked valid this is not a
347                         // first time install and we don't want to enable tracking
348                         _enableTracking = (alreadyValid[0] == 0);
349                         reportTrackingEvent("app_start", -1);
350
351                         // redownload any that are corrupt or invalid...
352                         log.info(toDownload.size() + " of " + _app.getAllActiveResources().size() +
353                                  " rsrcs require update (" + alreadyValid[0] + " assumed valid).");
354                         setStep(Step.REDOWNLOAD_RESOURCES);
355                         download(toDownload);
356
357                         reportTrackingEvent("app_complete", -1);
358
359                     } finally {
360                         _enableTracking = false;
361                     }
362
363                     // now we'll loop back and try it all again
364                     continue;
365                 }
366
367                 // if we aren't running in a JVM that meets our version requirements, either
368                 // complain or attempt to download and install the appropriate version
369                 if (!_app.haveValidJavaVersion()) {
370                     // download and install the necessary version of java, then loop back again and
371                     // reverify everything; if we can't download java; we'll throw an exception
372                     log.info("Attempting to update Java VM...");
373                     setStep(Step.UPDATE_JAVA);
374                     _enableTracking = true; // always track JVM downloads
375                     try {
376                         updateJava();
377                     } finally {
378                         _enableTracking = false;
379                     }
380                     continue;
381                 }
382
383                 // if we were downloaded in full from another service (say, Steam), we may
384                 // not have unpacked all of our resources yet
385                 if (Boolean.getBoolean("check_unpacked")) {
386                     File ufile = _app.getLocalPath("unpacked.dat");
387                     long version = -1;
388                     long aversion = _app.getVersion();
389                     if (!ufile.exists()) {
390                         ufile.createNewFile();
391                     } else {
392                         version = VersionUtil.readVersion(ufile);
393                     }
394
395                     if (version < aversion) {
396                         log.info("Performing unpack", "version", version, "aversion", aversion);
397                         setStep(Step.UNPACK);
398                         updateStatus("m.validating");
399                         _app.unpackResources(_progobs, unpacked);
400                         try {
401                             VersionUtil.writeVersion(ufile, aversion);
402                         } catch (IOException ioe) {
403                             log.warning("Failed to update unpacked version", ioe);
404                         }
405                     }
406                 }
407
408                 // assuming we're not doing anything funny, install the update
409                 _readyToInstall = true;
410                 install();
411
412                 // Only launch if we aren't in silent mode. Some mystery program starting out
413                 // of the blue would be disconcerting.
414                 if (!_silent || _launchInSilent) {
415                     // And another final check for the lock. It'll already be held unless
416                     // we're in silent mode.
417                     _app.lockForUpdates();
418                     launch();
419                 }
420                 return;
421             }
422
423             log.warning("Pants! We couldn't get the job done.");
424             throw new IOException("m.unable_to_repair");
425
426         } catch (Exception e) {
427             log.warning("getdown() failed.", e);
428             String msg = e.getMessage();
429             if (msg == null) {
430                 msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
431             } else if (!msg.startsWith("m.")) {
432                 // try to do something sensible based on the type of error
433                 if (e instanceof FileNotFoundException) {
434                     msg = MessageUtil.compose(
435                         "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
436                 } else {
437                     msg = MessageUtil.compose(
438                         "m.init_error", MessageUtil.taint(msg), _ifc.installError);
439                 }
440             }
441             // Since we're dead, clear off the 'time remaining' label along with displaying the
442             // error message
443             fail(msg);
444             _app.releaseLock();
445         }
446     }
447
448     // documentation inherited from interface
449     @Override
450     public void updateStatus (String message)
451     {
452         setStatusAsync(message, -1, -1L, true);
453     }
454
455     /**
456      * Load the image at the path. Before trying the exact path/file specified we will look to see
457      * if we can find a localized version by sticking a {@code _<language>} in front of the "." in
458      * the filename.
459      */
460     @Override
461     public BufferedImage loadImage (String path)
462     {
463         if (StringUtil.isBlank(path)) {
464             return null;
465         }
466
467         File imgpath = null;
468         try {
469             // First try for a localized image.
470             String localeStr = Locale.getDefault().getLanguage();
471             imgpath = _app.getLocalPath(path.replace(".", "_" + localeStr + "."));
472             return ImageIO.read(imgpath);
473         } catch (IOException ioe) {
474             // No biggie, we'll try the generic one.
475         }
476
477         // If that didn't work, try a generic one.
478         try {
479             imgpath = _app.getLocalPath(path);
480             return ImageIO.read(imgpath);
481         } catch (IOException ioe2) {
482             log.warning("Failed to load image", "path", imgpath, "error", ioe2);
483             return null;
484         }
485     }
486
487     /**
488      * Downloads and installs an Java VM bundled with the application. This is called if we are not
489      * running with the necessary Java version.
490      */
491     protected void updateJava ()
492         throws IOException
493     {
494         Resource vmjar = _app.getJavaVMResource();
495         if (vmjar == null) {
496             throw new IOException("m.java_download_failed");
497         }
498
499         reportTrackingEvent("jvm_start", -1);
500
501         updateStatus("m.downloading_java");
502         List<Resource> list = new ArrayList<>();
503         list.add(vmjar);
504         download(list);
505
506         reportTrackingEvent("jvm_unpack", -1);
507
508         updateStatus("m.unpacking_java");
509         vmjar.install(true);
510
511         // these only run on non-Windows platforms, so we use Unix file separators
512         String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
513         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
514         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
515         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
516
517         // lastly regenerate the .jsa dump file that helps Java to start up faster
518         String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
519         try {
520             log.info("Regenerating classes.jsa for " + vmpath + "...");
521             Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
522         } catch (Exception e) {
523             log.warning("Failed to regenerate .jsa dump file", "error", e);
524         }
525
526         reportTrackingEvent("jvm_complete", -1);
527     }
528
529     /**
530      * Called if the application is determined to be of an old version.
531      */
532     protected void update ()
533         throws IOException
534     {
535         // first clear all validation markers
536         _app.clearValidationMarkers();
537
538         // attempt to download the patch files
539         Resource patch = _app.getPatchResource(null);
540         if (patch != null) {
541             List<Resource> list = new ArrayList<>();
542             list.add(patch);
543
544             // add the auxiliary group patch files for activated groups
545             for (Application.AuxGroup aux : _app.getAuxGroups()) {
546                 if (_app.isAuxGroupActive(aux.name)) {
547                     patch = _app.getPatchResource(aux.name);
548                     if (patch != null) {
549                         list.add(patch);
550                     }
551                 }
552             }
553
554             // show the patch notes button, if applicable
555             if (!StringUtil.isBlank(_ifc.patchNotesUrl)) {
556                 createInterfaceAsync(false);
557                 EventQueue.invokeLater(new Runnable() {
558                     public void run () {
559                         _patchNotes.setVisible(true);
560                     }
561                 });
562             }
563
564             // download the patch files...
565             setStep(Step.DOWNLOAD);
566             download(list);
567
568             // and apply them...
569             setStep(Step.PATCH);
570             updateStatus("m.patching");
571
572             long[] sizes = new long[list.size()];
573             Arrays.fill(sizes, 1L);
574             ProgressAggregator pragg = new ProgressAggregator(_progobs, sizes);
575             int ii = 0; for (Resource prsrc : list) {
576                 ProgressObserver pobs = pragg.startElement(ii++);
577                 try {
578                     // install the patch file (renaming them from _new)
579                     prsrc.install(false);
580                     // now apply the patch
581                     Patcher patcher = new Patcher();
582                     patcher.patch(prsrc.getLocal().getParentFile(), prsrc.getLocal(), pobs);
583                 } catch (Exception e) {
584                     log.warning("Failed to apply patch", "prsrc", prsrc, e);
585                 }
586
587                 // clean up the patch file
588                 if (!FileUtil.deleteHarder(prsrc.getLocal())) {
589                     log.warning("Failed to delete '" + prsrc + "'.");
590                 }
591             }
592         }
593
594         // if the patch resource is null, that means something was booched in the application, so
595         // we skip the patching process but update the metadata which will result in a "brute
596         // force" upgrade
597
598         // finally update our metadata files...
599         _app.updateMetadata();
600         // ...and reinitialize the application
601         readConfig(false);
602     }
603
604     /**
605      * Called if the application is determined to require resource downloads.
606      */
607     protected void download (Collection<Resource> resources)
608         throws IOException
609     {
610         // create our user interface
611         createInterfaceAsync(false);
612
613         Downloader dl = new HTTPDownloader(_app.proxy) {
614             @Override protected void resolvingDownloads () {
615                 updateStatus("m.resolving");
616             }
617
618             @Override protected void downloadProgress (int percent, long remaining) {
619                 // check for another getdown running at 0 and every 10% after that
620                 if (_lastCheck == -1 || percent >= _lastCheck + 10) {
621                     if (_delay > 0) {
622                         // stop the presses if something else is holding the lock
623                         boolean locked = _app.lockForUpdates();
624                         _app.releaseLock();
625                         if (locked) abort();
626                     }
627                     _lastCheck = percent;
628                 }
629                 setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true);
630                 if (percent > 0) {
631                     reportTrackingEvent("progress", percent);
632                 }
633             }
634
635             @Override protected void downloadFailed (Resource rsrc, Exception e) {
636                 updateStatus(MessageUtil.tcompose("m.failure", e.getMessage()));
637                 log.warning("Download failed", "rsrc", rsrc, e);
638             }
639
640             /** The last percentage at which we checked for another getdown running, or -1 for not
641              * having checked at all. */
642             protected int _lastCheck = -1;
643         };
644         if (!dl.download(resources, _app.maxConcurrentDownloads())) {
645             // if we aborted due to detecting another getdown running, we want to report here
646             throw new MultipleGetdownRunning();
647         }
648     }
649
650     /**
651      * Called to launch the application if everything is determined to be ready to go.
652      */
653     protected void launch ()
654     {
655         setStep(Step.LAUNCH);
656         setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false);
657
658         try {
659             if (invokeDirect()) {
660                 // we want to close the Getdown window, as the app is launching
661                 disposeContainer();
662                 _app.releaseLock();
663                 _app.invokeDirect();
664
665             } else {
666                 Process proc;
667                 if (_app.hasOptimumJvmArgs()) {
668                     // if we have "optimum" arguments, we want to try launching with them first
669                     proc = _app.createProcess(true);
670
671                     long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME;
672                     boolean error = false;
673                     while (fallback > System.currentTimeMillis()) {
674                         try {
675                             error = proc.exitValue() != 0;
676                             break;
677                         } catch (IllegalThreadStateException e) {
678                             Thread.yield();
679                         }
680                     }
681
682                     if (error) {
683                         log.info("Failed to launch with optimum arguments; falling back.");
684                         proc = _app.createProcess(false);
685                     }
686                 } else {
687                     proc = _app.createProcess(false);
688                 }
689
690                 // close standard in to avoid choking standard out of the launched process
691                 proc.getInputStream().close();
692                 // close standard out, since we're not going to write to anything to it anyway
693                 proc.getOutputStream().close();
694
695                 // on Windows 98 and ME we need to stick around and read the output of stderr lest
696                 // the process fill its output buffer and choke, yay!
697                 final InputStream stderr = proc.getErrorStream();
698                 if (LaunchUtil.mustMonitorChildren()) {
699                     // close our window if it's around
700                     disposeContainer();
701                     _container = null;
702                     copyStream(stderr, System.err);
703                     log.info("Process exited: " + proc.waitFor());
704
705                 } else {
706                     // spawn a daemon thread that will catch the early bits of stderr in case the
707                     // launch fails
708                     Thread t = new Thread() {
709                         @Override public void run () {
710                             copyStream(stderr, System.err);
711                         }
712                     };
713                     t.setDaemon(true);
714                     t.start();
715                 }
716             }
717
718             // if we have a UI open and we haven't been around for at least 5 seconds (the default
719             // for min_show_seconds), don't stick a fork in ourselves straight away but give our
720             // lovely user a chance to see what we're doing
721             long uptime = System.currentTimeMillis() - _startup;
722             long minshow = _ifc.minShowSeconds * 1000L;
723             if (_container != null && uptime < minshow) {
724                 try {
725                     Thread.sleep(minshow - uptime);
726                 } catch (Exception e) {
727                 }
728             }
729
730             // pump the percent up to 100%
731             setStatusAsync(null, 100, -1L, false);
732             exit(0);
733
734         } catch (Exception e) {
735             log.warning("launch() failed.", e);
736         }
737     }
738
739     /**
740      * Creates our user interface, which we avoid doing unless we actually have to update
741      * something. NOTE: this happens on the next UI tick, not immediately.
742      *
743      * @param reinit - if the interface should be reinitialized if it already exists.
744      */
745     protected void createInterfaceAsync (final boolean reinit)
746     {
747         if (_silent || (_container != null && !reinit)) {
748             return;
749         }
750
751         EventQueue.invokeLater(new Runnable() {
752             public void run () {
753                 if (_container == null || reinit) {
754                     if (_container == null) {
755                         _container = createContainer();
756                     } else {
757                         _container.removeAll();
758                     }
759                     configureContainer();
760                     _layers = new JLayeredPane();
761                     _container.add(_layers, BorderLayout.CENTER);
762                     _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
763                         @Override public void actionPerformed (ActionEvent event) {
764                             showDocument(_ifc.patchNotesUrl);
765                         }
766                     });
767                     _patchNotes.setFont(StatusPanel.FONT);
768                     _layers.add(_patchNotes);
769                     _status = new StatusPanel(_msgs);
770                     _layers.add(_status);
771                     initInterface();
772                 }
773                 showContainer();
774             }
775         });
776     }
777
778     /**
779      * Initializes the interface with the current UpdateInterface and backgrounds.
780      */
781     protected void initInterface ()
782     {
783         RotatingBackgrounds newBackgrounds = getBackground();
784         if (_background == null || newBackgrounds.getNumImages() > 0) {
785             // Leave the old _background in place if there is an old one to leave in place
786             // and the new getdown.txt didn't yield any images.
787             _background = newBackgrounds;
788         }
789         _status.init(_ifc, _background, getProgressImage());
790         Dimension size = _status.getPreferredSize();
791         _status.setSize(size);
792         _layers.setPreferredSize(size);
793
794         _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y,
795                               _ifc.patchNotes.width, _ifc.patchNotes.height);
796         _patchNotes.setVisible(false);
797
798         // we were displaying progress while the UI wasn't up. Now that it is, whatever progress
799         // is left is scaled into a 0-100 DISPLAYED progress.
800         _uiDisplayPercent = _lastGlobalPercent;
801         _stepMinPercent = _lastGlobalPercent = 0;
802     }
803
804     protected RotatingBackgrounds getBackground ()
805     {
806         if (_ifc.rotatingBackgrounds != null) {
807             if (_ifc.backgroundImage != null) {
808                 log.warning("ui.background_image and ui.rotating_background were both specified. " +
809                             "The rotating images are being used.");
810             }
811             return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground,
812                 Getdown.this);
813         } else if (_ifc.backgroundImage != null) {
814             return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
815         } else {
816             return new RotatingBackgrounds();
817         }
818     }
819
820     protected Image getProgressImage ()
821     {
822         return loadImage(_ifc.progressImage);
823     }
824
825     protected void handleWindowClose ()
826     {
827         if (_dead) {
828             exit(0);
829         } else {
830             if (_abort == null) {
831                 _abort = new AbortPanel(Getdown.this, _msgs);
832             }
833             _abort.pack();
834             SwingUtil.centerWindow(_abort);
835             _abort.setVisible(true);
836             _abort.setState(JFrame.NORMAL);
837             _abort.requestFocus();
838         }
839     }
840
841     /**
842      * Update the status to indicate getdown has failed for the reason in <code>message</code>.
843      */
844     protected void fail (String message)
845     {
846         _dead = true;
847         setStatusAsync(message, stepToGlobalPercent(0), -1L, true);
848     }
849
850     /**
851      * Set the current step, which will be used to globalize per-step percentages.
852      */
853     protected void setStep (Step step)
854     {
855         int finalPercent = -1;
856         for (Integer perc : _ifc.stepPercentages.get(step)) {
857             if (perc > _stepMaxPercent) {
858                 finalPercent = perc;
859                 break;
860             }
861         }
862         if (finalPercent == -1) {
863             // we've gone backwards and this step will be ignored
864             return;
865         }
866
867         _stepMaxPercent = finalPercent;
868         _stepMinPercent = _lastGlobalPercent;
869     }
870
871     /**
872      * Convert a step percentage to the global percentage.
873      */
874     protected int stepToGlobalPercent (int percent)
875     {
876         int adjustedMaxPercent =
877             ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent);
878         _lastGlobalPercent = Math.max(_lastGlobalPercent,
879             _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100);
880         return _lastGlobalPercent;
881     }
882
883     /**
884      * Updates the status. NOTE: this happens on the next UI tick, not immediately.
885      */
886     protected void setStatusAsync (final String message, final int percent, final long remaining,
887                                    boolean createUI)
888     {
889         if (_status == null && createUI) {
890             createInterfaceAsync(false);
891         }
892
893         EventQueue.invokeLater(new Runnable() {
894             public void run () {
895                 if (_status == null) {
896                     if (message != null) {
897                         log.info("Dropping status '" + message + "'.");
898                     }
899                     return;
900                 }
901                 if (message != null) {
902                     _status.setStatus(message, _dead);
903                 }
904                 if (_dead) {
905                     _status.setProgress(0, -1L);
906                 } else if (percent >= 0) {
907                     _status.setProgress(percent, remaining);
908                 }
909             }
910         });
911     }
912
913     protected void reportTrackingEvent (String event, int progress)
914     {
915         if (!_enableTracking) {
916             return;
917
918         } else if (progress > 0) {
919             // we need to make sure we do the right thing if we skip over progress levels
920             do {
921                 URL url = _app.getTrackingProgressURL(++_reportedProgress);
922                 if (url != null) {
923                     new ProgressReporter(url).start();
924                 }
925             } while (_reportedProgress <= progress);
926
927         } else {
928             URL url = _app.getTrackingURL(event);
929             if (url != null) {
930                 new ProgressReporter(url).start();
931             }
932         }
933     }
934
935     /**
936      * Creates the container in which our user interface will be displayed.
937      */
938     protected abstract Container createContainer ();
939
940     /**
941      * Configures the interface container based on the latest UI config.
942      */
943     protected abstract void configureContainer ();
944
945     /**
946      * Shows the container in which our user interface will be displayed.
947      */
948     protected abstract void showContainer ();
949
950     /**
951      * Disposes the container in which we have our user interface.
952      */
953     protected abstract void disposeContainer ();
954
955     /**
956      * If this method returns true we will run the application in the same JVM, otherwise we will
957      * fork off a new JVM. Some options are not supported if we do not fork off a new JVM.
958      */
959     protected boolean invokeDirect ()
960     {
961         return SysProps.direct();
962     }
963
964     /**
965      * Requests to show the document at the specified URL in a new window.
966      */
967     protected abstract void showDocument (String url);
968
969     /**
970      * Requests that Getdown exit.
971      */
972     protected abstract void exit (int exitCode);
973
974     /**
975      * Copies the supplied stream from the specified input to the specified output. Used to copy
976      * our child processes stderr and stdout to our own stderr and stdout.
977      */
978     protected static void copyStream (InputStream in, PrintStream out)
979     {
980         try {
981             BufferedReader reader = new BufferedReader(new InputStreamReader(in));
982             String line;
983             while ((line = reader.readLine()) != null) {
984                 out.print(line);
985                 out.flush();
986             }
987         } catch (IOException ioe) {
988             log.warning("Failure copying", "in", in, "out", out, "error", ioe);
989         }
990     }
991
992     /** Used to fetch a progress report URL. */
993     protected class ProgressReporter extends Thread
994     {
995         public ProgressReporter (URL url) {
996             setDaemon(true);
997             _url = url;
998         }
999
1000         @Override
1001         public void run () {
1002             try {
1003                 HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
1004
1005                 // if we have a tracking cookie configured, configure the request with it
1006                 if (_app.getTrackingCookieName() != null &&
1007                     _app.getTrackingCookieProperty() != null) {
1008                     String val = System.getProperty(_app.getTrackingCookieProperty());
1009                     if (val != null) {
1010                         ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
1011                     }
1012                 }
1013
1014                 // now request our tracking URL and ensure that we get a non-error response
1015                 ucon.connect();
1016                 try {
1017                     if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1018                         log.warning("Failed to report tracking event",
1019                             "url", _url, "rcode", ucon.getResponseCode());
1020                     }
1021                 } finally {
1022                     ucon.disconnect();
1023                 }
1024
1025             } catch (IOException ioe) {
1026                 log.warning("Failed to report tracking event", "url", _url, "error", ioe);
1027             }
1028         }
1029
1030         protected URL _url;
1031     }
1032
1033     /** Used to pass progress on to our user interface. */
1034     protected ProgressObserver _progobs = new ProgressObserver() {
1035         public void progress (int percent) {
1036             setStatusAsync(null, stepToGlobalPercent(percent), -1L, false);
1037         }
1038     };
1039
1040     public void setStartupFilesFromParameterString(String p) {
1041       String q = "\"";
1042       if (p != null && p.length() > 0) {
1043         String[] filenames;
1044         if (p.startsWith(q) && p.endsWith(q)) {
1045           filenames = p.substring(1,p.length()-1).split("\" \"");
1046         } else {
1047           filenames = new String[]{p};
1048         }
1049         for (int i = 0; i < filenames.length; i++) {
1050           String filename = filenames[i];
1051           String ext = null;
1052           int j = filename.lastIndexOf('.');
1053           if (j > -1) {
1054             ext = filename.substring(j+1);
1055           }
1056           // jvp files etc
1057           boolean addedStartupFile = false; // only add one startup file
1058           if (_app.startupFileExtensions.contains(ext.toLowerCase()) && ! addedStartupFile) {
1059             File f = new File(filename);
1060             if (f.exists()) {
1061               _app.addStartupFile(f);
1062               addedStartupFile = true;
1063             }
1064           }
1065           // jvl files
1066           if (_app.locatorFileExtension.equals(ext.toLowerCase())) {
1067             // Do something special with this here
1068             File f = new File(filename);
1069             if (f.exists()) {
1070               String line = null;
1071               try {
1072                 FileReader fr = new FileReader(f);
1073                 BufferedReader br = new BufferedReader(fr);
1074                 line = br.readLine();
1075                 br.close();
1076               } catch(Exception e) {
1077                 log.warning("Something went wrong reading Jalview Version Locator file '"+filename+"'", e);
1078               }
1079               if (line != null) {
1080                 String urlString = line.trim();
1081                 try {
1082                   URL newAppbase = new URL(urlString);
1083                   _app.newAppbase(newAppbase);
1084                 } catch(MalformedURLException e) {
1085                   log.warning("Java Version Locator url '"+urlString+"' found in file '"+filename+"' is malformed", e);
1086                 }
1087               }
1088             }
1089           }
1090         }
1091       }
1092     }
1093
1094     protected Application _app;
1095     protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
1096
1097     protected ResourceBundle _msgs;
1098     protected Container _container;
1099     protected JLayeredPane _layers;
1100     protected StatusPanel _status;
1101     protected JButton _patchNotes;
1102     protected AbortPanel _abort;
1103     protected RotatingBackgrounds _background;
1104
1105     protected boolean _dead;
1106     protected boolean _silent;
1107     protected boolean _launchInSilent;
1108     protected boolean _noUpdate;
1109     protected long _startup;
1110
1111     protected Set<Resource> _toInstallResources;
1112     protected boolean _readyToInstall;
1113
1114     protected boolean _enableTracking = true;
1115     protected int _reportedProgress = 0;
1116
1117     /** Number of minutes to wait after startup before beginning any real heavy lifting. */
1118     protected int _delay;
1119
1120     protected int _stepMaxPercent;
1121     protected int _stepMinPercent;
1122     protected int _lastGlobalPercent;
1123     protected int _uiDisplayPercent;
1124
1125     protected static final int MAX_LOOPS = 5;
1126     protected static final long FALLBACK_CHECK_TIME = 1000L;
1127     
1128 }