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