feat: export necessary (debug.log, latest.log, launch script, hmcl.log and json) crash information when game got crashed.
This commit is contained in:
@@ -61,11 +61,18 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
|||||||
import org.jackhuang.hmcl.util.platform.*;
|
import org.jackhuang.hmcl.util.platform.*;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
@@ -616,7 +623,7 @@ public final class LauncherHelper {
|
|||||||
if (showLogs)
|
if (showLogs)
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
logWindow = new LogWindow();
|
logWindow = new LogWindow();
|
||||||
logWindow.show();
|
logWindow.showNormal();
|
||||||
logWindowLatch.countDown();
|
logWindowLatch.countDown();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -703,21 +710,37 @@ public final class LauncherHelper {
|
|||||||
if (logWindow == null) {
|
if (logWindow == null) {
|
||||||
logWindow = new LogWindow();
|
logWindow = new LogWindow();
|
||||||
|
|
||||||
switch (exitType) {
|
|
||||||
case JVM_ERROR:
|
|
||||||
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
|
|
||||||
break;
|
|
||||||
case APPLICATION_ERROR:
|
|
||||||
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
|
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
|
||||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
logWindow.showGameCrashReport();
|
switch (exitType) {
|
||||||
|
case JVM_ERROR:
|
||||||
|
logWindow.setTitle(i18n("launch.failed.cannot_create_jvm"));
|
||||||
|
break;
|
||||||
|
case APPLICATION_ERROR:
|
||||||
|
logWindow.setTitle(i18n("launch.failed.exited_abnormally"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logWindow.showGameCrashReport(logs -> {
|
||||||
|
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||||
|
LogExporter.exportLogs(logFile, repository, version, logs, new CommandBuilder().addAll(process.getCommands()).toString())
|
||||||
|
.thenRunAsync(() -> {
|
||||||
|
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
if (Desktop.isDesktopSupported()) {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().open(logFile.toFile());
|
||||||
|
} catch (IOException | IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Schedulers.javafx())
|
||||||
|
.exceptionally(e -> {
|
||||||
|
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java
Normal file
78
HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 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.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.Zipper;
|
||||||
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public final class LogExporter {
|
||||||
|
private LogExporter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
|
||||||
|
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
|
||||||
|
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
|
||||||
|
List<String> versions = new ArrayList<>();
|
||||||
|
|
||||||
|
String currentVersionId = versionId;
|
||||||
|
while (true) {
|
||||||
|
Version currentVersion = gameRepository.getVersion(currentVersionId);
|
||||||
|
versions.add(currentVersionId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
|
||||||
|
currentVersionId = currentVersion.getInheritsFrom();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try (Zipper zipper = new Zipper(zipFile)) {
|
||||||
|
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
|
||||||
|
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
|
||||||
|
}
|
||||||
|
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
|
||||||
|
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
|
||||||
|
}
|
||||||
|
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
|
||||||
|
zipper.putTextFile(logs, "minecraft.log");
|
||||||
|
zipper.putTextFile(launchScript, OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||||
|
|
||||||
|
for (String id : versions) {
|
||||||
|
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
|
||||||
|
if (Files.exists(versionJson)) {
|
||||||
|
zipper.putFile(versionJson, id + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}, Schedulers.io());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,12 +30,13 @@ import javafx.css.PseudoClass;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -49,6 +50,7 @@ import java.util.ArrayDeque;
|
|||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
@@ -83,6 +85,8 @@ public final class LogWindow extends Stage {
|
|||||||
private final LogWindowImpl impl = new LogWindowImpl();
|
private final LogWindowImpl impl = new LogWindowImpl();
|
||||||
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
|
||||||
|
|
||||||
|
private Consumer<String> exportGameCrashInfoCallback;
|
||||||
|
|
||||||
private boolean stopCheckLogCount = false;
|
private boolean stopCheckLogCount = false;
|
||||||
|
|
||||||
public LogWindow() {
|
public LogWindow() {
|
||||||
@@ -104,7 +108,9 @@ public final class LogWindow extends Stage {
|
|||||||
if (!stopCheckLogCount) checkLogCount();
|
if (!stopCheckLogCount) checkLogCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showGameCrashReport() {
|
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
|
||||||
|
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
|
||||||
|
this.impl.showCrashReport.set(true);
|
||||||
stopCheckLogCount = true;
|
stopCheckLogCount = true;
|
||||||
for (Log log : impl.listView.getItems()) {
|
for (Log log : impl.listView.getItems()) {
|
||||||
if (log.log.contains("Minecraft Crash Report")) {
|
if (log.log.contains("Minecraft Crash Report")) {
|
||||||
@@ -117,6 +123,11 @@ public final class LogWindow extends Stage {
|
|||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.level).get()).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
@@ -148,6 +159,7 @@ public final class LogWindow extends Stage {
|
|||||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||||
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||||
|
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||||
|
|
||||||
LogWindowImpl() {
|
LogWindowImpl() {
|
||||||
getStyleClass().add("log-window");
|
getStyleClass().add("log-window");
|
||||||
@@ -204,6 +216,11 @@ public final class LogWindow extends Stage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onExportGameCrashInfo() {
|
||||||
|
if (exportGameCrashInfoCallback == null) return;
|
||||||
|
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Skin<?> createDefaultSkin() {
|
protected Skin<?> createDefaultSkin() {
|
||||||
return new LogWindowSkin(this);
|
return new LogWindowSkin(this);
|
||||||
@@ -310,7 +327,15 @@ public final class LogWindow extends Stage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
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);
|
||||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
hBox.setPadding(new Insets(0, 3, 0, 3));
|
hBox.setPadding(new Insets(0, 3, 0, 3));
|
||||||
|
|
||||||
@@ -328,7 +353,7 @@ public final class LogWindow extends Stage {
|
|||||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
||||||
|
|
||||||
vbox.getChildren().add(hBox);
|
vbox.getChildren().add(bottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ logwindow.show_lines=Show Lines
|
|||||||
logwindow.terminate_game=Terminate Game
|
logwindow.terminate_game=Terminate Game
|
||||||
logwindow.title=Log
|
logwindow.title=Log
|
||||||
logwindow.autoscroll=Autoscroll
|
logwindow.autoscroll=Autoscroll
|
||||||
|
logwindow.export_game_crash_logs=Export game crash info
|
||||||
|
|
||||||
main_page=Home
|
main_page=Home
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ logwindow.show_lines=显示行数
|
|||||||
logwindow.terminate_game=结束游戏进程
|
logwindow.terminate_game=结束游戏进程
|
||||||
logwindow.title=日志
|
logwindow.title=日志
|
||||||
logwindow.autoscroll=自动滚动
|
logwindow.autoscroll=自动滚动
|
||||||
|
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||||
|
|
||||||
main_page=主页
|
main_page=主页
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user