使用 JNA 查询注册表 (#3913)

This commit is contained in:
Glavo 2025-05-14 22:18:02 +08:00 committed by GitHub
parent 76ed9353bd
commit 6e05b5ee58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 402 additions and 64 deletions

View File

@ -31,18 +31,17 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
@ -357,10 +356,10 @@ public final class JavaManager {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
@ -646,67 +645,22 @@ public final class JavaManager {
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.queryKeys(hkey, location)) {
if (!reg.queryKeys(hkey, java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
private static List<String> querySubFolders(String location) {
List<String> res = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query sub folders of " + location, e);
}
return res;
}
private static String queryRegisterValue(String location, String name) {
boolean last = false;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query register value of " + location, e);
}
return null;
}
}

View File

@ -19,4 +19,6 @@ dependencies {
api(libs.pci.ids)
compileOnlyApi(libs.jetbrains.annotations)
testImplementation(libs.jna.platform)
}

View File

@ -0,0 +1,59 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
/**
* @author Glavo
*/
public interface Advapi32 extends StdCallLibrary {
Advapi32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("advapi32", Advapi32.class)
: null;
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winreg/nf-winreg-regopenkeyexw">RegOpenKeyExW function</a>
*/
int RegOpenKeyExW(Pointer hKey, WString lpSubKey, int ulOptions, int samDesired, PointerByReference phkResult);
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winreg/nf-winreg-regclosekey">RegCloseKey function</a>
*/
int RegCloseKey(Pointer hKey);
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winreg/nf-winreg-regqueryvalueexw">RegQueryValueExW function</a>
*/
int RegQueryValueExW(Pointer hKey, WString lpValueName, Pointer lpReserved, IntByReference lpType, Pointer lpData, IntByReference lpcbData);
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winreg/nf-winreg-regenumkeyexw">RegEnumKeyExW function</a>
*/
int RegEnumKeyExW(Pointer hKey, int dwIndex,
Pointer lpName, IntByReference lpcchName,
IntByReference lpReserved,
Pointer lpClass, IntByReference lpcchClass,
Pointer lpftLastWriteTime);
}

View File

@ -22,9 +22,53 @@ package org.jackhuang.hmcl.util.platform.windows;
*/
public interface WinConstants {
// https://learn.microsoft.com/windows/win32/debug/system-error-codes--0-499-
int ERROR_SUCCESS = 0;
int ERROR_FILE_NOT_FOUND = 2;
int ERROR_PATH_NOT_FOUND = 3;
int ERROR_ACCESS_DENIED = 5;
int ERROR_INVALID_HANDLE = 6;
int ERROR_INVALID_DATA = 13;
int ERROR_NOT_SAME_DEVICE = 17;
int ERROR_NOT_READY = 21;
int ERROR_SHARING_VIOLATION = 32;
int ERROR_FILE_EXISTS = 80;
int ERROR_INVALID_PARAMETER = 87;
int ERROR_DISK_FULL = 112;
int ERROR_INSUFFICIENT_BUFFER = 122;
int ERROR_INVALID_LEVEL = 124;
int ERROR_DIR_NOT_ROOT = 144;
int ERROR_DIR_NOT_EMPTY = 145;
int ERROR_ALREADY_EXISTS = 183;
int ERROR_MORE_DATA = 234;
int ERROR_NO_MORE_ITEMS = 259;
int ERROR_DIRECTORY = 267;
int ERROR_NOTIFY_ENUM_DIR = 1022;
int ERROR_PRIVILEGE_NOT_HELD = 1314;
int ERROR_NONE_MAPPED = 1332;
int ERROR_CANT_ACCESS_FILE = 1920;
int ERROR_NOT_A_REPARSE_POINT = 4390;
int ERROR_INVALID_REPARSE_DATA = 4392;
// https://learn.microsoft.com/windows/win32/sysinfo/registry-key-security-and-access-rights
int KEY_QUERY_VALUE = 0x0001;
int KEY_ENUMERATE_SUB_KEYS = 0x0008;
int KEY_READ = 0x20019;
// https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types
int REG_NONE = 0;
int REG_SZ = 1;
int REG_EXPAND_SZ = 2;
int REG_BINARY = 3;
int REG_DWORD_LITTLE_ENDIAN = 4;
int REG_DWORD_BIG_ENDIAN = 5;
int REG_LINK = 6;
int REG_MULTI_SZ = 7;
int REG_RESOURCE_LIST = 8;
int REG_FULL_RESOURCE_DESCRIPTOR = 9;
int REG_RESOURCE_REQUIREMENTS_LIST = 10;
int REG_QWORD_LITTLE_ENDIAN = 11;
// https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys
long HKEY_CLASSES_ROOT = 0x80000000L;
long HKEY_CURRENT_USER = 0x80000001L;

View File

@ -17,11 +17,28 @@
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.WString;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public abstract class WinReg {
public static final WinReg INSTANCE = NativeUtils.USE_JNA && Advapi32.INSTANCE != null
? new JNAWinReg(Advapi32.INSTANCE) : null;
/**
* @see <a href="https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys">Predefined Keys</a>
*/
@ -36,7 +53,6 @@ public abstract class WinReg {
HKEY_CURRENT_CONFIG(0x80000005),
HKEY_DYN_DATA(0x80000006),
HKEY_CURRENT_USER_LOCAL_SETTINGS(0x80000007);
private final int value;
HKEY(int value) {
@ -46,5 +62,156 @@ public abstract class WinReg {
public int getValue() {
return value;
}
public Pointer toPointer() {
return Pointer.createConstant((long) value);
}
}
public abstract boolean exists(HKEY root, String key);
public abstract Object queryValue(HKEY root, String key, String valueName);
public abstract List<String> queryKeys(HKEY root, String key);
private static final class JNAWinReg extends WinReg {
private final Advapi32 advapi32;
JNAWinReg(Advapi32 advapi32) {
this.advapi32 = advapi32;
}
@Override
public boolean exists(HKEY root, String key) {
PointerByReference phkKey = new PointerByReference();
int status = advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey);
if (status == WinConstants.ERROR_SUCCESS) {
advapi32.RegCloseKey(phkKey.getValue());
return true;
} else
return false;
}
private static void checkLength(int expected, int actual) {
if (expected != actual) {
throw new IllegalStateException("Expected " + expected + " bytes, but got " + actual);
}
}
@Override
public Object queryValue(HKEY root, String key, String valueName) {
PointerByReference phkKey = new PointerByReference();
if (advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey) != WinConstants.ERROR_SUCCESS)
return null;
Pointer hkey = phkKey.getValue();
try {
IntByReference lpType = new IntByReference();
IntByReference lpcbData = new IntByReference();
int status = advapi32.RegQueryValueExW(hkey, new WString(valueName), null, lpType, null, lpcbData);
if (status != WinConstants.ERROR_SUCCESS) {
if (status == WinConstants.ERROR_FILE_NOT_FOUND)
return null;
else
throw new RuntimeException("Failed to query value: " + status);
}
int type = lpType.getValue();
int cbData = lpcbData.getValue();
try (Memory lpData = new Memory(cbData)) {
status = advapi32.RegQueryValueExW(hkey, new WString(valueName), null, null, lpData, lpcbData);
if (status != WinConstants.ERROR_SUCCESS) {
if (status == WinConstants.ERROR_FILE_NOT_FOUND)
return null;
else
throw new RuntimeException("Failed to query value: " + status);
}
checkLength(cbData, lpcbData.getValue());
switch (type) {
case WinConstants.REG_NONE:
case WinConstants.REG_BINARY:
return lpData.getByteArray(0L, cbData);
case WinConstants.REG_DWORD_LITTLE_ENDIAN:
case WinConstants.REG_DWORD_BIG_ENDIAN: {
checkLength(4, cbData);
int value = lpData.getInt(0L);
ByteOrder expectedOrder = type == WinConstants.REG_DWORD_LITTLE_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
if (expectedOrder != ByteOrder.nativeOrder())
value = Integer.reverseBytes(value);
return value;
}
case WinConstants.REG_QWORD_LITTLE_ENDIAN: {
checkLength(8, cbData);
long value = lpData.getLong(0L);
if (ByteOrder.nativeOrder() != ByteOrder.LITTLE_ENDIAN)
value = Long.reverseBytes(value);
return value;
}
case WinConstants.REG_SZ:
case WinConstants.REG_EXPAND_SZ:
case WinConstants.REG_LINK: {
if (cbData < 2)
throw new RuntimeException("Illegal length: " + cbData);
if (lpData.getChar(cbData - 2) != '\0')
throw new RuntimeException("The string does not end with \\0");
return lpData.getWideString(0L);
}
default:
throw new RuntimeException("Unknown reg type: " + type);
}
}
} catch (Throwable e) {
LOG.warning("Failed to query value", e);
} finally {
advapi32.RegCloseKey(hkey);
}
return null;
}
@Override
public List<String> queryKeys(HKEY root, String key) {
PointerByReference phkKey = new PointerByReference();
if (advapi32.RegOpenKeyExW(root.toPointer(), new WString(key), 0, WinConstants.KEY_READ, phkKey) != WinConstants.ERROR_SUCCESS)
return Collections.emptyList();
Pointer hkey = phkKey.getValue();
try {
String prefix = key.endsWith("\\") ? key : key + "\\";
ArrayList<String> res = new ArrayList<>();
int maxKeyLength = 256;
try (Memory lpName = new Memory(maxKeyLength * 2)) {
IntByReference lpcchName = new IntByReference();
int i = 0;
while (true) {
lpcchName.setValue(maxKeyLength);
int status = advapi32.RegEnumKeyExW(hkey, i, lpName, lpcchName, null, null, null, null);
if (status == WinConstants.ERROR_SUCCESS) {
res.add(prefix + lpName.getWideString(0L));
i++;
} else {
if (status != WinConstants.ERROR_NO_MORE_ITEMS)
LOG.warning("Failed to enum key: " + status);
break;
}
}
}
return res;
} catch (Throwable e) {
LOG.warning("Failed to query keys", e);
} finally {
advapi32.RegCloseKey(hkey);
}
return Collections.emptyList();
}
}
}

View File

@ -87,6 +87,4 @@ public interface WinTypes {
"ullTotalVirtual", "ullAvailVirtual", "ullAvailExtendedVirtual");
}
}
;
}

View File

@ -0,0 +1,113 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.platform.win32.Advapi32Util;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Glavo
*/
@EnabledIf("isEnabled")
public final class WinRegTest {
public static boolean isEnabled() {
return WinReg.INSTANCE != null;
}
private static final com.sun.jna.platform.win32.WinReg.HKEY ROOT_KEY = com.sun.jna.platform.win32.WinReg.HKEY_CURRENT_USER;
private static final String KEY_BASE = "Software\\JavaSoft\\Prefs\\hmcl\\test";
private static String key;
private static final String[] SUBKEYS = {
"Sub0", "Sub1", "Sub2", "Sub3"
};
private static final byte[] TEST_DATA = new byte[128];
static {
for (int i = 0; i < TEST_DATA.length; i++) {
TEST_DATA[i] = (byte) i;
}
}
@BeforeAll
public static void setup() {
key = KEY_BASE + "\\" + UUID.randomUUID();
if (!Advapi32Util.registryCreateKey(ROOT_KEY, key))
throw new AssertionError("Failed to create key");
Advapi32Util.registrySetBinaryValue(ROOT_KEY, key, "BINARY", TEST_DATA);
Advapi32Util.registrySetStringValue(ROOT_KEY, key, "SZ", "Hello World!");
Advapi32Util.registrySetIntValue(ROOT_KEY, key, "DWORD", 0xCAFEBABE);
Advapi32Util.registrySetLongValue(ROOT_KEY, key, "QWORD", 0xCAFEBABEL);
for (String subkey : SUBKEYS) {
if (!Advapi32Util.registryCreateKey(ROOT_KEY, key, subkey))
throw new AssertionError("Failed to create key");
}
}
@AfterAll
public static void cleanUp() {
if (key != null) {
if (!Advapi32Util.registryKeyExists(ROOT_KEY, key))
return;
for (String subKey : Advapi32Util.registryGetKeys(ROOT_KEY, key))
Advapi32Util.registryDeleteKey(ROOT_KEY, key, subKey);
Advapi32Util.registryDeleteKey(ROOT_KEY, key);
}
}
@Test
public void testQueryValue() {
WinReg.HKEY hkey = WinReg.HKEY.HKEY_CURRENT_USER;
WinReg reg = WinReg.INSTANCE;
assertArrayEquals(TEST_DATA, (byte[]) reg.queryValue(hkey, key, "BINARY"));
assertEquals("Hello World!", reg.queryValue(hkey, key, "SZ"));
assertEquals(0xCAFEBABE, reg.queryValue(hkey, key, "DWORD"));
assertEquals(0xCAFEBABEL, reg.queryValue(hkey, key, "QWORD"));
assertNull(reg.queryValue(hkey, key, "UNKNOWN"));
assertNull(reg.queryValue(hkey, KEY_BASE + "\\" + "NOT_EXIST", "UNKNOWN"));
}
@Test
public void testQueryKeys() {
WinReg.HKEY hkey = WinReg.HKEY.HKEY_CURRENT_USER;
WinReg reg = WinReg.INSTANCE;
assertEquals(Arrays.asList(SUBKEYS).stream().map(it -> key + "\\" + it).collect(Collectors.toList()),
reg.queryKeys(hkey, key).stream().sorted().collect(Collectors.toList()));
for (String subkey : SUBKEYS) {
assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\" + subkey));
}
assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\NOT_EXIST"));
}
}

View File

@ -34,6 +34,7 @@ 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" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
[plugins]