From 2ff147fc10ac14caf145536b45e16d305b33bf38 Mon Sep 17 00:00:00 2001 From: Henry Le Grys Date: Tue, 15 Jun 2021 04:44:52 +0100 Subject: [PATCH] Start implementing per-instance settings & better Java runtime selection --- .../com/skcraft/launcher/Configuration.java | 11 +- .../java/com/skcraft/launcher/Instance.java | 1 + .../skcraft/launcher/InstanceSettings.java | 30 ++++ .../skcraft/launcher/launch/JavaRuntime.java | 79 +++++++++ .../launcher/launch/JavaRuntimeFinder.java | 160 ++++++++++-------- .../launcher/launch/MemorySettings.java | 19 +++ .../com/skcraft/launcher/launch/Runner.java | 30 +++- .../launcher/model/minecraft/JavaVersion.java | 11 ++ .../model/minecraft/VersionManifest.java | 1 + .../launcher/util/EnvironmentParser.java | 27 +++ 10 files changed, 293 insertions(+), 76 deletions(-) create mode 100644 launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java create mode 100644 launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java create mode 100644 launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java create mode 100644 launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java create mode 100644 launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java diff --git a/launcher/src/main/java/com/skcraft/launcher/Configuration.java b/launcher/src/main/java/com/skcraft/launcher/Configuration.java index 50d996f..5d47891 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Configuration.java +++ b/launcher/src/main/java/com/skcraft/launcher/Configuration.java @@ -7,6 +7,8 @@ package com.skcraft.launcher; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.JavaRuntimeFinder; import lombok.Data; /** @@ -22,7 +24,7 @@ import lombok.Data; public class Configuration { private boolean offlineEnabled = false; - private String jvmPath; + private JavaRuntime javaRuntime; private String jvmArgs; private int minMemory = 1024; private int maxMemory = 0; // Updated in Launcher @@ -55,4 +57,11 @@ public class Configuration { public void setWidowHeight(int height) { this.windowHeight = height; } + + /** + * Backwards compatibility for old configs with jvmPaths + */ + public void setJvmPath(String jvmPath) { + this.javaRuntime = JavaRuntimeFinder.getRuntimeFromPath(jvmPath); + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/Instance.java b/launcher/src/main/java/com/skcraft/launcher/Instance.java index 3492e62..dd63900 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Instance.java +++ b/launcher/src/main/java/com/skcraft/launcher/Instance.java @@ -33,6 +33,7 @@ public class Instance implements Comparable { private Date lastAccessed; @JsonProperty("launch") private LaunchModifier launchModifier; + private InstanceSettings settings = new InstanceSettings(); @JsonIgnore private File dir; @JsonIgnore private URL manifestURL; diff --git a/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java b/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java new file mode 100644 index 0000000..08cf92d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java @@ -0,0 +1,30 @@ +package com.skcraft.launcher; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.MemorySettings; +import lombok.Data; + +import java.util.Optional; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class InstanceSettings { + private JavaRuntime runtime; + private MemorySettings memorySettings; + private String customJvmArgs; + + /** + * @return Empty optional if there is no custom runtime set, present optional if there is. + */ + public Optional getRuntime() { + return Optional.ofNullable(runtime); + } + + /** + * @return Empty optional if there are no custom memory settings, present optional if there are. + */ + public Optional getMemorySettings() { + return Optional.ofNullable(memorySettings); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java new file mode 100644 index 0000000..fd60404 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java @@ -0,0 +1,79 @@ +package com.skcraft.launcher.launch; + +import lombok.Data; + +import java.io.File; + +@Data +public class JavaRuntime implements Comparable { + private final File dir; + private final String version; + private final boolean is64Bit; + private boolean isMinecraftBundled = false; + + public int getMajorVersion() { + String[] parts = version.split("\\."); + + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid Java runtime version: " + version); + } + + if (parts[0].equals("1")) { + return Integer.parseInt(parts[1]); + } else { + return Integer.parseInt(parts[0]); + } + } + + @Override + public int compareTo(JavaRuntime o) { + if (isMinecraftBundled && !o.isMinecraftBundled) { + return -1; + } else if (!isMinecraftBundled && o.isMinecraftBundled) { + return 1; + } + + if (is64Bit && !o.is64Bit) { + return -1; + } else if (!is64Bit && o.is64Bit) { + return 1; + } + + String[] a = version.split("[\\._]"); + String[] b = o.version.split("[\\._]"); + int min = Math.min(a.length, b.length); + + for (int i = 0; i < min; i++) { + int first, second; + + try { + first = Integer.parseInt(a[i]); + } catch (NumberFormatException e) { + return -1; + } + + try { + second = Integer.parseInt(b[i]); + } catch (NumberFormatException e) { + return 1; + } + + if (first > second) { + return -1; + } else if (first < second) { + return 1; + } + } + + if (a.length == b.length) { + return 0; // Same + } + + return a.length > b.length ? -1 : 1; + } + + @Override + public String toString() { + return String.format("Java %s (%s) (%s)", version, is64Bit ? "64-bit" : "32-bit", dir); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java index 5bf1cb3..3aadfee 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java @@ -6,16 +6,16 @@ package com.skcraft.launcher.launch; +import com.skcraft.launcher.model.minecraft.JavaVersion; import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.EnvironmentParser; import com.skcraft.launcher.util.Platform; import com.skcraft.launcher.util.WinRegistry; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * Finds the best Java runtime to use. @@ -25,52 +25,110 @@ public final class JavaRuntimeFinder { private JavaRuntimeFinder() { } + public static List getAvailableRuntimes() { + Environment env = Environment.getInstance(); + List entries = new ArrayList<>(); + File launcherDir; + + if (env.getPlatform() == Platform.WINDOWS) { + try { + String launcherPath = WinRegistry.readString(WinRegistry.HKEY_CURRENT_USER, + "SOFTWARE\\Mojang\\InstalledProducts\\Minecraft Launcher", "InstallLocation"); + + launcherDir = new File(launcherPath); + } catch (Throwable ignored) { + launcherDir = new File(System.getenv("APPDATA"), ".minecraft"); + } + + try { + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); + } catch (Throwable ignored) { + } + Collections.sort(entries); + } else if (env.getPlatform() == Platform.LINUX) { + launcherDir = new File(System.getenv("HOME"), ".minecraft"); + } else { + return Collections.emptyList(); + } + + if (!launcherDir.isDirectory()) { + return entries; + } + + File runtimes = new File(launcherDir, "runtime"); + for (File potential : Objects.requireNonNull(runtimes.listFiles())) { + if (potential.getName().startsWith("jre-x")) { + boolean is64Bit = potential.getName().equals("jre-x64"); + + entries.add(new JavaRuntime(potential.getAbsoluteFile(), readVersionFromRelease(potential), is64Bit)); + } else { + String runtimeName = potential.getName(); + + String[] children = potential.list(); + if (children == null || children.length == 0) continue; + String platformName = children[0]; + + String[] parts = platformName.split("-"); + if (parts.length < 2) continue; + + String arch = parts[1]; + boolean is64Bit = arch.equals("x64"); + + File javaDir = new File(potential, String.format("%s/%s", platformName, runtimeName)); + + entries.add(new JavaRuntime(javaDir.getAbsoluteFile(), readVersionFromRelease(javaDir), is64Bit)); + } + } + + return entries; + } + /** * Return the path to the best found JVM location. * * @return the JVM location, or null */ public static File findBestJavaPath() { - if (Environment.getInstance().getPlatform() != Platform.WINDOWS) { - return null; - } - - List entries = new ArrayList(); - try { - getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); - getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); - } catch (Throwable ignored) { - } - Collections.sort(entries); - + List entries = getAvailableRuntimes(); if (entries.size() > 0) { - return new File(entries.get(0).dir, "bin"); + return new File(entries.get(0).getDir(), "bin"); } return null; } + + public static Optional findBestJavaRuntime(JavaVersion targetVersion) { + List entries = getAvailableRuntimes(); + + return entries.stream().sorted() + .filter(runtime -> runtime.getMajorVersion() == targetVersion.getMajorVersion()) + .findFirst(); + } + + public static JavaRuntime getRuntimeFromPath(String path) { + File target = new File(path); + + return new JavaRuntime(target, readVersionFromRelease(target), guessIf64Bit(target)); + } - private static void getEntriesFromRegistry(List entries, String basePath) + private static void getEntriesFromRegistry(List entries, String basePath) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { List subKeys = WinRegistry.readStringSubKeys(WinRegistry.HKEY_LOCAL_MACHINE, basePath); for (String subKey : subKeys) { - JREEntry entry = getEntryFromRegistry(basePath, subKey); + JavaRuntime entry = getEntryFromRegistry(basePath, subKey); if (entry != null) { entries.add(entry); } } } - private static JREEntry getEntryFromRegistry(String basePath, String version) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + private static JavaRuntime getEntryFromRegistry(String basePath, String version) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { String regPath = basePath + "\\" + version; String path = WinRegistry.readString(WinRegistry.HKEY_LOCAL_MACHINE, regPath, "JavaHome"); File dir = new File(path); if (dir.exists() && new File(dir, "bin/java.exe").exists()) { - JREEntry entry = new JREEntry(); - entry.dir = dir; - entry.version = version; - entry.is64Bit = guessIf64Bit(dir); - return entry; + return new JavaRuntime(dir, version, guessIf64Bit(dir)); } else { return null; } @@ -84,52 +142,18 @@ public final class JavaRuntimeFinder { return false; } } - - private static class JREEntry implements Comparable { - private File dir; - private String version; - private boolean is64Bit; - @Override - public int compareTo(JREEntry o) { - if (is64Bit && !o.is64Bit) { - return -1; - } else if (!is64Bit && o.is64Bit) { - return 1; + private static String readVersionFromRelease(File javaPath) { + File releaseFile = new File(javaPath, "release"); + if (releaseFile.exists()) { + try { + Map releaseDetails = EnvironmentParser.parse(releaseFile); + + return releaseDetails.get("JAVA_VERSION"); + } catch (IOException ignored) { } - - String[] a = version.split("[\\._]"); - String[] b = o.version.split("[\\._]"); - int min = Math.min(a.length, b.length); - - for (int i = 0; i < min; i++) { - int first, second; - - try { - first = Integer.parseInt(a[i]); - } catch (NumberFormatException e) { - return -1; - } - - try { - second = Integer.parseInt(b[i]); - } catch (NumberFormatException e) { - return 1; - } - - if (first > second) { - return -1; - } else if (first < second) { - return 1; - } - } - - if (a.length == b.length) { - return 0; // Same - } - - return a.length > b.length ? -1 : 1; } - } + return null; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java b/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java new file mode 100644 index 0000000..eed8b64 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java @@ -0,0 +1,19 @@ +package com.skcraft.launcher.launch; + +import lombok.Data; + +/** + * Settings for launched process memory allocation. + */ +@Data +public class MemorySettings { + /** + * Minimum memory in megabytes. + */ + private int minMemory; + + /** + * Maximum memory in megabytes. + */ + private int maxMemory; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java index b32314a..7eebe22 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; @@ -212,8 +213,14 @@ public class Runner implements Callable, ProgressObservable { * @throws IOException on I/O error */ private void addJvmArgs() throws IOException { - int minMemory = config.getMinMemory(); - int maxMemory = config.getMaxMemory(); + int minMemory = instance.getSettings().getMemorySettings() + .map(MemorySettings::getMinMemory) + .orElse(config.getMinMemory()); + + int maxMemory = instance.getSettings().getMemorySettings() + .map(MemorySettings::getMaxMemory) + .orElse(config.getMaxMemory()); + int permGen = config.getPermGen(); if (minMemory <= 0) { @@ -240,16 +247,25 @@ public class Runner implements Callable, ProgressObservable { builder.setMaxMemory(maxMemory); builder.setPermGen(permGen); - String rawJvmPath = config.getJvmPath(); + JavaRuntime selectedRuntime = instance.getSettings().getRuntime() + .orElseGet(() -> Optional.ofNullable(versionManifest.getJavaVersion()) + .flatMap(JavaRuntimeFinder::findBestJavaRuntime) + .orElse(config.getJavaRuntime()) + ); + String rawJvmPath = selectedRuntime.getDir().getAbsolutePath(); if (!Strings.isNullOrEmpty(rawJvmPath)) { builder.tryJvmPath(new File(rawJvmPath)); } List flags = builder.getFlags(); - String rawJvmArgs = config.getJvmArgs(); - if (!Strings.isNullOrEmpty(rawJvmArgs)) { - for (String arg : JavaProcessBuilder.splitArgs(rawJvmArgs)) { - flags.add(arg); + String[] rawJvmArgsList = new String[] { + config.getJvmArgs(), + instance.getSettings().getCustomJvmArgs() + }; + + for (String rawJvmArgs : rawJvmArgsList) { + if (!Strings.isNullOrEmpty(rawJvmArgs)) { + flags.addAll(JavaProcessBuilder.splitArgs(rawJvmArgs)); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java new file mode 100644 index 0000000..9cd2674 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java @@ -0,0 +1,11 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class JavaVersion { + private String component; + private int majorVersion; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java index d0fad1a..f270ca4 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -27,6 +27,7 @@ public class VersionManifest { private String mainClass; private int minimumLauncherVersion; private LinkedHashSet libraries; + private JavaVersion javaVersion; private Map downloads = new HashMap(); public String getAssetId() { diff --git a/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java b/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java new file mode 100644 index 0000000..02302bf --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java @@ -0,0 +1,27 @@ +package com.skcraft.launcher.util; + +import com.google.common.base.Splitter; +import com.google.common.io.CharSource; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * Parses dotenv-style files. + */ +public class EnvironmentParser { + public static Map parse(File target) throws IOException { + CharSource charSource = Files.asCharSource(target, StandardCharsets.UTF_8); + Map values = Splitter.onPattern("\r?\n").withKeyValueSeparator('=') + .split(charSource.read()); + + // Remove quotes + // TODO do this better. it works fine for the release file, though + values.replaceAll((key, value) -> value.substring(1, value.length() - 1)); + + return values; + } +}