diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 7c8f0f341..d9a02ede9 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -116,8 +116,14 @@ tasks.shadowJar { exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi") exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi") + listOf( + "aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*", + "*-ppc", "*-ppc64le", "*-s390x", "*-armel", + ).forEach { exclude("com/sun/jna/$it/**") } + minimize { exclude(dependency("com.google.code.gson:.*:.*")) + exclude(dependency("net.java.dev.jna:jna:.*")) exclude(dependency("libs:JFoenix:.*")) } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 30b4fb7ce..59e016a23 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -38,6 +38,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.CommandBuilder; +import org.jackhuang.hmcl.util.platform.NativeUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.io.File; @@ -246,12 +247,13 @@ public final class Launcher extends Application { LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found")); LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory")); LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB"); - LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB"); + LOG.info("Physical Memory: " + OperatingSystem.TOTAL_MEMORY + " MB"); LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream() .filter(bean -> bean.getName().equals("Metaspace")) .findAny() .map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB") .orElse("Unknown")); + LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None")); if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) { LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE")); LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP")); diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 6b009e936..098ee5bd0 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -15,5 +15,7 @@ dependencies { api(libs.nanohttpd) api(libs.jsoup) api(libs.chardet) + api(libs.jna) + compileOnlyApi(libs.jetbrains.annotations) } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java new file mode 100644 index 000000000..ede508155 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/NativeUtils.java @@ -0,0 +1,85 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Map; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class NativeUtils { + public static final boolean USE_JNA = useJNA(); + + public static @Nullable T load(String name, Class interfaceClass) { + return load(name, interfaceClass, Collections.emptyMap()); + } + + public static @Nullable T load(String name, Class interfaceClass, Map options) { + if (USE_JNA) { + try { + return Native.load(name, interfaceClass, options); + } catch (UnsatisfiedLinkError e) { + LOG.warning("Failed to load native library: " + name, e); + } + } + + return null; + } + + private static boolean useJNA() { + String backend = System.getProperty("hmcl.native.backend"); + if (backend == null || "auto".equalsIgnoreCase(backend)) { + try { + if (Platform.isWindows()) { + String osVersion = System.getProperty("os.version"); + + // Requires Windows 7 or later (6.1+) + // https://learn.microsoft.com/windows/win32/sysinfo/operating-system-version + if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0")) + return false; + + // Currently we only need to use JNA on Windows + Native.getDefaultStringEncoding(); + return true; + } + + return false; + } catch (Throwable ignored) { + return false; + } + } else if ("jna".equalsIgnoreCase(backend)) { + // Ensure JNA is available + Native.getDefaultStringEncoding(); + return true; + } else if ("none".equalsIgnoreCase(backend)) + return false; + else + throw new Error("Unsupported native backend: " + backend); + } + + private NativeUtils() { + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java index 334500bd1..94efbbcc4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java @@ -18,6 +18,8 @@ package org.jackhuang.hmcl.util.platform; import org.jackhuang.hmcl.util.KeyValuePairProperties; +import org.jackhuang.hmcl.util.platform.windows.Kernel32; +import org.jackhuang.hmcl.util.platform.windows.WinTypes; import java.io.BufferedReader; import java.io.File; @@ -29,10 +31,7 @@ import java.nio.charset.UnsupportedCharsetException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -159,47 +158,68 @@ public enum OperatingSystem { if (CURRENT_OS == WINDOWS) { String versionNumber = null; int buildNumber = -1; + int codePage = -1; - try { - Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"}); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) { - Matcher matcher = Pattern.compile("(?[0-9]+\\.[0-9]+\\.(?[0-9]+)(\\.[0-9]+)?)]$") - .matcher(reader.readLine().trim()); + Kernel32 kernel32 = Kernel32.INSTANCE; - if (matcher.find()) { - versionNumber = matcher.group("version"); - buildNumber = Integer.parseInt(matcher.group("build")); - } - } - process.destroy(); - } catch (Throwable ignored) { + // Get Windows version number + if (kernel32 != null) { + WinTypes.OSVERSIONINFOEXW osVersionInfo = new WinTypes.OSVERSIONINFOEXW(); + if (kernel32.GetVersionExW(osVersionInfo)) { + int majorVersion = osVersionInfo.dwMajorVersion; + int minorVersion = osVersionInfo.dwMinorVersion; + + buildNumber = osVersionInfo.dwBuildNumber; + versionNumber = majorVersion + "." + minorVersion + "." + buildNumber; + } else + System.err.println("Failed to obtain OS version number (" + kernel32.GetLastError() + ")"); } if (versionNumber == null) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) { + Matcher matcher = Pattern.compile("(?[0-9]+\\.[0-9]+\\.(?[0-9]+)(\\.[0-9]+)?)]$") + .matcher(reader.readLine().trim()); + + if (matcher.find()) { + versionNumber = matcher.group("version"); + buildNumber = Integer.parseInt(matcher.group("build")); + } + } + process.destroy(); + } catch (Throwable ignored) { + } + } + + if (versionNumber == null) versionNumber = System.getProperty("os.version"); + + // Get Code Page + + if (kernel32 != null) + codePage = kernel32.GetACP(); + else { + try { + Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) { + Matcher matcher = Pattern.compile("(?[0-9]+)$") + .matcher(reader.readLine().trim()); + + if (matcher.find()) { + codePage = Integer.parseInt(matcher.group("cp")); + } + } + process.destroy(); + } catch (Throwable ignored) { + } } String osName = System.getProperty("os.name"); // Java 17 or earlier recognizes Windows 11 as Windows 10 - if (osName.equals("Windows 10") && buildNumber >= 22000) { + if (osName.equals("Windows 10") && buildNumber >= 22000) osName = "Windows 11"; - } - - int codePage = -1; - try { - Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"}); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) { - Matcher matcher = Pattern.compile("(?[0-9]+)$") - .matcher(reader.readLine().trim()); - - if (matcher.find()) { - codePage = Integer.parseInt(matcher.group("cp")); - } - } - process.destroy(); - } catch (Throwable ignored) { - } SYSTEM_NAME = osName; SYSTEM_VERSION = versionNumber; @@ -443,4 +463,5 @@ public enum OperatingSystem { public static final PhysicalMemoryStatus INVALID = new PhysicalMemoryStatus(0, -1); } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Kernel32.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Kernel32.java new file mode 100644 index 000000000..f179155bf --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Kernel32.java @@ -0,0 +1,47 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.win32.StdCallLibrary; +import org.jackhuang.hmcl.util.platform.NativeUtils; + +/** + * @author Glavo + */ +public interface Kernel32 extends StdCallLibrary { + + Kernel32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows() + ? NativeUtils.load("kernel32", Kernel32.class) + : null; + + /** + * @see GetLastError function + */ + int GetLastError(); + + /** + * @see GetVersionExW function + */ + boolean GetVersionExW(WinTypes.OSVERSIONINFOEXW lpVersionInfo); + + /** + * @see GetACP function + */ + int GetACP(); + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java new file mode 100644 index 000000000..0bd878bb4 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java @@ -0,0 +1,39 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +/** + * @author Glavo + */ +public interface WinConstants { + + // https://learn.microsoft.com/windows/win32/sysinfo/registry-key-security-and-access-rights + int KEY_READ = 0x20019; + + // https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys + long HKEY_CLASSES_ROOT = 0x80000000L; + long HKEY_CURRENT_USER = 0x80000001L; + long HKEY_LOCAL_MACHINE = 0x80000002L; + long HKEY_USERS = 0x80000003L; + long HKEY_PERFORMANCE_DATA = 0x80000004L; + long HKEY_PERFORMANCE_TEXT = 0x80000050L; + long HKEY_PERFORMANCE_NLSTEXT = 0x80000060L; + long HKEY_CURRENT_CONFIG = 0x80000005L; + long HKEY_DYN_DATA = 0x80000006L; + long HKEY_CURRENT_USER_LOCAL_SETTINGS = 0x80000007L; +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java new file mode 100644 index 000000000..53177a797 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinTypes.java @@ -0,0 +1,63 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.*; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Glavo + */ +public interface WinTypes { + /** + * @see OSVERSIONINFOEXW structure + */ + final class OSVERSIONINFOEXW extends Structure { + public int dwOSVersionInfoSize; + public int dwMajorVersion; + public int dwMinorVersion; + public int dwBuildNumber; + public int dwPlatformId; + public char[] szCSDVersion; + public short wServicePackMajor; + public short wServicePackMinor; + public short wSuiteMask; + public byte wProductType; + public byte wReserved; + + public OSVERSIONINFOEXW() { + szCSDVersion = new char[128]; + dwOSVersionInfoSize = size(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList( + "dwOSVersionInfoSize", + "dwMajorVersion", "dwMinorVersion", "dwBuildNumber", + "dwPlatformId", + "szCSDVersion", + "wServicePackMajor", "wServicePackMinor", + "wSuiteMask", "wProductType", + "wReserved" + ); + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ee0e9de3..7ff9ac52a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ nanohttpd = "2.3.1" jsoup = "1.19.1" chardet = "2.5.0" twelvemonkeys = "3.12.0" +jna = "5.17.0" # plugins shadow = "8.3.6" @@ -31,6 +32,7 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } chardet = { module = "org.glavo:chardet", version.ref = "chardet" } twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" } +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } [plugins] shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }