diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 750acba806..2230728e26 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -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 { @@ -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 logs; - private final ArrayDeque levels; - private final CountDownLatch logWindowLatch = new CountDownLatch(1); + private final CircularArrayList logs; private final CountDownLatch launchingLatch; private final String forbiddenAccessToken; + private Thread submitLogThread; + private LinkedBlockingQueue logBuffer; public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) { this.repository = repository; @@ -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 @@ -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 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() { @@ -796,44 +836,37 @@ private void finishLaunch() { @Override public void onLog(String log, boolean isErrorStream) { - String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, ""); - 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, ""); + 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(); + } + } } } } @@ -841,7 +874,13 @@ public void onLog(String log, boolean isErrorStream) { @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(); @@ -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> 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(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java new file mode 100644 index 0000000000..ea01aee3ec --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java @@ -0,0 +1,72 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 huangyuhui 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 . + */ +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; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index f9611ed483..b124c509a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -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 logLines = new SimpleObjectProperty<>(); @SerializedName("titleTransparent") private BooleanProperty titleTransparent = new SimpleBooleanProperty(false); @@ -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 logLinesProperty() { return logLines; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index d9d78d5f75..79bc0a8970 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -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; @@ -998,4 +999,11 @@ public static TextFlow segmentToTextFlow(final String segment, Consumer 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); + } } 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 50ec8535a2..653dd6d3f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -89,9 +89,9 @@ public class GameCrashWindow extends Stage { private final LaunchOptions launchOptions; private final View view; - private final Collection> logs; + private final List logs; - public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection> logs) { + public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List logs) { this.managedProcess = managedProcess; this.exitType = exitType; this.repository = repository; @@ -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; @@ -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 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) -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index c216bb74a2..7e28b6d7d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -25,8 +25,8 @@ import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.*; -import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -38,13 +38,14 @@ import javafx.stage.Stage; import org.jackhuang.hmcl.game.GameDumpGenerator; import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.CircularArrayList; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.platform.ManagedProcess; -import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemUtils; import java.io.IOException; @@ -54,14 +55,11 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.function.Consumer; import java.util.stream.Collectors; -import java.util.stream.IntStream; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.Lang.thread; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; /** @@ -69,128 +67,120 @@ */ public final class LogWindow extends Stage { - private final ArrayDeque logs = new ArrayDeque<>(); - private final Map levelCountMap = new EnumMap(Log4jLevel.class) { - { - for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty()); - } - }; - private final Map levelShownMap = new EnumMap(Log4jLevel.class) { - { - for (Log4jLevel level : Log4jLevel.values()) { - SimpleBooleanProperty property = new SimpleBooleanProperty(true); - put(level, property); - } - } - }; - private final LogWindowImpl impl = new LogWindowImpl(); - private final ChangeListener logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount()); + private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG}; - private Consumer exportGameCrashInfoCallback; + private final CircularArrayList logs; + private final Map levelCountMap = new EnumMap<>(Log4jLevel.class); + private final Map levelShownMap = new EnumMap<>(Log4jLevel.class); - private boolean stopCheckLogCount = false; + { + for (Log4jLevel level : Log4jLevel.values()) { + levelCountMap.put(level, new SimpleIntegerProperty()); + levelShownMap.put(level, new SimpleBooleanProperty(true)); + } + } + private final LogWindowImpl impl; private final ManagedProcess gameProcess; public LogWindow(ManagedProcess gameProcess) { + this(gameProcess, new CircularArrayList<>()); + } + + public LogWindow(ManagedProcess gameProcess, CircularArrayList logs) { + this.logs = logs; + this.impl = new LogWindowImpl(); setScene(new Scene(impl, 800, 480)); getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily())); setTitle(i18n("logwindow.title")); FXUtils.setIcon(this); - levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs())); + for (SimpleBooleanProperty property : levelShownMap.values()) { + property.addListener(o -> shakeLogs()); + } this.gameProcess = gameProcess; } - public void logLine(String filteredLine, Log4jLevel level) { - Log log = new Log(parseEscapeSequence(filteredLine), level); + public void logLine(Log log) { + Log4jLevel level = log.getLevel(); logs.add(log); if (levelShownMap.get(level).get()) impl.listView.getItems().add(log); - levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1); - if (!stopCheckLogCount) checkLogCount(); + SimpleIntegerProperty property = levelCountMap.get(log.getLevel()); + property.set(property.get() + 1); + checkLogCount(); + autoScroll(); } - public void showGameCrashReport(Consumer exportGameCrashInfoCallback) { - this.exportGameCrashInfoCallback = exportGameCrashInfoCallback; - this.impl.showCrashReport.set(true); - stopCheckLogCount = true; - for (Log log : impl.listView.getItems()) { - if (log.log.contains("Minecraft Crash Report")) { - Platform.runLater(() -> { - impl.listView.scrollTo(log); - }); - break; - } - } - show(); - } + public void logLines(List logs) { + for (Log log : logs) { + Log4jLevel level = log.getLevel(); + this.logs.add(log); + if (levelShownMap.get(level).get()) + impl.listView.getItems().add(log); - public void showNormal() { - this.impl.showCrashReport.set(false); - show(); + SimpleIntegerProperty property = levelCountMap.get(log.getLevel()); + property.set(property.get() + 1); + } + checkLogCount(); + autoScroll(); } private void shakeLogs() { - impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList())); + impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList())); + autoScroll(); } private void checkLogCount() { - while (logs.size() > config().getLogLines()) { + int nRemove = logs.size() - Log.getLogLines(); + if (nRemove <= 0) + return; + + ObservableList items = impl.listView.getItems(); + int itemsSize = items.size(); + int count = 0; + + for (int i = 0; i < nRemove; i++) { Log removedLog = logs.removeFirst(); - if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) { - impl.listView.getItems().remove(0); - } + if (itemsSize > count && items.get(count) == removedLog) + count++; } - } - private static class Log { - private final String log; - private final Log4jLevel level; - private boolean selected = false; + items.remove(0, count); + } - public Log(String log, Log4jLevel level) { - this.log = log; - this.level = level; - } + private void autoScroll() { + if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get()) + impl.listView.scrollTo(impl.listView.getItems().size() - 1); } - public class LogWindowImpl extends Control { + private final class LogWindowImpl extends Control { private final ListView listView = new JFXListView<>(); private final BooleanProperty autoScroll = new SimpleBooleanProperty(); - private final List buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList()); - private final List showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList()); - private final JFXComboBox cboLines = new JFXComboBox<>(); - private final BooleanProperty showCrashReport = new SimpleBooleanProperty(); + private final StringProperty[] buttonText = new StringProperty[LEVELS.length]; + private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length]; + private final JFXComboBox cboLines = new JFXComboBox<>(); LogWindowImpl() { getStyleClass().add("log-window"); - listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1))); - - boolean flag = false; - cboLines.getItems().setAll("500", "2000", "5000", "10000"); - for (String i : cboLines.getItems()) - if (Integer.toString(config().getLogLines()).equals(i)) { - cboLines.getSelectionModel().select(i); - flag = true; - } + listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size()))); - if (!flag) - cboLines.getSelectionModel().select(2); + for (int i = 0; i < LEVELS.length; i++) { + buttonText[i] = new SimpleStringProperty(); + showLevel[i] = new SimpleBooleanProperty(true); + } - cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> { - config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue)); - }); + cboLines.getItems().setAll(500, 2000, 5000, 10000); + cboLines.setValue(Log.getLogLines()); + cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue)); - Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG}; - String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"}; - for (int i = 0; i < 5; ++i) { - buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i])); - levelShownMap.get(levels[i]).bind(showLevel.get(i)); + for (int i = 0; i < LEVELS.length; ++i) { + buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s")); + levelShownMap.get(LEVELS[i]).bind(showLevel[i]); } } @@ -207,7 +197,7 @@ private void onExportLogs() { thread(() -> { Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath(); try { - Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList())); + Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList())); } catch (IOException e) { LOG.warning("Failed to export logs", e); return; @@ -223,38 +213,31 @@ private void onExportLogs() { }); } - private void onExportDump(JFXButton button) { - thread(() -> { - if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) { - if (SystemUtils.supportJVMAttachment()) { - Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button"))); + private void onExportDump(SpinnerPane pane) { + assert SystemUtils.supportJVMAttachment(); - Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath(); + pane.setLoading(true); - try { - if (gameProcess.isRunning()) { - GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile); - FXUtils.showFileInExplorer(dumpFile); - } - } catch (Throwable e) { - LOG.warning("Failed to create minecraft jstack dump", e); - - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button")); - alert.setTitle(i18n("message.error")); - alert.showAndWait(); - }); - } + thread(() -> { + Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath(); - Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button"))); + try { + if (gameProcess.isRunning()) { + GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile); + FXUtils.showFileInExplorer(dumpFile); } + } catch (Throwable e) { + LOG.warning("Failed to create minecraft jstack dump", e); + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump")); + alert.setTitle(i18n("message.error")); + alert.showAndWait(); + }); } - }); - } - private void onExportGameCrashInfo() { - if (exportGameCrashInfoCallback == null) return; - exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR))); + Platform.runLater(() -> pane.setLoading(false)); + }); } @Override @@ -263,7 +246,7 @@ protected Skin createDefaultSkin() { } } - private static class LogWindowSkin extends SkinBase { + private static final class LogWindowSkin extends SkinBase { private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty"); private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal"); private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); @@ -275,17 +258,7 @@ private static class LogWindowSkin extends SkinBase { private final Set> selected = new HashSet<>(); - private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) { - ToggleButton button = new ToggleButton(); - button.setStyle("-fx-background-color: " + backgroundColor + ";"); - button.getStyleClass().add("log-toggle"); - button.textProperty().bind(buttonText); - button.setSelected(true); - showLevel.bind(button.selectedProperty()); - return button; - } - - protected LogWindowSkin(LogWindowImpl control) { + LogWindowSkin(LogWindowImpl control) { super(control); VBox vbox = new VBox(3); @@ -310,13 +283,16 @@ protected LogWindowSkin(LogWindowImpl control) { { HBox hBox = new HBox(3); - hBox.getChildren().setAll( - createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)), - createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)), - createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)), - createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)), - createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4)) - ); + for (int i = 0; i < LEVELS.length; i++) { + ToggleButton button = new ToggleButton(); + button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";"); + button.getStyleClass().add("log-toggle"); + button.textProperty().bind(control.buttonText[i]); + button.setSelected(true); + control.showLevel[i].bind(button.selectedProperty()); + hBox.getChildren().add(button); + } + borderPane.setRight(hBox); } @@ -347,11 +323,11 @@ protected LogWindowSkin(LogWindowImpl control) { setOnMouseClicked(event -> { if (!event.isControlDown()) { - for (ListCell logListCell: selected) { + for (ListCell logListCell : selected) { if (logListCell != this) { logListCell.pseudoClassStateChanged(SELECTED, false); if (logListCell.getItem() != null) { - logListCell.getItem().selected = false; + logListCell.getItem().setSelected(false); } } } @@ -362,7 +338,7 @@ protected LogWindowSkin(LogWindowImpl control) { selected.add(this); pseudoClassStateChanged(SELECTED, true); if (getItem() != null) { - getItem().selected = true; + getItem().setSelected(true); } }); } @@ -377,18 +353,18 @@ protected void updateItem(Log item, boolean empty) { lastCell.value = this; pseudoClassStateChanged(EMPTY, empty); - pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL); - pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR); - pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN); - pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO); - pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG); - pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE); - pseudoClassStateChanged(SELECTED, !empty && item.selected); + pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL); + pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR); + pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN); + pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO); + pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG); + pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE); + pseudoClassStateChanged(SELECTED, !empty && item.isSelected()); if (empty) { setText(null); } else { - setText(item.log); + setText(item.getLog()); } } }); @@ -398,10 +374,9 @@ protected void updateItem(Log item, boolean empty) { StringBuilder stringBuilder = new StringBuilder(); for (Log item : listView.getItems()) { - if (item != null && item.selected) { - if (item.log != null) { - stringBuilder.append(item.log); - } + if (item != null && item.isSelected()) { + if (item.getLog() != null) + stringBuilder.append(item.getLog()); stringBuilder.append('\n'); } } @@ -417,11 +392,6 @@ protected void updateItem(Log item, boolean empty) { { BorderPane bottom = new BorderPane(); - JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs")); - exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo()); - exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport); - bottom.setLeft(exportGameCrashInfoButton); - HBox hBox = new HBox(3); bottom.setRight(hBox); hBox.setAlignment(Pos.CENTER_RIGHT); @@ -432,24 +402,24 @@ protected void updateItem(Log item, boolean empty) { control.autoScroll.bind(autoScrollCheckBox.selectedProperty()); JFXButton exportLogsButton = new JFXButton(i18n("button.export")); - exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs()); + exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs()); JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game")); - terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame()); + terminateButton.setOnAction(e -> getSkinnable().onTerminateGame()); - JFXButton exportDumpButton = new JFXButton(); + SpinnerPane exportDumpPane = new SpinnerPane(); + JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump")); if (SystemUtils.supportJVMAttachment()) { - exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button")); - exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton)); + exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane)); } else { - exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button")); - exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip"))); + exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency"))); exportDumpButton.setDisable(true); } + exportDumpPane.setContent(exportDumpButton); JFXButton clearButton = new JFXButton(i18n("button.clear")); - clearButton.setOnMouseClicked(e -> getSkinnable().onClear()); - hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton); + clearButton.setOnAction(e -> getSkinnable().onClear()); + hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton); vbox.getChildren().add(bottom); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2f48a992f0..a0b1cc636e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -728,10 +728,8 @@ logwindow.title=Log logwindow.help=You can go to the HMCL community and find others for help logwindow.autoscroll=Auto-scroll logwindow.export_game_crash_logs=Export Crash Logs -logwindow.export_dump.dependency_ok.button=Export Game Stack Dump -logwindow.export_dump.dependency_ok.doing_button=Exporting Game Stack Dump (May take up to 15 seconds) -logwindow.export_dump.no_dependency.button=Export Game Stack Dump (Not compatible) -logwindow.export_dump.no_dependency.tooltip=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help. +logwindow.export_dump=Export Game Stack Dump +logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL QQ group or HMCL Discord for help. main_page=Home diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 57cd8908d8..666b0bd011 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -673,10 +673,8 @@ logwindow.title=Registro logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar logwindow.autoscroll=Desplazamiento automático logwindow.export_game_crash_logs=Exportar registros de errores -logwindow.export_dump.dependency_ok.button=Exportar volcado de pila de juegos -logwindow.export_dump.dependency_ok.doing_button=Exportación de volcado de pila de juego (puede tardar hasta 15 segundos) -logwindow.export_dump.no_dependency.button=Exportar volcado de pila de juegos (no compatible) -logwindow.export_dump.no_dependency.tooltip=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda. +logwindow.export_dump=Exportar volcado de pila de juegos +logwindow.export_dump.no_dependency=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL QQ o HMCL Discord para obtener ayuda. main_page=Inicio diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 57fa6e02a6..27b7b87696 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -607,10 +607,8 @@ logwindow.title=記錄 logwindow.help=你可以前往 HMCL 社區,尋找他人幫助 logwindow.autoscroll=自動滾動 logwindow.export_game_crash_logs=導出遊戲崩潰訊息 -logwindow.export_dump.dependency_ok.button=導出遊戲運行棧 -logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒) -logwindow.export_dump.no_dependency.button=導出遊戲運行棧(不兼容) -logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。 +logwindow.export_dump=導出遊戲運行棧 +logwindow.export_dump.no_dependency=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。 main_page=首頁 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 7746fe32af..df4623147b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -606,10 +606,8 @@ logwindow.title=日志 logwindow.help=你可以前往 HMCL 社区,寻找他人帮助 logwindow.autoscroll=自动滚动 logwindow.export_game_crash_logs=导出游戏崩溃信息 -logwindow.export_dump.dependency_ok.button=导出游戏运行栈 -logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒) -logwindow.export_dump.no_dependency.button=导出游戏运行栈(不兼容) -logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。 +logwindow.export_dump=导出游戏运行栈 +logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。 main_page=主页 diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java index 016e790a63..8734e8367b 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java @@ -20,8 +20,8 @@ import org.jackhuang.hmcl.JavaFXLauncher; import org.jackhuang.hmcl.game.ClassicVersion; import org.jackhuang.hmcl.game.LaunchOptions; +import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.launch.ProcessListener; -import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.ManagedProcess; @@ -35,8 +35,6 @@ import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; -import static org.jackhuang.hmcl.util.Pair.pair; - public class GameCrashWindowTest { @Test @@ -57,7 +55,7 @@ public void test() throws Exception { .setGameDir(new File(".")) .create(), Arrays.stream(logs.split("\\n")) - .map(log -> pair(log, Log4jLevel.guessLevel(log))) + .map(Log::new) .collect(Collectors.toList())); window.showAndWait(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index 06a1f8ab73..fadf40bd21 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -346,9 +346,15 @@ public static String parseColorEscapes(String original) { } public static String parseEscapeSequence(String str) { - StringBuilder builder = new StringBuilder(); + int idx = str.indexOf('\033'); + if (idx < 0) + return str; + + StringBuilder builder = new StringBuilder(str.length()); boolean inEscape = false; - for (int i = 0; i < str.length(); i++) { + + builder.append(str, 0, idx); + for (int i = idx; i < str.length(); i++) { char ch = str.charAt(i); if (ch == '\033') { inEscape = true;