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.GraphicsEnvironment;
13 import java.awt.Image;
14 import java.awt.event.ActionEvent;
15 import java.awt.image.BufferedImage;
16 import java.io.BufferedReader;
18 import java.io.FileInputStream;
19 import java.io.FileNotFoundException;
20 import java.io.FileReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.PrintStream;
25 import java.net.HttpURLConnection;
26 import java.net.MalformedURLException;
30 import javax.imageio.ImageIO;
31 import javax.swing.AbstractAction;
32 import javax.swing.JButton;
33 import javax.swing.JFrame;
34 import javax.swing.JLayeredPane;
36 import com.samskivert.swing.util.SwingUtil;
37 import com.threerings.getdown.data.*;
38 import com.threerings.getdown.data.Application.UpdateInterface.Step;
39 import com.threerings.getdown.net.Downloader;
40 import com.threerings.getdown.net.HTTPDownloader;
41 import com.threerings.getdown.tools.Patcher;
42 import com.threerings.getdown.util.*;
44 import static com.threerings.getdown.Log.log;
47 * Manages the main control for the Getdown application updater and deployment system.
49 public abstract class Getdown extends Thread
50 implements Application.StatusDisplay, RotatingBackgrounds.ImageLoader
52 public Getdown (EnvConfig envc)
56 // If the silent property exists, install without bringing up any gui. If it equals
57 // launch, start the application after installing. Otherwise, just install and exit.
58 _silent = SysProps.silent();
60 _launchInSilent = SysProps.launchInSilent();
61 _noUpdate = SysProps.noUpdate();
63 // If we're running in a headless environment and have not otherwise customized
64 // silence, operate without a UI and do launch the app.
65 if (!_silent && GraphicsEnvironment.isHeadless()) {
66 log.info("Running in headless JVM, will attempt to operate without UI.");
68 _launchInSilent = true;
70 _delay = SysProps.startDelay();
71 } catch (SecurityException se) {
72 // don't freak out, just assume non-silent and no delay; we're probably already
73 // recovering from a security failure
76 _msgs = ResourceBundle.getBundle("com.threerings.getdown.messages");
77 } catch (Exception e) {
78 // welcome to hell, where java can't cope with a classpath that contains jars that live
79 // in a directory that contains a !, at least the same bug happens on all platforms
80 String dir = envc.appDir.toString();
81 if (dir.equals(".")) {
82 dir = System.getProperty("user.dir");
84 String errmsg = "The directory in which this application is installed:\n" + dir +
85 "\nis invalid (" + e.getMessage() + "). If the full path to the app directory " +
86 "contains the '!' character, this will trigger this error.";
89 _app = new Application(envc);
90 _startup = System.currentTimeMillis();
94 * Returns true if there are pending new resources, waiting to be installed.
96 public boolean isUpdateAvailable ()
98 return _readyToInstall && !_toInstallResources.isEmpty();
102 * Installs the currently pending new resources.
104 public void install () throws IOException
106 if (SysProps.noInstall()) {
107 log.info("Skipping install due to 'no_install' sysprop.");
108 } else if (_readyToInstall) {
109 log.info("Installing " + _toInstallResources.size() + " downloaded resources:");
110 for (Resource resource : _toInstallResources) {
111 resource.install(true);
113 _toInstallResources.clear();
114 _readyToInstall = false;
115 log.info("Install completed.");
117 log.info("Nothing to install.");
124 // if we have no messages, just bail because we're hosed; the error message will be
125 // displayed to the user already
130 log.info("Getdown starting", "version", Build.version(), "built", Build.time());
132 // determine whether or not we can write to our install directory
133 File instdir = _app.getLocalPath("");
134 if (!instdir.canWrite()) {
135 String path = instdir.getPath();
136 if (path.equals(".")) {
137 path = System.getProperty("user.dir");
139 fail(MessageUtil.tcompose("m.readonly_error", path));
145 // if we fail to detect a proxy, but we're allowed to run offline, then go ahead and
146 // run the app anyway because we're prepared to cope with not being able to update
147 if (detectProxy() || _app.allowOffline()) {
149 } else if (_silent) {
150 log.warning("Need a proxy, but we don't want to bother anyone. Exiting.");
152 // create a panel they can use to configure the proxy settings
153 _container = createContainer();
154 // allow them to close the window to abort the proxy configuration
156 configureContainer();
157 ProxyPanel panel = new ProxyPanel(this, _msgs);
158 // set up any existing configured proxy
159 String[] hostPort = ProxyUtil.loadProxy(_app);
160 panel.setProxy(hostPort[0], hostPort[1]);
161 _container.add(panel, BorderLayout.CENTER);
165 } catch (Exception e) {
166 log.warning("run() failed.", e);
167 String msg = e.getMessage();
169 msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
170 } else if (!msg.startsWith("m.")) {
171 // try to do something sensible based on the type of error
172 if (e instanceof FileNotFoundException) {
173 msg = MessageUtil.compose(
174 "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
176 msg = MessageUtil.compose(
177 "m.init_error", MessageUtil.taint(msg), _ifc.installError);
185 * Configures our proxy settings (called by {@link ProxyPanel}) and fires up the launcher.
187 public void configProxy (String host, String port, String username, String password)
189 log.info("User configured proxy", "host", host, "port", port);
191 if (!StringUtil.isBlank(host)) {
192 ProxyUtil.configProxy(_app, host, port, username, password);
199 // fire up a new thread
200 new Thread(this).start();
203 protected boolean detectProxy () {
204 if (ProxyUtil.autoDetectProxy(_app)) {
208 // otherwise see if we actually need a proxy; first we have to initialize our application
209 // to get some sort of interface configuration and the appbase URL
210 log.info("Checking whether we need to use a proxy...");
213 } catch (IOException ioe) {
216 updateStatus("m.detecting_proxy");
217 if (!ProxyUtil.canLoadWithoutProxy(_app.getConfigResource().getRemote())) {
221 // we got through, so we appear not to require a proxy; make a blank proxy config so that
222 // we don't go through this whole detection process again next time
223 log.info("No proxy appears to be needed.");
224 ProxyUtil.saveProxy(_app, null, null);
228 protected void readConfig (boolean preloads) throws IOException {
229 Config config = _app.init(true);
230 if (preloads) doPredownloads(_app.getResources());
231 _ifc = new Application.UpdateInterface(config);
235 * Downloads and installs (without verifying) any resources that are marked with a
236 * {@code PRELOAD} attribute.
237 * @param resources the full set of resources from the application (the predownloads will be
238 * extracted from it).
240 protected void doPredownloads (Collection<Resource> resources) {
241 List<Resource> predownloads = new ArrayList<>();
242 for (Resource rsrc : resources) {
243 if (rsrc.shouldPredownload() && !rsrc.getLocal().exists()) {
244 predownloads.add(rsrc);
249 download(predownloads);
250 for (Resource rsrc : predownloads) {
251 rsrc.install(false); // install but don't validate yet
253 } catch (IOException ioe) {
254 log.warning("Failed to predownload resources. Continuing...", ioe);
259 * Does the actual application validation, update and launching business.
261 protected void getdown ()
264 // first parses our application deployment file
267 } catch (IOException ioe) {
268 log.warning("Failed to initialize: " + ioe);
269 _app.attemptRecovery(this);
272 // and force our UI to be recreated with the updated info
273 createInterfaceAsync(true);
275 if (!_noUpdate && !_app.lockForUpdates()) {
276 throw new MultipleGetdownRunning();
279 // Update the config modtime so a sleeping getdown will notice the change.
280 File config = _app.getLocalPath(Application.CONFIG_FILE);
281 if (!config.setLastModified(System.currentTimeMillis())) {
282 log.warning("Unable to set modtime on config file, will be unable to check for " +
283 "another instance of getdown running while this one waits.");
286 // don't hold the lock while waiting, let another getdown proceed if it starts.
288 // Store the config modtime before waiting the delay amount of time
289 long lastConfigModtime = config.lastModified();
290 log.info("Waiting " + _delay + " minutes before beginning actual work.");
291 Thread.sleep(_delay * 60 * 1000);
292 if (lastConfigModtime < config.lastModified()) {
293 log.warning("getdown.txt was modified while getdown was waiting.");
294 throw new MultipleGetdownRunning();
298 // if no_update was specified, directly start the app without updating
300 log.info("Launching without update!");
305 // we create this tracking counter here so that we properly note the first time through
306 // the update process whether we previously had validated resources (which means this
307 // is not a first time install); we may, in the course of updating, wipe out our
308 // validation markers and revalidate which would make us think we were doing a fresh
309 // install if we didn't specifically remember that we had validated resources the first
311 int[] alreadyValid = new int[1];
313 // we'll keep track of all the resources we unpack
314 Set<Resource> unpacked = new HashSet<>();
316 _toInstallResources = new HashSet<>();
317 _readyToInstall = false;
319 // setStep(Step.START);
320 for (int ii = 0; ii < MAX_LOOPS; ii++) {
321 // make sure we have the desired version and that the metadata files are valid...
322 setStep(Step.VERIFY_METADATA);
323 setStatusAsync("m.validating", -1, -1L, false);
324 if (_app.verifyMetadata(this)) {
325 log.info("Application requires update.");
327 // loop back again and reverify the metadata
331 // now verify (and download) our resources...
332 setStep(Step.VERIFY_RESOURCES);
333 setStatusAsync("m.validating", -1, -1L, false);
334 Set<Resource> toDownload = new HashSet<>();
335 _app.verifyResources(_progobs, alreadyValid, unpacked,
336 _toInstallResources, toDownload);
338 if (toDownload.size() > 0) {
339 // we have resources to download, also note them as to-be-installed
340 for (Resource r : toDownload) {
341 if (!_toInstallResources.contains(r)) {
342 _toInstallResources.add(r);
347 // if any of our resources have already been marked valid this is not a
348 // first time install and we don't want to enable tracking
349 _enableTracking = (alreadyValid[0] == 0);
350 reportTrackingEvent("app_start", -1);
352 // redownload any that are corrupt or invalid...
353 log.info(toDownload.size() + " of " + _app.getAllActiveResources().size() +
354 " rsrcs require update (" + alreadyValid[0] + " assumed valid).");
355 setStep(Step.REDOWNLOAD_RESOURCES);
356 download(toDownload);
358 reportTrackingEvent("app_complete", -1);
361 _enableTracking = false;
364 // now we'll loop back and try it all again
368 // if we aren't running in a JVM that meets our version requirements, either
369 // complain or attempt to download and install the appropriate version
370 if (!_app.haveValidJavaVersion()) {
371 // download and install the necessary version of java, then loop back again and
372 // reverify everything; if we can't download java; we'll throw an exception
373 log.info("Attempting to update Java VM...");
374 setStep(Step.UPDATE_JAVA);
375 _enableTracking = true; // always track JVM downloads
379 _enableTracking = false;
384 // if we were downloaded in full from another service (say, Steam), we may
385 // not have unpacked all of our resources yet
386 if (Boolean.getBoolean("check_unpacked")) {
387 File ufile = _app.getLocalPath("unpacked.dat");
389 long aversion = _app.getVersion();
390 if (!ufile.exists()) {
391 ufile.createNewFile();
393 version = VersionUtil.readVersion(ufile);
396 if (version < aversion) {
397 log.info("Performing unpack", "version", version, "aversion", aversion);
398 setStep(Step.UNPACK);
399 updateStatus("m.validating");
400 _app.unpackResources(_progobs, unpacked);
402 VersionUtil.writeVersion(ufile, aversion);
403 } catch (IOException ioe) {
404 log.warning("Failed to update unpacked version", ioe);
409 // assuming we're not doing anything funny, install the update
410 _readyToInstall = true;
413 // Only launch if we aren't in silent mode. Some mystery program starting out
414 // of the blue would be disconcerting.
415 if (!_silent || _launchInSilent) {
416 // And another final check for the lock. It'll already be held unless
417 // we're in silent mode.
418 _app.lockForUpdates();
424 log.warning("Pants! We couldn't get the job done.");
425 throw new IOException("m.unable_to_repair");
427 } catch (Exception e) {
428 log.warning("getdown() failed.", e);
429 String msg = e.getMessage();
431 msg = MessageUtil.compose("m.unknown_error", _ifc.installError);
432 } else if (!msg.startsWith("m.")) {
433 // try to do something sensible based on the type of error
434 if (e instanceof FileNotFoundException) {
435 msg = MessageUtil.compose(
436 "m.missing_resource", MessageUtil.taint(msg), _ifc.installError);
438 msg = MessageUtil.compose(
439 "m.init_error", MessageUtil.taint(msg), _ifc.installError);
442 // Since we're dead, clear off the 'time remaining' label along with displaying the
449 // documentation inherited from interface
451 public void updateStatus (String message)
453 setStatusAsync(message, -1, -1L, true);
457 * Load the image at the path. Before trying the exact path/file specified we will look to see
458 * if we can find a localized version by sticking a {@code _<language>} in front of the "." in
462 public BufferedImage loadImage (String path)
464 if (StringUtil.isBlank(path)) {
470 // First try for a localized image.
471 String localeStr = Locale.getDefault().getLanguage();
472 imgpath = _app.getLocalPath(path.replace(".", "_" + localeStr + "."));
473 return ImageIO.read(imgpath);
474 } catch (IOException ioe) {
475 // No biggie, we'll try the generic one.
478 // If that didn't work, try a generic one.
480 imgpath = _app.getLocalPath(path);
481 return ImageIO.read(imgpath);
482 } catch (IOException ioe2) {
483 log.warning("Failed to load image", "path", imgpath, "error", ioe2);
489 * Downloads and installs an Java VM bundled with the application. This is called if we are not
490 * running with the necessary Java version.
492 protected void updateJava ()
495 Resource vmjar = _app.getJavaVMResource();
497 throw new IOException("m.java_download_failed");
500 reportTrackingEvent("jvm_start", -1);
502 updateStatus("m.downloading_java");
503 List<Resource> list = new ArrayList<>();
507 reportTrackingEvent("jvm_unpack", -1);
509 updateStatus("m.unpacking_java");
512 // these only run on non-Windows platforms, so we use Unix file separators
513 String localJavaDir = LaunchUtil.LOCAL_JAVA_DIR + "/";
514 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "bin/java"));
515 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/jspawnhelper"));
516 FileUtil.makeExecutable(_app.getLocalPath(localJavaDir + "lib/amd64/jspawnhelper"));
518 // lastly regenerate the .jsa dump file that helps Java to start up faster
519 String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath(""));
521 log.info("Regenerating classes.jsa for " + vmpath + "...");
522 Runtime.getRuntime().exec(vmpath + " -Xshare:dump");
523 } catch (Exception e) {
524 log.warning("Failed to regenerate .jsa dump file", "error", e);
527 reportTrackingEvent("jvm_complete", -1);
531 * Called if the application is determined to be of an old version.
533 protected void update ()
536 // first clear all validation markers
537 _app.clearValidationMarkers();
539 // attempt to download the patch files
540 Resource patch = _app.getPatchResource(null);
542 List<Resource> list = new ArrayList<>();
545 // add the auxiliary group patch files for activated groups
546 for (Application.AuxGroup aux : _app.getAuxGroups()) {
547 if (_app.isAuxGroupActive(aux.name)) {
548 patch = _app.getPatchResource(aux.name);
555 // show the patch notes button, if applicable
556 if (!StringUtil.isBlank(_ifc.patchNotesUrl)) {
557 createInterfaceAsync(false);
558 EventQueue.invokeLater(new Runnable() {
560 _patchNotes.setVisible(true);
565 // download the patch files...
566 setStep(Step.DOWNLOAD);
571 updateStatus("m.patching");
573 long[] sizes = new long[list.size()];
574 Arrays.fill(sizes, 1L);
575 ProgressAggregator pragg = new ProgressAggregator(_progobs, sizes);
576 int ii = 0; for (Resource prsrc : list) {
577 ProgressObserver pobs = pragg.startElement(ii++);
579 // install the patch file (renaming them from _new)
580 prsrc.install(false);
581 // now apply the patch
582 Patcher patcher = new Patcher();
583 patcher.patch(prsrc.getLocal().getParentFile(), prsrc.getLocal(), pobs);
584 } catch (Exception e) {
585 log.warning("Failed to apply patch", "prsrc", prsrc, e);
588 // clean up the patch file
589 if (!FileUtil.deleteHarder(prsrc.getLocal())) {
590 log.warning("Failed to delete '" + prsrc + "'.");
595 // if the patch resource is null, that means something was booched in the application, so
596 // we skip the patching process but update the metadata which will result in a "brute
599 // finally update our metadata files...
600 _app.updateMetadata();
601 // ...and reinitialize the application
606 * Called if the application is determined to require resource downloads.
608 protected void download (Collection<Resource> resources)
611 // create our user interface
612 createInterfaceAsync(false);
614 Downloader dl = new HTTPDownloader(_app.proxy) {
615 @Override protected void resolvingDownloads () {
616 updateStatus("m.resolving");
619 @Override protected void downloadProgress (int percent, long remaining) {
620 // check for another getdown running at 0 and every 10% after that
621 if (_lastCheck == -1 || percent >= _lastCheck + 10) {
623 // stop the presses if something else is holding the lock
624 boolean locked = _app.lockForUpdates();
628 _lastCheck = percent;
630 setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true);
632 reportTrackingEvent("progress", percent);
636 @Override protected void downloadFailed (Resource rsrc, Exception e) {
637 updateStatus(MessageUtil.tcompose("m.failure", e.getMessage()));
638 log.warning("Download failed", "rsrc", rsrc, e);
641 /** The last percentage at which we checked for another getdown running, or -1 for not
642 * having checked at all. */
643 protected int _lastCheck = -1;
645 if (!dl.download(resources, _app.maxConcurrentDownloads())) {
646 // if we aborted due to detecting another getdown running, we want to report here
647 throw new MultipleGetdownRunning();
652 * Called to launch the application if everything is determined to be ready to go.
654 protected void launch ()
656 setStep(Step.LAUNCH);
657 setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false);
660 if (invokeDirect()) {
661 // we want to close the Getdown window, as the app is launching
668 if (_app.hasOptimumJvmArgs()) {
669 // if we have "optimum" arguments, we want to try launching with them first
670 proc = _app.createProcess(true);
672 long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME;
673 boolean error = false;
674 while (fallback > System.currentTimeMillis()) {
676 error = proc.exitValue() != 0;
678 } catch (IllegalThreadStateException e) {
684 log.info("Failed to launch with optimum arguments; falling back.");
685 proc = _app.createProcess(false);
688 proc = _app.createProcess(false);
691 // close standard in to avoid choking standard out of the launched process
692 proc.getInputStream().close();
693 // close standard out, since we're not going to write to anything to it anyway
694 proc.getOutputStream().close();
696 // on Windows 98 and ME we need to stick around and read the output of stderr lest
697 // the process fill its output buffer and choke, yay!
698 final InputStream stderr = proc.getErrorStream();
699 if (LaunchUtil.mustMonitorChildren()) {
700 // close our window if it's around
703 copyStream(stderr, System.err);
704 log.info("Process exited: " + proc.waitFor());
707 // spawn a daemon thread that will catch the early bits of stderr in case the
709 Thread t = new Thread() {
710 @Override public void run () {
711 copyStream(stderr, System.err);
719 // if we have a UI open and we haven't been around for at least 5 seconds (the default
720 // for min_show_seconds), don't stick a fork in ourselves straight away but give our
721 // lovely user a chance to see what we're doing
722 long uptime = System.currentTimeMillis() - _startup;
723 long minshow = _ifc.minShowSeconds * 1000L;
724 if (_container != null && uptime < minshow) {
726 Thread.sleep(minshow - uptime);
727 } catch (Exception e) {
731 // pump the percent up to 100%
732 setStatusAsync(null, 100, -1L, false);
735 } catch (Exception e) {
736 log.warning("launch() failed.", e);
741 * Creates our user interface, which we avoid doing unless we actually have to update
742 * something. NOTE: this happens on the next UI tick, not immediately.
744 * @param reinit - if the interface should be reinitialized if it already exists.
746 protected void createInterfaceAsync (final boolean reinit)
748 if (_silent || (_container != null && !reinit)) {
752 EventQueue.invokeLater(new Runnable() {
754 if (_container == null || reinit) {
755 if (_container == null) {
756 _container = createContainer();
758 _container.removeAll();
760 configureContainer();
761 _layers = new JLayeredPane();
762 _container.add(_layers, BorderLayout.CENTER);
763 _patchNotes = new JButton(new AbstractAction(_msgs.getString("m.patch_notes")) {
764 @Override public void actionPerformed (ActionEvent event) {
765 showDocument(_ifc.patchNotesUrl);
768 _patchNotes.setFont(StatusPanel.FONT);
769 _layers.add(_patchNotes);
770 _status = new StatusPanel(_msgs);
771 _layers.add(_status);
780 * Initializes the interface with the current UpdateInterface and backgrounds.
782 protected void initInterface ()
784 RotatingBackgrounds newBackgrounds = getBackground();
785 if (_background == null || newBackgrounds.getNumImages() > 0) {
786 // Leave the old _background in place if there is an old one to leave in place
787 // and the new getdown.txt didn't yield any images.
788 _background = newBackgrounds;
790 _status.init(_ifc, _background, getProgressImage());
791 Dimension size = _status.getPreferredSize();
792 _status.setSize(size);
793 _layers.setPreferredSize(size);
795 _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y,
796 _ifc.patchNotes.width, _ifc.patchNotes.height);
797 _patchNotes.setVisible(false);
799 // we were displaying progress while the UI wasn't up. Now that it is, whatever progress
800 // is left is scaled into a 0-100 DISPLAYED progress.
801 _uiDisplayPercent = _lastGlobalPercent;
802 _stepMinPercent = _lastGlobalPercent = 0;
805 protected RotatingBackgrounds getBackground ()
807 if (_ifc.rotatingBackgrounds != null) {
808 if (_ifc.backgroundImage != null) {
809 log.warning("ui.background_image and ui.rotating_background were both specified. " +
810 "The rotating images are being used.");
812 return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground,
814 } else if (_ifc.backgroundImage != null) {
815 return new RotatingBackgrounds(loadImage(_ifc.backgroundImage));
817 return new RotatingBackgrounds();
821 protected Image getProgressImage ()
823 return loadImage(_ifc.progressImage);
826 protected void handleWindowClose ()
831 if (_abort == null) {
832 _abort = new AbortPanel(Getdown.this, _msgs);
835 SwingUtil.centerWindow(_abort);
836 _abort.setVisible(true);
837 _abort.setState(JFrame.NORMAL);
838 _abort.requestFocus();
843 * Update the status to indicate getdown has failed for the reason in <code>message</code>.
845 protected void fail (String message)
848 setStatusAsync(message, stepToGlobalPercent(0), -1L, true);
852 * Set the current step, which will be used to globalize per-step percentages.
854 protected void setStep (Step step)
856 int finalPercent = -1;
857 for (Integer perc : _ifc.stepPercentages.get(step)) {
858 if (perc > _stepMaxPercent) {
863 if (finalPercent == -1) {
864 // we've gone backwards and this step will be ignored
868 _stepMaxPercent = finalPercent;
869 _stepMinPercent = _lastGlobalPercent;
873 * Convert a step percentage to the global percentage.
875 protected int stepToGlobalPercent (int percent)
877 int adjustedMaxPercent =
878 ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent);
879 _lastGlobalPercent = Math.max(_lastGlobalPercent,
880 _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100);
881 return _lastGlobalPercent;
885 * Updates the status. NOTE: this happens on the next UI tick, not immediately.
887 protected void setStatusAsync (final String message, final int percent, final long remaining,
890 if (_status == null && createUI) {
891 createInterfaceAsync(false);
894 EventQueue.invokeLater(new Runnable() {
896 if (_status == null) {
897 if (message != null) {
898 log.info("Dropping status '" + message + "'.");
902 if (message != null) {
903 _status.setStatus(message, _dead);
906 _status.setProgress(0, -1L);
907 } else if (percent >= 0) {
908 _status.setProgress(percent, remaining);
914 protected void reportTrackingEvent (String event, int progress)
916 if (!_enableTracking) {
919 } else if (progress > 0) {
920 // we need to make sure we do the right thing if we skip over progress levels
922 URL url = _app.getTrackingProgressURL(++_reportedProgress);
924 new ProgressReporter(url).start();
926 } while (_reportedProgress <= progress);
929 URL url = _app.getTrackingURL(event);
931 new ProgressReporter(url).start();
937 * Creates the container in which our user interface will be displayed.
939 protected abstract Container createContainer ();
942 * Configures the interface container based on the latest UI config.
944 protected abstract void configureContainer ();
947 * Shows the container in which our user interface will be displayed.
949 protected abstract void showContainer ();
952 * Disposes the container in which we have our user interface.
954 protected abstract void disposeContainer ();
957 * If this method returns true we will run the application in the same JVM, otherwise we will
958 * fork off a new JVM. Some options are not supported if we do not fork off a new JVM.
960 protected boolean invokeDirect ()
962 return SysProps.direct();
966 * Requests to show the document at the specified URL in a new window.
968 protected abstract void showDocument (String url);
971 * Requests that Getdown exit.
973 protected abstract void exit (int exitCode);
976 * Copies the supplied stream from the specified input to the specified output. Used to copy
977 * our child processes stderr and stdout to our own stderr and stdout.
979 protected static void copyStream (InputStream in, PrintStream out)
982 BufferedReader reader = new BufferedReader(new InputStreamReader(in));
984 while ((line = reader.readLine()) != null) {
988 } catch (IOException ioe) {
989 log.warning("Failure copying", "in", in, "out", out, "error", ioe);
993 /** Used to fetch a progress report URL. */
994 protected class ProgressReporter extends Thread
996 public ProgressReporter (URL url) {
1002 public void run () {
1004 HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0);
1006 // if we have a tracking cookie configured, configure the request with it
1007 if (_app.getTrackingCookieName() != null &&
1008 _app.getTrackingCookieProperty() != null) {
1009 String val = System.getProperty(_app.getTrackingCookieProperty());
1011 ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val);
1015 // now request our tracking URL and ensure that we get a non-error response
1018 if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) {
1019 log.warning("Failed to report tracking event",
1020 "url", _url, "rcode", ucon.getResponseCode());
1026 } catch (IOException ioe) {
1027 log.warning("Failed to report tracking event", "url", _url, "error", ioe);
1034 /** Used to pass progress on to our user interface. */
1035 protected ProgressObserver _progobs = new ProgressObserver() {
1036 public void progress (int percent) {
1037 setStatusAsync(null, stepToGlobalPercent(percent), -1L, false);
1041 public void setStartupFilesFromParameterString(String p) {
1043 if (p != null && p.length() > 0) {
1045 if (p.startsWith(q) && p.endsWith(q)) {
1046 filenames = p.substring(1,p.length()-1).split("\" \"");
1048 filenames = new String[]{p};
1050 for (int i = 0; i < filenames.length; i++) {
1051 String filename = filenames[i];
1053 int j = filename.lastIndexOf('.');
1055 ext = filename.substring(j+1);
1058 if (_app.locatorFileExtension.equals(ext.toLowerCase())) {
1059 // Do something special with this here
1060 File f = new File(filename);
1062 String appbase = null;
1064 java.util.Properties jvlprops = new java.util.Properties();
1065 FileInputStream in = new FileInputStream(f);
1068 appbase = jvlprops.getProperty("appbase");
1069 } catch(Exception e) {
1070 log.warning("Something went wrong reading Jalview Version Locator file '"+filename+"'", e);
1072 if (appbase != null) {
1074 URL newAppbase = new URL(appbase);
1075 _app.newAppbase(newAppbase);
1076 log.warning("Java Version Locator url '"+appbase+"' found in file '"+filename+"' being used");
1077 } catch(MalformedURLException e) {
1078 log.warning("Java Version Locator url '"+appbase+"' found in file '"+filename+"' is malformed", e);
1084 boolean addedStartupFile = false; // only add one startup file
1085 if (! addedStartupFile) {
1086 File f = new File(filename);
1088 _app.addStartupFile(f);
1089 addedStartupFile = true;
1097 protected Application _app;
1098 protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
1100 protected ResourceBundle _msgs;
1101 protected Container _container;
1102 protected JLayeredPane _layers;
1103 protected StatusPanel _status;
1104 protected JButton _patchNotes;
1105 protected AbortPanel _abort;
1106 protected RotatingBackgrounds _background;
1108 protected boolean _dead;
1109 protected boolean _silent;
1110 protected boolean _launchInSilent;
1111 protected boolean _noUpdate;
1112 protected long _startup;
1114 protected Set<Resource> _toInstallResources;
1115 protected boolean _readyToInstall;
1117 protected boolean _enableTracking = true;
1118 protected int _reportedProgress = 0;
1120 /** Number of minutes to wait after startup before beginning any real heavy lifting. */
1121 protected int _delay;
1123 protected int _stepMaxPercent;
1124 protected int _stepMinPercent;
1125 protected int _lastGlobalPercent;
1126 protected int _uiDisplayPercent;
1128 protected static final int MAX_LOOPS = 5;
1129 protected static final long FALLBACK_CHECK_TIME = 1000L;