启动时在日志中记录 CPU 信息 (#3914)

This commit is contained in:
Glavo
2025-05-16 14:50:43 +08:00
committed by GitHub
parent 6e05b5ee58
commit 973b7a9716
25 changed files with 1271 additions and 299 deletions

View File

@@ -650,8 +650,8 @@ public final class JavaManager {
if (reg == null)
return;
for (String java : reg.queryKeys(hkey, location)) {
if (!reg.queryKeys(hkey, java).contains(java + "\\MSI"))
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {

View File

@@ -83,7 +83,6 @@ import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -411,22 +410,6 @@ public final class FXUtils {
});
}
private static String which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath().toString();
} catch (Throwable ignored) {
}
}
return null;
}
public static void showFileInExplorer(Path file) {
String path = file.toAbsolutePath().toString();
@@ -435,7 +418,7 @@ public final class FXUtils {
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && SystemUtils.which("dbus-send") != null)
openCommands = new String[]{
"dbus-send",
"--print-reply",
@@ -501,10 +484,10 @@ public final class FXUtils {
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
for (String browser : linuxBrowsers) {
String path = which(browser);
Path path = SystemUtils.which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{browser, link});
Runtime.getRuntime().exec(new String[]{path.toString(), link});
return;
} catch (Throwable ignored) {
}

View File

@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.java;
import kala.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.util.KeyValuePairProperties;
import org.jackhuang.hmcl.util.KeyValuePairUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -33,6 +33,7 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
/**
* @author Glavo
@@ -60,7 +61,7 @@ public final class JavaInfo {
}
public static JavaInfo fromReleaseFile(BufferedReader reader) throws IOException {
KeyValuePairProperties properties = KeyValuePairProperties.load(reader);
Map<String, String> properties = KeyValuePairUtils.loadProperties(reader);
String osName = properties.get("OS_NAME");
String osArch = properties.get("OS_ARCH");
String vendor = properties.get("IMPLEMENTOR");

View File

@@ -19,25 +19,34 @@ package org.jackhuang.hmcl.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.*;
/**
* @author Glavo
*/
public final class KeyValuePairProperties extends LinkedHashMap<String, String> {
public static KeyValuePairProperties load(Path file) throws IOException {
public final class KeyValuePairUtils {
public static Map<String, String> loadProperties(Path file) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return load(reader);
return loadProperties(reader);
}
}
public static KeyValuePairProperties load(BufferedReader reader) throws IOException {
KeyValuePairProperties result = new KeyValuePairProperties();
public static Map<String, String> loadProperties(BufferedReader reader) throws IOException {
try {
return loadProperties(reader.lines().iterator());
} catch (UncheckedIOException e) {
throw e.getCause();
}
}
public static Map<String, String> loadProperties(Iterator<String> lineIterator) {
Map<String, String> result = new LinkedHashMap<>();
while (lineIterator.hasNext()) {
String line = lineIterator.next();
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#"))
continue;
@@ -91,4 +100,78 @@ public final class KeyValuePairProperties extends LinkedHashMap<String, String>
}
return result;
}
public static Map<String, String> loadPairs(Path file) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return loadPairs(reader);
}
}
public static Map<String, String> loadPairs(BufferedReader reader) throws IOException {
try {
return loadPairs(reader.lines().iterator());
} catch (UncheckedIOException e) {
throw e.getCause();
}
}
public static Map<String, String> loadPairs(Iterator<String> lineIterator) {
Map<String, String> result = new LinkedHashMap<>();
while (lineIterator.hasNext()) {
String line = lineIterator.next();
int idx = line.indexOf(':');
if (idx > 0) {
String name = line.substring(0, idx).trim();
String value = line.substring(idx + 1).trim();
result.put(name, value);
}
}
return result;
}
public static List<Map<String, String>> loadList(Path file) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(file)) {
return loadList(reader);
}
}
public static List<Map<String, String>> loadList(BufferedReader reader) throws IOException {
try {
return loadList(reader.lines().iterator());
} catch (UncheckedIOException e) {
throw e.getCause();
}
}
public static List<Map<String, String>> loadList(Iterator<String> lineIterator) {
ArrayList<Map<String, String>> result = new ArrayList<>();
Map<String, String> current = new LinkedHashMap<>();
while (lineIterator.hasNext()) {
String line = lineIterator.next();
int idx = line.indexOf(':');
if (idx < 0) {
if (!current.isEmpty()) {
result.add(current);
current = new LinkedHashMap<>();
}
continue;
}
String name = line.substring(0, idx).trim();
String value = line.substring(idx + 1).trim();
current.put(name, value);
}
if (!current.isEmpty())
result.add(current);
return result;
}
private KeyValuePairUtils() {
}
}

View File

@@ -54,6 +54,13 @@ public final class StringUtils {
return !isBlank(str);
}
public static String capitalizeFirst(String str) {
if (str == null || str.isEmpty())
return str;
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
public static String substringBeforeLast(String str, char delimiter) {
return substringBeforeLast(str, delimiter, str);
}

View File

@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.util.KeyValuePairProperties;
import org.jackhuang.hmcl.util.KeyValuePairUtils;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
@@ -225,7 +225,7 @@ public enum OperatingSystem {
Path osReleaseFile = Paths.get("/etc/os-release");
if (Files.exists(osReleaseFile)) {
try {
osRelease = KeyValuePairProperties.load(osReleaseFile);
osRelease = KeyValuePairUtils.loadProperties(osReleaseFile);
} catch (IOException e) {
e.printStackTrace(System.err);
}

View File

@@ -18,12 +18,13 @@
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.util.DataSizeUnit;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareDetector;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import org.jackhuang.hmcl.util.platform.linux.LinuxHardwareDetector;
import org.jackhuang.hmcl.util.platform.macos.MacOSHardwareDetector;
import org.jackhuang.hmcl.util.platform.windows.WindowsHardwareDetector;
import org.jackhuang.hmcl.util.platform.windows.*;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -47,12 +48,18 @@ public final class SystemInfo {
}
public static final long TOTAL_MEMORY = DETECTOR.getTotalMemorySize();
public static final @Nullable CentralProcessor CENTRAL_PROCESSOR = DETECTOR.detectCentralProcessor();
public static final @Nullable List<GraphicsCard> GRAPHICS_CARDS = DETECTOR.detectGraphicsCards();
}
public static void initialize() {
StringBuilder builder = new StringBuilder("System Info:");
// CPU
CentralProcessor cpu = getCentralProcessor();
if (cpu != null)
builder.append("\n - CPU: ").append(cpu);
// Graphics Card
List<GraphicsCard> graphicsCards = getGraphicsCards();
if (graphicsCards != null) {
@@ -108,6 +115,10 @@ public final class SystemInfo {
return Long.max(0, totalMemorySize - getFreeMemorySize());
}
public static @Nullable CentralProcessor getCentralProcessor() {
return Holder.CENTRAL_PROCESSOR;
}
public static @Nullable List<GraphicsCard> getGraphicsCards() {
return Holder.GRAPHICS_CARDS;
}

View File

@@ -18,15 +18,47 @@
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SystemUtils {
private SystemUtils() {}
private SystemUtils() {
}
public static @Nullable Path which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
try {
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath();
} catch (Throwable ignored) {
}
}
} catch (Throwable ignored) {
}
return null;
}
public static int callExternalProcess(String... command) throws IOException, InterruptedException {
return callExternalProcess(Arrays.asList(command));
@@ -43,6 +75,34 @@ public final class SystemUtils {
return managedProcess.getProcess().waitFor();
}
public static <T> T run(List<String> command, ExceptionalFunction<InputStream, T, ?> convert) throws Exception {
File nul = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new File("NUL")
: new File("/dev/null");
Process process = new ProcessBuilder(command)
.redirectInput(nul)
.redirectError(nul)
.start();
try {
InputStream inputStream = process.getInputStream();
CompletableFuture<T> future = CompletableFuture.supplyAsync(
Lang.wrap(() -> convert.apply(inputStream)),
Schedulers.io());
if (!process.waitFor(15, TimeUnit.SECONDS))
throw new TimeoutException();
if (process.exitValue() != 0)
throw new IOException("Bad exit code: " + process.exitValue());
return future.get();
} finally {
if (process.isAlive())
process.destroy();
}
}
public static boolean supportJVMAttachment() {
return JavaRuntime.CURRENT_VERSION >= 9 && Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null;
}

View File

@@ -0,0 +1,132 @@
/*
* 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.hardware;
import org.jetbrains.annotations.Nullable;
/**
* @author Glavo
*/
public final class CentralProcessor {
private final String name;
private final @Nullable HardwareVendor vendor;
private final @Nullable Cores cores;
private CentralProcessor(String name, @Nullable HardwareVendor vendor, @Nullable Cores cores) {
this.name = name;
this.vendor = vendor;
this.cores = cores;
}
public String getName() {
return name;
}
public @Nullable HardwareVendor getVendor() {
return vendor;
}
public @Nullable Cores getCores() {
return cores;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(128);
if (cores != null && cores.packages > 1)
builder.append(cores.packages).append(" x ");
builder.append(name);
if (cores != null) {
builder.append(" (");
builder.append(cores.physical).append(" Cores");
if (cores.logical > 0 && cores.logical != cores.physical)
builder.append(" / ").append(cores.logical).append(" Threads");
builder.append(")");
}
return builder.toString();
}
public static final class Cores {
public final int physical;
public final int logical;
public final int packages;
public Cores(int logical) {
this(logical, logical, 1);
}
public Cores(int physical, int logical, int packages) {
this.physical = physical;
this.logical = logical;
this.packages = packages;
}
@Override
public String toString() {
return String.format("Cores[physical=%d, logical=%d, packages=%d]", physical, logical, packages);
}
}
public static final class Builder {
private String name;
private @Nullable HardwareVendor vendor;
private @Nullable Cores cores;
public CentralProcessor build() {
String name = this.name;
if (name == null) {
if (vendor != null)
name = vendor + " Processor";
else
name = "Unknown";
}
return new CentralProcessor(name, vendor, cores);
}
public String getName() {
return name;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public HardwareVendor getVendor() {
return vendor;
}
public Builder setVendor(HardwareVendor vendor) {
this.vendor = vendor;
return this;
}
public Cores getCores() {
return cores;
}
public Builder setCores(Cores cores) {
this.cores = cores;
return this;
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* 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.hardware;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
final class FastFetchUtils {
private FastFetchUtils() {
}
private static <T> T get(String type, TypeToken<T> resultType) {
Path fastfetch = SystemUtils.which("fastfetch");
if (fastfetch == null)
return null;
String json;
try {
json = SystemUtils.run(Arrays.asList(fastfetch.toString(), "--structure", type, "--format", "json"),
inputStream -> IOUtils.readFullyAsString(inputStream, OperatingSystem.NATIVE_CHARSET));
} catch (Throwable e) {
LOG.warning("Failed to get result from fastfetch", e);
return null;
}
try {
List<Result<T>> list = JsonUtils.GSON.fromJson(json, JsonUtils.listTypeOf(Result.typeOf(resultType)));
Result<T> result;
if (list == null
|| list.size() != 1
|| (result = list.get(0)) == null
|| !type.equalsIgnoreCase(result.type)
|| result.result == null) {
throw new IOException("Illegal output format");
}
return result.result;
} catch (Throwable e) {
LOG.warning("Failed to parse fastfetch output: " + json, e);
return null;
}
}
static @Nullable CentralProcessor detectCentralProcessor() {
CPUInfo cpuInfo = get("CPU", TypeToken.get(CPUInfo.class));
if (cpuInfo == null)
return null;
CentralProcessor.Builder builder = new CentralProcessor.Builder()
.setName(cpuInfo.cpu)
.setVendor(HardwareVendor.of(cpuInfo.vendor));
if (cpuInfo.cores != null) {
try {
String physical = cpuInfo.cores.get("physical");
String logical = cpuInfo.cores.get("logical");
int cores = physical != null ? Integer.parseInt(physical) : 0;
int threads = logical != null ? Integer.parseInt(logical) : 0;
int packages = Integer.max(cpuInfo.packages, 1);
if (cores > 0 && threads == 0)
threads = cores;
else if (threads > 0 && cores == 0)
cores = threads;
builder.setCores(new CentralProcessor.Cores(cores, threads, packages));
} catch (Throwable ignored) {
}
}
return builder.build();
}
static @Nullable List<GraphicsCard> detectGraphicsCards() {
List<GPUInfo> gpuInfos = get("GPU", JsonUtils.listTypeOf(GPUInfo.class));
if (gpuInfos == null)
return null;
ArrayList<GraphicsCard> result = new ArrayList<>(gpuInfos.size());
for (GPUInfo gpuInfo : gpuInfos) {
if (gpuInfo == null)
continue;
GraphicsCard.Builder builder = new GraphicsCard.Builder()
.setName(gpuInfo.name)
.setVendor(HardwareVendor.of(gpuInfo.vendor))
.setDriver(gpuInfo.driver);
if ("Discrete".equalsIgnoreCase(gpuInfo.type))
builder.setType(GraphicsCard.Type.Discrete);
else if ("Integrated".equalsIgnoreCase(gpuInfo.type))
builder.setType(GraphicsCard.Type.Integrated);
result.add(builder.build());
}
return result;
}
@SuppressWarnings("unchecked")
private static final class Result<T> {
static <T> TypeToken<Result<T>> typeOf(TypeToken<T> type) {
return (TypeToken<Result<T>>) TypeToken.getParameterized(Result.class, type.getType());
}
String type;
T result;
}
private static final class CPUInfo {
String cpu;
String vendor;
int packages;
Map<String, String> cores;
}
private static final class GPUInfo {
String name;
String vendor;
String type;
String driver;
}
}

View File

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.util.platform.hardware;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -32,12 +31,12 @@ public final class GraphicsCard {
}
private final String name;
private final @Nullable Vendor vendor;
private final @Nullable HardwareVendor vendor;
private final @Nullable Type type;
private final @Nullable String driver;
private final @Nullable String driverVersion;
private GraphicsCard(String name, @Nullable Vendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
this.name = Objects.requireNonNull(name);
this.vendor = vendor;
this.type = type;
@@ -49,7 +48,7 @@ public final class GraphicsCard {
return name;
}
public @Nullable Vendor getVendor() {
public @Nullable HardwareVendor getVendor() {
return vendor;
}
@@ -68,132 +67,6 @@ public final class GraphicsCard {
return builder.toString();
}
public static final class Vendor {
public static final Vendor INTEL = new Vendor("Intel");
public static final Vendor NVIDIA = new Vendor("NVIDIA");
public static final Vendor AMD = new Vendor("AMD");
public static final Vendor APPLE = new Vendor("Apple");
public static final Vendor QUALCOMM = new Vendor("Qualcomm");
public static final Vendor MTK = new Vendor("MTK");
public static final Vendor VMWARE = new Vendor("VMware");
public static final Vendor PARALLEL = new Vendor("Parallel");
public static final Vendor MICROSOFT = new Vendor("Microsoft");
public static final Vendor MOORE_THREADS = new Vendor("Moore Threads");
public static final Vendor BROADCOM = new Vendor("Broadcom");
public static final Vendor IMG = new Vendor("Imagination");
public static final Vendor LOONGSON = new Vendor("Loongson");
public static final Vendor JINGJIA_MICRO = new Vendor("Jingjia Micro");
public static final Vendor HUAWEI = new Vendor("Huawei");
public static final Vendor ZHAOXIN = new Vendor("Zhaoxin");
public static @Nullable Vendor getKnown(String name) {
if (name == null)
return null;
String lower = name.toLowerCase(Locale.ROOT);
if (lower.startsWith("intel")) return INTEL;
if (lower.startsWith("nvidia")) return NVIDIA;
if (lower.startsWith("advanced micro devices")
|| (lower.startsWith("amd") && !(lower.length() > 3 && Character.isAlphabetic(lower.charAt(3)))))
return AMD;
if (lower.equals("brcm") || lower.startsWith("broadcom")) return BROADCOM;
if (lower.startsWith("mediatek")) return MTK;
if (lower.startsWith("qualcomm")) return QUALCOMM;
if (lower.startsWith("apple")) return APPLE;
if (lower.startsWith("microsoft")) return MICROSOFT;
if (lower.startsWith("imagination") || lower.equals("img")) return IMG;
if (lower.startsWith("loongson")) return LOONGSON;
if (lower.startsWith("moore threads")) return MOORE_THREADS;
if (lower.startsWith("jingjia")) return JINGJIA_MICRO;
if (lower.startsWith("huawei")) return HUAWEI;
if (lower.startsWith("zhaoxin")) return ZHAOXIN;
return null;
}
@Contract("null -> null; !null -> !null")
public static Vendor of(String name) {
if (name == null)
return null;
Vendor known = getKnown(name);
return known != null ? known : new Vendor(name);
}
public static @Nullable Vendor ofId(int vendorId) {
// https://devicehunt.com/all-pci-vendors
switch (vendorId) {
case 0x106b:
return APPLE;
case 0x1002:
case 0x1022:
case 0x1dd8: // AMD Pensando Systems
case 0x1924: // AMD Solarflare
return AMD;
case 0x8086:
case 0x8087:
case 0x03e7:
return INTEL;
case 0x0955:
case 0x10de:
case 0x12d2:
return NVIDIA;
case 0x1ed5:
return MOORE_THREADS;
case 0x168c:
case 0x5143:
return QUALCOMM;
case 0x14c3:
return MTK;
case 0x15ad:
return VMWARE;
case 0x1ab8:
return PARALLEL;
case 0x1414:
return MICROSOFT;
case 0x182f:
case 0x14e4:
return BROADCOM;
case 0x0014:
return LOONGSON;
case 0x0731:
return JINGJIA_MICRO;
case 0x19e5:
return HUAWEI;
case 0x1d17:
return ZHAOXIN;
default:
return null;
}
}
private final String name;
public Vendor(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
return o instanceof Vendor && name.equals(((Vendor) o).name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return name;
}
}
public enum Type {
Integrated,
Discrete
@@ -201,14 +74,19 @@ public final class GraphicsCard {
public static final class Builder {
private String name;
private Vendor vendor;
private HardwareVendor vendor;
private Type type;
private String driver;
private String driverVersion;
public GraphicsCard build() {
if (name == null)
throw new IllegalStateException("Name not set");
String name = this.name;
if (name == null) {
if (vendor != null)
name = vendor + " Graphics";
else
name = "Unknown";
}
return new GraphicsCard(name, vendor, type, driver, driverVersion);
}
@@ -222,11 +100,11 @@ public final class GraphicsCard {
return this;
}
public Vendor getVendor() {
public HardwareVendor getVendor() {
return vendor;
}
public Builder setVendor(Vendor vendor) {
public Builder setVendor(HardwareVendor vendor) {
this.vendor = vendor;
return this;
}

View File

@@ -28,8 +28,12 @@ import java.util.List;
*/
@SuppressWarnings("ALL")
public class HardwareDetector {
public @Nullable CentralProcessor detectCentralProcessor() {
return FastFetchUtils.detectCentralProcessor();
}
public @Nullable List<GraphicsCard> detectGraphicsCards() {
return null;
return FastFetchUtils.detectGraphicsCards();
}
public long getTotalMemorySize() {

View File

@@ -0,0 +1,199 @@
/*
* 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.hardware;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
public final class HardwareVendor {
public static final HardwareVendor INTEL = new HardwareVendor("Intel");
public static final HardwareVendor NVIDIA = new HardwareVendor("NVIDIA");
public static final HardwareVendor AMD = new HardwareVendor("AMD");
public static final HardwareVendor APPLE = new HardwareVendor("Apple");
public static final HardwareVendor ARM = new HardwareVendor("ARM");
public static final HardwareVendor QUALCOMM = new HardwareVendor("Qualcomm");
public static final HardwareVendor MTK = new HardwareVendor("MTK");
public static final HardwareVendor VMWARE = new HardwareVendor("VMware");
public static final HardwareVendor PARALLEL = new HardwareVendor("Parallel");
public static final HardwareVendor MICROSOFT = new HardwareVendor("Microsoft");
public static final HardwareVendor MOORE_THREADS = new HardwareVendor("Moore Threads");
public static final HardwareVendor BROADCOM = new HardwareVendor("Broadcom");
public static final HardwareVendor IMG = new HardwareVendor("Imagination");
public static final HardwareVendor LOONGSON = new HardwareVendor("Loongson");
public static final HardwareVendor JINGJIA_MICRO = new HardwareVendor("Jingjia Micro");
public static final HardwareVendor HUAWEI = new HardwareVendor("Huawei");
public static final HardwareVendor ZHAOXIN = new HardwareVendor("Zhaoxin");
public static final HardwareVendor SAMSUNG = new HardwareVendor("Samsung");
public static final HardwareVendor MARVELL = new HardwareVendor("Marvell");
public static final HardwareVendor AMPERE = new HardwareVendor("Ampere");
public static final HardwareVendor ROCKCHIP = new HardwareVendor("Rockchip");
// RISC-V
public static final HardwareVendor THEAD = new HardwareVendor("T-Head");
public static final HardwareVendor STARFIVE = new HardwareVendor("StarFive");
public static final HardwareVendor ESWIN = new HardwareVendor("ESWIN");
public static final HardwareVendor SPACEMIT = new HardwareVendor("SpacemiT");
public static @Nullable HardwareVendor getKnown(String name) {
if (name == null)
return null;
String lower = name.toLowerCase(Locale.ROOT);
if (lower.startsWith("intel") || lower.startsWith("genuineintel")) return INTEL;
if (lower.startsWith("nvidia")) return NVIDIA;
if (lower.startsWith("advanced micro devices")
|| lower.startsWith("authenticamd")
|| (lower.startsWith("amd") && !(lower.length() > 3 && Character.isAlphabetic(lower.charAt(3)))))
return AMD;
if (lower.equals("brcm") || lower.startsWith("broadcom")) return BROADCOM;
if (lower.startsWith("mediatek")) return MTK;
if (lower.equals("qcom") || lower.startsWith("qualcomm")) return QUALCOMM;
if (lower.startsWith("apple")) return APPLE;
if (lower.startsWith("microsoft")) return MICROSOFT;
if (lower.startsWith("imagination") || lower.equals("img")) return IMG;
if (lower.startsWith("loongson")) return LOONGSON;
if (lower.startsWith("moore threads")) return MOORE_THREADS;
if (lower.startsWith("jingjia")) return JINGJIA_MICRO;
if (lower.startsWith("huawei") || lower.startsWith("hisilicon")) return HUAWEI;
if (lower.startsWith("zhaoxin")) return ZHAOXIN;
if (lower.startsWith("marvell")) return MARVELL;
if (lower.startsWith("samsung")) return SAMSUNG;
if (lower.startsWith("ampere")) return AMPERE;
if (lower.startsWith("rockchip")) return ROCKCHIP;
if (lower.startsWith("thead") || lower.startsWith("t-head")) return THEAD;
if (lower.startsWith("starfive")) return STARFIVE;
if (lower.startsWith("eswin")) return ESWIN;
if (lower.startsWith("spacemit")) return SPACEMIT;
return null;
}
@Contract("null -> null; !null -> !null")
public static HardwareVendor of(String name) {
if (name == null)
return null;
HardwareVendor known = getKnown(name);
return known != null ? known : new HardwareVendor(name);
}
public static @Nullable HardwareVendor ofPciVendorId(int vendorId) {
// https://devicehunt.com/all-pci-vendors
switch (vendorId) {
case 0x106b:
return APPLE;
case 0x1002:
case 0x1022:
case 0x1dd8: // AMD Pensando Systems
case 0x1924: // AMD Solarflare
return AMD;
case 0x8086:
case 0x8087:
case 0x03e7:
return INTEL;
case 0x0955:
case 0x10de:
case 0x12d2:
return NVIDIA;
case 0x1ed5:
return MOORE_THREADS;
case 0x168c:
case 0x5143:
return QUALCOMM;
case 0x14c3:
return MTK;
case 0x15ad:
return VMWARE;
case 0x1ab8:
return PARALLEL;
case 0x1414:
return MICROSOFT;
case 0x182f:
case 0x14e4:
return BROADCOM;
case 0x0014:
return LOONGSON;
case 0x0731:
return JINGJIA_MICRO;
case 0x19e5:
return HUAWEI;
case 0x1d17:
return ZHAOXIN;
default:
return null;
}
}
public static @Nullable HardwareVendor ofArmImplementerId(int implementerId) {
// https://github.com/util-linux/util-linux/blob/0a21358af3e50fcb13a9bf3702779f11a4739667/sys-utils/lscpu-arm.c#L301
switch (implementerId) {
case 0x41:
return ARM;
case 0x42:
return BROADCOM;
case 0x48:
return HUAWEI;
case 0x4e:
return NVIDIA;
case 0x51:
return QUALCOMM;
case 0x53:
return SAMSUNG;
case 0x56:
return MARVELL;
case 0x61:
return APPLE;
case 0x69:
return INTEL;
case 0x6D:
return MICROSOFT;
case 0xc0:
return AMPERE;
default:
return null;
}
}
private final String name;
public HardwareVendor(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
return o instanceof HardwareVendor && name.equals(((HardwareVendor) o).name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,218 @@
/*
* 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.linux;
import org.jackhuang.hmcl.util.KeyValuePairUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
* @see <a href="https://github.com/fastfetch-cli/fastfetch/blob/e13eb05073297aa6f1dc244ab3414e98170a43e1/src/detection/cpu/cpu_linux.c">cpu_linux.c</a>
*/
final class LinuxCPUDetector {
private static final String CPUINFO_PATH = "/proc/cpuinfo";
private static TreeMap<Integer, Map<String, String>> loadCPUInfo() {
try {
List<Map<String, String>> list = KeyValuePairUtils.loadList(Paths.get(CPUINFO_PATH));
TreeMap<Integer, Map<String, String>> result = new TreeMap<>();
for (Map<String, String> map : list) {
String id = map.get("processor");
if (id != null) {
result.put(Integer.parseInt(id), map);
}
}
return result;
} catch (Throwable e) {
LOG.warning("Failed to load /proc/cpuinfo", e);
return null;
}
}
// https://asahilinux.org/docs/hw/soc/soc-codenames/
private static String appleCodeToName(int code) {
switch (code) {
case 8103:
return "Apple M1";
case 6000:
return "Apple M1 Pro";
case 6001:
return "Apple M1 Max";
case 6002:
return "Apple M1 Ultra";
case 8112:
return "Apple M2";
case 6020:
return "Apple M2 Pro";
case 6021:
return "Apple M2 Max";
case 6022:
return "Apple M2 Ultra";
case 8122:
return "Apple M3";
case 6030:
return "Apple M3 Pro";
case 6031:
case 6034:
return "Apple M3 Max";
case 8132:
return "Apple M4";
case 6040:
return "Apple M4 Pro";
case 6041:
return "Apple M4 Max";
default:
return null;
}
}
private static void detectName(CentralProcessor.Builder builder, TreeMap<Integer, Map<String, String>> cpuInfo) {
// assert !cpuInfo.isEmpty();
Map<String, String> firstCore = cpuInfo.firstEntry().getValue();
String modelName = firstCore.get("model name");
if (modelName == null)
modelName = firstCore.get("Model Name");
if (modelName == null)
modelName = firstCore.get("cpu model");
if (modelName != null) {
builder.setName(modelName);
builder.setVendor(HardwareVendor.of(firstCore.get("vendor_id")));
if (builder.getVendor() == null && modelName.startsWith("Loongson"))
builder.setVendor(HardwareVendor.LOONGSON);
return;
}
try {
Path compatiblePath = Paths.get("/proc/device-tree/compatible");
if (Files.isRegularFile(compatiblePath)) {
// device-vendor,device-model\0soc-vendor,soc-model\0
String[] data = FileUtils.readText(compatiblePath).split("\0");
for (int i = data.length - 1; i >= 0; i--) {
String device = data[i];
int idx = device.indexOf(',');
if (idx <= 0 || idx >= device.length() - 1)
continue;
String vendor = device.substring(0, idx);
String model = device.substring(idx + 1);
if (model.startsWith("generic-"))
continue;
builder.setVendor(HardwareVendor.getKnown(vendor));
if (builder.getVendor() == null)
builder.setVendor(HardwareVendor.of(StringUtils.capitalizeFirst(vendor)));
if (builder.getVendor() == HardwareVendor.APPLE) {
// https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/boot/dts/apple
if (model.matches("t[0-9]+"))
builder.setName(appleCodeToName(Integer.parseInt(model.substring(1))));
if (builder.getName() == null)
builder.setName("Apple Silicon " + model);
} else if (builder.getVendor() == HardwareVendor.QUALCOMM) {
// https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/boot/dts/qcom
if (model.startsWith("x"))
builder.setName("Qualcomm Snapdragon X Elite " + model.toUpperCase(Locale.ROOT));
} else if (builder.getVendor() == HardwareVendor.BROADCOM)
// Raspberry Pi
builder.setName("Broadcom " + model.toUpperCase(Locale.ROOT));
if (builder.getName() == null)
builder.setName(builder.getVendor() + " " + model.toUpperCase(Locale.ROOT));
return;
}
}
} catch (Throwable e) {
LOG.warning("Failed to detect CPU name from /proc/device-tree/compatible", e);
}
}
private static void detectCores(CentralProcessor.Builder builder, TreeMap<Integer, Map<String, String>> cpuInfo) {
// assert !cpuInfo.isEmpty();
int logical = cpuInfo.size();
try {
Map<String, String> firstCore = cpuInfo.firstEntry().getValue();
if (firstCore.containsKey("cpu cores") && firstCore.containsKey("physical id")) {
TreeMap<Integer, Integer> cpuCores = new TreeMap<>();
TreeSet<Integer> physicalIds = new TreeSet<>();
for (Map<String, String> core : cpuInfo.values()) {
int cores = Integer.parseInt(core.get("cpu cores"));
int physicalId = Integer.parseInt(core.get("physical id"));
cpuCores.put(physicalId, cores);
physicalIds.add(physicalId);
}
int physical = 0;
for (Integer value : cpuCores.values()) {
physical += value;
}
builder.setCores(new CentralProcessor.Cores(physical, logical, physicalIds.size()));
return;
}
} catch (Throwable e) {
LOG.warning("Failed to detect CPU cores", e);
}
// We can check /sys/devices/system/cpu, but I don't think it's necessary.
builder.setCores(new CentralProcessor.Cores(logical));
}
static @Nullable CentralProcessor detect() {
TreeMap<Integer, Map<String, String>> cpuInfo = loadCPUInfo();
if (cpuInfo == null || cpuInfo.isEmpty()) return null;
CentralProcessor.Builder builder = new CentralProcessor.Builder();
detectName(builder, cpuInfo);
if (builder.getName() == null)
return null;
detectCores(builder, cpuInfo);
return builder.build();
}
private LinuxCPUDetector() {
}
}

View File

@@ -21,8 +21,10 @@ import org.glavo.pci.ids.PCIIDsDatabase;
import org.glavo.pci.ids.model.Device;
import org.glavo.pci.ids.model.Vendor;
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.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import java.io.*;
import java.lang.ref.SoftReference;
@@ -52,7 +54,7 @@ final class LinuxGPUDetector {
private static final Pattern PCI_DEVICE_PATTERN =
Pattern.compile("(?<pciDomain>\\p{XDigit}+):(?<pciBus>\\p{XDigit}+):(?<pciDevice>\\p{XDigit}+)\\.(?<pciFunc>\\p{XDigit}+)");
private static final Pattern OF_DEVICE_PATTERN =
Pattern.compile("of:NgpuT[^C]*C(?<compatible>.*)");
Pattern.compile("of:N(img)?gpuT[^C]*C(?<compatible>.*)");
private static PCIIDsDatabase getPCIIDsDatabase() {
SoftReference<PCIIDsDatabase> databaseWeakReference = LinuxGPUDetector.databaseCache;
@@ -136,12 +138,11 @@ final class LinuxGPUDetector {
int pciDevice = Integer.parseInt(matcher.group("pciDevice"), 16);
int pciFunc = Integer.parseInt(matcher.group("pciFunc"), 16);
builder.setVendor(GraphicsCard.Vendor.ofId(vendorId));
builder.setVendor(HardwareVendor.ofPciVendorId(vendorId));
detectDriver(builder, deviceDir);
try {
if (builder.getVendor() == GraphicsCard.Vendor.AMD) {
if (builder.getVendor() == HardwareVendor.AMD) {
Path hwmon = deviceDir.resolve("hwmon");
try (Stream<Path> subDirs = Files.list(hwmon)) {
for (Path subDir : Lang.toIterable(subDirs)) {
@@ -173,7 +174,7 @@ final class LinuxGPUDetector {
}
}
} else if (builder.getVendor() == GraphicsCard.Vendor.INTEL) {
} else if (builder.getVendor() == HardwareVendor.INTEL) {
builder.setType(pciDevice == 20 ? GraphicsCard.Type.Integrated : GraphicsCard.Type.Discrete);
}
} catch (Throwable ignored) {
@@ -185,7 +186,7 @@ final class LinuxGPUDetector {
Vendor vendor = database.findVendor(vendorId);
if (vendor != null) {
if (builder.getVendor() == null)
builder.setVendor(GraphicsCard.Vendor.of(vendor.getName()));
builder.setVendor(HardwareVendor.of(vendor.getName()));
if (builder.getName() == null) {
Device device = vendor.getDevices().get(deviceId);
@@ -223,13 +224,13 @@ final class LinuxGPUDetector {
}
if (builder.getType() == null) {
if (builder.getVendor() == GraphicsCard.Vendor.NVIDIA) {
if (builder.getVendor() == HardwareVendor.NVIDIA) {
if (builder.getName().startsWith("GeForce")
|| builder.getName().startsWith("Quadro")
|| builder.getName().startsWith("Tesla"))
builder.setType(GraphicsCard.Type.Discrete);
} else if (builder.getVendor() == GraphicsCard.Vendor.MOORE_THREADS) {
} else if (builder.getVendor() == HardwareVendor.MOORE_THREADS) {
if (builder.getName().startsWith("MTT "))
builder.setType(GraphicsCard.Type.Discrete);
}
@@ -248,15 +249,25 @@ final class LinuxGPUDetector {
String compatible = matcher.group("compatible");
int idx = compatible.indexOf(',');
if (idx < 0) {
builder.setName(compatible.trim());
String name = compatible.trim().toUpperCase(Locale.ROOT);
if (name.equals("IMG-GPU")) // Fucking Imagination
builder.setVendor(HardwareVendor.IMG);
else
builder.setName(name);
} else {
String vendorName = compatible.substring(0, idx).trim();
GraphicsCard.Vendor vendor = GraphicsCard.Vendor.getKnown(vendorName);
HardwareVendor vendor = HardwareVendor.getKnown(vendorName);
if (vendor == null)
vendor = new GraphicsCard.Vendor(vendorName.toUpperCase(Locale.ROOT));
vendor = new HardwareVendor(StringUtils.capitalizeFirst(vendorName));
builder.setName(vendor + " " + compatible.substring(idx + 1).trim());
builder.setVendor(vendor);
String name = compatible.substring(idx + 1).trim().toUpperCase(Locale.ROOT);
if (vendor == HardwareVendor.IMG) {
if (!name.equals("GPU"))
builder.setName(vendor + " " + name);
} else
builder.setName(vendor + " " + name);
}
builder.setType(GraphicsCard.Type.Integrated);
@@ -265,7 +276,7 @@ final class LinuxGPUDetector {
return builder.build();
}
public static List<GraphicsCard> detectAll() {
static List<GraphicsCard> detect() {
Path drm = Paths.get("/sys/class/drm");
if (!Files.isDirectory(drm))
return Collections.emptyList();
@@ -304,7 +315,6 @@ final class LinuxGPUDetector {
LOG.warning("Failed to get graphics card info", e);
} finally {
databaseCache = null;
System.gc();
}
return Collections.unmodifiableList(cards);

View File

@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.util.platform.linux;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareDetector;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
@@ -37,11 +39,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/
public final class LinuxHardwareDetector extends HardwareDetector {
@Override
public @Nullable CentralProcessor detectCentralProcessor() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.LINUX)
return null;
return LinuxCPUDetector.detect();
}
@Override
public List<GraphicsCard> detectGraphicsCards() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.LINUX)
return null;
return LinuxGPUDetector.detectAll();
return LinuxGPUDetector.detect();
}
private static final Path MEMINFO = Paths.get("/proc/meminfo");

View File

@@ -20,24 +20,21 @@ package org.jackhuang.hmcl.util.platform.macos;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.KeyValuePairUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareDetector;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -46,34 +43,68 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/
public final class MacOSHardwareDetector extends HardwareDetector {
@Override
public @Nullable CentralProcessor detectCentralProcessor() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.OSX)
return null;
try {
Map<String, String> values = SystemUtils.run(Arrays.asList("/usr/sbin/sysctl", "machdep.cpu"),
inputStream -> KeyValuePairUtils.loadProperties(
new BufferedReader(new InputStreamReader(inputStream, OperatingSystem.NATIVE_CHARSET))));
String brandString = values.get("machdep.cpu.brand_string");
String coreCount = values.get("machdep.cpu.core_count");
String threadCount = values.get("machdep.cpu.thread_count");
String coresPerPackage = values.get("machdep.cpu.cores_per_package");
CentralProcessor.Builder builder = new CentralProcessor.Builder();
if (brandString != null) {
builder.setName(brandString);
String lower = brandString.toLowerCase(Locale.ROOT);
if (lower.startsWith("apple"))
builder.setVendor(HardwareVendor.APPLE);
else if (lower.startsWith("intel"))
builder.setVendor(HardwareVendor.INTEL);
} else
builder.setName("Unknown");
if (coreCount != null || threadCount != null) {
int cores = coreCount != null ? Integer.parseInt(coreCount) : 0;
int threads = threadCount != null ? Integer.parseInt(threadCount) : 0;
int coresPerPackageCount = coresPerPackage != null ? Integer.parseInt(coresPerPackage) : 0;
if (cores > 0 && threads == 0)
threads = cores;
else if (threads > 0 && cores == 0)
cores = threads;
int packages = 1;
if (cores > 0 && coresPerPackageCount > 0)
packages = Integer.max(cores / coresPerPackageCount, 1);
builder.setCores(new CentralProcessor.Cores(cores, threads, packages));
} else
builder.setCores(new CentralProcessor.Cores(Runtime.getRuntime().availableProcessors()));
return builder.build();
} catch (Throwable e) {
LOG.warning("Failed to get CPU info", e);
return null;
}
}
@Override
public List<GraphicsCard> detectGraphicsCards() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.OSX)
return null;
Process process = null;
String json = null;
try {
File devNull = new File("/dev/null");
Process finalProcess = process = new ProcessBuilder("/usr/sbin/system_profiler",
"SPDisplaysDataType",
"-json")
.redirectInput(devNull)
.redirectError(devNull)
.start();
CompletableFuture<String> future = CompletableFuture.supplyAsync(Lang.wrap(() ->
IOUtils.readFullyAsString(finalProcess.getInputStream(), OperatingSystem.NATIVE_CHARSET)),
Schedulers.io());
if (!process.waitFor(15, TimeUnit.SECONDS))
throw new TimeoutException();
if (process.exitValue() != 0)
throw new IOException("Bad exit code: " + process.exitValue());
json = future.get();
json = SystemUtils.run(Arrays.asList("/usr/sbin/system_profiler", "SPDisplaysDataType", "-json"),
inputStream -> IOUtils.readFullyAsString(inputStream, OperatingSystem.NATIVE_CHARSET));
JsonObject object = JsonUtils.GSON.fromJson(json, JsonObject.class);
JsonArray spDisplaysDataType = object.getAsJsonArray("SPDisplaysDataType");
@@ -98,7 +129,7 @@ public final class MacOSHardwareDetector extends HardwareDetector {
.setName(model.getAsString());
if (vendor != null)
builder.setVendor(GraphicsCard.Vendor.of(StringUtils.removePrefix(vendor.getAsString(), "sppci_vendor_")));
builder.setVendor(HardwareVendor.of(StringUtils.removePrefix(vendor.getAsString(), "sppci_vendor_")));
GraphicsCard.Type type = GraphicsCard.Type.Integrated;
if (bus != null) {
@@ -114,9 +145,6 @@ public final class MacOSHardwareDetector extends HardwareDetector {
return Collections.unmodifiableList(cards);
}
} catch (Throwable e) {
if (process != null && process.isAlive())
process.destroy();
LOG.warning("Failed to get graphics card info" + (json != null ? ": " + json : ""), e);
return Collections.emptyList();
}

View File

@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
@@ -53,4 +55,9 @@ public interface Kernel32 extends StdCallLibrary {
* @see <a href="https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-globalmemorystatusex">GlobalMemoryStatusEx function</a>
*/
boolean GlobalMemoryStatusEx(WinTypes.MEMORYSTATUSEX lpBuffer);
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlogicalprocessorinformationex">GetLogicalProcessorInformationEx function</a>
*/
boolean GetLogicalProcessorInformationEx(int relationshipType, Pointer buffer, IntByReference returnedLength);
}

View File

@@ -81,7 +81,19 @@ public interface WinConstants {
long HKEY_DYN_DATA = 0x80000006L;
long HKEY_CURRENT_USER_LOCAL_SETTINGS = 0x80000007L;
// https://learn.microsoft.com/windows/win32/api/winnls/ne-winnls-sysgeoclass
int GEOCLASS_NATION = 16;
int GEOCLASS_REGION = 14;
int GEOCLASS_ALL = 0;
// https://learn.microsoft.com/windows/win32/api/winnt/ne-winnt-logical_processor_relationship
int RelationProcessorCore = 0;
int RelationNumaNode = 1;
int RelationCache = 2;
int RelationProcessorPackage = 3;
int RelationGroup = 4;
int RelationProcessorDie = 5;
int RelationNumaNodeEx = 6;
int RelationProcessorModule = 7;
int RelationAll = 0xffff;
}

View File

@@ -72,7 +72,20 @@ public abstract class WinReg {
public abstract Object queryValue(HKEY root, String key, String valueName);
public abstract List<String> queryKeys(HKEY root, String key);
public abstract List<String> querySubKeyNames(HKEY root, String key);
public List<String> querySubKeys(HKEY root, String key) {
List<String> list = querySubKeyNames(root, key);
if (list.isEmpty())
return list;
if (!(list instanceof ArrayList))
list = new ArrayList<>(list);
String prefix = key.endsWith("\\") ? key : key + "\\";
list.replaceAll(str -> prefix + str);
return list;
}
private static final class JNAWinReg extends WinReg {
@@ -176,14 +189,13 @@ public abstract class WinReg {
}
@Override
public List<String> queryKeys(HKEY root, String key) {
public List<String> querySubKeyNames(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)) {
@@ -194,7 +206,7 @@ public abstract class WinReg {
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));
res.add(lpName.getWideString(0L));
i++;
} else {
if (status != WinConstants.ERROR_NO_MORE_ITEMS)

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.*;
import com.sun.jna.ptr.LongByReference;
import java.util.Arrays;
import java.util.List;
@@ -87,4 +88,111 @@ public interface WinTypes {
"ullTotalVirtual", "ullAvailVirtual", "ullAvailExtendedVirtual");
}
}
final class GROUP_AFFINITY extends Structure {
public LongByReference mask;
public short group;
public short[] reserved = new short[3];
public GROUP_AFFINITY(Pointer memory) {
super(memory);
}
public GROUP_AFFINITY() {
super();
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList(
"mask", "group", "reserved"
);
}
}
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-processor_group_info">PROCESSOR_GROUP_INFO structure</a>
*/
final class PROCESSOR_GROUP_INFO extends Structure {
public byte maximumProcessorCount;
public byte activeProcessorCount;
public byte[] reserved = new byte[38];
public LongByReference activeProcessorMask;
public PROCESSOR_GROUP_INFO(Pointer memory) {
super(memory);
}
public PROCESSOR_GROUP_INFO() {
super();
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("maximumProcessorCount", "activeProcessorCount", "reserved", "activeProcessorMask");
}
}
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-processor_relationship">PROCESSOR_RELATIONSHIP structure</a>
*/
final class PROCESSOR_RELATIONSHIP extends Structure {
public byte flags;
public byte efficiencyClass;
public byte[] reserved = new byte[20];
public short groupCount;
public GROUP_AFFINITY[] groupMask = new GROUP_AFFINITY[1];
public PROCESSOR_RELATIONSHIP() {
}
public PROCESSOR_RELATIONSHIP(Pointer memory) {
super(memory);
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("flags", "efficiencyClass", "reserved", "groupCount", "groupMask");
}
@Override
public void read() {
readField("groupCount");
if (groupCount != groupMask.length) {
groupMask = new GROUP_AFFINITY[groupCount];
}
super.read();
}
}
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-group_relationship">GROUP_RELATIONSHIP structure</a>
*/
final class GROUP_RELATIONSHIP extends Structure {
public short maximumGroupCount;
public short activeGroupCount;
public byte[] reserved = new byte[20];
public PROCESSOR_GROUP_INFO[] groupInfo = new PROCESSOR_GROUP_INFO[1];
public GROUP_RELATIONSHIP() {
}
public GROUP_RELATIONSHIP(Pointer memory) {
super(memory);
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("maximumGroupCount", "activeGroupCount", "reserved", "groupInfo");
}
@Override
public void read() {
readField("activeGroupCount");
if (activeGroupCount != groupInfo.length)
groupInfo = new PROCESSOR_GROUP_INFO[activeGroupCount];
super.read();
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.Memory;
import com.sun.jna.ptr.IntByReference;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jetbrains.annotations.Nullable;
/**
* @author Glavo
*/
final class WindowsCPUDetector {
private static void detectName(CentralProcessor.Builder builder, WinReg reg) {
Object name = reg.queryValue(WinReg.HKEY.HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString");
Object vendor = reg.queryValue(WinReg.HKEY.HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "VendorIdentifier");
if (name instanceof String)
builder.setName((String) name);
if (vendor instanceof String)
builder.setVendor(HardwareVendor.of((String) vendor));
}
private static void detectCores(CentralProcessor.Builder builder, Kernel32 kernel32) {
int coresLogical = 0;
int coresPhysical = 0;
int packages = 0;
IntByReference length = new IntByReference();
if (!kernel32.GetLogicalProcessorInformationEx(WinConstants.RelationAll, null, length) && length.getValue() == 0)
throw new AssertionError("Failed to get logical processor information length: " + kernel32.GetLastError());
try (Memory pProcessorInfo = new Memory(Integer.toUnsignedLong(length.getValue()))) {
if (!kernel32.GetLogicalProcessorInformationEx(WinConstants.RelationAll, pProcessorInfo, length))
throw new AssertionError("Failed to get logical processor information length: " + kernel32.GetLastError());
for (long offset = 0L; offset < pProcessorInfo.size(); ) {
int relationship = pProcessorInfo.getInt(offset);
long size = Integer.toUnsignedLong(pProcessorInfo.getInt(offset + 4L));
if (relationship == WinConstants.RelationGroup) {
WinTypes.GROUP_RELATIONSHIP groupRelationship = new WinTypes.GROUP_RELATIONSHIP(pProcessorInfo.share(offset + 8L, size - 8L));
groupRelationship.read();
int activeGroupCount = Short.toUnsignedInt(groupRelationship.activeGroupCount);
for (int i = 0; i < activeGroupCount; i++) {
coresLogical += Short.toUnsignedInt(groupRelationship.groupInfo[i].maximumProcessorCount);
}
} else if (relationship == WinConstants.RelationProcessorCore)
coresPhysical++;
else if (relationship == WinConstants.RelationProcessorPackage)
packages++;
offset += size;
}
}
builder.setCores(new CentralProcessor.Cores(coresPhysical, coresLogical, packages));
}
static @Nullable CentralProcessor detect() {
WinReg reg = WinReg.INSTANCE;
Kernel32 kernel32 = Kernel32.INSTANCE;
if (reg == null)
return null;
CentralProcessor.Builder builder = new CentralProcessor.Builder();
detectName(builder, reg);
if (kernel32 != null)
detectCores(builder, kernel32);
return builder.build();
}
private WindowsCPUDetector() {
}
}

View File

@@ -17,21 +17,20 @@
*/
package org.jackhuang.hmcl.util.platform.windows;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.KeyValuePairUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareDetector;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -40,31 +39,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/
public final class WindowsHardwareDetector extends HardwareDetector {
private static List<Map<String, String>> parsePowerShellFormatList(Iterable<String> lines) {
ArrayList<Map<String, String>> result = new ArrayList<>();
Map<String, String> current = new LinkedHashMap<>();
for (String line : lines) {
int idx = line.indexOf(':');
if (idx < 0) {
if (!current.isEmpty()) {
result.add(current);
current = new LinkedHashMap<>();
}
continue;
}
String key = line.substring(0, idx).trim();
String value = line.substring(idx + 1).trim();
current.put(key, value);
}
if (!current.isEmpty())
result.add(current);
return result;
@Override
public @Nullable CentralProcessor detectCentralProcessor() {
if (!OperatingSystem.isWindows7OrLater())
return null;
return WindowsCPUDetector.detect();
}
@Override
@@ -72,39 +51,21 @@ public final class WindowsHardwareDetector extends HardwareDetector {
if (!OperatingSystem.isWindows7OrLater())
return null;
Process process = null;
String list = null;
try {
File nul = new File("NUL");
String getCimInstance = OperatingSystem.SYSTEM_VERSION.startsWith("6.1")
? "Get-WmiObject"
: "Get-CimInstance";
Process finalProcess = process = new ProcessBuilder("powershell.exe",
"-Command",
String.join(" | ",
getCimInstance + " -Class Win32_VideoController",
"Select-Object Name,AdapterCompatibility,DriverVersion,AdapterDACType",
"Format-List"
))
.redirectInput(nul)
.redirectError(nul)
.start();
List<Map<String, String>> videoControllers = SystemUtils.run(Arrays.asList(
"powershell.exe",
"-Command",
String.join(" | ",
getCimInstance + " -Class Win32_VideoController",
"Select-Object Name,AdapterCompatibility,DriverVersion,AdapterDACType",
"Format-List"
)),
inputStream -> KeyValuePairUtils.loadList(new BufferedReader(new InputStreamReader(inputStream, OperatingSystem.NATIVE_CHARSET))));
CompletableFuture<String> future = CompletableFuture.supplyAsync(Lang.wrap(() ->
IOUtils.readFullyAsString(finalProcess.getInputStream(), OperatingSystem.NATIVE_CHARSET)),
Schedulers.io());
if (!process.waitFor(15, TimeUnit.SECONDS))
throw new TimeoutException();
if (process.exitValue() != 0)
throw new IOException("Bad exit code: " + process.exitValue());
list = future.get();
List<Map<String, String>> videoControllers = parsePowerShellFormatList(Arrays.asList(list.split("\\R")));
ArrayList<GraphicsCard> cards = new ArrayList<>(videoControllers.size());
for (Map<String, String> videoController : videoControllers) {
String name = videoController.get("Name");
@@ -114,7 +75,7 @@ public final class WindowsHardwareDetector extends HardwareDetector {
if (StringUtils.isNotBlank(name)) {
cards.add(GraphicsCard.builder().setName(name)
.setVendor(GraphicsCard.Vendor.of(adapterCompatibility))
.setVendor(HardwareVendor.of(adapterCompatibility))
.setDriverVersion(driverVersion)
.setType(StringUtils.isBlank(adapterDACType)
|| "Internal".equalsIgnoreCase(adapterDACType)
@@ -128,9 +89,7 @@ public final class WindowsHardwareDetector extends HardwareDetector {
return cards;
} catch (Throwable e) {
if (process != null && process.isAlive())
process.destroy();
LOG.warning("Failed to get graphics card info" + (list != null ? ": " + list : ""), e);
LOG.warning("Failed to get graphics card info", e);
return Collections.emptyList();
}
}

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Glavo
*/
public final class KeyValuePairPropertiesTest {
public final class KeyValuePairUtilsTest {
@Test
public void test() throws IOException {
String content = "#test: key0=value0\n \n" +
@@ -20,7 +21,7 @@ public final class KeyValuePairPropertiesTest {
"key2=\"value2\"\n" +
"key3=\"\\\" \\n\"\n";
KeyValuePairProperties properties = KeyValuePairProperties.load(new BufferedReader(new StringReader(content)));
Map<String, String> properties = KeyValuePairUtils.loadProperties(new BufferedReader(new StringReader(content)));
assertEquals(Lang.mapOf(
pair("key1", "value1"),

View File

@@ -99,15 +99,15 @@ public final class WinRegTest {
}
@Test
public void testQueryKeys() {
public void testQuerySubKeys() {
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()));
reg.querySubKeys(hkey, key).stream().sorted().collect(Collectors.toList()));
for (String subkey : SUBKEYS) {
assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\" + subkey));
assertEquals(Collections.emptyList(), reg.querySubKeys(hkey, key + "\\" + subkey));
}
assertEquals(Collections.emptyList(), reg.queryKeys(hkey, key + "\\NOT_EXIST"));
assertEquals(Collections.emptyList(), reg.querySubKeys(hkey, key + "\\NOT_EXIST"));
}
}