From fe622579cf95dd1f810ac62dfbb9f9d301f994c8 Mon Sep 17 00:00:00 2001 From: Burning_TNT <88144530+burningtnt@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:14:20 +0800 Subject: [PATCH] Support #2416 Enable HMCL to analyze crash reasons from latest.log (#2433) * Support analyze crash reasons from latest.log * Fix --- .../jackhuang/hmcl/ui/GameCrashWindow.java | 56 +++++++++++++------ .../hmcl/game/CrashReportAnalyzer.java | 26 ++++++++- .../hmcl/game/CrashReportAnalyzerTest.java | 2 +- 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 9f6e47ff4..ac53e69b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -41,17 +41,19 @@ import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.launch.ProcessListener; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; -import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.CommandBuilder; import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; @@ -119,11 +121,13 @@ public class GameCrashWindow extends Stage { analyzeCrashReport(); } + @SuppressWarnings("unchecked") private void analyzeCrashReport() { loading.set(true); - CompletableFuture.supplyAsync(() -> { + Task.allOf(Task.supplyAsync(() -> { String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n")); - Set keywords = Collections.emptySet(); + + // Get the crash-report from the crash-reports/xxx, or the output of console. String crashReport = null; try { crashReport = CrashReportAnalyzer.findCrashReport(rawLog); @@ -133,31 +137,48 @@ public class GameCrashWindow extends Stage { if (crashReport == null) { crashReport = CrashReportAnalyzer.extractCrashReport(rawLog); } - if (crashReport != null) { - keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport); + + return pair(CrashReportAnalyzer.anaylze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>()); + }), Task.supplyAsync(() -> { + Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log"); + if (!Files.isReadable(latestLog)) { + return pair(new HashSet(), new HashSet()); } - return pair( - CrashReportAnalyzer.anaylze(rawLog), - keywords); - }).whenCompleteAsync((pair, exception) -> { + + String log; + try { + log = FileUtils.readText(latestLog); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to read logs/latest.log", e); + return pair(new HashSet(), new HashSet()); + } + + return pair(CrashReportAnalyzer.anaylze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log)); + })).whenComplete(Schedulers.javafx(), (taskResult, exception) -> { loading.set(false); if (exception != null) { LOG.log(Level.WARNING, "Failed to analyze crash report", exception); reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction)); } else { - List results = pair.getKey(); - Set keywords = pair.getValue(); + EnumMap results = new EnumMap<>(CrashReportAnalyzer.Rule.class); + Set keywords = new HashSet<>(); + for (Pair, Set> pair : (List, Set>>) (List) taskResult) { + for (CrashReportAnalyzer.Result result : pair.getKey()) { + results.put(result.getRule(), result); + } + keywords.addAll(pair.getValue()); + } List segments = new ArrayList<>(); - - boolean hasMultipleRules = results.stream().map(CrashReportAnalyzer.Result::getRule).distinct().count() > 1; + + boolean hasMultipleRules = results.keySet().stream().distinct().count() > 1; if (hasMultipleRules) { segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.multiple"), Controllers::onHyperlinkAction)); LOG.log(Level.INFO, "Multiple reasons detected"); } - for (CrashReportAnalyzer.Result result : results) { + for (CrashReportAnalyzer.Result result : results.values()) { switch (result.getRule()) { case TOO_OLD_JAVA: segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.too_old_java", @@ -211,7 +232,7 @@ public class GameCrashWindow extends Stage { reasonTextFlow.getChildren().setAll(segments); } } - }, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException); + }).start(); } private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?.*?) @ (?.*?)}"); @@ -247,7 +268,8 @@ public class GameCrashWindow extends Stage { LogWindow logWindow = new LogWindow(); logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO); - if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO); + if (managedProcess.getClasspath() != null) + logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO); for (Map.Entry entry : logs) logWindow.logLine(entry.getKey(), entry.getValue()); @@ -406,7 +428,7 @@ public class GameCrashWindow extends Stage { JFXButton helpButton = FXUtils.newRaisedButton(i18n("help")); helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); - + toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CrashReportAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CrashReportAnalyzer.java index 4db0f8988..3a8841bb5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CrashReportAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/CrashReportAnalyzer.java @@ -114,7 +114,7 @@ public final class CrashReportAnalyzer { FORGE_REPEAT_INSTALLATION(Pattern.compile("--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W]*?--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W\\n\\r]*?MultipleArgumentsForOptionException: Found multiple arguments for option gameDir, but you asked for only one")),//https://github.com/huanghongxun/HMCL/issues/1880 OPTIFINE_REPEAT_INSTALLATION(Pattern.compile("ResolutionException: Module optifine reads another module named optifine")),//Optifine 重复安装(及Mod文件夹有,自动安装也有) JAVA_VERSION_IS_TOO_HIGH(Pattern.compile("(Unable to make protected final java\\.lang\\.Class java\\.lang\\.ClassLoader\\.defineClass|java\\.lang\\.NoSuchFieldException: ucp|Unsupported class file major version|because module java\\.base does not export|java\\.lang\\.ClassNotFoundException: jdk\\.nashorn\\.api\\.scripting\\.NashornScriptEngineFactory|java\\.lang\\.ClassNotFoundException: java\\.lang\\.invoke\\.LambdaMetafactory)")),//Java版本过高 - + //Forge 默认会把每一个 mod jar 都当做一个 JPMS 的模块(Module)加载。在这个 jar 没有给出 module-info 声明的情况下,JPMS 会采用这样的顺序决定 module 名字: //1. META-INF/MANIFEST.MF 里的 Automatic-Module-Name //2. 根据文件名生成。文件名里的 .jar 后缀名先去掉,然后检查是否有 -(\\d+(\\.|$)) 的部分,有的话只取 - 前面的部分,- 后面的部分成为 module 的版本号(即尝试判断文件名里是否有版本号,有的话去掉),然后把不是拉丁字母和数字的字符(正则表达式 [^A-Za-z0-9])都换成点,然后把连续的多个点换成一个点,最后去掉开头和结尾的点。那么 @@ -178,10 +178,30 @@ public final class CrashReportAnalyzer { public Matcher getMatcher() { return matcher; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Result result = (Result) o; + + if (rule != result.rule) return false; + if (!log.equals(result.log)) return false; + return matcher.equals(result.matcher); + } + + @Override + public int hashCode() { + int result = rule.hashCode(); + result = 31 * result + log.hashCode(); + result = 31 * result + matcher.hashCode(); + return result; + } } - public static List anaylze(String log) { - List results = new ArrayList<>(); + public static Set anaylze(String log) { + Set results = new HashSet<>(); for (Rule rule : Rule.values()) { Matcher matcher = rule.pattern.matcher(log); if (matcher.find()) { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java index 84fff31e1..0923ba838 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/CrashReportAnalyzerTest.java @@ -38,7 +38,7 @@ public class CrashReportAnalyzerTest { return IOUtils.readFullyAsString(is); } - private CrashReportAnalyzer.Result findResultByRule(List results, CrashReportAnalyzer.Rule rule) { + private CrashReportAnalyzer.Result findResultByRule(Set results, CrashReportAnalyzer.Rule rule) { CrashReportAnalyzer.Result r = results.stream().filter(result -> result.getRule() == rule).findFirst().orElse(null); assertNotNull(r); return r;