1 /*
   2  * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.application;
  27 
  28 import javafx.application.Application;
  29 import javafx.application.Preloader;
  30 import javafx.application.Preloader.ErrorNotification;
  31 import javafx.application.Preloader.PreloaderNotification;
  32 import javafx.application.Preloader.StateChangeNotification;
  33 import javafx.stage.Stage;
  34 import java.io.File;
  35 import java.io.IOException;
  36 import java.lang.reflect.Constructor;
  37 import java.lang.reflect.InvocationTargetException;
  38 import java.lang.reflect.Method;
  39 import java.net.MalformedURLException;
  40 import java.net.URL;
  41 import java.net.URLClassLoader;
  42 import java.security.AccessController;
  43 import java.security.PrivilegedAction;
  44 import java.text.Normalizer;
  45 import java.util.ArrayList;
  46 import java.util.LinkedList;
  47 import java.util.List;
  48 import java.util.concurrent.CountDownLatch;
  49 import java.util.concurrent.atomic.AtomicBoolean;
  50 import java.util.concurrent.atomic.AtomicReference;
  51 import java.util.jar.Attributes;
  52 import java.util.jar.JarFile;
  53 import java.util.jar.Manifest;
  54 import java.util.Base64;
  55 import java.util.Optional;
  56 import com.sun.javafx.stage.StageHelper;
  57 
  58 
  59 public class LauncherImpl {
  60     /**
  61      * When passed as launchMode to launchApplication, tells the method that
  62      * launchName is the name of the JavaFX application class to launch.
  63      */
  64     public static final String LAUNCH_MODE_CLASS = "LM_CLASS";
  65 
  66     /**
  67      * When passed as launchMode to launchApplication, tells the method that
  68      * launchName is a path to a JavaFX application jar file to be launched.
  69      */
  70     public static final String LAUNCH_MODE_JAR = "LM_JAR";
  71 
  72     /**
  73      * When passed as launchMode to launchApplication, tells the method that
  74      * launchName is the name of the JavaFX application class within a module
  75      * to be launched. Either the class name will be provided or the main class
  76      * will be defined in the module's descriptor.
  77      */
  78     public static final String LAUNCH_MODE_MODULE = "LM_MODULE";
  79 
  80     // set to true to debug launch issues from Java launcher
  81     private static final boolean trace = false;
  82 
  83     // set system property javafx.verbose to true to make the launcher noisy
  84     private static final boolean verbose;
  85 
  86     private static final String MF_MAIN_CLASS = "Main-Class";
  87     private static final String MF_JAVAFX_MAIN = "JavaFX-Application-Class";
  88     private static final String MF_JAVAFX_PRELOADER = "JavaFX-Preloader-Class";
  89     private static final String MF_JAVAFX_CLASS_PATH = "JavaFX-Class-Path";
  90     private static final String MF_JAVAFX_ARGUMENT_PREFIX = "JavaFX-Argument-";
  91     private static final String MF_JAVAFX_PARAMETER_NAME_PREFIX = "JavaFX-Parameter-Name-";
  92     private static final String MF_JAVAFX_PARAMETER_VALUE_PREFIX = "JavaFX-Parameter-Value-";
  93 
  94     // Set to true to simulate a slow download progress
  95     private static final boolean simulateSlowProgress = false;
  96 
  97     // Ensure that launchApplication method is only called once
  98     private static AtomicBoolean launchCalled = new AtomicBoolean(false);
  99 
 100     // Flag indicating that the toolkit has been started
 101     private static final AtomicBoolean toolkitStarted = new AtomicBoolean(false);
 102 
 103     // Exception found during launching
 104     private static volatile RuntimeException launchException = null;
 105 
 106     // The current preloader, used for notification in the standalone
 107     // launcher mode
 108     private static Preloader currentPreloader = null;
 109 
 110     // Saved preloader class from the launchApplicationWithArgs method (called
 111     // from the Java 8 launcher). It is used in the case where we call main,
 112     // which is turn calls into launchApplication.
 113     private static Class<? extends Preloader> savedPreloaderClass = null;
 114 
 115     // The following is used to determine whether the main() method
 116     // has set the CCL in the case where main is called after the FX toolkit
 117     // is started.
 118     private static ClassLoader savedMainCcl = null;
 119 
 120     static {
 121         verbose = AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
 122                 Boolean.getBoolean("javafx.verbose"));
 123     }
 124 
 125     /**
 126      * This method is called by the Application.launch method.
 127      * It must not be called more than once or an exception will be thrown.
 128      *
 129      * Note that it is always called on a thread other than the FX application
 130      * thread, since that thread is only created at startup.
 131      *
 132      * @param appClass application class
 133      * @param args command line arguments
 134      */
 135     @SuppressWarnings("unchecked")
 136     public static void launchApplication(final Class<? extends Application> appClass,
 137             final String[] args) {
 138 
 139         Class<? extends Preloader> preloaderClass = savedPreloaderClass;
 140 
 141         if (preloaderClass == null) {
 142             String preloaderByProperty = AccessController.doPrivileged((PrivilegedAction<String>) () ->
 143                     System.getProperty("javafx.preloader"));
 144             if (preloaderByProperty != null) {
 145                 try {
 146                     preloaderClass = (Class<? extends Preloader>) Class.forName(preloaderByProperty,
 147                             false, appClass.getClassLoader());
 148                 } catch (Exception e) {
 149                     System.err.printf("Could not load preloader class '" + preloaderByProperty +
 150                             "', continuing without preloader.");
 151                     e.printStackTrace();
 152                 }
 153             }
 154         }
 155 
 156         launchApplication(appClass, preloaderClass, args);
 157     }
 158 
 159     /**
 160      * This method is called by the standalone launcher.
 161      * It must not be called more than once or an exception will be thrown.
 162      *
 163      * Note that it is always called on a thread other than the FX application
 164      * thread, since that thread is only created at startup.
 165      *
 166      * @param appClass application class
 167      * @param preloaderClass preloader class, may be null
 168      * @param args command line arguments
 169      */
 170     public static void launchApplication(final Class<? extends Application> appClass,
 171             final Class<? extends Preloader> preloaderClass,
 172             final String[] args) {
 173 
 174         if (launchCalled.getAndSet(true)) {
 175             throw new IllegalStateException("Application launch must not be called more than once");
 176         }
 177 
 178         if (! Application.class.isAssignableFrom(appClass)) {
 179             throw new IllegalArgumentException("Error: " + appClass.getName()
 180                     + " is not a subclass of javafx.application.Application");
 181         }
 182 
 183         if (preloaderClass != null && ! Preloader.class.isAssignableFrom(preloaderClass)) {
 184             throw new IllegalArgumentException("Error: " + preloaderClass.getName()
 185                     + " is not a subclass of javafx.application.Preloader");
 186         }
 187 
 188 //        System.err.println("launch standalone app: preloader class = "
 189 //                + preloaderClass);
 190 
 191         // Create a new Launcher thread and then wait for that thread to finish
 192         final CountDownLatch launchLatch = new CountDownLatch(1);
 193         Thread launcherThread = new Thread(() -> {
 194             try {
 195                 launchApplication1(appClass, preloaderClass, args);
 196             } catch (RuntimeException rte) {
 197                 launchException = rte;
 198             } catch (Exception ex) {
 199                 launchException =
 200                     new RuntimeException("Application launch exception", ex);
 201             } catch (Error err) {
 202                 launchException =
 203                     new RuntimeException("Application launch error", err);
 204             } finally {
 205                 launchLatch.countDown();
 206             }
 207         });
 208         launcherThread.setName("JavaFX-Launcher");
 209         launcherThread.start();
 210 
 211         // Wait for FX launcher thread to finish before returning to user
 212         try {
 213             launchLatch.await();
 214         } catch (InterruptedException ex) {
 215             throw new RuntimeException("Unexpected exception: ", ex);
 216         }
 217 
 218         if (launchException != null) {
 219             throw launchException;
 220         }
 221     }
 222 
 223     /**
 224      * This method is called by the Java launcher. This allows us to be launched
 225      * directly from the command line via "java -jar fxapp.jar",
 226      * "java -cp path some.fx.App", or "java -m module/some.fx.App". The launchMode
 227      * argument must be one of "LM_CLASS", "LM_JAR", or "LM_MODULE" or execution will
 228      * abort with an error.
 229      *
 230      * @param launchName The path to a jar file, the application class name to launch,
 231      * or the module and optional class name to launch
 232      * @param launchMode The method of launching the application, one of LM_JAR,
 233      * LM_CLASS, or LM_MODULE
 234      * @param args Application arguments from the command line
 235      */
 236     public static void launchApplication(final String launchName,
 237             final String launchMode,
 238             final String[] args) {
 239 
 240         if (verbose) {
 241             System.err.println("JavaFX launchApplication method: launchMode="
 242                     + launchMode);
 243         }
 244 
 245         /*
 246          * For now, just open the jar and get JavaFX-Application-Class and
 247          * JavaFX-Preloader and pass them to launchApplication. In the future
 248          * we'll need to load requested jar files
 249          */
 250         String mainClassName = null;
 251         String preloaderClassName = null;
 252         String[] appArgs = args;
 253         ClassLoader appLoader = null;
 254         ModuleAccess mainModule = null;
 255 
 256         if (launchMode.equals(LAUNCH_MODE_JAR)) {
 257             Attributes jarAttrs = getJarAttributes(launchName);
 258             if (jarAttrs == null) {
 259                 abort(null, "Can't get manifest attributes from jar");
 260             }
 261 
 262             // If we ever need to check JavaFX-Version, do that here...
 263 
 264             // Support JavaFX-Class-Path, but warn that it's deprecated if used
 265             String fxClassPath = jarAttrs.getValue(MF_JAVAFX_CLASS_PATH);
 266             if (fxClassPath != null) {
 267                 if (fxClassPath.trim().length() == 0) {
 268                     fxClassPath = null;
 269                 } else {
 270                     if (verbose) {
 271                         System.err.println("WARNING: Application jar uses deprecated JavaFX-Class-Path attribute."
 272                                +" Please use Class-Path instead.");
 273                     }
 274 
 275                     /*
 276                      * create a new ClassLoader to pull in the requested jar files
 277                      * OK if it returns null, that just means we didn't need to load
 278                      * anything
 279                      */
 280                     appLoader = setupJavaFXClassLoader(new File(launchName), fxClassPath);
 281                 }
 282             }
 283 
 284             // process arguments and parameters if no args have been passed by the launcher
 285             if (args.length == 0) {
 286                 appArgs = getAppArguments(jarAttrs);
 287             }
 288 
 289             // grab JavaFX-Application-Class
 290             mainClassName = jarAttrs.getValue(MF_JAVAFX_MAIN);
 291             if (mainClassName == null) {
 292                 // fall back on Main-Class if no JAC
 293                 mainClassName = jarAttrs.getValue(MF_MAIN_CLASS);
 294                 if (mainClassName == null) {
 295                     // Should not happen as the launcher enforces the presence of Main-Class
 296                     abort(null, "JavaFX jar manifest requires a valid JavaFX-Appliation-Class or Main-Class entry");
 297                 }
 298             }
 299             mainClassName = mainClassName.trim();
 300 
 301             // grab JavaFX-Preloader-Class
 302             preloaderClassName = jarAttrs.getValue(MF_JAVAFX_PRELOADER);
 303             if (preloaderClassName != null) {
 304                 preloaderClassName = preloaderClassName.trim();
 305             }
 306         } else if (launchMode.equals(LAUNCH_MODE_CLASS)) {
 307             mainClassName = launchName;
 308         } else if (launchMode.equals(LAUNCH_MODE_MODULE)) {
 309             // This is largely copied from java.base/sun.launcher.LauncherHelper
 310             int i = launchName.indexOf('/');
 311             String moduleName;
 312             if (i == -1) {
 313                 moduleName = launchName;
 314                 mainClassName = null;
 315             } else {
 316                 moduleName = launchName.substring(0, i);
 317                 mainClassName = launchName.substring(i+1);
 318             }
 319 
 320             mainModule = ModuleAccess.load(moduleName);
 321 
 322             // get main class from module descriptor
 323             if (mainClassName == null) {
 324                 Optional<String> omc = mainModule.getDescriptor().mainClass();
 325                 if (!omc.isPresent()) {
 326                     abort(null, "Module %1$s does not have a MainClass attribute, use -m <module>/<main-class>",
 327                             moduleName);
 328                 }
 329                 mainClassName = omc.get();
 330             }
 331         } else {
 332             abort(new IllegalArgumentException(
 333                     "The launchMode argument must be one of LM_CLASS, LM_JAR or LM_MODULE"),
 334                     "Invalid launch mode: %1$s", launchMode);
 335         }
 336 
 337         // fall back if no MF_JAVAFX_PRELOADER attribute, or not launching using -jar
 338         if (preloaderClassName == null) {
 339             preloaderClassName = System.getProperty("javafx.preloader");
 340         }
 341 
 342         if (mainClassName == null) {
 343             abort(null, "No main JavaFX class to launch");
 344         }
 345 
 346         // check if we have to load through a custom classloader
 347         if (appLoader != null) {
 348             try {
 349                 // reload this class through the app classloader
 350                 Class<?> launcherClass = appLoader.loadClass(LauncherImpl.class.getName());
 351 
 352                 // then invoke the second part of this launcher using reflection
 353                 Method lawa = launcherClass.getMethod("launchApplicationWithArgs",
 354                         new Class[] { ModuleAccess.class, String.class, String.class, (new String[0]).getClass()});
 355 
 356                 // set the thread context class loader before we continue, or it won't load properly
 357                 Thread.currentThread().setContextClassLoader(appLoader);
 358                 lawa.invoke(null, new Object[] {null, mainClassName, preloaderClassName, appArgs});
 359             } catch (Exception e) {
 360                 abort(e, "Exception while launching application");
 361             }
 362         } else {
 363             launchApplicationWithArgs(mainModule, mainClassName, preloaderClassName, appArgs);
 364         }
 365     }
 366 
 367     // wrapper for Class.forName that handles cases where diacritical marks in the name
 368     // cause the class to not be loaded, also largely copied from LauncherHelper.java
 369     // this method returns null if the class cannot be loaded
 370     private static Class<?> loadClass(final ModuleAccess mainModule, final String className) {
 371         Class<?> clz = null;
 372         final ClassLoader loader = Thread.currentThread().getContextClassLoader();
 373 
 374         // loader is ignored for modular mode
 375         // the only time we need to use a separate loader is LM_JAR with
 376         // a MF_JAVAFX_CLASS_PATH attribute which is deprecated
 377 
 378         if (mainModule != null) {
 379             clz = mainModule.classForName(className);
 380         } else {
 381             try {
 382                 clz = Class.forName(className, true, loader);
 383             } catch (ClassNotFoundException | NoClassDefFoundError cnfe) {}
 384         }
 385 
 386         if (clz == null && System.getProperty("os.name", "").contains("OS X")
 387                     && Normalizer.isNormalized(className, Normalizer.Form.NFD)) {
 388             // macOS may have decomposed diacritical marks in mainClassName
 389             // recompose them and try again
 390             String cn = Normalizer.normalize(className, Normalizer.Form.NFC);
 391 
 392             if (mainModule != null) {
 393                 clz = mainModule.classForName(cn);
 394             } else {
 395                 try {
 396                     clz = Class.forName(cn, true, loader);
 397                 } catch (ClassNotFoundException | NoClassDefFoundError cnfe) {}
 398             }
 399         }
 400 
 401         return clz;
 402     }
 403 
 404     // Must be public since we could be called from a different class loader
 405     public static void launchApplicationWithArgs(final ModuleAccess mainModule,
 406             final String mainClassName,
 407             final String preloaderClassName, String[] args) {
 408         try {
 409             startToolkit();
 410         } catch (InterruptedException ex) {
 411             abort(ex, "Toolkit initialization error", mainClassName);
 412         }
 413 
 414         Class<? extends Application> appClass;
 415         Class<? extends Preloader> preClass = null;
 416         Class<?> tempAppClass = null;
 417 
 418         final AtomicReference<Class<?>> tmpClassRef = new AtomicReference<>();
 419         final AtomicReference<Class<? extends Preloader>> preClassRef = new AtomicReference<>();
 420         PlatformImpl.runAndWait(() -> {
 421             Class<?> clz = loadClass(mainModule, mainClassName);
 422             if (clz == null) {
 423                 if (mainModule != null) {
 424                     abort(null, "Missing JavaFX application class %1$s in module %2$s",
 425                             mainClassName, mainModule.getName());
 426                 } else {
 427                     abort(null, "Missing JavaFX application class %1$s", mainClassName);
 428                 }
 429             }
 430 
 431             tmpClassRef.set(clz);
 432 
 433             if (preloaderClassName != null) {
 434                 // TODO: modular preloader?
 435                 clz = loadClass(null, preloaderClassName);
 436                 if (clz == null) {
 437                     abort(null, "Missing JavaFX preloader class %1$s", preloaderClassName);
 438                 }
 439 
 440                 if (!Preloader.class.isAssignableFrom(clz)) {
 441                     abort(null, "JavaFX preloader class %1$s does not extend javafx.application.Preloader", clz.getName());
 442                 }
 443                 preClassRef.set(clz.asSubclass(Preloader.class));
 444             }
 445         });
 446         preClass = preClassRef.get();
 447         tempAppClass = tmpClassRef.get();
 448 
 449         // Save the preloader class in a static field for later use when
 450         // main calls back into launchApplication.
 451         savedPreloaderClass = preClass;
 452 
 453         // If there is a public static void main(String[]) method then call it
 454         // otherwise just hand off to the other launchApplication method
 455 
 456         Exception theEx = null;
 457         try {
 458             Method mainMethod = tempAppClass.getMethod("main",
 459                     new Class[] { (new String[0]).getClass() });
 460             if (verbose) {
 461                 System.err.println("Calling main(String[]) method");
 462             }
 463             savedMainCcl = Thread.currentThread().getContextClassLoader();
 464             mainMethod.invoke(null, new Object[] { args });
 465             return;
 466         } catch (NoSuchMethodException | IllegalAccessException ex) {
 467             theEx = ex;
 468             savedPreloaderClass = null;
 469             if (verbose) {
 470                 System.err.println("WARNING: Cannot access application main method: " + ex);
 471             }
 472         } catch (InvocationTargetException ex) {
 473             ex.printStackTrace();
 474             abort(null, "Exception running application %1$s", tempAppClass.getName());
 475             return;
 476         }
 477 
 478         // Verify appClass extends Application
 479         if (!Application.class.isAssignableFrom(tempAppClass)) {
 480             abort(theEx, "JavaFX application class %1$s does not extend javafx.application.Application", tempAppClass.getName());
 481         }
 482         appClass = tempAppClass.asSubclass(Application.class);
 483 
 484         if (verbose) {
 485             System.err.println("Launching application directly");
 486         }
 487         launchApplication(appClass, preClass, args);
 488     }
 489 
 490     private static URL fileToURL(File file) throws IOException {
 491         return file.getCanonicalFile().toURI().toURL();
 492     }
 493 
 494     private static ClassLoader setupJavaFXClassLoader(File appJar, String fxClassPath) {
 495         try {
 496             File baseDir = appJar.getParentFile();
 497             ArrayList jcpList = new ArrayList();
 498 
 499             // Add in the jars from the JavaFX-Class-Path entry
 500             // TODO: should check current classpath for duplicate entries and ignore them
 501             String cp = fxClassPath;
 502             if (cp != null) {
 503                 // these paths are relative to baseDir, which should be the
 504                 // directory containing the app jar file
 505                 while (cp.length() > 0) {
 506                     int pathSepIdx = cp.indexOf(" ");
 507                     if (pathSepIdx < 0) {
 508                         String pathElem = cp;
 509                         File f = (baseDir == null) ?
 510                                 new File(pathElem) : new File(baseDir, pathElem);
 511                         if (f.exists()) {
 512                             jcpList.add(fileToURL(f));
 513                         } else if (verbose) {
 514                             System.err.println("Class Path entry \""+pathElem
 515                                     +"\" does not exist, ignoring");
 516                         }
 517                         break;
 518                     } else if (pathSepIdx > 0) {
 519                         String pathElem = cp.substring(0, pathSepIdx);
 520                         File f = (baseDir == null) ?
 521                                 new File(pathElem) : new File(baseDir, pathElem);
 522                         if (f.exists()) {
 523                             jcpList.add(fileToURL(f));
 524                         } else if (verbose) {
 525                             System.err.println("Class Path entry \""+pathElem
 526                                     +"\" does not exist, ignoring");
 527                         }
 528                     }
 529                     cp = cp.substring(pathSepIdx + 1);
 530                 }
 531             }
 532 
 533             // don't bother if there's nothing to add
 534             if (!jcpList.isEmpty()) {
 535                 ArrayList<URL> urlList = new ArrayList<URL>();
 536 
 537                 // prepend the existing classpath
 538                 // this will already have the app jar, so no need to worry about it
 539                 cp = System.getProperty("java.class.path");
 540                 if (cp != null) {
 541                     while (cp.length() > 0) {
 542                         int pathSepIdx = cp.indexOf(File.pathSeparatorChar);
 543                         if (pathSepIdx < 0) {
 544                             String pathElem = cp;
 545                             urlList.add(fileToURL(new File(pathElem)));
 546                             break;
 547                         } else if (pathSepIdx > 0) {
 548                             String pathElem = cp.substring(0, pathSepIdx);
 549                             urlList.add(fileToURL(new File(pathElem)));
 550                         }
 551                         cp = cp.substring(pathSepIdx + 1);
 552                     }
 553                 }
 554 
 555                 // and finally append the JavaFX-Class-Path entries
 556                 urlList.addAll(jcpList);
 557 
 558                 URL[] urls = (URL[])urlList.toArray(new URL[0]);
 559                 if (verbose) {
 560                     System.err.println("===== URL list");
 561                     for (int i = 0; i < urls.length; i++) {
 562                         System.err.println("" + urls[i]);
 563                     }
 564                     System.err.println("=====");
 565                 }
 566                 return new URLClassLoader(urls, ClassLoader.getPlatformClassLoader());
 567             }
 568         } catch (Exception ex) {
 569             if (trace) {
 570                 System.err.println("Exception creating JavaFX class loader: "+ex);
 571                 ex.printStackTrace();
 572             }
 573         }
 574         return null;
 575     }
 576 
 577     private static String decodeBase64(String inp) throws IOException {
 578         return new String(Base64.getDecoder().decode(inp));
 579     }
 580 
 581     private static String[] getAppArguments(Attributes attrs) {
 582         List args = new LinkedList();
 583 
 584         try {
 585             int idx = 1;
 586             String argNamePrefix = MF_JAVAFX_ARGUMENT_PREFIX;
 587             while (attrs.getValue(argNamePrefix + idx) != null) {
 588                 args.add(decodeBase64(attrs.getValue(argNamePrefix + idx)));
 589                 idx++;
 590             }
 591 
 592             String paramNamePrefix = MF_JAVAFX_PARAMETER_NAME_PREFIX;
 593             String paramValuePrefix = MF_JAVAFX_PARAMETER_VALUE_PREFIX;
 594             idx = 1;
 595             while (attrs.getValue(paramNamePrefix + idx) != null) {
 596                 String k = decodeBase64(attrs.getValue(paramNamePrefix + idx));
 597                 String v = null;
 598                 if (attrs.getValue(paramValuePrefix + idx) != null) {
 599                     v = decodeBase64(attrs.getValue(paramValuePrefix + idx));
 600                 }
 601                 args.add("--" + k + "=" + (v != null ? v : ""));
 602                 idx++;
 603             }
 604         } catch (IOException ioe) {
 605             if (verbose) {
 606                 System.err.println("Failed to extract application parameters");
 607             }
 608             ioe.printStackTrace();
 609         }
 610 
 611         return (String[]) args.toArray(new String[0]);
 612     }
 613 
 614     // FIXME: needs localization, since these are presented to the user
 615     private static void abort(final Throwable cause, final String fmt, final Object... args) {
 616         String msg = String.format(fmt, args);
 617         if (msg != null) {
 618             System.err.println(msg);
 619         }
 620 
 621         if (trace) {
 622             if (cause != null) {
 623                 cause.printStackTrace();
 624             } else {
 625                 Thread.dumpStack();
 626             }
 627         }
 628         System.exit(1);
 629     }
 630 
 631     private static Attributes getJarAttributes(String jarPath) {
 632         JarFile jarFile = null;
 633         try {
 634             jarFile = new JarFile(jarPath);
 635             Manifest manifest = jarFile.getManifest();
 636             if (manifest == null) {
 637                 abort(null, "No manifest in jar file %1$s", jarPath);
 638             }
 639             return manifest.getMainAttributes();
 640         } catch (IOException ioe) {
 641             abort(ioe, "Error launching jar file %1%s", jarPath);
 642         } finally {
 643             try {
 644                 jarFile.close();
 645             } catch (IOException ioe) {}
 646         }
 647         return null;
 648     }
 649 
 650     private static void startToolkit() throws InterruptedException {
 651         if (toolkitStarted.getAndSet(true)) {
 652             return;
 653         }
 654 
 655         final CountDownLatch startupLatch = new CountDownLatch(1);
 656 
 657         // Note, this method is called on the FX Application Thread
 658         PlatformImpl.startup(() -> startupLatch.countDown());
 659 
 660         // Wait for FX platform to start
 661         startupLatch.await();
 662     }
 663 
 664     private static volatile boolean error = false;
 665     private static volatile Throwable pConstructorError = null;
 666     private static volatile Throwable pInitError = null;
 667     private static volatile Throwable pStartError = null;
 668     private static volatile Throwable pStopError = null;
 669     private static volatile Throwable constructorError = null;
 670     private static volatile Throwable initError = null;
 671     private static volatile Throwable startError = null;
 672     private static volatile Throwable stopError = null;
 673 
 674     private static void launchApplication1(final Class<? extends Application> appClass,
 675             final Class<? extends Preloader> preloaderClass,
 676             final String[] args) throws Exception {
 677 
 678         startToolkit();
 679 
 680         if (savedMainCcl != null) {
 681             /*
 682              * The toolkit was already started by the java launcher, and the
 683              * main method of the application class was called. Check to see
 684              * whether the CCL has been changed. If so, then we need
 685              * to pass the context class loader to the FX app thread so that it
 686              * correctly picks up the current setting.
 687              */
 688             final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
 689             if (ccl != null && ccl != savedMainCcl) {
 690                 PlatformImpl.runLater(() -> {
 691                     Thread.currentThread().setContextClassLoader(ccl);
 692                 });
 693             }
 694         }
 695 
 696         final AtomicBoolean pStartCalled = new AtomicBoolean(false);
 697         final AtomicBoolean startCalled = new AtomicBoolean(false);
 698         final AtomicBoolean exitCalled = new AtomicBoolean(false);
 699         final AtomicBoolean pExitCalled = new AtomicBoolean(false);
 700         final CountDownLatch shutdownLatch = new CountDownLatch(1);
 701         final CountDownLatch pShutdownLatch = new CountDownLatch(1);
 702 
 703         final PlatformImpl.FinishListener listener = new PlatformImpl.FinishListener() {
 704             @Override public void idle(boolean implicitExit) {
 705                 if (!implicitExit) {
 706                     return;
 707                 }
 708 
 709 //                System.err.println("JavaFX Launcher: system is idle");
 710                 if (startCalled.get()) {
 711                     shutdownLatch.countDown();
 712                 } else if (pStartCalled.get()) {
 713                     pShutdownLatch.countDown();
 714                 }
 715             }
 716 
 717             @Override public void exitCalled() {
 718 //                System.err.println("JavaFX Launcher: received exit notification");
 719                 exitCalled.set(true);
 720                 shutdownLatch.countDown();
 721             }
 722         };
 723         PlatformImpl.addListener(listener);
 724 
 725         try {
 726             final AtomicReference<Preloader> pldr = new AtomicReference<>();
 727             if (preloaderClass != null) {
 728                 // Construct an instance of the preloader on the FX thread, then
 729                 // call its init method on this (launcher) thread. Then call
 730                 // the start method on the FX thread.
 731                 PlatformImpl.runAndWait(() -> {
 732                     try {
 733                         Constructor<? extends Preloader> c = preloaderClass.getConstructor();
 734                         pldr.set(c.newInstance());
 735                         // Set startup parameters
 736                         ParametersImpl.registerParameters(pldr.get(), new ParametersImpl(args));
 737                     } catch (Throwable t) {
 738                         System.err.println("Exception in Preloader constructor");
 739                         pConstructorError = t;
 740                         error = true;
 741                     }
 742                 });
 743             }
 744             currentPreloader = pldr.get();
 745 
 746             // Call init method unless exit called or error detected
 747             if (currentPreloader != null && !error && !exitCalled.get()) {
 748                 try {
 749                     // Call the application init method (on the Launcher thread)
 750                     currentPreloader.init();
 751                 } catch (Throwable t) {
 752                     System.err.println("Exception in Preloader init method");
 753                     pInitError = t;
 754                     error = true;
 755                 }
 756             }
 757 
 758             // Call start method unless exit called or error detected
 759             if (currentPreloader != null && !error && !exitCalled.get()) {
 760                 // Call the application start method on FX thread
 761                 PlatformImpl.runAndWait(() -> {
 762                     try {
 763                         pStartCalled.set(true);
 764 
 765                         // Create primary stage and call preloader start method
 766                         final Stage primaryStage = new Stage();
 767                         StageHelper.setPrimary(primaryStage, true);
 768                         currentPreloader.start(primaryStage);
 769                     } catch (Throwable t) {
 770                         System.err.println("Exception in Preloader start method");
 771                         pStartError = t;
 772                         error = true;
 773                     }
 774                 });
 775 
 776                 // Notify preloader of progress
 777                 if (!error && !exitCalled.get()) {
 778                     notifyProgress(currentPreloader, 0.0);
 779                 }
 780             }
 781 
 782             // Construct an instance of the application on the FX thread, then
 783             // call its init method on this (launcher) thread. Then call
 784             // the start method on the FX thread.
 785             final AtomicReference<Application> app = new AtomicReference<>();
 786             if (!error && !exitCalled.get()) {
 787                 if (currentPreloader != null) {
 788                     if (simulateSlowProgress) {
 789                         for (int i = 0; i < 100; i++) {
 790                             notifyProgress(currentPreloader, (double)i / 100.0);
 791                             Thread.sleep(10);
 792                         }
 793                     }
 794                     notifyProgress(currentPreloader, 1.0);
 795                     notifyStateChange(currentPreloader,
 796                             StateChangeNotification.Type.BEFORE_LOAD, null);
 797                 }
 798 
 799                 PlatformImpl.runAndWait(() -> {
 800                     try {
 801                         Constructor<? extends Application> c = appClass.getConstructor();
 802                         app.set(c.newInstance());
 803                         // Set startup parameters
 804                         ParametersImpl.registerParameters(app.get(), new ParametersImpl(args));
 805                         PlatformImpl.setApplicationName(appClass);
 806                     } catch (Throwable t) {
 807                         System.err.println("Exception in Application constructor");
 808                         constructorError = t;
 809                         error = true;
 810                     }
 811                 });
 812             }
 813             final Application theApp = app.get();
 814 
 815             // Call init method unless exit called or error detected
 816             if (!error && !exitCalled.get()) {
 817                 if (currentPreloader != null) {
 818                     notifyStateChange(currentPreloader,
 819                             StateChangeNotification.Type.BEFORE_INIT, theApp);
 820                 }
 821 
 822                 try {
 823                     // Call the application init method (on the Launcher thread)
 824                     theApp.init();
 825                 } catch (Throwable t) {
 826                     System.err.println("Exception in Application init method");
 827                     initError = t;
 828                     error = true;
 829                 }
 830             }
 831 
 832             // Call start method unless exit called or error detected
 833             if (!error && !exitCalled.get()) {
 834                 if (currentPreloader != null) {
 835                     notifyStateChange(currentPreloader,
 836                             StateChangeNotification.Type.BEFORE_START, theApp);
 837                 }
 838                 // Call the application start method on FX thread
 839                 PlatformImpl.runAndWait(() -> {
 840                     try {
 841                         startCalled.set(true);
 842 
 843                         // Create primary stage and call application start method
 844                         final Stage primaryStage = new Stage();
 845                         StageHelper.setPrimary(primaryStage, true);
 846                         theApp.start(primaryStage);
 847                     } catch (Throwable t) {
 848                         System.err.println("Exception in Application start method");
 849                         startError = t;
 850                         error = true;
 851                     }
 852                 });
 853             }
 854 
 855             if (!error) {
 856                 shutdownLatch.await();
 857 //                System.err.println("JavaFX Launcher: time to call stop");
 858             }
 859 
 860             // Call stop method if start was called
 861             if (startCalled.get()) {
 862                 // Call Application stop method on FX thread
 863                 PlatformImpl.runAndWait(() -> {
 864                     try {
 865                         theApp.stop();
 866                     } catch (Throwable t) {
 867                         System.err.println("Exception in Application stop method");
 868                         stopError = t;
 869                         error = true;
 870                     }
 871                 });
 872             }
 873 
 874             if (error) {
 875                 if (pConstructorError != null) {
 876                     throw new RuntimeException("Unable to construct Preloader instance: "
 877                             + appClass, pConstructorError);
 878                 } else if (pInitError != null) {
 879                     throw new RuntimeException("Exception in Preloader init method",
 880                             pInitError);
 881                 } else if(pStartError != null) {
 882                     throw new RuntimeException("Exception in Preloader start method",
 883                             pStartError);
 884                 } else if (pStopError != null) {
 885                     throw new RuntimeException("Exception in Preloader stop method",
 886                             pStopError);
 887                 } else if (constructorError != null) {
 888                     String msg = "Unable to construct Application instance: " + appClass;
 889                     if (!notifyError(msg, constructorError)) {
 890                         throw new RuntimeException(msg, constructorError);
 891                     }
 892                 } else if (initError != null) {
 893                     String msg = "Exception in Application init method";
 894                     if (!notifyError(msg, initError)) {
 895                         throw new RuntimeException(msg, initError);
 896                     }
 897                 } else if(startError != null) {
 898                     String msg = "Exception in Application start method";
 899                     if (!notifyError(msg, startError)) {
 900                         throw new RuntimeException(msg, startError);
 901                     }
 902                 } else if (stopError != null) {
 903                     String msg = "Exception in Application stop method";
 904                     if (!notifyError(msg, stopError)) {
 905                         throw new RuntimeException(msg, stopError);
 906                     }
 907                 }
 908             }
 909         } finally {
 910             PlatformImpl.removeListener(listener);
 911             PlatformImpl.tkExit();
 912         }
 913     }
 914 
 915     private static void notifyStateChange(final Preloader preloader,
 916             final StateChangeNotification.Type type,
 917             final Application app) {
 918 
 919         PlatformImpl.runAndWait(() -> preloader.handleStateChangeNotification(
 920             new StateChangeNotification(type, app)));
 921     }
 922 
 923     private static void notifyProgress(final Preloader preloader, final double d) {
 924         PlatformImpl.runAndWait(() -> preloader.handleProgressNotification(
 925                 new Preloader.ProgressNotification(d)));
 926     }
 927 
 928     private static boolean notifyError(final String msg, final Throwable constructorError) {
 929         final AtomicBoolean result = new AtomicBoolean(false);
 930         PlatformImpl.runAndWait(() -> {
 931             if (currentPreloader != null) {
 932                 try {
 933                     ErrorNotification evt = new ErrorNotification(null, msg, constructorError);
 934                     boolean rval = currentPreloader.handleErrorNotification(evt);
 935                     result.set(rval);
 936                 } catch (Throwable t) {
 937                     t.printStackTrace();
 938                 }
 939             }
 940         });
 941 
 942         return result.get();
 943     }
 944 
 945     private static void notifyCurrentPreloader(final PreloaderNotification pe) {
 946         PlatformImpl.runAndWait(() -> {
 947             if (currentPreloader != null) {
 948                 currentPreloader.handleApplicationNotification(pe);
 949             }
 950         });
 951     }
 952 
 953     private static Method notifyMethod = null;
 954 
 955     public static void notifyPreloader(Application app, final PreloaderNotification info) {
 956         if (launchCalled.get()) {
 957             // Standalone launcher mode
 958             notifyCurrentPreloader(info);
 959             return;
 960         }
 961     }
 962 
 963     // Not an instantiable class.
 964     private LauncherImpl() {
 965         // Should never get here.
 966         throw new InternalError();
 967     }
 968 
 969 }