Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final class LogExporter {
private LogExporter() {
}

public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript, Path crashReportFile) {
Path runDirectory = gameRepository.getRunDirectory(versionId);
Path baseDirectory = gameRepository.getBaseDirectory();
List<String> versions = new ArrayList<>();
Expand All @@ -65,6 +65,14 @@ public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameReposi

return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(crashReportFile)) {
try {
zipper.putFile(crashReportFile, "hmcl-game-crash-report.log");
} catch (IOException e) {
LOG.warning("Failed to add crash report file to zip", e);
}
}

processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
processLogs(runDirectory, "*.log", "runDirectory", zipper);
Expand Down
215 changes: 197 additions & 18 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
Expand All @@ -45,14 +47,21 @@
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
Expand Down Expand Up @@ -265,28 +274,198 @@ private void showLogWindow() {
}

private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();

CompletableFuture.supplyAsync(() ->
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
Alert alert;

if (exception == null) {
FXUtils.showFileInExplorer(logFile);
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
Path logFile = Paths.get("minecraft-exported-crash-info-" + time + ".zip").toAbsolutePath();
Path crashReportFile;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我没看懂这里到底在干什么,为什么不在 LogExporter 里直接生成日志到最终文件,而是在 GameCrashWindow 里实现,还要倒腾一个临时文件。

try {
crashReportFile = Files.createTempFile("hmcl-game-crash-report-", ".log");
} catch (IOException e) {
LOG.warning("Failed to create temporary crash report file, using run directory instead", e);
crashReportFile = repository.getRunDirectory(version.getId()).resolve("hmcl-game-crash-report.txt");
}

Path finalCrashReportFile = crashReportFile;
CompletableFuture.supplyAsync(() -> {
try (BufferedWriter report = Files.newBufferedWriter(finalCrashReportFile, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
report.write("=== HMCL Game Crash Report ===\n");
report.write("Generation Time: " + time + "\n");
report.write("Exit Type: " + exitType + "\n");

report.write("\n== HMCL Information ==\n");
report.write("HMCL Version: " + Metadata.VERSION + "\n");
report.write("Current Directory: " + Metadata.CURRENT_DIRECTORY + "\n");
report.write("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY + "\n");
report.write("HMCL Current Directory: " + Metadata.HMCL_CURRENT_DIRECTORY + "\n");
report.write("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found") + "\n");
report.write("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory") + "\n");
report.write("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()) + "\n");
report.write("Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n");
report.write("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n");
report.write("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n");
report.write("Java Home: " + System.getProperty("java.home") + "\n");

report.write("\n== System Information ==\n");
report.write("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')') + "\n");

if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
report.write("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER") + "\n");
}

report.write("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n");
report.write("Native Encoding: " + OperatingSystem.NATIVE_CHARSET + "\n");
report.write("JNU Encoding: " + System.getProperty("sun.jnu.encoding") + "\n");

if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
report.write("Code Page: " + OperatingSystem.CODE_PAGE + "\n");
}

if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
report.write("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE") + "\n");
report.write("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP") + "\n");
}

report.write("Total Memory: " + total_memory + "\n");

report.write("\n== Hardware Information ==\n");

// CPU information
CentralProcessor cpu = SystemInfo.getCentralProcessor();
if (cpu != null) {
report.write("CPU: " + cpu.toString().replace("\n", " ") + "\n");
}

// Graphics cards information
List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
if (graphicsCards != null) {
if (graphicsCards.isEmpty()) {
report.write("GPU: Not Found\n");
} else if (graphicsCards.size() == 1) {
report.write("GPU: " + graphicsCards.get(0).toString().replace("\n", " ") + "\n");
} else {
LOG.warning("Failed to export game crash info", exception);
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
int index = 1;
for (GraphicsCard graphicsCard : graphicsCards) {
report.write("GPU " + (index++) + ": " + graphicsCard.toString().replace("\n", " ") + "\n");
}
}
}

// Memory information
long totalMemorySize = SystemInfo.getTotalMemorySize();
long usedMemorySize = SystemInfo.getUsedMemorySize();
report.append("Memory: " + DataSizeUnit.format(usedMemorySize) + " / " + DataSizeUnit.format(totalMemorySize));
if (totalMemorySize > 0 && usedMemorySize > 0) {
report.append(" (" + (int) (((double) usedMemorySize / totalMemorySize) * 100) + "%)\n");
} else {
report.append("\n");
}

alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
report.write("\n== Java Information ==\n");
report.write("Game Java Version: " + java + "\n");
report.write("Game Java Path: " + launchOptions.getJava().getBinary().toAbsolutePath() + "\n");
report.write("Game Min Memory: " + launchOptions.getMinMemory() + "\n");
report.write("Game Max Memory: " + launchOptions.getMaxMemory() + "\n");

report.write("\n== Game Information ==\n");
report.write("Game Version: " + version.getId() + "\n");
report.write("IsModpack: " + repository.isModpack(version.getId()) + "\n");
report.write("Game RunDirectory: " + repository.getRunDirectory(version.getId()) + "\n");
report.write("Game VersionJson: " + repository.getVersionJson(version.getId()) + "\n");
report.write("Game VersionJar: " + repository.getVersionJar(version.getId()) + "\n");
report.write("Game AssetDirectory: " + repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId()) + "\n");
report.write("Game AssetIndexUrl: " + version.getAssetIndex().getUrl() + "\n");
report.write("Game MainClass: " + version.getMainClass() + "\n");
report.write("ServerIP: " + launchOptions.getServerIp() + "\n");
report.write("EnvironmentVariables: " + launchOptions.getEnvironmentVariables() + "\n");
report.write("Fullscreen: " + launchOptions.isFullscreen() + "\n");
report.write("WindowWidth: " + launchOptions.getWidth() + "\n");
report.write("WindowHeight: " + launchOptions.getHeight() + "\n");
report.write("PreLaunchCommand: " + launchOptions.getPreLaunchCommand() + "\n");
report.write("PostExitCommand: " + launchOptions.getPostExitCommand() + "\n");
report.write("isUseNativeOpenAL: " + launchOptions.isUseNativeOpenAL() + "\n");
report.write("isUseNativeGLFW: " + launchOptions.isUseNativeGLFW() + "\n");
if (managedProcess.getClasspath() != null)
report.write("ClassPath: " + managedProcess.getClasspath());

report.write("\n== Available Java Runtimes ==\n");
try {
Collection<JavaRuntime> allJavaRuntimes = JavaManager.getAllJava();
if (allJavaRuntimes.isEmpty()) {
report.write("No Java runtimes found\n");
} else {
for (JavaRuntime runtime : allJavaRuntimes) {
report.write(String.format("%s %s (%s, %s): %s\n",
runtime.isJDK() ? "JDK" : "JRE",
runtime.getVersion(),
runtime.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(runtime.getVendor(), "Unknown"),
runtime.getBinary()));
}
}
} catch (InterruptedException e) {
report.write("Failed to get Java runtime information.\n");
LOG.warning("Failed to get Java runtime information.", e);
}

report.write("\n== Mod Loader Information ==\n");
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
if (!type.getPatchId().isEmpty()) {
analyzer.getVersion(type).ifPresent(ver -> {
try {
report.write(type.getPatchId() + ": " + ver + "\n");
} catch (IOException ignored) {
}
});
}
}

report.write("\n== HMCL Crash Analysis ==\n");
for (Node node : reasonTextFlow.getChildren()) {
if (node instanceof Text) {
report.write(((Text) node).getText());
}
}
report.write("\n\n");

report.write("\n== Game Mod Directory Structure ==\n");
report.write(FileUtils.printFileStructure(repository.getModManager(version.getId()).getModsDirectory(), 10));

report.write("\n\n");

report.write("\n== Game Resourcepacks Directory Structure ==\n");
report.write(FileUtils.printFileStructure(repository.getRunDirectory(version.getId()).resolve("resourcepacks").toAbsolutePath(), 10));

report.write("\n\n");

report.write("\n== Game Shaderpacks Directory Structure ==\n");
report.write(FileUtils.printFileStructure(repository.getRunDirectory(version.getId()).resolve("shaderpacks").toAbsolutePath(), 10));

report.write("\n\n");

report.write("\n== Game Config Directory Structure ==\n");
report.write(FileUtils.printFileStructure(repository.getRunDirectory(version.getId()).resolve("config").toAbsolutePath(), 10));

report.flush();
} catch (IOException e) {
LOG.warning("Failed to write crash report file", e);
}
return logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
}).thenComposeAsync(logs -> LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString(), finalCrashReportFile)).handleAsync((result, exception) -> {
Alert alert;

if (exception == null) {
FXUtils.showFileInExplorer(logFile);
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
} else {
LOG.warning("Failed to export game crash info", exception);
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
}

return null;
}, Schedulers.javafx());
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
return null;
}, Schedulers.javafx());
}

private final class View extends VBox {
Expand Down