Support #2416 Enable HMCL to analyze crash reasons from latest.log (#2433)

* Support analyze crash reasons from latest.log

* Fix
This commit is contained in:
Burning_TNT 2023-08-11 15:14:20 +08:00 committed by GitHub
parent edcdeaf018
commit fe622579cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 21 deletions

View File

@ -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<String> 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<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
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<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
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<CrashReportAnalyzer.Result> results = pair.getKey();
Set<String> keywords = pair.getValue();
EnumMap<CrashReportAnalyzer.Rule, CrashReportAnalyzer.Result> results = new EnumMap<>(CrashReportAnalyzer.Rule.class);
Set<String> keywords = new HashSet<>();
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
for (CrashReportAnalyzer.Result result : pair.getKey()) {
results.put(result.getRule(), result);
}
keywords.addAll(pair.getValue());
}
List<Node> 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("\\{(?<modid>.*?) @ (?<version>.*?)}");
@ -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<String, Log4jLevel> 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);

View File

@ -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<Result> anaylze(String log) {
List<Result> results = new ArrayList<>();
public static Set<Result> anaylze(String log) {
Set<Result> results = new HashSet<>();
for (Rule rule : Rule.values()) {
Matcher matcher = rule.pattern.matcher(log);
if (matcher.find()) {

View File

@ -38,7 +38,7 @@ public class CrashReportAnalyzerTest {
return IOUtils.readFullyAsString(is);
}
private CrashReportAnalyzer.Result findResultByRule(List<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {
private CrashReportAnalyzer.Result findResultByRule(Set<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {
CrashReportAnalyzer.Result r = results.stream().filter(result -> result.getRule() == rule).findFirst().orElse(null);
assertNotNull(r);
return r;