mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-11 04:46:18 -04:00
parent
6409841cca
commit
8a816f7f35
@ -55,18 +55,13 @@ import java.net.SocketTimeoutException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.AccessDeniedException;
|
import java.nio.file.AccessDeniedException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
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.ui.FXUtils.runInFX;
|
||||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public final class LauncherHelper {
|
public final class LauncherHelper {
|
||||||
@ -717,14 +712,14 @@ public final class LauncherHelper {
|
|||||||
private final Version version;
|
private final Version version;
|
||||||
private final LaunchOptions launchOptions;
|
private final LaunchOptions launchOptions;
|
||||||
private ManagedProcess process;
|
private ManagedProcess process;
|
||||||
private boolean lwjgl;
|
private volatile boolean lwjgl;
|
||||||
private LogWindow logWindow;
|
private LogWindow logWindow;
|
||||||
private final boolean detectWindow;
|
private final boolean detectWindow;
|
||||||
private final ArrayDeque<String> logs;
|
private final CircularArrayList<Log> logs;
|
||||||
private final ArrayDeque</*Log4jLevel*/Object> levels;
|
|
||||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
|
||||||
private final CountDownLatch launchingLatch;
|
private final CountDownLatch launchingLatch;
|
||||||
private final String forbiddenAccessToken;
|
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) {
|
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@ -733,10 +728,7 @@ public final class LauncherHelper {
|
|||||||
this.launchingLatch = launchingLatch;
|
this.launchingLatch = launchingLatch;
|
||||||
this.detectWindow = detectWindow;
|
this.detectWindow = detectWindow;
|
||||||
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
|
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
|
||||||
|
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
|
||||||
final int numLogs = config().getLogLines() + 1;
|
|
||||||
this.logs = new ArrayDeque<>(numLogs);
|
|
||||||
this.levels = new ArrayDeque<>(numLogs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -752,12 +744,60 @@ public final class LauncherHelper {
|
|||||||
LOG.info("Process ClassPath: " + classpath);
|
LOG.info("Process ClassPath: " + classpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLogs)
|
if (showLogs) {
|
||||||
|
CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
logWindow = new LogWindow(process);
|
logWindow = new LogWindow(process, logs);
|
||||||
logWindow.showNormal();
|
logWindow.show();
|
||||||
logWindowLatch.countDown();
|
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() {
|
private void finishLaunch() {
|
||||||
@ -796,44 +836,37 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLog(String log, boolean isErrorStream) {
|
public void onLog(String log, boolean isErrorStream) {
|
||||||
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
|
|
||||||
|
|
||||||
if (isErrorStream)
|
if (isErrorStream)
|
||||||
System.err.println(filteredLog);
|
System.err.println(log);
|
||||||
else
|
else
|
||||||
System.out.println(filteredLog);
|
System.out.println(log);
|
||||||
|
|
||||||
Log4jLevel level;
|
log = StringUtils.parseEscapeSequence(log);
|
||||||
if (isErrorStream && !filteredLog.startsWith("[authlib-injector]"))
|
if (forbiddenAccessToken != null)
|
||||||
level = Log4jLevel.ERROR;
|
log = log.replace(forbiddenAccessToken, "<access token>");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
|
||||||
if (showLogs) {
|
if (showLogs) {
|
||||||
try {
|
if (level == null)
|
||||||
logWindowLatch.await();
|
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||||
} catch (InterruptedException e) {
|
logBuffer.add(new Log(log, level));
|
||||||
Thread.currentThread().interrupt();
|
} else {
|
||||||
return;
|
synchronized (this) {
|
||||||
|
logs.addLast(new Log(log, level));
|
||||||
|
if (logs.size() > Log.getLogLines())
|
||||||
|
logs.removeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lwjgl) {
|
if (!lwjgl) {
|
||||||
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
|
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
|
||||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||||
lwjgl = true;
|
synchronized (this) {
|
||||||
finishLaunch();
|
if (!lwjgl) {
|
||||||
|
lwjgl = true;
|
||||||
|
finishLaunch();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -841,7 +874,13 @@ public final class LauncherHelper {
|
|||||||
@Override
|
@Override
|
||||||
public void onExit(int exitCode, ExitType exitType) {
|
public void onExit(int exitCode, ExitType exitType) {
|
||||||
if (showLogs) {
|
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();
|
launchingLatch.countDown();
|
||||||
@ -850,14 +889,16 @@ public final class LauncherHelper {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Game crashed before opening the game window.
|
// Game crashed before opening the game window.
|
||||||
if (!lwjgl) finishLaunch();
|
if (!lwjgl) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (!lwjgl)
|
||||||
|
finishLaunch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (exitType != ExitType.NORMAL) {
|
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());
|
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();
|
checkExit();
|
||||||
|
72
HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java
Normal file
72
HMCL/src/main/java/org/jackhuang/hmcl/game/Log.java
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -164,7 +164,7 @@ public final class Config implements Cloneable, Observable {
|
|||||||
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
private StringProperty launcherFontFamily = new SimpleStringProperty();
|
||||||
|
|
||||||
@SerializedName("logLines")
|
@SerializedName("logLines")
|
||||||
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
|
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
@SerializedName("titleTransparent")
|
@SerializedName("titleTransparent")
|
||||||
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
|
||||||
@ -573,15 +573,15 @@ public final class Config implements Cloneable, Observable {
|
|||||||
this.launcherFontFamily.set(launcherFontFamily);
|
this.launcherFontFamily.set(launcherFontFamily);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getLogLines() {
|
public Integer getLogLines() {
|
||||||
return logLines.get();
|
return logLines.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLogLines(int logLines) {
|
public void setLogLines(Integer logLines) {
|
||||||
this.logLines.set(logLines);
|
this.logLines.set(logLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IntegerProperty logLinesProperty() {
|
public ObjectProperty<Integer> logLinesProperty() {
|
||||||
return logLines;
|
return logLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ import javafx.scene.layout.ColumnConstraints;
|
|||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.shape.Rectangle;
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
@ -998,4 +999,11 @@ public final class FXUtils {
|
|||||||
return tf;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,9 +89,9 @@ public class GameCrashWindow extends Stage {
|
|||||||
private final LaunchOptions launchOptions;
|
private final LaunchOptions launchOptions;
|
||||||
private final View view;
|
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.managedProcess = managedProcess;
|
||||||
this.exitType = exitType;
|
this.exitType = exitType;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@ -124,7 +124,7 @@ public class GameCrashWindow extends Stage {
|
|||||||
private void analyzeCrashReport() {
|
private void analyzeCrashReport() {
|
||||||
loading.set(true);
|
loading.set(true);
|
||||||
Task.allOf(Task.supplyAsync(() -> {
|
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.
|
// Get the crash-report from the crash-reports/xxx, or the output of console.
|
||||||
String crashReport = null;
|
String crashReport = null;
|
||||||
@ -264,20 +264,18 @@ public class GameCrashWindow extends Stage {
|
|||||||
private void showLogWindow() {
|
private void showLogWindow() {
|
||||||
LogWindow logWindow = new LogWindow(managedProcess);
|
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)
|
if (managedProcess.getClasspath() != null)
|
||||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
|
||||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
logWindow.logLines(logs);
|
||||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
logWindow.show();
|
||||||
|
|
||||||
logWindow.showNormal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportGameCrashInfo() {
|
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();
|
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(() ->
|
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 ->
|
.thenComposeAsync(logs ->
|
||||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||||
.handleAsync((result, exception) -> {
|
.handleAsync((result, exception) -> {
|
||||||
|
@ -25,8 +25,8 @@ import javafx.application.Platform;
|
|||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@ -38,13 +38,14 @@ import javafx.scene.layout.*;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||||
|
import org.jackhuang.hmcl.game.Log;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
|
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||||
import org.jackhuang.hmcl.util.Holder;
|
import org.jackhuang.hmcl.util.Holder;
|
||||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
|
||||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -54,14 +55,11 @@ import java.nio.file.Paths;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,128 +67,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|||||||
*/
|
*/
|
||||||
public final class LogWindow extends Stage {
|
public final class LogWindow extends Stage {
|
||||||
|
|
||||||
private final ArrayDeque<Log> logs = new ArrayDeque<>();
|
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
|
||||||
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
|
|
||||||
{
|
private final CircularArrayList<Log> logs;
|
||||||
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
|
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
|
||||||
|
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
|
||||||
|
|
||||||
|
{
|
||||||
|
for (Log4jLevel level : Log4jLevel.values()) {
|
||||||
|
levelCountMap.put(level, new SimpleIntegerProperty());
|
||||||
|
levelShownMap.put(level, new SimpleBooleanProperty(true));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
|
|
||||||
{
|
|
||||||
for (Log4jLevel level : Log4jLevel.values()) {
|
|
||||||
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
|
|
||||||
put(level, property);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private final LogWindowImpl impl = new LogWindowImpl();
|
|
||||||
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
|
||||||
|
|
||||||
private Consumer<String> exportGameCrashInfoCallback;
|
|
||||||
|
|
||||||
private boolean stopCheckLogCount = false;
|
|
||||||
|
|
||||||
|
private final LogWindowImpl impl;
|
||||||
private final ManagedProcess gameProcess;
|
private final ManagedProcess gameProcess;
|
||||||
|
|
||||||
public LogWindow(ManagedProcess gameProcess) {
|
public LogWindow(ManagedProcess gameProcess) {
|
||||||
|
this(gameProcess, new CircularArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
|
||||||
|
this.logs = logs;
|
||||||
|
this.impl = new LogWindowImpl();
|
||||||
setScene(new Scene(impl, 800, 480));
|
setScene(new Scene(impl, 800, 480));
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||||
setTitle(i18n("logwindow.title"));
|
setTitle(i18n("logwindow.title"));
|
||||||
FXUtils.setIcon(this);
|
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;
|
this.gameProcess = gameProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logLine(String filteredLine, Log4jLevel level) {
|
public void logLine(Log log) {
|
||||||
Log log = new Log(parseEscapeSequence(filteredLine), level);
|
Log4jLevel level = log.getLevel();
|
||||||
logs.add(log);
|
logs.add(log);
|
||||||
if (levelShownMap.get(level).get())
|
if (levelShownMap.get(level).get())
|
||||||
impl.listView.getItems().add(log);
|
impl.listView.getItems().add(log);
|
||||||
|
|
||||||
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
|
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||||
if (!stopCheckLogCount) checkLogCount();
|
property.set(property.get() + 1);
|
||||||
|
checkLogCount();
|
||||||
|
autoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
|
public void logLines(List<Log> logs) {
|
||||||
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
|
for (Log log : logs) {
|
||||||
this.impl.showCrashReport.set(true);
|
Log4jLevel level = log.getLevel();
|
||||||
stopCheckLogCount = true;
|
this.logs.add(log);
|
||||||
for (Log log : impl.listView.getItems()) {
|
if (levelShownMap.get(level).get())
|
||||||
if (log.log.contains("Minecraft Crash Report")) {
|
impl.listView.getItems().add(log);
|
||||||
Platform.runLater(() -> {
|
|
||||||
impl.listView.scrollTo(log);
|
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
|
||||||
});
|
property.set(property.get() + 1);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
show();
|
checkLogCount();
|
||||||
}
|
autoScroll();
|
||||||
|
|
||||||
public void showNormal() {
|
|
||||||
this.impl.showCrashReport.set(false);
|
|
||||||
show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void shakeLogs() {
|
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() {
|
private void checkLogCount() {
|
||||||
while (logs.size() > config().getLogLines()) {
|
int nRemove = logs.size() - Log.getLogLines();
|
||||||
|
if (nRemove <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ObservableList<Log> items = impl.listView.getItems();
|
||||||
|
int itemsSize = items.size();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < nRemove; i++) {
|
||||||
Log removedLog = logs.removeFirst();
|
Log removedLog = logs.removeFirst();
|
||||||
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
|
if (itemsSize > count && items.get(count) == removedLog)
|
||||||
impl.listView.getItems().remove(0);
|
count++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.remove(0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Log {
|
private void autoScroll() {
|
||||||
private final String log;
|
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
|
||||||
private final Log4jLevel level;
|
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
|
||||||
private boolean selected = false;
|
|
||||||
|
|
||||||
public Log(String log, Log4jLevel level) {
|
|
||||||
this.log = log;
|
|
||||||
this.level = level;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LogWindowImpl extends Control {
|
private final class LogWindowImpl extends Control {
|
||||||
|
|
||||||
private final ListView<Log> listView = new JFXListView<>();
|
private final ListView<Log> listView = new JFXListView<>();
|
||||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||||
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
|
||||||
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
|
||||||
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
|
||||||
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
|
||||||
|
|
||||||
LogWindowImpl() {
|
LogWindowImpl() {
|
||||||
getStyleClass().add("log-window");
|
getStyleClass().add("log-window");
|
||||||
|
|
||||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
|
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||||
|
|
||||||
boolean flag = false;
|
for (int i = 0; i < LEVELS.length; i++) {
|
||||||
cboLines.getItems().setAll("500", "2000", "5000", "10000");
|
buttonText[i] = new SimpleStringProperty();
|
||||||
for (String i : cboLines.getItems())
|
showLevel[i] = new SimpleBooleanProperty(true);
|
||||||
if (Integer.toString(config().getLogLines()).equals(i)) {
|
}
|
||||||
cboLines.getSelectionModel().select(i);
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flag)
|
cboLines.getItems().setAll(500, 2000, 5000, 10000);
|
||||||
cboLines.getSelectionModel().select(2);
|
cboLines.setValue(Log.getLogLines());
|
||||||
|
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
|
||||||
|
|
||||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
for (int i = 0; i < LEVELS.length; ++i) {
|
||||||
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
|
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
|
||||||
});
|
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +197,7 @@ public final class LogWindow extends Stage {
|
|||||||
thread(() -> {
|
thread(() -> {
|
||||||
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||||
try {
|
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) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to export logs", e);
|
LOG.warning("Failed to export logs", e);
|
||||||
return;
|
return;
|
||||||
@ -223,38 +213,31 @@ public final class LogWindow extends Stage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onExportDump(JFXButton button) {
|
private void onExportDump(SpinnerPane pane) {
|
||||||
|
assert SystemUtils.supportJVMAttachment();
|
||||||
|
|
||||||
|
pane.setLoading(true);
|
||||||
|
|
||||||
thread(() -> {
|
thread(() -> {
|
||||||
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
|
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||||
if (SystemUtils.supportJVMAttachment()) {
|
|
||||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
|
|
||||||
|
|
||||||
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
try {
|
||||||
|
if (gameProcess.isRunning()) {
|
||||||
try {
|
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||||
if (gameProcess.isRunning()) {
|
FXUtils.showFileInExplorer(dumpFile);
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
|
|
||||||
}
|
}
|
||||||
}
|
} catch (Throwable e) {
|
||||||
});
|
LOG.warning("Failed to create minecraft jstack dump", e);
|
||||||
}
|
|
||||||
|
|
||||||
private void onExportGameCrashInfo() {
|
Platform.runLater(() -> {
|
||||||
if (exportGameCrashInfoCallback == null) return;
|
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
|
||||||
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
alert.setTitle(i18n("message.error"));
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> pane.setLoading(false));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -263,7 +246,7 @@ public final class LogWindow extends Stage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||||
@ -275,17 +258,7 @@ public final class LogWindow extends Stage {
|
|||||||
|
|
||||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||||
|
|
||||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
LogWindowSkin(LogWindowImpl control) {
|
||||||
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) {
|
|
||||||
super(control);
|
super(control);
|
||||||
|
|
||||||
VBox vbox = new VBox(3);
|
VBox vbox = new VBox(3);
|
||||||
@ -310,13 +283,16 @@ public final class LogWindow extends Stage {
|
|||||||
|
|
||||||
{
|
{
|
||||||
HBox hBox = new HBox(3);
|
HBox hBox = new HBox(3);
|
||||||
hBox.getChildren().setAll(
|
for (int i = 0; i < LEVELS.length; i++) {
|
||||||
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
|
ToggleButton button = new ToggleButton();
|
||||||
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
|
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
|
||||||
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
|
button.getStyleClass().add("log-toggle");
|
||||||
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
|
button.textProperty().bind(control.buttonText[i]);
|
||||||
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
|
button.setSelected(true);
|
||||||
);
|
control.showLevel[i].bind(button.selectedProperty());
|
||||||
|
hBox.getChildren().add(button);
|
||||||
|
}
|
||||||
|
|
||||||
borderPane.setRight(hBox);
|
borderPane.setRight(hBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,11 +323,11 @@ public final class LogWindow extends Stage {
|
|||||||
|
|
||||||
setOnMouseClicked(event -> {
|
setOnMouseClicked(event -> {
|
||||||
if (!event.isControlDown()) {
|
if (!event.isControlDown()) {
|
||||||
for (ListCell<Log> logListCell: selected) {
|
for (ListCell<Log> logListCell : selected) {
|
||||||
if (logListCell != this) {
|
if (logListCell != this) {
|
||||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||||
if (logListCell.getItem() != null) {
|
if (logListCell.getItem() != null) {
|
||||||
logListCell.getItem().selected = false;
|
logListCell.getItem().setSelected(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -362,7 +338,7 @@ public final class LogWindow extends Stage {
|
|||||||
selected.add(this);
|
selected.add(this);
|
||||||
pseudoClassStateChanged(SELECTED, true);
|
pseudoClassStateChanged(SELECTED, true);
|
||||||
if (getItem() != null) {
|
if (getItem() != null) {
|
||||||
getItem().selected = true;
|
getItem().setSelected(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -377,18 +353,18 @@ public final class LogWindow extends Stage {
|
|||||||
lastCell.value = this;
|
lastCell.value = this;
|
||||||
|
|
||||||
pseudoClassStateChanged(EMPTY, empty);
|
pseudoClassStateChanged(EMPTY, empty);
|
||||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
|
||||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
|
||||||
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
|
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
|
||||||
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
|
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
|
||||||
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
|
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
|
||||||
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
|
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
|
||||||
pseudoClassStateChanged(SELECTED, !empty && item.selected);
|
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
setText(null);
|
setText(null);
|
||||||
} else {
|
} else {
|
||||||
setText(item.log);
|
setText(item.getLog());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -398,10 +374,9 @@ public final class LogWindow extends Stage {
|
|||||||
StringBuilder stringBuilder = new StringBuilder();
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
for (Log item : listView.getItems()) {
|
for (Log item : listView.getItems()) {
|
||||||
if (item != null && item.selected) {
|
if (item != null && item.isSelected()) {
|
||||||
if (item.log != null) {
|
if (item.getLog() != null)
|
||||||
stringBuilder.append(item.log);
|
stringBuilder.append(item.getLog());
|
||||||
}
|
|
||||||
stringBuilder.append('\n');
|
stringBuilder.append('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,11 +392,6 @@ public final class LogWindow extends Stage {
|
|||||||
{
|
{
|
||||||
BorderPane bottom = new BorderPane();
|
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);
|
HBox hBox = new HBox(3);
|
||||||
bottom.setRight(hBox);
|
bottom.setRight(hBox);
|
||||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
@ -432,24 +402,24 @@ public final class LogWindow extends Stage {
|
|||||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||||
|
|
||||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
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"));
|
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()) {
|
if (SystemUtils.supportJVMAttachment()) {
|
||||||
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
|
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
|
||||||
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
|
|
||||||
} else {
|
} else {
|
||||||
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
|
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
|
||||||
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
|
|
||||||
exportDumpButton.setDisable(true);
|
exportDumpButton.setDisable(true);
|
||||||
}
|
}
|
||||||
|
exportDumpPane.setContent(exportDumpButton);
|
||||||
|
|
||||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
|
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||||
|
|
||||||
vbox.getChildren().add(bottom);
|
vbox.getChildren().add(bottom);
|
||||||
}
|
}
|
||||||
|
@ -728,10 +728,8 @@ logwindow.title=Log
|
|||||||
logwindow.help=You can go to the HMCL community and find others for help
|
logwindow.help=You can go to the HMCL community and find others for help
|
||||||
logwindow.autoscroll=Auto-scroll
|
logwindow.autoscroll=Auto-scroll
|
||||||
logwindow.export_game_crash_logs=Export Crash Logs
|
logwindow.export_game_crash_logs=Export Crash Logs
|
||||||
logwindow.export_dump.dependency_ok.button=Export Game Stack Dump
|
logwindow.export_dump=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=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.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.
|
|
||||||
|
|
||||||
main_page=Home
|
main_page=Home
|
||||||
|
|
||||||
|
@ -673,10 +673,8 @@ logwindow.title=Registro
|
|||||||
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
|
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
|
||||||
logwindow.autoscroll=Desplazamiento automático
|
logwindow.autoscroll=Desplazamiento automático
|
||||||
logwindow.export_game_crash_logs=Exportar registros de errores
|
logwindow.export_game_crash_logs=Exportar registros de errores
|
||||||
logwindow.export_dump.dependency_ok.button=Exportar volcado de pila de juegos
|
logwindow.export_dump=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=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.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.
|
|
||||||
|
|
||||||
|
|
||||||
main_page=Inicio
|
main_page=Inicio
|
||||||
|
@ -607,10 +607,8 @@ logwindow.title=記錄
|
|||||||
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
|
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
|
||||||
logwindow.autoscroll=自動滾動
|
logwindow.autoscroll=自動滾動
|
||||||
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
|
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
|
||||||
logwindow.export_dump.dependency_ok.button=導出遊戲運行棧
|
logwindow.export_dump=導出遊戲運行棧
|
||||||
logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒)
|
logwindow.export_dump.no_dependency=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
|
||||||
logwindow.export_dump.no_dependency.button=導出遊戲運行棧(不兼容)
|
|
||||||
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL QQ 群或 Discord 频道尋求幫助。
|
|
||||||
|
|
||||||
main_page=首頁
|
main_page=首頁
|
||||||
|
|
||||||
|
@ -606,10 +606,8 @@ logwindow.title=日志
|
|||||||
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
||||||
logwindow.autoscroll=自动滚动
|
logwindow.autoscroll=自动滚动
|
||||||
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||||
logwindow.export_dump.dependency_ok.button=导出游戏运行栈
|
logwindow.export_dump=导出游戏运行栈
|
||||||
logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒)
|
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
|
||||||
logwindow.export_dump.no_dependency.button=导出游戏运行栈(不兼容)
|
|
||||||
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 频道寻求帮助。
|
|
||||||
|
|
||||||
main_page=主页
|
main_page=主页
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui;
|
|||||||
import org.jackhuang.hmcl.JavaFXLauncher;
|
import org.jackhuang.hmcl.JavaFXLauncher;
|
||||||
import org.jackhuang.hmcl.game.ClassicVersion;
|
import org.jackhuang.hmcl.game.ClassicVersion;
|
||||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||||
|
import org.jackhuang.hmcl.game.Log;
|
||||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||||
@ -35,8 +35,6 @@ import java.util.Arrays;
|
|||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
|
||||||
|
|
||||||
public class GameCrashWindowTest {
|
public class GameCrashWindowTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -57,7 +55,7 @@ public class GameCrashWindowTest {
|
|||||||
.setGameDir(new File("."))
|
.setGameDir(new File("."))
|
||||||
.create(),
|
.create(),
|
||||||
Arrays.stream(logs.split("\\n"))
|
Arrays.stream(logs.split("\\n"))
|
||||||
.map(log -> pair(log, Log4jLevel.guessLevel(log)))
|
.map(Log::new)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
window.showAndWait();
|
window.showAndWait();
|
||||||
|
@ -346,9 +346,15 @@ public final class StringUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String parseEscapeSequence(String str) {
|
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;
|
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);
|
char ch = str.charAt(i);
|
||||||
if (ch == '\033') {
|
if (ch == '\033') {
|
||||||
inEscape = true;
|
inEscape = true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user