2 // Getdown - application installer, patcher and launcher
3 // Copyright (C) 2004-2018 Getdown authors
4 // https://github.com/threerings/getdown/blob/master/LICENSE
6 package com.threerings.getdown.launcher;
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;
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;
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;
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.*;
46 import static com.threerings.getdown.Log.log;
49 * Manages the main control for the Getdown application updater and deployment system.
51 public abstract class Getdown extends Thread
52 implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
54 public Getdown (EnvConfig envc)
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();
62 _launchInSilent = SysProps.launchInSilent();
63 _noUpdate = SysProps.noUpdate();
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.");
70 _launchInSilent = true;
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
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");
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.";
91 _app = new Application(envc);
92 _startup = System.currentTimeMillis();
96 * Returns true if there are pending new resources, waiting to be installed.
98 public boolean isUpdateAvailable ()
100 return _readyToInstall && !_toInstallResources.isEmpty();
104 * Installs the currently pending new resources.
106 public void install () throws IOException
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);
115 _toInstallResources.clear();
116 _readyToInstall = false;
117 log.info("Install completed.");
119 log.info("Nothing to install.");
126 // if we have no messages, just bail because we're hosed; the error message will be
127 // displayed to the user already
132 log.info("Getdown starting", "version", Build.version(), "built", Build.time());
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");
141 fail(MessageUtil.tcompose("m.readonly_error", path));
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()) {
151 } else if (_silent) {
152 log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
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
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);
167 } catch (Exception e) {
168 log.warning("run() failed.", e);
169 String msg = e.getMessage();
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);
178 msg = MessageUtil.compose(
179 "m.init_error", MessageUtil.taint(msg), _ifc.installError);
187 * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
189 public void configProxy (String host, String port, String username, String password)
191 log.info("User configured proxy", "host", host, "port", port);
193 if (!StringUtil.isBlank(host)) {
194 ProxyUtil.configProxy(_app, host, port, username, password);
201 // fire up a new thread
202 new Thread(this).start();
205 protected boolean detectProxy () {
206 if (ProxyUtil.autoDetectProxy(_app)) {
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...");
215 } catch (IOException ioe) {
218 updateStatus("m.detecting_proxy");
219 if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
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);
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 if (_status != null) {
235 _status.setAppbase(_app.getAppbase());
240 * Downloads and installs (without verifying) any resources that are marked with a
241 * {@code PRELOAD} attribute.
242 * @param resources the full set of resources from the application (the predownloads will be
243 * extracted from it).
245 protected void doPredownloads (Collection<Resource> resources) {
246 List<Resource> predownloads = new ArrayList<>();
247 for (Resource rsrc : resources) {
248 if (rsrc.shouldPredownload() && !rsrc.getLocal().exists()) {
249 predownloads.add(rsrc);
254 download(predownloads);
255 for (Resource rsrc : predownloads) {
256 rsrc.install(false); // install but don't validate yet
258 } catch (IOException ioe) {
259 log.warning("Failed to predownload resources. Continuing...", ioe);
264 * Does the actual application validation, update and launching business.
266 protected void getdown ()
269 // first parses our application deployment file
272 } catch (IOException ioe) {
273 log.warning("Failed to initialize: " + ioe);
274 _app.attemptRecovery(this);
277 // and force our UI to be recreated with the updated info
278 createInterfaceAsync(true);
280 if (!_noUpdate && !_app.lockForUpdates()) {
281 throw new MultipleGetdownRunning();
284 // Update the config modtime so a sleeping getdown will notice the change.
285 File config = _app.getLocalPath(Application.CONFIG_FILE);
286 if (!config.setLastModified(System.currentTimeMillis())) {
287 log.warning("Unable to set modtime on config file, will be unable to check for " +
288 "another instance of getdown running while this one waits.");
291 // don't hold the lock while waiting, let another getdown proceed if it starts.
293 // Store the config modtime before waiting the delay amount of time
294 long lastConfigModtime = config.lastModified();
295 log.info("Waiting " + _delay + " minutes before beginning actual work.");
296 Thread.sleep(_delay * 60 * 1000);
297 if (lastConfigModtime < config.lastModified()) {
298 log.warning("getdown.txt was modified while getdown was waiting.");
299 throw new MultipleGetdownRunning();
303 // if no_update was specified, directly start the app without updating
305 log.info("Launching without update!");
310 // we create this tracking counter here so that we properly note the first time through
311 // the update process whether we previously had validated resources (which means this
312 // is not a first time install); we may, in the course of updating, wipe out our
313 // validation markers and revalidate which would make us think we were doing a fresh
314 // install if we didn't specifically remember that we had validated resources the first
316 int[] alreadyValid = new int[1];
318 // we'll keep track of all the resources we unpack
319 Set<Resource> unpacked = new HashSet<>();
321 _toInstallResources = new HashSet<>();
322 _readyToInstall = false;
324 // setStep(Step.START);
325 for (int ii = 0; ii < MAX_LOOPS; ii++) {
326 // make sure we have the desired version and that the metadata files are valid...
327 setStep(Step.VERIFY_METADATA);
328 setStatusAsync("m.validating", -1, -1L, false);
329 if (_app.verifyMetadata(this)) {
330 log.info("Application requires update.");
332 // loop back again and reverify the metadata
336 // now verify (and download) our resources...
337 setStep(Step.VERIFY_RESOURCES);
338 setStatusAsync("m.validating", -1, -1L, false);
339 Set<Resource> toDownload = new HashSet<>();
340 _app.verifyResources(_progobs, alreadyValid, unpacked,
341 _toInstallResources, toDownload);
343 if (toDownload.size() > 0) {
344 // we have resources to download, also note them as to-be-installed
345 for (Resource r : toDownload) {
346 if (!_toInstallResources.contains(r)) {
347 _toInstallResources.add(r);
352 // if any of our resources have already been marked valid this is not a
353 // first time install and we don't want to enable tracking
354 _enableTracking = (alreadyValid[0] == 0);
355 reportTrackingEvent("app_start", -1);
357 // redownload any that are corrupt or invalid...
358 log.info(toDownload.size() + " of " + _app.getAllActiveResources().size() +
359 " rsrcs require update (" + alreadyValid[0] + " assumed valid).");
360 setStep(Step.REDOWNLOAD_RESOURCES);
361 download(toDownload);
363 reportTrackingEvent("app_complete", -1);
366 _enableTracking = false;
369 // now we'll loop back and try it all again
373 // if we aren't running in a JVM that meets our version requirements, either
374 // complain or attempt to download and install the appropriate version
375 if (!_app.haveValidJavaVersion()) {
376 // download and install the necessary version of java, then loop back again and
377 // reverify everything; if we can't download java; we'll throw an exception
378 log.info("Attempting to update Java VM...");
379 setStep(Step.UPDATE_JAVA);
380 _enableTracking = true; // always track JVM downloads
384 _enableTracking = false;
389 // if we were downloaded in full from another service (say, Steam), we may
390 // not have unpacked all of our resources yet
391 if (Boolean.getBoolean("check_unpacked")) {
392 File ufile = _app.getLocalPath("unpacked.dat");
394 long aversion = _app.getVersion();
395 if (!ufile.exists()) {
396 ufile.createNewFile();
398 version = VersionUtil.readVersion(ufile);
401 if (version < aversion) {
402 log.info("Performing unpack", "version", version, "aversion", aversion);
403 setStep(Step.UNPACK);
404 updateStatus("m.validating");
405 _app.unpackResources(_progobs, unpacked);
407 VersionUtil.writeVersion(ufile, aversion);
408 } catch (IOException ioe) {
409 log.warning("Failed to update unpacked version", ioe);
414 // assuming we're not doing anything funny, install the update
415 _readyToInstall = true;
418 // Only launch if we aren't in silent mode. Some mystery program starting out
419 // of the blue would be disconcerting.
420 if (!_silent || _launchInSilent) {
421 // And another final check for the lock. It'll already be held unless
422 // we're in silent mode.
423 _app.lockForUpdates();
429 log.warning("Pants! We couldn't get the job done.");
430 throw new IOException("m.unable_to_repair");
432 } catch (Exception e) {
433 log.warning("getdown() failed.", e);
434 String msg = e.getMessage();
436 msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
437 } else if (!msg.startsWith("m.")) {
438 // try to do something sensible based on the type of error
439 if (e instanceof FileNotFoundException) {
440 msg = MessageUtil.compose(
441 "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
443 msg = MessageUtil.compose(
444 "m.init_error", MessageUtil.taint(msg), _ifc.installError);
447 // Since we're dead, clear off the 'time remaining' label along with displaying the
454 // documentation inherited from interface
456 public void updateStatus (String message)
458 setStatusAsync(message, -1, -1L, true);
462 * Load the image at the path. Before trying the exact path/file specified we will look to see
463 * if we can find a localized version by sticking a {@code _<language>} in front of the "." in
467 public BufferedImage loadImage (String path)
469 if (StringUtil.isBlank(path)) {
475 // First try for a localized image.
476 String localeStr = Locale.getDefault().getLanguage();
477 imgpath = _app.getLocalPath(path.replace(".", "_" + localeStr + "."));
478 return ImageIO.read(imgpath);
479 } catch (IOException ioe) {
480 // No biggie, we'll try the generic one.
483 // If that didn't work, try a generic one.
485 imgpath = _app.getLocalPath(path);
486 return ImageIO.read(imgpath);
487 } catch (IOException ioe2) {
488 log.warning("Failed to load image", "path", imgpath, "error", ioe2);
494 * Downloads and installs an Java VM bundled with the application. This is called if we are not
495 * running with the necessary Java version.
497 protected void updateJava ()
500 Resource vmjar = _app.getJavaVMResource();
502 throw new IOException("m.java_download_failed");
505 // on Windows, if the local JVM is in use, we will not be able to replace it with an
506 // updated JVM; we detect this by attempting to rename the java.dll to its same name, which
507 // will fail on Windows for in use files; hackery!
508 File javaLocalDir = new File(_app.getAppDir(), LaunchUtil.LOCAL_JAVA_DIR+File.separator);
509 File javaDll = new File(javaLocalDir, "bin" + File.separator + "java.dll");
510 if (javaDll.exists()) {
511 if (!javaDll.renameTo(javaDll)) {
512 log.info("Cannot update local Java VM as it is in use.");
515 log.info("Can update local Java VM as it is not in use.");
518 reportTrackingEvent("jvm_start", -1);
520 updateStatus("m.downloading_java");
521 List<Resource> list = new ArrayList<>();
525 reportTrackingEvent("jvm_unpack", -1);
527 updateStatus("m.unpacking_java");
530 // these only run on non-Windows platforms, so we use Unix file separators
531 String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
532 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
533 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
534 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
536 // lastly regenerate the .jsa dump file that helps Java to start up faster
537 String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
539 log.info("Regenerating classes.jsa for " + vmpath + "...");
540 Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
541 } catch (Exception e) {
542 log.warning("Failed to regenerate .jsa dump file", "error", e);
545 reportTrackingEvent("jvm_complete", -1);
549 * Called if the application is determined to be of an old version.
551 protected void update ()
554 // first clear all validation markers
555 _app.clearValidationMarkers();
557 // attempt to download the patch files
558 Resource patch = _app.getPatchResource(null);
560 List<Resource> list = new ArrayList<>();
563 // add the auxiliary group patch files for activated groups
564 for (Application.AuxGroup aux : _app.getAuxGroups()) {
565 if (_app.isAuxGroupActive(aux.name)) {
566 patch = _app.getPatchResource(aux.name);
573 // show the patch notes button, if applicable
574 if (!StringUtil.isBlank(_ifc.patchNotesUrl)) {
575 createInterfaceAsync(false);
576 EQinvoke(new Runnable() {
578 _patchNotes.setVisible(true);
583 // download the patch files...
584 setStep(Step.DOWNLOAD);
589 updateStatus("m.patching");
591 long[] sizes = new long[list.size()];
592 Arrays.fill(sizes, 1L);
593 ProgressAggregator pragg = new ProgressAggregator(_progobs, sizes);
594 int ii = 0; for (Resource prsrc : list) {
595 ProgressObserver pobs = pragg.startElement(ii++);
597 // install the patch file (renaming them from _new)
598 prsrc.install(false);
599 // now apply the patch
600 Patcher patcher = new Patcher();
601 patcher.patch(prsrc.getLocal().getParentFile(), prsrc.getLocal(), pobs);
602 } catch (Exception e) {
603 log.warning("Failed to apply patch", "prsrc", prsrc, e);
606 // clean up the patch file
607 if (!FileUtil.deleteHarder(prsrc.getLocal())) {
608 log.warning("Failed to delete '" + prsrc + "'.");
613 // if the patch resource is null, that means something was booched in the application, so
614 // we skip the patching process but update the metadata which will result in a "brute
617 // finally update our metadata files...
618 _app.updateMetadata();
619 // ...and reinitialize the application
624 * Called if the application is determined to require resource downloads.
626 protected void download (Collection<Resource> resources)
629 // create our user interface
630 createInterfaceAsync(false);
632 Downloader dl = new HTTPDownloader(_app.proxy) {
633 @Override protected void resolvingDownloads () {
634 updateStatus("m.resolving");
637 @Override protected void downloadProgress (int percent, long remaining) {
638 // check for another getdown running at 0 and every 10% after that
639 if (_lastCheck == -1 || percent >= _lastCheck + 10) {
641 // stop the presses if something else is holding the lock
642 boolean locked = _app.lockForUpdates();
646 _lastCheck = percent;
648 setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true);
650 reportTrackingEvent("progress", percent);
654 @Override protected void downloadFailed (Resource rsrc, Exception e) {
655 updateStatus(MessageUtil.tcompose("m.failure", e.getMessage()));
656 log.warning("Download failed", "rsrc", rsrc, e);
659 /** The last percentage at which we checked for another getdown running, or -1 for not
660 * having checked at all. */
661 protected int _lastCheck = -1;
663 if (!dl.download(resources, _app.maxConcurrentDownloads())) {
664 // if we aborted due to detecting another getdown running, we want to report here
665 throw new MultipleGetdownRunning();
670 * Called to launch the application if everything is determined to be ready to go.
672 protected void launch ()
674 setStep(Step.LAUNCH);
675 setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false);
678 if (invokeDirect()) {
679 // we want to close the Getdown window, as the app is launching
686 if (_app.hasOptimumJvmArgs()) {
687 // if we have "optimum" arguments, we want to try launching with them first
688 proc = _app.createProcess(true);
690 long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME;
691 boolean error = false;
692 while (fallback > System.currentTimeMillis()) {
694 error = proc.exitValue() != 0;
696 } catch (IllegalThreadStateException e) {
702 log.info("Failed to launch with optimum arguments; falling back.");
703 proc = _app.createProcess(false);
706 proc = _app.createProcess(false);
709 // close standard in to avoid choking standard out of the launched process
710 proc.getInputStream().close();
711 // close standard out, since we're not going to write to anything to it anyway
712 proc.getOutputStream().close();
714 // on Windows 98 and ME we need to stick around and read the output of stderr lest
715 // the process fill its output buffer and choke, yay!
716 final InputStream stderr = proc.getErrorStream();
717 if (LaunchUtil.mustMonitorChildren()) {
718 // close our window if it's around
721 copyStream(stderr, System.err);
722 log.info("Process exited: " + proc.waitFor());
725 // spawn a daemon thread that will catch the early bits of stderr in case the
727 Thread t = new Thread() {
728 @Override public void run () {
729 copyStream(stderr, System.err);
737 _container.setVisible(true);
738 _container.validate();
740 // if we have a UI open and we haven't been around for at least 5 seconds (the default
741 // for min_show_seconds), don't stick a fork in ourselves straight away but give our
742 // lovely user a chance to see what we're doing
743 long uptime = System.currentTimeMillis() - _startup;
744 long minshow = _ifc.minShowSeconds * 1000L;
745 if (_container != null && uptime < minshow) {
747 Thread.sleep(minshow - uptime);
748 } catch (Exception e) {
752 // pump the percent up to 100%
753 setStatusAsync(null, 100, -1L, false);
756 } catch (Exception e) {
757 log.warning("launch() failed.", e);
762 * Creates our user interface, which we avoid doing unless we actually have to update
763 * something. NOTE: this happens on the next UI tick, not immediately.
765 * @param reinit - if the interface should be reinitialized if it already exists.
767 protected void createInterfaceAsync (final boolean reinit)
769 if (_silent || (_container != null && !reinit)) {
773 EQinvoke (new Runnable() {
776 if (_container == null || reinit) {
777 if (_container == null) {
778 _container = createContainer();
780 _container.removeAll();
782 configureContainer();
783 _layers = new JLayeredPane();
787 // added in the instant display of a splashscreen
790 Graphics g = _container.getGraphics();
791 BufferedImage iBgImage = loadImage(_ifc.instantBackgroundImage);
793 if (iBgImage == null) {
794 iBgImage = loadImage(_ifc.backgroundImage);
797 if (iBgImage != null) {
798 final BufferedImage bgImage = iBgImage;
799 int bwidth = bgImage.getWidth();
800 int bheight = bgImage.getHeight();
802 log.info("Displaying instant background image", ibg?"instant_background_image":"background_image");
804 instantSplashPane = new JPanel() {
806 protected void paintComponent(Graphics g)
808 super.paintComponent(g);
809 // attempt to draw a background image...
810 if (bgImage != null) {
811 g.drawImage(bgImage, 0, 0, this);
816 instantSplashPane.setSize(bwidth,bheight);
817 instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight));
819 _layers.add(instantSplashPane, Integer.valueOf(0));
821 _container.setPreferredSize(new Dimension(bwidth,bheight));
823 } catch (Exception e) {
824 log.warning("Failed to set instant background image", "ibg", _ifc.instantBackgroundImage);
827 _container.add(_layers, BorderLayout.CENTER);
828 _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
829 @Override public void actionPerformed (ActionEvent event) {
830 showDocument(_ifc.patchNotesUrl);
833 _patchNotes.setFont(StatusPanel.FONT);
834 _layers.add(_patchNotes);
835 _status = new StatusPanel(_msgs);
836 //setStatusAsync("test", stepToGlobalPercent(1), -1L, false);
837 _layers.add(_status, Integer.valueOf(10));
847 * Initializes the interface with the current UpdateInterface and backgrounds.
849 protected void initInterface ()
851 RotatingBackgrounds newBackgrounds = getBackground();
852 if (_background == null || newBackgrounds.getNumImages() > 0) {
853 // Leave the old _background in place if there is an old one to leave in place
854 // and the new getdown.txt didn't yield any images.
855 _background = newBackgrounds;
857 _status.init(_ifc, _background, getProgressImage());
858 Dimension size = _status.getPreferredSize();
859 _status.setSize(size);
860 //_status.updateStatusLabel();
861 _layers.setPreferredSize(size);
863 _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y,
864 _ifc.patchNotes.width, _ifc.patchNotes.height);
865 _patchNotes.setVisible(false);
867 // we were displaying progress while the UI wasn't up. Now that it is, whatever progress
868 // is left is scaled into a 0-100 DISPLAYED progress.
869 _uiDisplayPercent = _lastGlobalPercent;
870 _stepMinPercent = _lastGlobalPercent = 0;
873 protected RotatingBackgrounds getBackground ()
875 if (_ifc.rotatingBackgrounds != null && _ifc.rotatingBackgrounds.size() > 0) {
876 if (_ifc.backgroundImage != null) {
877 log.warning("ui.background_image and ui.rotating_background were both specified. " +
878 "The background image is being used.");
880 return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this);
881 } else if (_ifc.backgroundImage != null) {
882 return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
884 return new RotatingBackgrounds();
888 protected Image getProgressImage ()
890 return loadImage(_ifc.progressImage);
893 protected void handleWindowClose ()
898 if (_abort == null) {
899 _abort = new AbortPanel(Getdown.this, _msgs);
902 SwingUtil.centerWindow(_abort);
903 _abort.setVisible(true);
904 _abort.setState(JFrame.NORMAL);
905 _abort.requestFocus();
910 * Update the status to indicate getdown has failed for the reason in <code>message</code>.
912 protected void fail (String message)
915 setStatusAsync(message, stepToGlobalPercent(0), -1L, true);
919 * Set the current step, which will be used to globalize per-step percentages.
921 protected void setStep (Step step)
923 int finalPercent = -1;
924 for (Integer perc : _ifc.stepPercentages.get(step)) {
925 if (perc > _stepMaxPercent) {
930 if (finalPercent == -1) {
931 // we've gone backwards and this step will be ignored
935 _stepMaxPercent = finalPercent;
936 _stepMinPercent = _lastGlobalPercent;
940 * Convert a step percentage to the global percentage.
942 protected int stepToGlobalPercent (int percent)
944 int adjustedMaxPercent =
945 ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent);
946 _lastGlobalPercent = Math.max(_lastGlobalPercent,
947 _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100);
948 return _lastGlobalPercent;
952 * Updates the status. NOTE: this happens on the next UI tick, not immediately.
954 protected void setStatusAsync (final String message, final int percent, final long remaining,
957 if (_status == null && createUI) {
958 createInterfaceAsync(false);
961 EQinvoke(new Runnable() {
964 if (_status == null) {
965 if (message != null) {
966 log.info("Dropping status '" + message + "'.");
970 if (message != null) {
971 _status.setStatus(message, _dead);
974 _status.setProgress(0, -1L);
975 } else if (percent >= 0) {
976 _status.setProgress(percent, remaining);
982 protected void reportTrackingEvent (String event, int progress)
984 if (!_enableTracking) {
987 } else if (progress > 0) {
988 // we need to make sure we do the right thing if we skip over progress levels
990 URL url = _app.getTrackingProgressURL(++_reportedProgress);
992 new ProgressReporter(url).start();
994 } while (_reportedProgress <= progress);
997 URL url = _app.getTrackingURL(event);
999 new ProgressReporter(url).start();
1005 * Creates the container in which our user interface will be displayed.
1007 protected abstract Container createContainer ();
1010 * Configures the interface container based on the latest UI config.
1012 protected abstract void configureContainer ();
1015 * Shows the container in which our user interface will be displayed.
1017 protected abstract void showContainer ();
1020 * Disposes the container in which we have our user interface.
1022 protected abstract void disposeContainer ();
1025 * If this method returns true we will run the application in the same JVM, otherwise we will
1026 * fork off a new JVM. Some options are not supported if we do not fork off a new JVM.
1028 protected boolean invokeDirect ()
1030 return SysProps.direct();
1034 * Requests to show the document at the specified URL in a new window.
1036 protected abstract void showDocument (String url);
1039 * Requests that Getdown exit.
1041 protected abstract void exit (int exitCode);
1044 * Copies the supplied stream from the specified input to the specified output. Used to copy
1045 * our child processes stderr and stdout to our own stderr and stdout.
1047 protected static void copyStream (InputStream in, PrintStream out)
1050 BufferedReader reader = new BufferedReader(new InputStreamReader(in));
1052 while ((line = reader.readLine()) != null) {
1056 } catch (IOException ioe) {
1057 log.warning("Failure copying", "in", in, "out", out, "error", ioe);
1061 /** Used to fetch a progress report URL. */
1062 protected class ProgressReporter extends Thread
1064 public ProgressReporter (URL url) {
1070 public void run () {
1072 HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
1074 // if we have a tracking cookie configured, configure the request with it
1075 if (_app.getTrackingCookieName() != null &&
1076 _app.getTrackingCookieProperty() != null) {
1077 String val = System.getProperty(_app.getTrackingCookieProperty());
1079 ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
1083 // now request our tracking URL and ensure that we get a non-error response
1086 if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1087 log.warning("Failed to report tracking event",
1088 "url", _url, "rcode", ucon.getResponseCode());
1094 } catch (IOException ioe) {
1095 log.warning("Failed to report tracking event", "url", _url, "error", ioe);
1102 /** Used to pass progress on to our user interface. */
1103 protected ProgressObserver _progobs = new ProgressObserver() {
1104 public void progress (int percent) {
1105 setStatusAsync(null, stepToGlobalPercent(percent), -1L, false);
1109 // Asynchronous or synchronous progress updates
1110 protected void EQinvoke(Runnable r) {
1114 } catch (Exception e) {
1115 log.warning("Could't read config when invoking GUI action", "Exception", e.getMessage());
1117 if (! (_ifc == null) && _ifc.progressSync) {
1119 EventQueue.invokeAndWait(r);
1120 } catch (Exception e) {
1121 log.warning("Tried to invokeAndWait but couldn't. Going to invokeLater instead", "Exception", e.getMessage());
1122 EventQueue.invokeLater(r);
1125 EventQueue.invokeLater(r);
1130 protected Application _app;
1131 protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
1133 protected ResourceBundle _msgs;
1134 protected Container _container;
1135 protected JLayeredPane _layers;
1136 protected JPanel instantSplashPane;
1137 protected StatusPanel _status;
1138 protected JButton _patchNotes;
1139 protected AbortPanel _abort;
1140 protected RotatingBackgrounds _background;
1142 protected boolean _dead;
1143 protected boolean _silent;
1144 protected boolean _launchInSilent;
1145 protected boolean _noUpdate;
1146 protected long _startup;
1148 protected Set<Resource> _toInstallResources;
1149 protected boolean _readyToInstall;
1151 protected boolean _enableTracking = true;
1152 protected int _reportedProgress = 0;
1154 /** Number of minutes to wait after startup before beginning any real heavy lifting. */
1155 protected int _delay;
1157 protected int _stepMaxPercent;
1158 protected int _stepMinPercent;
1159 protected int _lastGlobalPercent;
1160 protected int _uiDisplayPercent;
1162 protected static final int MAX_LOOPS = 5;
1163 protected static final long FALLBACK_CHECK_TIME = 1000L;