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 f43ed7183..7c3523565 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -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 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; + }); + }); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java new file mode 100644 index 000000000..361e31a58 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java @@ -0,0 +1,78 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.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 exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) { + Path runDirectory = gameRepository.getRunDirectory(versionId).toPath(); + Path baseDirectory = gameRepository.getBaseDirectory().toPath(); + List 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()); + } +} 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 249527463..74da97ed4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -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 logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount()); + private Consumer 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 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 buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList()); private List showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList()); private JFXComboBox 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); } } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 329818b67..ac2b7aa3c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 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 2cc74d7dc..8df4d3625 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -306,6 +306,7 @@ logwindow.show_lines=显示行数 logwindow.terminate_game=结束游戏进程 logwindow.title=日志 logwindow.autoscroll=自动滚动 +logwindow.export_game_crash_logs=导出游戏崩溃信息 main_page=主页