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