feat: export necessary (debug.log, latest.log, launch script, hmcl.log and json) crash information when game got crashed.

This commit is contained in:
huanghongxun 2021-08-29 05:07:41 +08:00
parent 5a1ebe906d
commit de99213d8a
5 changed files with 142 additions and 14 deletions

View File

@ -61,11 +61,18 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
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.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -616,7 +623,7 @@ public final class LauncherHelper {
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow();
logWindow.show();
logWindow.showNormal();
logWindowLatch.countDown();
});
}
@ -703,21 +710,37 @@ public final class LauncherHelper {
if (logWindow == null) {
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);
for (Map.Entry<String, Log4jLevel> entry : logs)
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;
});
});
});
}

View 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());
}
}

View File

@ -30,12 +30,13 @@ import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.*;
import java.awt.*;
@ -49,6 +50,7 @@ import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -83,6 +85,8 @@ public final class LogWindow extends Stage {
private final LogWindowImpl impl = new LogWindowImpl();
private final WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
public LogWindow() {
@ -104,7 +108,9 @@ public final class LogWindow extends Stage {
if (!stopCheckLogCount) checkLogCount();
}
public void showGameCrashReport() {
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
@ -117,6 +123,11 @@ public final class LogWindow extends Stage {
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
}
private void shakeLogs() {
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<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private JFXComboBox<String> cboLines = new JFXComboBox<>();
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
LogWindowImpl() {
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
protected Skin<?> createDefaultSkin() {
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);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setPadding(new Insets(0, 3, 0, 3));
@ -328,7 +353,7 @@ public final class LogWindow extends Stage {
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
vbox.getChildren().add(hBox);
vbox.getChildren().add(bottom);
}
}
}

View File

@ -305,6 +305,7 @@ logwindow.show_lines=Show Lines
logwindow.terminate_game=Terminate Game
logwindow.title=Log
logwindow.autoscroll=Autoscroll
logwindow.export_game_crash_logs=Export game crash info
main_page=Home

View File

@ -306,6 +306,7 @@ logwindow.show_lines=显示行数
logwindow.terminate_game=结束游戏进程
logwindow.title=日志
logwindow.autoscroll=自动滚动
logwindow.export_game_crash_logs=导出游戏崩溃信息
main_page=主页