Skip to content

Commit

Permalink
优化 LogWindow (#3274)
Browse files Browse the repository at this point in the history
* update

* update

* update

* update

* update
  • Loading branch information
Glavo authored Sep 2, 2024
1 parent 6409841 commit 8a816f7
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 245 deletions.
143 changes: 92 additions & 51 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,13 @@
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public final class LauncherHelper {
Expand Down Expand Up @@ -717,14 +712,14 @@ private final class HMCLProcessListener implements ProcessListener {
private final Version version;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CircularArrayList<Log> logs;
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
private Thread submitLogThread;
private LinkedBlockingQueue<Log> logBuffer;

public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
Expand All @@ -733,10 +728,7 @@ public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthI
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;

final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
}

@Override
Expand All @@ -752,12 +744,60 @@ public void setProcess(ManagedProcess process) {
LOG.info("Process ClassPath: " + classpath);
}

if (showLogs)
if (showLogs) {
CountDownLatch logWindowLatch = new CountDownLatch(1);
Platform.runLater(() -> {
logWindow = new LogWindow(process);
logWindow.showNormal();
logWindow = new LogWindow(process, logs);
logWindow.show();
logWindowLatch.countDown();
});

logBuffer = new LinkedBlockingQueue<>();
submitLogThread = Lang.thread(new Runnable() {
private final ArrayList<Log> currentLogs = new ArrayList<>();
private final Semaphore semaphore = new Semaphore(0);

private void submitLogs() {
if (currentLogs.size() == 1) {
Log log = currentLogs.get(0);
Platform.runLater(() -> logWindow.logLine(log));
} else {
Platform.runLater(() -> {
logWindow.logLines(currentLogs);
semaphore.release();
});
semaphore.acquireUninterruptibly();
}
currentLogs.clear();
}

@Override
public void run() {
while (true) {
try {
currentLogs.add(logBuffer.take());
//noinspection BusyWait
Thread.sleep(200); // Wait for more logs
} catch (InterruptedException e) {
break;
}

logBuffer.drainTo(currentLogs);
submitLogs();
}

do {
submitLogs();
} while (logBuffer.drainTo(currentLogs) > 0);
}
}, "Game Log Submitter", true);

try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

private void finishLaunch() {
Expand Down Expand Up @@ -796,52 +836,51 @@ private void finishLaunch() {

@Override
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");

if (isErrorStream)
System.err.println(filteredLog);
System.err.println(log);
else
System.out.println(filteredLog);
System.out.println(log);

Log4jLevel level;
if (isErrorStream && !filteredLog.startsWith("[authlib-injector]"))
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;

synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
log = StringUtils.parseEscapeSequence(log);
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");

Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
}

Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}

if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
}
}
}
}

@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

launchingLatch.countDown();
Expand All @@ -850,14 +889,16 @@ public void onExit(int exitCode, ExitType exitType) {
return;

// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (!lwjgl) {
synchronized (this) {
if (!lwjgl)
finishLaunch();
}
}

if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}

checkExit();
Expand Down
72 changes: 72 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <[email protected]> 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.game;

import org.jackhuang.hmcl.util.Log4jLevel;

import static org.jackhuang.hmcl.setting.ConfigHolder.config;

public final class Log {
public static final int DEFAULT_LOG_LINES = 2000;

public static int getLogLines() {
Integer lines = config().getLogLines();
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
}

private final String log;
private Log4jLevel level;
private boolean selected = false;

public Log(String log) {
this.log = log;
}

public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}

public String getLog() {
return log;
}

public Log4jLevel getLevel() {
Log4jLevel level = this.level;
if (level == null) {
level = Log4jLevel.guessLevel(log);
if (level == null)
level = Log4jLevel.INFO;
this.level = level;
}
return level;
}

public boolean isSelected() {
return selected;
}

public void setSelected(boolean selected) {
this.selected = selected;
}

@Override
public String toString() {
return log;
}
}
8 changes: 4 additions & 4 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public static Config fromJson(String json) throws JsonParseException {
private StringProperty launcherFontFamily = new SimpleStringProperty();

@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();

@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
Expand Down Expand Up @@ -573,15 +573,15 @@ public void setLauncherFontFamily(String launcherFontFamily) {
this.launcherFontFamily.set(launcherFontFamily);
}

public int getLogLines() {
public Integer getLogLines() {
return logLines.get();
}

public void setLogLines(int logLines) {
public void setLogLines(Integer logLines) {
this.logLines.set(logLines);
}

public IntegerProperty logLinesProperty() {
public ObjectProperty<Integer> logLinesProperty() {
return logLines;
}

Expand Down
8 changes: 8 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
Expand Down Expand Up @@ -998,4 +999,11 @@ public static TextFlow segmentToTextFlow(final String segment, Consumer<String>
return tf;
}

public static String toWeb(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);

return String.format("#%02x%02x%02x", r, g, b);
}
}
18 changes: 8 additions & 10 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;

private final Collection<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;

public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
Expand Down Expand Up @@ -124,7 +124,7 @@ public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType e
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));

// Get the crash-report from the crash-reports/xxx, or the output of console.
String crashReport = null;
Expand Down Expand Up @@ -264,20 +264,18 @@ private String parseFabricModId(String modName) {
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);

logWindow.logLine(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), 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());

logWindow.showNormal();
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
logWindow.logLines(logs);
logWindow.show();
}

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(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
Expand Down
Loading

0 comments on commit 8a816f7

Please sign in to comment.