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 { Getdown(EnvConfig envc)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 */ isUpdateAvailable()98 public boolean isUpdateAvailable () 99 { 100 return _readyToInstall && !_toInstallResources.isEmpty(); 101 } 102 103 /** 104 * Installs the currently pending new resources. 105 */ install()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 run()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 */ configProxy(String host, String port, String username, String password)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 detectProxy()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 readConfig(boolean preloads)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()); 236 } 237 } 238 239 /** 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). 244 */ doPredownloads(Collection<Resource> resources)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); 250 } 251 } 252 253 try { 254 download(predownloads); 255 for (Resource rsrc : predownloads) { 256 rsrc.install(false); // install but don't validate yet 257 } 258 } catch (IOException ioe) { 259 log.warning("Failed to predownload resources. Continuing...", ioe); 260 } 261 } 262 263 /** 264 * Does the actual application validation, update and launching business. 265 */ getdown()266 protected void getdown () 267 { 268 try { 269 // first parses our application deployment file 270 try { 271 readConfig(true); 272 } catch (IOException ioe) { 273 log.warning("Failed to initialize: " + ioe); 274 _app.attemptRecovery(this); 275 // and re-initalize 276 readConfig(true); 277 // and force our UI to be recreated with the updated info 278 createInterfaceAsync(true); 279 } 280 if (!_noUpdate && !_app.lockForUpdates()) { 281 throw new MultipleGetdownRunning(); 282 } 283 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."); 289 } 290 if (_delay > 0) { 291 // don't hold the lock while waiting, let another getdown proceed if it starts. 292 _app.releaseLock(); 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(); 300 } 301 } 302 303 // if no_update was specified, directly start the app without updating 304 if (_noUpdate) { 305 log.info("Launching without update!"); 306 launch(); 307 return; 308 } 309 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 315 // time through 316 int[] alreadyValid = new int[1]; 317 318 // we'll keep track of all the resources we unpack 319 Set<Resource> unpacked = new HashSet<>(); 320 321 _toInstallResources = new HashSet<>(); 322 _readyToInstall = false; 323 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."); 331 update(); 332 // loop back again and reverify the metadata 333 continue; 334 } 335 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); 342 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); 348 } 349 } 350 351 try { 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); 356 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); 362 363 reportTrackingEvent("app_complete", -1); 364 365 } finally { 366 _enableTracking = false; 367 } 368 369 // now we'll loop back and try it all again 370 continue; 371 } 372 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 381 try { 382 updateJava(); 383 } finally { 384 _enableTracking = false; 385 } 386 continue; 387 } 388 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"); 393 long version = -1; 394 long aversion = _app.getVersion(); 395 if (!ufile.exists()) { 396 ufile.createNewFile(); 397 } else { 398 version = VersionUtil.readVersion(ufile); 399 } 400 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); 406 try { 407 VersionUtil.writeVersion(ufile, aversion); 408 } catch (IOException ioe) { 409 log.warning("Failed to update unpacked version", ioe); 410 } 411 } 412 } 413 414 // assuming we're not doing anything funny, install the update 415 _readyToInstall = true; 416 install(); 417 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(); 424 launch(); 425 } 426 return; 427 } 428 429 log.warning("Pants! We couldn't get the job done."); 430 throw new IOException("m.unable_to_repair"); 431 432 } catch (Exception e) { 433 log.warning("getdown() failed.", e); 434 String msg = e.getMessage(); 435 if (msg == null) { 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); 442 } else { 443 msg = MessageUtil.compose( 444 "m.init_error", MessageUtil.taint(msg), _ifc.installError); 445 } 446 } 447 // Since we're dead, clear off the 'time remaining' label along with displaying the 448 // error message 449 fail(msg); 450 _app.releaseLock(); 451 } 452 } 453 454 // documentation inherited from interface 455 @Override updateStatus(String message)456 public void updateStatus (String message) 457 { 458 setStatusAsync(message, -1, -1L, true); 459 } 460 461 /** 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 464 * the filename. 465 */ 466 @Override loadImage(String path)467 public BufferedImage loadImage (String path) 468 { 469 if (StringUtil.isBlank(path)) { 470 return null; 471 } 472 473 File imgpath = null; 474 try { 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. 481 } 482 483 // If that didn't work, try a generic one. 484 try { 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); 489 return null; 490 } 491 } 492 493 /** 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. 496 */ updateJava()497 protected void updateJava () 498 throws IOException 499 { 500 Resource vmjar = _app.getJavaVMResource(); 501 if (vmjar == null) { 502 throw new IOException("m.java_download_failed"); 503 } 504 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."); 513 return; 514 } 515 log.info("Can update local Java VM as it is not in use."); 516 } 517 518 reportTrackingEvent("jvm_start", -1); 519 520 updateStatus("m.downloading_java"); 521 List<Resource> list = new ArrayList<>(); 522 list.add(vmjar); 523 download(list); 524 525 reportTrackingEvent("jvm_unpack", -1); 526 527 updateStatus("m.unpacking_java"); 528 vmjar.install(true); 529 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")); 535 536 // lastly regenerate the .jsa dump file that helps Java to start up faster 537 String vmpath = LaunchUtil.getJVMPath(_app.getLocalPath("")); 538 try { 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); 543 } 544 545 reportTrackingEvent("jvm_complete", -1); 546 } 547 548 /** 549 * Called if the application is determined to be of an old version. 550 */ update()551 protected void update () 552 throws IOException 553 { 554 // first clear all validation markers 555 _app.clearValidationMarkers(); 556 557 // attempt to download the patch files 558 Resource patch = _app.getPatchResource(null); 559 if (patch != null) { 560 List<Resource> list = new ArrayList<>(); 561 list.add(patch); 562 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); 567 if (patch != null) { 568 list.add(patch); 569 } 570 } 571 } 572 573 // show the patch notes button, if applicable 574 if (!StringUtil.isBlank(_ifc.patchNotesUrl)) { 575 createInterfaceAsync(false); 576 EQinvoke(new Runnable() { 577 public void run () { 578 _patchNotes.setVisible(true); 579 } 580 }); 581 } 582 583 // download the patch files... 584 setStep(Step.DOWNLOAD); 585 download(list); 586 587 // and apply them... 588 setStep(Step.PATCH); 589 updateStatus("m.patching"); 590 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++); 596 try { 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); 604 } 605 606 // clean up the patch file 607 if (!FileUtil.deleteHarder(prsrc.getLocal())) { 608 log.warning("Failed to delete '" + prsrc + "'."); 609 } 610 } 611 } 612 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 615 // force" upgrade 616 617 // finally update our metadata files... 618 _app.updateMetadata(); 619 // ...and reinitialize the application 620 readConfig(false); 621 } 622 623 /** 624 * Called if the application is determined to require resource downloads. 625 */ download(Collection<Resource> resources)626 protected void download (Collection<Resource> resources) 627 throws IOException 628 { 629 // create our user interface 630 createInterfaceAsync(false); 631 632 Downloader dl = new HTTPDownloader(_app.proxy) { 633 @Override protected void resolvingDownloads () { 634 updateStatus("m.resolving"); 635 } 636 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) { 640 if (_delay > 0) { 641 // stop the presses if something else is holding the lock 642 boolean locked = _app.lockForUpdates(); 643 _app.releaseLock(); 644 if (locked) abort(); 645 } 646 _lastCheck = percent; 647 } 648 setStatusAsync("m.downloading", stepToGlobalPercent(percent), remaining, true); 649 if (percent > 0) { 650 reportTrackingEvent("progress", percent); 651 } 652 } 653 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); 657 } 658 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; 662 }; 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(); 666 } 667 } 668 669 /** 670 * Called to launch the application if everything is determined to be ready to go. 671 */ launch()672 protected void launch () 673 { 674 setStep(Step.LAUNCH); 675 setStatusAsync("m.launching", stepToGlobalPercent(100), -1L, false); 676 677 try { 678 if (invokeDirect()) { 679 // we want to close the Getdown window, as the app is launching 680 disposeContainer(); 681 _app.releaseLock(); 682 _app.invokeDirect(); 683 684 } else { 685 Process proc; 686 if (_app.hasOptimumJvmArgs()) { 687 // if we have "optimum" arguments, we want to try launching with them first 688 proc = _app.createProcess(true); 689 690 long fallback = System.currentTimeMillis() + FALLBACK_CHECK_TIME; 691 boolean error = false; 692 while (fallback > System.currentTimeMillis()) { 693 try { 694 error = proc.exitValue() != 0; 695 break; 696 } catch (IllegalThreadStateException e) { 697 Thread.yield(); 698 } 699 } 700 701 if (error) { 702 log.info("Failed to launch with optimum arguments; falling back."); 703 proc = _app.createProcess(false); 704 } 705 } else { 706 proc = _app.createProcess(false); 707 } 708 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(); 713 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 719 disposeContainer(); 720 _container = null; 721 copyStream(stderr, System.err); 722 log.info("Process exited: " + proc.waitFor()); 723 724 } else { 725 // spawn a daemon thread that will catch the early bits of stderr in case the 726 // launch fails 727 Thread t = new Thread() { 728 @Override public void run () { 729 copyStream(stderr, System.err); 730 } 731 }; 732 t.setDaemon(true); 733 t.start(); 734 } 735 } 736 737 //_container.setVisible(true); 738 //_container.validate(); 739 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) { 746 try { 747 Thread.sleep(minshow - uptime); 748 } catch (Exception e) { 749 } 750 } 751 752 // pump the percent up to 100% 753 setStatusAsync(null, 100, -1L, false); 754 exit(0); 755 756 } catch (Exception e) { 757 log.warning("launch() failed.", e); 758 } 759 } 760 761 /** 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. 764 * 765 * @param reinit - if the interface should be reinitialized if it already exists. 766 */ createInterfaceAsync(final boolean reinit)767 protected void createInterfaceAsync (final boolean reinit) 768 { 769 if (_silent || (_container != null && !reinit)) { 770 return; 771 } 772 773 EQinvoke (new Runnable() { 774 public void run () { 775 776 if (_container == null || reinit) { 777 if (_container == null) { 778 _container = createContainer(); 779 } else { 780 _container.removeAll(); 781 } 782 configureContainer(); 783 _layers = new JLayeredPane(); 784 785 786 787 // added in the instant display of a splashscreen 788 try { 789 readConfig(false); 790 Graphics g = _container.getGraphics(); 791 BufferedImage iBgImage = loadImage(_ifc.instantBackgroundImage); 792 boolean ibg = true; 793 if (iBgImage == null) { 794 iBgImage = loadImage(_ifc.backgroundImage); 795 ibg = false; 796 } 797 if (iBgImage != null) { 798 final BufferedImage bgImage = iBgImage; 799 int bwidth = bgImage.getWidth(); 800 int bheight = bgImage.getHeight(); 801 802 log.info("Displaying instant background image", ibg?"instant_background_image":"background_image"); 803 804 instantSplashPane = new JPanel() { 805 @Override 806 protected void paintComponent(Graphics g) 807 { 808 super.paintComponent(g); 809 // attempt to draw a background image... 810 if (bgImage != null) { 811 g.drawImage(bgImage, 0, 0, this); 812 } 813 } 814 }; 815 816 instantSplashPane.setSize(bwidth,bheight); 817 instantSplashPane.setPreferredSize(new Dimension(bwidth,bheight)); 818 819 _layers.add(instantSplashPane, Integer.valueOf(1)); 820 821 _container.setPreferredSize(new Dimension(bwidth,bheight)); 822 } 823 } catch (Exception e) { 824 log.warning("Failed to set instant background image", "ibg", _ifc.instantBackgroundImage); 825 } 826 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); 831 } 832 }); 833 _patchNotes.setFont(StatusPanel.FONT); 834 _layers.add(_patchNotes); 835 _status = new StatusPanel(_msgs); 836 setStatusAsync("m.initialising", 0, -1L, true); 837 //_layers.add(_status, Integer.valueOf(2)); 838 initInterface(); 839 } 840 showContainer(); 841 } 842 }); 843 844 } 845 846 /** 847 * Initializes the interface with the current UpdateInterface and backgrounds. 848 */ initInterface()849 protected void initInterface () 850 { 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; 856 } 857 _status.init(_ifc, _background, getProgressImage()); 858 Dimension size = _status.getPreferredSize(); 859 _status.setSize(size); 860 //_status.updateStatusLabel(); 861 _layers.setPreferredSize(size); 862 863 _patchNotes.setBounds(_ifc.patchNotes.x, _ifc.patchNotes.y, 864 _ifc.patchNotes.width, _ifc.patchNotes.height); 865 _patchNotes.setVisible(false); 866 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; 871 } 872 getBackground()873 protected RotatingBackgrounds getBackground () 874 { 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."); 879 } 880 return new RotatingBackgrounds(_ifc.rotatingBackgrounds, _ifc.errorBackground, Getdown.this); 881 } else if (_ifc.backgroundImage != null) { 882 return new RotatingBackgrounds(loadImage(_ifc.backgroundImage)); 883 } else { 884 return new RotatingBackgrounds(); 885 } 886 } 887 getProgressImage()888 protected Image getProgressImage () 889 { 890 return loadImage(_ifc.progressImage); 891 } 892 handleWindowClose()893 protected void handleWindowClose () 894 { 895 if (_dead) { 896 exit(0); 897 } else { 898 if (_abort == null) { 899 _abort = new AbortPanel(Getdown.this, _msgs); 900 } 901 _abort.pack(); 902 SwingUtil.centerWindow(_abort); 903 _abort.setVisible(true); 904 _abort.setState(JFrame.NORMAL); 905 _abort.requestFocus(); 906 } 907 } 908 909 /** 910 * Update the status to indicate getdown has failed for the reason in <code>message</code>. 911 */ fail(String message)912 protected void fail (String message) 913 { 914 _dead = true; 915 setStatusAsync(message, stepToGlobalPercent(0), -1L, true); 916 } 917 918 /** 919 * Set the current step, which will be used to globalize per-step percentages. 920 */ setStep(Step step)921 protected void setStep (Step step) 922 { 923 int finalPercent = -1; 924 for (Integer perc : _ifc.stepPercentages.get(step)) { 925 if (perc > _stepMaxPercent) { 926 finalPercent = perc; 927 break; 928 } 929 } 930 if (finalPercent == -1) { 931 // we've gone backwards and this step will be ignored 932 return; 933 } 934 935 _stepMaxPercent = finalPercent; 936 _stepMinPercent = _lastGlobalPercent; 937 } 938 939 /** 940 * Convert a step percentage to the global percentage. 941 */ stepToGlobalPercent(int percent)942 protected int stepToGlobalPercent (int percent) 943 { 944 int adjustedMaxPercent = 945 ((_stepMaxPercent - _uiDisplayPercent) * 100) / (100 - _uiDisplayPercent); 946 _lastGlobalPercent = Math.max(_lastGlobalPercent, 947 _stepMinPercent + (percent * (adjustedMaxPercent - _stepMinPercent)) / 100); 948 return _lastGlobalPercent; 949 } 950 951 /** 952 * Updates the status. NOTE: this happens on the next UI tick, not immediately. 953 */ setStatusAsync(final String message, final int percent, final long remaining, boolean createUI)954 protected void setStatusAsync (final String message, final int percent, final long remaining, 955 boolean createUI) 956 { 957 if (_status == null && createUI) { 958 createInterfaceAsync(false); 959 } 960 961 EQinvoke(new Runnable() { 962 public void run () { 963 964 if (_status == null) { 965 if (message != null) { 966 log.info("Dropping status '" + message + "'."); 967 } 968 return; 969 } 970 if (message != null) { 971 _status.setStatus(message, _dead); 972 } 973 if (_dead) { 974 _status.setProgress(0, -1L); 975 } else if (percent >= 0) { 976 _status.setProgress(percent, remaining); 977 } else { 978 //_status.setStatus("m.initialising", false); 979 } 980 981 } 982 }); 983 984 if (_status != null && ! _addedStatusLayer) { 985 _layers.add(_status, Integer.valueOf(2)); 986 _addedStatusLayer = true; 987 initInterface(); 988 } 989 990 } 991 reportTrackingEvent(String event, int progress)992 protected void reportTrackingEvent (String event, int progress) 993 { 994 if (!_enableTracking) { 995 return; 996 997 } else if (progress > 0) { 998 // we need to make sure we do the right thing if we skip over progress levels 999 do { 1000 URL url = _app.getTrackingProgressURL(++_reportedProgress); 1001 if (url != null) { 1002 new ProgressReporter(url).start(); 1003 } 1004 } while (_reportedProgress <= progress); 1005 1006 } else { 1007 URL url = _app.getTrackingURL(event); 1008 if (url != null) { 1009 new ProgressReporter(url).start(); 1010 } 1011 } 1012 } 1013 1014 /** 1015 * Creates the container in which our user interface will be displayed. 1016 */ createContainer()1017 protected abstract Container createContainer (); 1018 1019 /** 1020 * Configures the interface container based on the latest UI config. 1021 */ configureContainer()1022 protected abstract void configureContainer (); 1023 1024 /** 1025 * Shows the container in which our user interface will be displayed. 1026 */ showContainer()1027 protected abstract void showContainer (); 1028 1029 /** 1030 * Disposes the container in which we have our user interface. 1031 */ disposeContainer()1032 protected abstract void disposeContainer (); 1033 1034 /** 1035 * If this method returns true we will run the application in the same JVM, otherwise we will 1036 * fork off a new JVM. Some options are not supported if we do not fork off a new JVM. 1037 */ invokeDirect()1038 protected boolean invokeDirect () 1039 { 1040 return SysProps.direct(); 1041 } 1042 1043 /** 1044 * Requests to show the document at the specified URL in a new window. 1045 */ showDocument(String url)1046 protected abstract void showDocument (String url); 1047 1048 /** 1049 * Requests that Getdown exit. 1050 */ exit(int exitCode)1051 protected abstract void exit (int exitCode); 1052 1053 /** 1054 * Copies the supplied stream from the specified input to the specified output. Used to copy 1055 * our child processes stderr and stdout to our own stderr and stdout. 1056 */ copyStream(InputStream in, PrintStream out)1057 protected static void copyStream (InputStream in, PrintStream out) 1058 { 1059 try { 1060 BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 1061 String line; 1062 while ((line = reader.readLine()) != null) { 1063 out.print(line); 1064 out.flush(); 1065 } 1066 } catch (IOException ioe) { 1067 log.warning("Failure copying", "in", in, "out", out, "error", ioe); 1068 } 1069 } 1070 1071 /** Used to fetch a progress report URL. */ 1072 protected class ProgressReporter extends Thread 1073 { ProgressReporter(URL url)1074 public ProgressReporter (URL url) { 1075 setDaemon(true); 1076 _url = url; 1077 } 1078 1079 @Override run()1080 public void run () { 1081 try { 1082 HttpURLConnection ucon = ConnectionUtil.openHttp(_app.proxy, _url, 0, 0); 1083 1084 // if we have a tracking cookie configured, configure the request with it 1085 if (_app.getTrackingCookieName() != null && 1086 _app.getTrackingCookieProperty() != null) { 1087 String val = System.getProperty(_app.getTrackingCookieProperty()); 1088 if (val != null) { 1089 ucon.setRequestProperty("Cookie", _app.getTrackingCookieName() + "=" + val); 1090 } 1091 } 1092 1093 // now request our tracking URL and ensure that we get a non-error response 1094 ucon.connect(); 1095 try { 1096 if (ucon.getResponseCode() != HttpURLConnection.HTTP_OK) { 1097 log.warning("Failed to report tracking event", 1098 "url", _url, "rcode", ucon.getResponseCode()); 1099 } 1100 } finally { 1101 ucon.disconnect(); 1102 } 1103 1104 } catch (IOException ioe) { 1105 log.warning("Failed to report tracking event", "url", _url, "error", ioe); 1106 } 1107 } 1108 1109 protected URL _url; 1110 } 1111 1112 /** Used to pass progress on to our user interface. */ 1113 protected ProgressObserver _progobs = new ProgressObserver() { 1114 public void progress (int percent) { 1115 setStatusAsync(null, stepToGlobalPercent(percent), -1L, false); 1116 } 1117 }; 1118 1119 // Asynchronous or synchronous progress updates EQinvoke(Runnable r)1120 protected void EQinvoke(Runnable r) { 1121 1122 try { 1123 readConfig(false); 1124 } catch (Exception e) { 1125 log.warning("Could't read config when invoking GUI action", "Exception", e.getMessage()); 1126 } 1127 if (_ifc != null && (_shownContainer?_ifc.progressSyncAfterShown:_ifc.progressSync)) { 1128 try { 1129 EventQueue.invokeAndWait(r); 1130 //r.run(); 1131 } catch (Exception e) { 1132 log.warning("Tried to invokeAndWait but couldn't. Going to invokeLater instead", "Exception", e.getMessage()); 1133 EventQueue.invokeLater(r); 1134 } 1135 } else { 1136 EventQueue.invokeLater(r); 1137 //r.run(); 1138 } 1139 1140 //try { Thread.sleep(500); } catch (Exception e) {} 1141 1142 } 1143 1144 protected Application _app; 1145 protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY); 1146 1147 protected ResourceBundle _msgs; 1148 protected Container _container; 1149 protected JLayeredPane _layers; 1150 protected JPanel instantSplashPane; 1151 protected StatusPanel _status; 1152 protected JButton _patchNotes; 1153 protected AbortPanel _abort; 1154 protected RotatingBackgrounds _background; 1155 1156 protected boolean _addedStatusLayer; 1157 protected boolean _dead; 1158 protected boolean _silent; 1159 protected boolean _launchInSilent; 1160 protected boolean _noUpdate; 1161 protected long _startup; 1162 1163 protected Set<Resource> _toInstallResources; 1164 protected boolean _readyToInstall; 1165 1166 protected boolean _enableTracking = true; 1167 protected int _reportedProgress = 0; 1168 1169 protected boolean _shownContainer; 1170 1171 /** Number of minutes to wait after startup before beginning any real heavy lifting. */ 1172 protected int _delay; 1173 1174 protected int _stepMaxPercent; 1175 protected int _stepMinPercent; 1176 protected int _lastGlobalPercent; 1177 protected int _uiDisplayPercent; 1178 1179 protected static final int MAX_LOOPS = 5; 1180 protected static final long FALLBACK_CHECK_TIME = 1000L; 1181 1182 } 1183