Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,9 @@ public void onLog(String log, boolean isErrorStream) {
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");

Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
Log4jLevel level = log.startsWith("[authlib-injector]")
? Log4jLevel.guessLevel(log)
: Log4jLevel.guessLevel(log, isErrorStream);
if (showLogs) {
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
Comment thread
Wulian233 marked this conversation as resolved.
Comment thread
Wulian233 marked this conversation as resolved.
Expand Down
130 changes: 77 additions & 53 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/util/Log4jLevel.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util;

import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -50,66 +51,54 @@ public boolean lessOrEqual(Log4jLevel level) {
public static final Pattern MINECRAFT_LOGGER = Pattern.compile("\\[(?<timestamp>[0-9:]+)] \\[[^/]+/(?<level>[^]]+)]");
public static final Pattern MINECRAFT_LOGGER_CATEGORY = Pattern.compile("\\[(?<timestamp>[0-9:]+)] \\[[^/]+/(?<level>[^]]+)] \\[(?<category>[^]]+)]");
public static final String JAVA_SYMBOL = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
private static final String WRAPPED_PRINT_STREAM = "[java.lang.Throwable$WrappedPrintStream:println";

private static final String[] INFO_MARKERS = markers(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark the marker arrays as unmodifiable

These static marker arrays are initialized once and treated as immutable lookup tables, but their types omit the required type-use @Unmodifiable annotation. Annotate each added array declaration as String @Unmodifiable [] so the immutability contract is explicit.

AGENTS.md reference: AGENTS.md:L11-L14

Useful? React with 👍 / 👎.

Level.INFO,
Level.CONFIG,
Level.FINE,
Level.FINER,
Level.FINEST
);
private static final String[] ERROR_MARKERS = markers(Level.SEVERE);
private static final String[] WARN_MARKERS = markers(Level.WARNING);

public static Log4jLevel guessLevel(String line) {
Log4jLevel level = null;
Matcher m = MINECRAFT_LOGGER.matcher(line);
if (m.find()) {
// New style logs from log4j
String levelStr = m.group("level");
if (null != levelStr)
switch (levelStr) {
case "INFO":
level = INFO;
break;
case "WARN":
level = WARN;
break;
case "ERROR":
level = ERROR;
break;
case "FATAL":
level = FATAL;
break;
case "TRACE":
level = TRACE;
break;
case "DEBUG":
level = DEBUG;
break;
default:
break;
}
level = parseLevel(m.group("level"));
Matcher m2 = MINECRAFT_LOGGER_CATEGORY.matcher(line);
if (m2.find()) {
String level2Str = m2.group("category");
if (null != level2Str)
switch (level2Str) {
case "STDOUT":
level = INFO;
break;
case "STDERR":
level = ERROR;
break;
}
}

if (line.contains("STDERR]") || line.contains("[STDERR/]")) {
level = ERROR;
if (level2Str != null) {
level = switch (level2Str) {
case "STDOUT" -> INFO;
case "STDERR" -> guessStderrLevel(line, level);
default -> level;
};
}
} else if (line.contains("STDERR]") || line.contains("[STDERR/]")) {
level = guessStderrLevel(line, level);
}
} else {
if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]")
|| line.contains("[FINER]") || line.contains("[FINEST]"))
if (containsAny(line, INFO_MARKERS)) {
level = INFO;
if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
}
if (containsAny(line, ERROR_MARKERS) || line.contains("[STDERR]")) {
level = ERROR;
if (line.contains("[WARNING]"))
}
if (containsAny(line, WARN_MARKERS)) {
level = WARN;
if (line.contains("[DEBUG]"))
}
if (line.contains("[DEBUG]")) {
level = DEBUG;
}
}
if (line.contains("overwriting existing"))

if (line.contains("overwriting existing")) {
level = FATAL;
}

/*if (line.contains("Exception in thread")
|| line.matches("\\s+at " + JAVA_SYMBOL)
Expand All @@ -120,17 +109,52 @@ public static Log4jLevel guessLevel(String line) {
return level;
}

public static boolean isError(Log4jLevel a) {
return a != null && a.lessOrEqual(Log4jLevel.ERROR);
public static Log4jLevel guessLevel(String line, boolean isErrorStream) {
Log4jLevel level = guessLevel(line);
return level != null || !isErrorStream ? level : ERROR;
Comment on lines +112 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Declare nullable results in the new classifier API

This overload returns null whenever an unrecognized stdout line is supplied, but its return type and nullable local are unannotated; the added parseLevel return and guessStderrLevel fallback similarly have nullable contracts. Add @NotNullByDefault to the enum and explicitly annotate every nullable declaration so callers and nullability tooling receive the actual contract.

AGENTS.md reference: AGENTS.md:L7-L9

Useful? React with 👍 / 👎.

}

public static Log4jLevel mergeLevel(Log4jLevel a, Log4jLevel b) {
if (a == null)
return b;
else if (b == null)
return a;
else
return a.level < b.level ? a : b;
private static Log4jLevel parseLevel(String level) {
Comment thread
Wulian233 marked this conversation as resolved.
return switch (level) {
case "FATAL" -> FATAL;
case "ERROR" -> ERROR;
case "WARN" -> WARN;
case "INFO" -> INFO;
case "DEBUG" -> DEBUG;
case "TRACE" -> TRACE;
case "ALL" -> ALL;
default -> null;
};
}

private static Log4jLevel guessStderrLevel(String line, Log4jLevel fallback) {
if (line.contains(WRAPPED_PRINT_STREAM) && fallback != null) {
return fallback;
}
return ERROR;
}

private static String[] markers(Level... levels) {
String[] markers = new String[levels.length * 2];
int i = 0;
for (Level level : levels) {
markers[i++] = '[' + level.getName() + ']';
markers[i++] = '[' + level.getLocalizedName() + ']';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Match the game JVM's locale when building markers

Level#getLocalizedName() uses HMCL's current default locale, which SettingsManager changes to the selected launcher UI language, while the separately launched game JVM derives its logging locale from its own JVM/OS settings. When those locales differ—for example, English HMCL on a Chinese OS—Chinese legacy [信息]/[警告] markers are absent from these arrays, so stderr lines still fall through to ERROR and the reported issue remains. Build markers for the possible game locales or recognize the localized level names independently of HMCL's locale.

Useful? React with 👍 / 👎.

}
return markers;
}
Comment thread
Wulian233 marked this conversation as resolved.

private static boolean containsAny(String line, String[] markers) {
for (String marker : markers) {
if (line.contains(marker)) {
return true;
}
}
return false;
}

public static boolean isError(Log4jLevel a) {
return a != null && a.lessOrEqual(Log4jLevel.ERROR);
}

public static boolean guessLogLineError(String log) {
Expand Down