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