/* * This file is part of the Illarion project. * * Copyright © 2015 - Illarion e.V. * * Illarion is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Illarion 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. */ package illarion.download.launcher; import illarion.common.config.Config; import illarion.common.util.DirectoryManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.*; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * The use of this class is to start a independent JVM that runs the chosen application. This class requires calls * that are system dependent. * * @author Martin Karing */ public final class JavaLauncher { /** * This instance of the logger takes care for the logging output of this class. */ @Nonnull private static final Logger log = LoggerFactory.getLogger(JavaLauncher.class); /** * This text contains the error data in case the launch failed. */ @Nullable private String errorData; private final boolean snapshot; @Nonnull private final Config cfg; /** * Construct a new launcher and set the classpath and the class to launch. */ public JavaLauncher(@Nonnull Config cfg, boolean snapshot) { this.snapshot = snapshot; this.cfg = cfg; } /** * Calling this function causes the selected application to launch. * * @return {@code true} in case launching the application was successful */ public boolean launch(@Nonnull Collection<File> classpath, @Nonnull String startupClass) { String classPathString = buildClassPathString(classpath); Iterable<Path> executablePaths; executablePaths = OSDetection.isMacOSX() ? new MacOsXJavaExecutableIterable() : new JavaExecutableIterable(); for (Path executable : executablePaths) { if (isJavaExecutableWorking(executable)) { List<String> callList = new ArrayList<>(); callList.add(escapePath(executable.toString())); callList.add("-classpath"); callList.add(classPathString); if (snapshot) { callList.add("-Dillarion.server=devserver"); } if (cfg.getBoolean("launchAggressive")) { callList.add("-XX:+AggressiveOpts"); } callList.add(startupClass); printCallList(callList); if (launchCallList(callList)) { return true; } else { log.error("Error while launching application: {}", errorData); } } } return false; } /** * This function is used to check if the java executable has the proper version. * * @param executable the path to the executable * @return {@code true} in case java meets the required specifications */ private static boolean isJavaExecutableWorking(@Nonnull Path executable) { try { ProcessBuilder processBuilder = new ProcessBuilder(executable.toString(), "-version"); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { Pattern versionRegex = Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)_(\\d+)"); Optional<Matcher> versionMatcher = reader.lines() .filter(s -> s.startsWith("java version")) .map(versionRegex::matcher) .filter(Matcher::find) .findFirst(); if (versionMatcher.isPresent()) { Matcher matcher = versionMatcher.get(); int mainVersion = Integer.parseInt(matcher.group(1)); int majorVersion = Integer.parseInt(matcher.group(2)); int minorVersion = Integer.parseInt(matcher.group(3)); int buildNumber = Integer.parseInt(matcher.group(4)); log.info("Matched Java version to {}.{}.{}_b{}", mainVersion, majorVersion, minorVersion, buildNumber); return (mainVersion >= 1) && (majorVersion >= 8) && (buildNumber >= 0); } } finally { process.destroy(); } } catch (IOException e) { log.error("Launching {} failed.", executable); } return false; } /** * Build the class path string that contain a list of files pointing to each file needed to include to this * application. * * @return the string that represents the class path */ @Nonnull private static String buildClassPathString(@Nonnull Collection<File> classpath) { if (classpath.isEmpty()) { return ""; } String cp = classpath.stream().map(File::getAbsolutePath).collect(Collectors.joining(File.pathSeparator)); return (cp == null) ? "" : escapePath(cp); } /** * This small utility function takes care for escaping a path. This operation is platform dependent so the result * will differ on different platforms. * * @param orgPath the original plain path * @return the escaped path */ @Nonnull private static String escapePath(@Nonnull String orgPath) { if (OSDetection.isWindows()) { if (orgPath.contains(" ")) { return '"' + orgPath + '"'; } return orgPath; } //noinspection DynamicRegexReplaceableByCompiledPattern return orgPath.replace(" ", "\\ "); } /** * Print the call list to the logger. * * @param callList the call list to print */ private static void printCallList(@Nonnull Collection<String> callList) { if (log.isDebugEnabled()) { String prefix = "Calling: " + System.getProperty("line.separator"); log.debug(callList.stream().collect(Collectors.joining(" ", prefix, ""))); } } /** * Launch the specified call list. * * @param callList launch the call list * @return {@code true} in case the launch was successful */ private boolean launchCallList(@Nonnull List<String> callList) { try { ProcessBuilder pBuilder = new ProcessBuilder(callList); Path workingDirectory = DirectoryManager.getInstance().getWorkingDirectory(); pBuilder.directory(workingDirectory.toFile()); pBuilder.redirectErrorStream(true); Process proc = pBuilder.start(); //noinspection EmptyTryBlock try (OutputStream ignored = proc.getOutputStream()) { } StringBuilder outputBuffer = new StringBuilder(); try (final BufferedReader outputReader = new BufferedReader( new InputStreamReader(proc.getInputStream(), Charset.defaultCharset()))) { TimerTask timeoutTask = new TimerTask() { @Override public void run() { try { outputReader.close(); } catch (IOException ignored) { // nothing to do } } }; new Timer("Startup Timeout Timer", true).schedule(timeoutTask, 10000); while (true) { String line = outputReader.readLine(); if (line == null) { errorData = outputBuffer.toString().trim(); return false; } if (line.endsWith("Startup done.")) { timeoutTask.cancel(); outputReader.close(); return true; } outputBuffer.append(line); outputBuffer.append('\n'); } } } catch (@Nonnull Exception e) { StringWriter sWriter = new StringWriter(); PrintWriter writer = new PrintWriter(sWriter); e.printStackTrace(writer); writer.flush(); errorData = sWriter.toString(); return false; } } /** * Get the information about the launch error. * * @return the string containing the data about the crash */ @Nullable public String getErrorData() { return errorData; } }