/* Copyright (c) 2013-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.launcher; import javafx.stage.Stage; import se.llbit.chunky.PersistentSettings; import se.llbit.chunky.launcher.ui.ChunkyLauncherFx; import se.llbit.chunky.launcher.ui.DebugConsole; import se.llbit.chunky.launcher.ui.FirstTimeSetupDialog; import se.llbit.chunky.resources.SettingsDirectory; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.Scanner; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; /** * The Chunky launcher sets up the command to launch Chunky. * The launcher locates the Jar dependencies and builds the Java * command line for launching Chunky with the Jars on the classpath. * * <p>Various settings can be controlled * via the launcher, and it can show a debug console while running * Chunky. * * @author Jesper Öqvist <jesper.oqvist@cs.lth.se> */ public class ChunkyLauncher { public static final String LAUNCHER_VERSION = "v1.9.4"; protected String java; /** * Print a launch error message to the console. * Prints the command that was used to try to launch Chunky. */ protected static void launchFailure(String command) { System.out.println("Failed to launch Chunky. Command used:"); System.out.println(command); } public static void main(String[] args) throws FileNotFoundException { final LauncherSettings settings = new LauncherSettings(); settings.load(); /* * If there are command line arguments then we assume that Chunky should run * in headless mode, unless the --nolauncher command is present in which case * we strip that and start regularly, but without launcher. The --launcher * option overrides everything else and forces the launcher to appear. */ // TODO(llbit): the control flow for launching Chunky needs to be simplified. boolean forceLauncher = false; LaunchMode mode = LaunchMode.GUI; String headlessOptions = ""; if (args.length > 0) { mode = LaunchMode.HEADLESS; for (String arg : args) { switch (arg) { case "--nolauncher": mode = LaunchMode.GUI; break; case "--launcher": forceLauncher = true; break; case "--version": System.out.println("Chunky Launcher " + LAUNCHER_VERSION); return; case "--verbose": settings.verboseLauncher = true; break; case "--console": settings.forceGuiConsole = true; break; case "--update": case "--updateAlpha": if (arg.equals("--updateAlpha")) { System.out.println("Checking for Chunky alpha/snapshot updates.."); settings.downloadSnapshots = true; } else { System.out.println("Checking for updates.."); } UpdateChecker updateThread = new UpdateChecker(settings, new UpdateListener() { @Override public void updateError(String message) { } @Override public void updateAvailable(VersionInfo latest) { try { headlessCreateSettingsDirectory(); } catch (FileNotFoundException e) { throw new Error(e); } System.out.println("Downloading Chunky " + latest + ":"); ConsoleUpdater.update(latest); } @Override public void noUpdateAvailable() { System.out.println("No updates found."); if (!settings.downloadSnapshots) { System.out .println("Alpha/snapshot updates are disabled, enable with --updateAlpha"); } } }); updateThread.start(); return; case "--setup": // Configure launcher settings. doSetup(settings); settings.save(); return; default: if (!headlessOptions.isEmpty()) { headlessOptions += " "; } headlessOptions += arg; break; } } if (forceLauncher) { mode = LaunchMode.GUI; } } ChunkyDeployer.LoggerBuilder loggerBuilder = () -> { if (settings.forceGuiConsole || (!settings.headless && settings.debugConsole)) { AtomicReference<DebugConsole> console = new AtomicReference<>(null); CountDownLatch latch = new CountDownLatch(1); ChunkyLauncherFx.withLauncher(settings, stage -> { DebugConsole debugConsole = new DebugConsole(settings.closeConsoleOnExit); debugConsole.show(); console.set(debugConsole); latch.countDown(); }); try { latch.await(); } catch (InterruptedException ignored) { // Ignored. } return console.get(); } else { return new ConsoleLogger(); } }; if (mode == LaunchMode.HEADLESS) { // Chunky is being run from the console, i.e. headless mode. headlessCreateSettingsDirectory(); settings.debugConsole = true; settings.headless = true; settings.chunkyOptions = headlessOptions; ChunkyDeployer.deploy(settings); // Install the embedded version. VersionInfo version = ChunkyDeployer.resolveVersion(settings.version); if (ChunkyDeployer.canLaunch(version, null, false)) { int exitCode = ChunkyDeployer.launchChunky(settings, version, LaunchMode.HEADLESS, ChunkyLauncher::launchFailure, loggerBuilder); if (exitCode != 0) { System.exit(exitCode); } } else { System.err.println("Could not launch selected Chunky version. Try updating with --update"); System.exit(1); } } else { final boolean finalForceLauncher = forceLauncher; // A callback is used to decide if the launcher should be displayed after // the first time setup dialog has been shown (if needed). firstTimeSetup(settings, () -> { ChunkyDeployer.deploy(settings); // Install the embedded version. if (!finalForceLauncher && !settings.showLauncher) { // Skip the launcher only if we can launch this version. VersionInfo version = ChunkyDeployer.resolveVersion(settings.version); if (ChunkyDeployer.canLaunch(version, null, false)) { if (ChunkyDeployer.launchChunky(settings, version, LaunchMode.GUI, ChunkyLauncher::launchFailure, loggerBuilder) == 0) { return false; } } } return true; }); } } /** Ensure that the settings directory exists. */ private static void headlessCreateSettingsDirectory() throws FileNotFoundException { // We will try to set up a Chunky settings directory if one is not already configured. File directory = SettingsDirectory.getSettingsDirectory(); if (directory == null || !directory.isDirectory()) { System.out.println("Chunky settings directory not found - trying to create one."); if (directory != null && !directory.mkdir()) { System.out.format("Failed to create directory: %s%n", directory.getAbsolutePath()); } if (directory == null || !directory.isDirectory()) { directory = SettingsDirectory.getHomeDirectory(); if (directory != null && !directory.mkdir()) { System.out.format("Failed to create directory: %s%n", directory.getAbsolutePath()); } } if (directory != null && directory.isDirectory()) { System.out.format("Created settings directory: %s%n", directory.getAbsolutePath()); } } if (directory != null) { // Initialize empty settings files. File settingsFile = new File(directory, PersistentSettings.SETTINGS_FILE); if (!settingsFile.exists()) { try (PrintStream out = new PrintStream(new FileOutputStream(settingsFile))) { // Create an empty settings file (default settings will be used). out.println("{}"); } } settingsFile = new File(directory, LauncherSettings.LAUNCHER_SETTINGS_FILE); if (!settingsFile.exists()) { try (PrintStream out = new PrintStream(new FileOutputStream(settingsFile))) { // Create an empty settings file (default settings will be used). out.println("{}"); } } } } interface ShowLauncher { boolean showLauncher(); } private static void doSetup(LauncherSettings settings) throws FileNotFoundException { headlessCreateSettingsDirectory(); System.out.print("Memory limit (MiB): "); Scanner in = new Scanner(System.in); settings.memoryLimit = in.nextInt(); in.nextLine(); System.out.print("Java options: "); settings.javaOptions = in.nextLine(); } /** * Shows the first-time setup dialog if needed and then calls the afterFirstTimeSetup * callback. Ensures that the JavaFX application (ChunkyLauncherFx) is initialized * before calling afterFirstTimeSetup. */ private static void firstTimeSetup(LauncherSettings settings, ShowLauncher afterFirstTimeSetup) { if (SettingsDirectory.findSettingsDirectory()) { if (afterFirstTimeSetup.showLauncher()) { ChunkyLauncherFx.withLauncher(settings, Stage::show); } } else { ChunkyLauncherFx.withLauncher(settings, stage -> { FirstTimeSetupDialog picker = new FirstTimeSetupDialog(() -> { if (afterFirstTimeSetup.showLauncher()) { stage.show(); } }); picker.show(); }); } } public static DownloadStatus tryDownload(File libDir, VersionInfo.Library lib, String theUrl) { try { URL url = new URL(theUrl); ReadableByteChannel inChannel = Channels.newChannel(url.openStream()); FileOutputStream out = new FileOutputStream(lib.getFile(libDir)); out.getChannel().transferFrom(inChannel, 0, Long.MAX_VALUE); out.close(); VersionInfo.LibraryStatus status = lib.testIntegrity(libDir); if (status == VersionInfo.LibraryStatus.PASSED) { return DownloadStatus.SUCCESS; } else { return DownloadStatus.DOWNLOAD_FAILED; } } catch (MalformedURLException e) { return DownloadStatus.MALFORMED_URL; } catch (FileNotFoundException e) { return DownloadStatus.FILE_NOT_FOUND; } catch (IOException e) { return DownloadStatus.DOWNLOAD_FAILED; } } public static String prettyPrintSize(int size) { // Pretty print library size. float fSize = size; String unit = "B"; if (size >= 1024 * 1024) { fSize /= 1024 * 1024; unit = "MiB"; } else if (size >= 1024) { fSize /= 1024; unit = "KiB"; } if (fSize >= 10) { return String.format("%d %s", (int) fSize, unit); } else { return String.format("%.1f %s", fSize, unit); } } }