JAL-3321 Corrected the badly configured getdown-launcher.jar for updating and updated...
[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         // on Windows, if the local JVM is in use, we will not be able to replace it with an
503         // updated JVM; we detect this by attempting to rename the java.dll to its same name, which
504         // will fail on Windows for in use files; hackery!
505         File javaLocalDir = new File(_app.getAppDir(), LaunchUtil.LOCAL_JAVA_DIR+File.separator);
506         File javaDll = new File(javaLocalDir, "bin" + File.separator + "java.dll");
507         if (javaDll.exists()) {
508             if (!javaDll.renameTo(javaDll)) {
509                 log.info("Cannot update local Java VM as it is in use.");
510                 return;
511             }
512             log.info("Can update local Java VM as it is not in use.");
513         }
514
515         reportTrackingEvent("jvm_start", -1);
516
517         updateStatus("m.downloading_java");
518         List<Resource> list = new ArrayList<>();
519         list.add(vmjar);
520         download(list);
521
522         reportTrackingEvent("jvm_unpack", -1);
523
524         updateStatus("m.unpacking_java");
525         vmjar.install(true);
526
527         // these only run on non-Windows platforms, so we use Unix file separators
528         String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
529         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
530         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
531         FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
532
533         // lastly regenerate the .jsa dump file that helps Java to start up faster
534         String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
535         try {
536             log.info("Regenerating classes.jsa for " + vmpath + "...");
537             Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
538         } catch (Exception e) {
539             log.warning("Failed to regenerate .jsa dump file", "error", e);
540         }
541
542         reportTrackingEvent("jvm_complete", -1);
543     }
544
545     /**
546      * Called if the application is determined to be of an old version.
547      */
548     protected void update ()
549         throws IOException
550     {
551         // first clear all validation markers
552         _app.clearValidationMarkers();
553
554         // attempt to download the patch files
555         Resource patch = _app.getPatchResource(null);
556         if (patch != null) {
557             List<Resource> list = new ArrayList<>();
558             list.add(patch);
559
560             // add the auxiliary group patch files for activated groups
561             for (Application.AuxGroup aux : _app.getAuxGroups()) {
562                 if (_app.isAuxGroupActive(aux.name)) {
563                     patch = _app.getPatchResource(aux.name);
564                     if (patch != null) {
565                         list.add(patch);
566                     }
567                 }
568             }
569
570             // show the patch notes button, if applicable
571             if (!StringUtil.isBlank(_ifc.patchNotesUrl)) {
572                 createInterfaceAsync(false);
573                 EventQueue.invokeLater(new Runnable() {
574                     public void run () {
575                         _patchNotes.setVisible(true);
576                     }
577                 });
578             }
579
580             // download the patch files...
581             setStep(Step.DOWNLOAD);
582             download(list);
583
584             // and apply them...
585             setStep(Step.PATCH);
586             updateStatus("m.patching");
587
588             long[] sizes = new long[list.size()];
589             Arrays.fill(sizes, 1L);
590             ProgressAggregator pragg = new ProgressAggregator(_progobs, sizes);
591             int ii = 0; for (Resource prsrc : list) {
592                 ProgressObserver pobs = pragg.startElement(ii++);
593                 try {
594                     // install the patch file (renaming them from _new)
595                     prsrc.install(false);
596                     // now apply the patch
597                     Patcher patcher = new Patcher();
598                     patcher.patch(prsrc.getLocal().getParentFile(), prsrc.getLocal(), pobs);
599                 } catch (Exception e) {
600                     log.warning("Failed to apply patch", "prsrc", prsrc, e);
601                 }
602
603                 // clean up the patch file
604                 if (!FileUtil.deleteHarder(prsrc.getLocal())) {
605                     log.warning("Failed to delete '" + prsrc + "'.");
606                 }
607             }
608         }
609
610         // if the patch resource is null, that means something was booched in the application, so
611         // we skip the patching process but update the metadata which will result in a "brute
612         // force" upgrade
613
614         // finally update our metadata files...
615         _app.updateMetadata();
616         // ...and reinitialize the application
617         readConfig(false);
618     }
619
620     /**
621      * Called if the application is determined to require resource downloads.
622      */
623     protected void download (Collection<Resource> resources)
624         throws IOException
625     {
626         // create our user interface
627         createInterfaceAsync(false);
628
629         Downloader dl = new HTTPDownloader(_app.proxy) {
630             @Override protected void resolvingDownloads () {
631                 updateStatus("m.resolving");
632             }
633
634             @Override protected void downloadProgress (int percent, long remaining) {
635                 // check for another getdown running at 0 and every 10% after that
636                 if (_lastCheck == -1 || percent >= _lastCheck + 10) {
637                     if (_delay > 0) {
638                         // stop the presses if something else is holding the lock
639                         boolean locked = _app.lockForUpdates();
640                         _app.releaseLock();
641                         if (locked) abort();
642                     }
643                     _lastCheck = percent;
644                 }
645                 setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true);
646                 if (percent > 0) {
647                     reportTrackingEvent("progress", percent);
648                 }
649             }
650
651             @Override protected void downloadFailed (Resource rsrc, Exception e) {
652                 updateStatus(MessageUtil.tcompose("m.failure", e.getMessage()));
653                 log.warning("Download failed", "rsrc", rsrc, e);
654             }
655
656             /** The last percentage at which we checked for another getdown running, or -1 for not
657              * having checked at all. */
658             protected int _lastCheck = -1;
659         };
660         if (!dl.download(resources, _app.maxConcurrentDownloads())) {
661             // if we aborted due to detecting another getdown running, we want to report here
662             throw new MultipleGetdownRunning();
663         }
664     }
665
666     /**
667      * Called to launch the application if everything is determined to be ready to go.
668      */
669     protected void launch ()
670     {
671         setStep(Step.LAUNCH);
672         setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false);
673
674         try {
675             if (invokeDirect()) {
676                 // we want to close the Getdown window, as the app is launching
677                 disposeContainer();
678                 _app.releaseLock();
679                 _app.invokeDirect();
680
681             } else {
682                 Process proc;
683                 if (_app.hasOptimumJvmArgs()) {
684                     // if we have "optimum" arguments, we want to try launching with them first
685                     proc = _app.createProcess(true);
686
687                     long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME;
688                     boolean error = false;
689                     while (fallback > System.currentTimeMillis()) {
690                         try {
691                             error = proc.exitValue() != 0;
692                             break;
693                         } catch (IllegalThreadStateException e) {
694                             Thread.yield();
695                         }
696                     }
697
698                     if (error) {
699                         log.info("Failed to launch with optimum arguments; falling back.");
700                         proc = _app.createProcess(false);
701                     }
702                 } else {
703                     proc = _app.createProcess(false);
704                 }
705
706                 // close standard in to avoid choking standard out of the launched process
707                 proc.getInputStream().close();
708                 // close standard out, since we're not going to write to anything to it anyway
709                 proc.getOutputStream().close();
710
711                 // on Windows 98 and ME we need to stick around and read the output of stderr lest
712                 // the process fill its output buffer and choke, yay!
713                 final InputStream stderr = proc.getErrorStream();
714                 if (LaunchUtil.mustMonitorChildren()) {
715                     // close our window if it's around
716                     disposeContainer();
717                     _container = null;
718                     copyStream(stderr, System.err);
719                     log.info("Process exited: " + proc.waitFor());
720
721                 } else {
722                     // spawn a daemon thread that will catch the early bits of stderr in case the
723                     // launch fails
724                     Thread t = new Thread() {
725                         @Override public void run () {
726                             copyStream(stderr, System.err);
727                         }
728                     };
729                     t.setDaemon(true);
730                     t.start();
731                 }
732             }
733
734             // if we have a UI open and we haven't been around for at least 5 seconds (the default
735             // for min_show_seconds), don't stick a fork in ourselves straight away but give our
736             // lovely user a chance to see what we're doing
737             long uptime = System.currentTimeMillis() - _startup;
738             long minshow = _ifc.minShowSeconds * 1000L;
739             if (_container != null && uptime < minshow) {
740                 try {
741                     Thread.sleep(minshow - uptime);
742                 } catch (Exception e) {
743                 }
744             }
745
746             // pump the percent up to 100%
747             setStatusAsync(null, 100, -1L, false);
748             exit(0);
749
750         } catch (Exception e) {
751             log.warning("launch() failed.", e);
752         }
753     }
754
755     /**
756      * Creates our user interface, which we avoid doing unless we actually have to update
757      * something. NOTE: this happens on the next UI tick, not immediately.
758      *
759      * @param reinit - if the interface should be reinitialized if it already exists.
760      */
761     protected void createInterfaceAsync (final boolean reinit)
762     {
763         if (_silent || (_container != null && !reinit)) {
764             return;
765         }
766 /*
767         EventQueue.invokeLater(new Runnable() {
768             public void run () {
769 */
770                 if (_container == null || reinit) {
771                     if (_container == null) {
772                         _container = createContainer();
773                     } else {
774                         _container.removeAll();
775                     }
776                     configureContainer();
777                     _layers = new JLayeredPane();
778                     
779                     
780                     
781                     // added in the instant display of a splashscreen
782                     try {
783                       readConfig(false);
784                       Graphics g = _container.getGraphics();
785                       String imageFile = _ifc.backgroundImage;
786                       BufferedImage bgImage = loadImage(_ifc.backgroundImage);
787                       int bwidth = bgImage.getWidth();
788                       int bheight = bgImage.getHeight();
789
790                       instantSplashPane = new JPanel() {
791                         @Override
792                         protected void paintComponent(Graphics g)
793                         {
794                           super.paintComponent(g);
795                           // attempt to draw a background image...
796                           if (bgImage != null) {
797                             g.drawImage(bgImage, 0, 0, this);
798                           }
799                         }
800                       };
801
802                       instantSplashPane.setSize(bwidth,bheight);
803                       instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight));
804
805                       _layers.add(instantSplashPane, Integer.valueOf(0));
806                     
807                       _container.setPreferredSize(new Dimension(bwidth,bheight));
808                     } catch (Exception e) {
809                       log.warning("Failed to set instant background image", "bg", _ifc.backgroundImage);
810                     }
811
812                     
813                     
814                     _container.add(_layers, BorderLayout.CENTER);
815                     _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
816                         @Override public void actionPerformed (ActionEvent event) {
817                             showDocument(_ifc.patchNotesUrl);
818                         }
819                     });
820                     _patchNotes.setFont(StatusPanel.FONT);
821                     _layers.add(_patchNotes);
822                     _status = new StatusPanel(_msgs);
823                     _layers.add(_status, Integer.valueOf(10));
824                     initInterface();
825                 }
826                 showContainer();
827 /*
828             }
829         });
830 */
831     }
832
833     /**
834      * Initializes the interface with the current UpdateInterface and backgrounds.
835      */
836     protected void initInterface ()
837     {
838         RotatingBackgrounds newBackgrounds = getBackground();
839         if (_background == null || newBackgrounds.getNumImages() > 0) {
840             // Leave the old _background in place if there is an old one to leave in place
841             // and the new getdown.txt didn't yield any images.
842             _background = newBackgrounds;
843         }
844         _status.init(_ifc, _background, getProgressImage());
845         Dimension size = _status.getPreferredSize();
846         _status.setSize(size);
847         _layers.setPreferredSize(size);
848
849         _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y,
850                               _ifc.patchNotes.width, _ifc.patchNotes.height);
851         _patchNotes.setVisible(false);
852
853         // we were displaying progress while the UI wasn't up. Now that it is, whatever progress
854         // is left is scaled into a 0-100 DISPLAYED progress.
855         _uiDisplayPercent = _lastGlobalPercent;
856         _stepMinPercent = _lastGlobalPercent = 0;
857     }
858
859     protected RotatingBackgrounds getBackground ()
860     {
861         if (_ifc.rotatingBackgrounds != null && _ifc.rotatingBackgrounds.size() > 0) {
862             if (_ifc.backgroundImage != null) {
863                 log.warning("ui.background_image and ui.rotating_background were both specified. " +
864                             "The background image is being used.");
865             }
866             return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this);
867         } else if (_ifc.backgroundImage != null) {
868             return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
869         } else {
870             return new RotatingBackgrounds();
871         }
872     }
873
874     protected Image getProgressImage ()
875     {
876         return loadImage(_ifc.progressImage);
877     }
878
879     protected void handleWindowClose ()
880     {
881         if (_dead) {
882             exit(0);
883         } else {
884             if (_abort == null) {
885                 _abort = new AbortPanel(Getdown.this, _msgs);
886             }
887             _abort.pack();
888             SwingUtil.centerWindow(_abort);
889             _abort.setVisible(true);
890             _abort.setState(JFrame.NORMAL);
891             _abort.requestFocus();
892         }
893     }
894
895     /**
896      * Update the status to indicate getdown has failed for the reason in <code>message</code>.
897      */
898     protected void fail (String message)
899     {
900         _dead = true;
901         setStatusAsync(message, stepToGlobalPercent(0), -1L, true);
902     }
903
904     /**
905      * Set the current step, which will be used to globalize per-step percentages.
906      */
907     protected void setStep (Step step)
908     {
909         int finalPercent = -1;
910         for (Integer perc : _ifc.stepPercentages.get(step)) {
911             if (perc > _stepMaxPercent) {
912                 finalPercent = perc;
913                 break;
914             }
915         }
916         if (finalPercent == -1) {
917             // we've gone backwards and this step will be ignored
918             return;
919         }
920
921         _stepMaxPercent = finalPercent;
922         _stepMinPercent = _lastGlobalPercent;
923     }
924
925     /**
926      * Convert a step percentage to the global percentage.
927      */
928     protected int stepToGlobalPercent (int percent)
929     {
930         int adjustedMaxPercent =
931             ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent);
932         _lastGlobalPercent = Math.max(_lastGlobalPercent,
933             _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100);
934         return _lastGlobalPercent;
935     }
936
937     /**
938      * Updates the status. NOTE: this happens on the next UI tick, not immediately.
939      */
940     protected void setStatusAsync (final String message, final int percent, final long remaining,
941                                    boolean createUI)
942     {
943         if (_status == null && createUI) {
944             createInterfaceAsync(false);
945         }
946
947         EventQueue.invokeLater(new Runnable() {
948             public void run () {
949                 if (_status == null) {
950                     if (message != null) {
951                         log.info("Dropping status '" + message + "'.");
952                     }
953                     return;
954                 }
955                 if (message != null) {
956                     _status.setStatus(message, _dead);
957                 }
958                 if (_dead) {
959                     _status.setProgress(0, -1L);
960                 } else if (percent >= 0) {
961                     _status.setProgress(percent, remaining);
962                 }
963             }
964         });
965     }
966
967     protected void reportTrackingEvent (String event, int progress)
968     {
969         if (!_enableTracking) {
970             return;
971
972         } else if (progress > 0) {
973             // we need to make sure we do the right thing if we skip over progress levels
974             do {
975                 URL url = _app.getTrackingProgressURL(++_reportedProgress);
976                 if (url != null) {
977                     new ProgressReporter(url).start();
978                 }
979             } while (_reportedProgress <= progress);
980
981         } else {
982             URL url = _app.getTrackingURL(event);
983             if (url != null) {
984                 new ProgressReporter(url).start();
985             }
986         }
987     }
988
989     /**
990      * Creates the container in which our user interface will be displayed.
991      */
992     protected abstract Container createContainer ();
993
994     /**
995      * Configures the interface container based on the latest UI config.
996      */
997     protected abstract void configureContainer ();
998
999     /**
1000      * Shows the container in which our user interface will be displayed.
1001      */
1002     protected abstract void showContainer ();
1003
1004     /**
1005      * Disposes the container in which we have our user interface.
1006      */
1007     protected abstract void disposeContainer ();
1008
1009     /**
1010      * If this method returns true we will run the application in the same JVM, otherwise we will
1011      * fork off a new JVM. Some options are not supported if we do not fork off a new JVM.
1012      */
1013     protected boolean invokeDirect ()
1014     {
1015         return SysProps.direct();
1016     }
1017
1018     /**
1019      * Requests to show the document at the specified URL in a new window.
1020      */
1021     protected abstract void showDocument (String url);
1022
1023     /**
1024      * Requests that Getdown exit.
1025      */
1026     protected abstract void exit (int exitCode);
1027
1028     /**
1029      * Copies the supplied stream from the specified input to the specified output. Used to copy
1030      * our child processes stderr and stdout to our own stderr and stdout.
1031      */
1032     protected static void copyStream (InputStream in, PrintStream out)
1033     {
1034         try {
1035             BufferedReader reader = new BufferedReader(new InputStreamReader(in));
1036             String line;
1037             while ((line = reader.readLine()) != null) {
1038                 out.print(line);
1039                 out.flush();
1040             }
1041         } catch (IOException ioe) {
1042             log.warning("Failure copying", "in", in, "out", out, "error", ioe);
1043         }
1044     }
1045
1046     /** Used to fetch a progress report URL. */
1047     protected class ProgressReporter extends Thread
1048     {
1049         public ProgressReporter (URL url) {
1050             setDaemon(true);
1051             _url = url;
1052         }
1053
1054         @Override
1055         public void run () {
1056             try {
1057                 HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
1058
1059                 // if we have a tracking cookie configured, configure the request with it
1060                 if (_app.getTrackingCookieName() != null &&
1061                     _app.getTrackingCookieProperty() != null) {
1062                     String val = System.getProperty(_app.getTrackingCookieProperty());
1063                     if (val != null) {
1064                         ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
1065                     }
1066                 }
1067
1068                 // now request our tracking URL and ensure that we get a non-error response
1069                 ucon.connect();
1070                 try {
1071                     if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1072                         log.warning("Failed to report tracking event",
1073                             "url", _url, "rcode", ucon.getResponseCode());
1074                     }
1075                 } finally {
1076                     ucon.disconnect();
1077                 }
1078
1079             } catch (IOException ioe) {
1080                 log.warning("Failed to report tracking event", "url", _url, "error", ioe);
1081             }
1082         }
1083
1084         protected URL _url;
1085     }
1086
1087     /** Used to pass progress on to our user interface. */
1088     protected ProgressObserver _progobs = new ProgressObserver() {
1089         public void progress (int percent) {
1090             setStatusAsync(null, stepToGlobalPercent(percent), -1L, false);
1091         }
1092     };
1093
1094     protected Application _app;
1095     protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
1096
1097     protected ResourceBundle _msgs;
1098     protected Container _container;
1099     protected JLayeredPane _layers;
1100     protected JPanel instantSplashPane;
1101     protected StatusPanel _status;
1102     protected JButton _patchNotes;
1103     protected AbortPanel _abort;
1104     protected RotatingBackgrounds _background;
1105
1106     protected boolean _dead;
1107     protected boolean _silent;
1108     protected boolean _launchInSilent;
1109     protected boolean _noUpdate;
1110     protected long _startup;
1111
1112     protected Set<Resource> _toInstallResources;
1113     protected boolean _readyToInstall;
1114
1115     protected boolean _enableTracking = true;
1116     protected int _reportedProgress = 0;
1117
1118     /** Number of minutes to wait after startup before beginning any real heavy lifting. */
1119     protected int _delay;
1120
1121     protected int _stepMaxPercent;
1122     protected int _stepMinPercent;
1123     protected int _lastGlobalPercent;
1124     protected int _uiDisplayPercent;
1125
1126     protected static final int MAX_LOOPS = 5;
1127     protected static final long FALLBACK_CHECK_TIME = 1000L;
1128     
1129 }