From bced94a149389b72f38c4a861586fa69db0a6b87 Mon Sep 17 00:00:00 2001 From: YELANDAOKONG Date: Sun, 23 Jun 2024 04:03:53 +0800 Subject: [PATCH 1/4] =?UTF-8?q?Support=20#2971:=20=E5=9C=A8=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E7=AA=97=E5=8F=A3=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/GameCrashWindow.java | 38 ++++++- .../jackhuang/hmcl/util/GameLogUploader.java | 104 ++++++++++++++++++ .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 3 + .../assets/lang/I18N_zh_CN.properties | 3 + 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java 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 327d17a308..c6c1281566 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -29,6 +29,8 @@ import javafx.scene.control.Alert; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -42,7 +44,9 @@ import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.util.GameLogUploader; import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.logging.Logger; import org.jackhuang.hmcl.util.Pair; @@ -298,6 +302,36 @@ private void exportGameCrashInfo() { }); } + private void uploadGameLog(JFXButton uploadButton) { + String defaultText = i18n("logwindow.upload_game_crash_logs"); + uploadButton.setDisable(true); + Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log"); + String log = null; + try { + log = FileUtils.readText(latestLog); + } catch (IOException ex) { + LOG.warning("Failed to read latest.log", ex); + uploadButton.setText(i18n("logwindow.upload_game_crash_logs.failed")); + uploadButton.setDisable(false); + return; + } + + GameLogUploader.UploadResult result = GameLogUploader.upload(GameLogUploader.HostingPlatform.MCLOGS, log); + if (result != null) { + LOG.info("Uploaded game logs to " + result.getUrl()); + Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ + putString(result.getUrl()); + }}); + uploadButton.setText(i18n("logwindow.upload_game_crash_logs.copied") + " " + result.getUrl()); + return; + }else{ + LOG.warning("Failed to upload game logs"); + uploadButton.setText(i18n("logwindow.upload_game_crash_logs.failed")); + uploadButton.setDisable(false); + return; + } + } + private final class View extends VBox { View() { @@ -431,11 +465,13 @@ private final class View extends VBox { helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); + JFXButton uploadButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); + uploadButton.setOnMouseClicked(e -> uploadGameLog(uploadButton)); toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); toolBar.getStyleClass().add("jfx-tool-bar"); - toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton); + toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadButton); } getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java new file mode 100644 index 0000000000..66bb40170b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java @@ -0,0 +1,104 @@ +package org.jackhuang.hmcl.util; + +import com.google.gson.Gson; +import org.jackhuang.hmcl.util.io.HttpRequest; +import org.jackhuang.hmcl.util.logging.Logger; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class GameLogUploader { + + public enum HostingPlatform { + /* + * HTTP: POST https://api.mclo.gs/1/log + * Data Type: application/x-www-form-urlencoded + * Field: content + * Type: string + * Description: The raw log file content as string. Maximum length is 10MiB and 25k lines, will be shortened if necessary. + */ + MCLOGS("mclo.gs", "https://mclo.gs/", "https://docs.mclo.gs/api/v1/log"), + /* + * HTTP: POST https://file.io/ + * Data Type: multipart/form-data + * Fields: + * file string($binary) + expires + maxDownloads integer + autoDelete boolean + */ + FILE_IO("file.io", "https://www.file.io/", "https://www.file.io/developers") + + + + ; + + private final String name; + private final String url; + private final String documents; + + HostingPlatform(String name, String url, String documents) { + this.name = name; + this.url = url; + this.documents = documents; + } + } + + public static class UploadResult { + private String url; + private String id; + private String raw; + + public UploadResult(String url, String id, String raw) { + this.url = url; + this.id = id; + this.raw = raw; + } + + public String getUrl() { + return url; + } + + public String getId() { + return id; + } + + public String getDocuments() { + return raw; + } + } + + public static UploadResult upload(HostingPlatform platform, String content) { + Gson gson = new Gson(); + try { + switch (platform) { + case MCLOGS: + HttpRequest.HttpPostRequest request = HttpRequest.POST("https://api.mclo.gs/1/log"); + request.header("Content-Type", "application/x-www-form-urlencoded"); + HashMap payload = new HashMap<>(); + payload.put("content", content); + request.form(payload); + + String response = request.getString(); + Map json = gson.fromJson(response, Map.class); + if (!json.containsKey("success")) { + return null; + } + if ((boolean) json.get("success")) { + return new UploadResult( + (String) json.get("url"), + (String) json.get("id"), + (String) json.get("raw") + ); + } + return null; + default: + return null; + } + } catch (IOException ex) { + Logger.LOG.error("Failed to upload game log", ex); + return null; + } + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 77ed7f4190..58217574dc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -727,6 +727,9 @@ logwindow.terminate_game=Kill Game Process logwindow.title=Log logwindow.help=You can go to the HMCL community and find others for help logwindow.autoscroll=Auto-scroll +logwindow.upload_game_crash_logs=Upload Game Logs +logwindow.upload_game_crash_logs.failed=Upload Failed +logwindow.upload_game_crash_logs.copied=Copied URL to clipboard 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) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0063a9cb93..e7be5be7fb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -606,6 +606,9 @@ logwindow.terminate_game=結束遊戲處理程式 logwindow.title=記錄 logwindow.help=你可以前往 HMCL 社區,尋找他人幫助 logwindow.autoscroll=自動滾動 +logwindow.upload_game_crash_logs=上傳遊戲日誌訊息 +logwindow.upload_game_crash_logs.failed=上傳遊戲日誌失敗 +logwindow.upload_game_crash_logs.copied=網址已複製到剪貼板 logwindow.export_game_crash_logs=導出遊戲崩潰訊息 logwindow.export_dump.dependency_ok.button=導出遊戲運行棧 logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒) 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 ee0fca1ee2..291fd0ebd5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -605,6 +605,9 @@ logwindow.terminate_game=结束游戏进程 logwindow.title=日志 logwindow.help=你可以前往 HMCL 社区,寻找他人帮助 logwindow.autoscroll=自动滚动 +logwindow.upload_game_crash_logs=上传游戏日志 +logwindow.upload_game_crash_logs.failed=上传游戏日志失败 +logwindow.upload_game_crash_logs.copied=链接已复制到剪切板 logwindow.export_game_crash_logs=导出游戏崩溃信息 logwindow.export_dump.dependency_ok.button=导出游戏运行栈 logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒) From 33325e381dce42b0fce7368e44f09b07fec68865 Mon Sep 17 00:00:00 2001 From: YELANDAOKONG Date: Sun, 23 Jun 2024 04:19:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?Support=20#2971:=20=E5=9C=A8=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E7=AA=97=E5=8F=A3=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/GameCrashWindow.java | 23 ++++++++++++------- .../jackhuang/hmcl/util/GameLogUploader.java | 8 ++++++- 2 files changed, 22 insertions(+), 9 deletions(-) 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 c6c1281566..2b35e963cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -303,16 +303,17 @@ private void exportGameCrashInfo() { } private void uploadGameLog(JFXButton uploadButton) { - String defaultText = i18n("logwindow.upload_game_crash_logs"); + Alert alert; uploadButton.setDisable(true); Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log"); String log = null; try { log = FileUtils.readText(latestLog); } catch (IOException ex) { - LOG.warning("Failed to read latest.log", ex); - uploadButton.setText(i18n("logwindow.upload_game_crash_logs.failed")); uploadButton.setDisable(false); + alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_crash_logs.failed") + "\n" + StringUtils.getStackTrace(ex)); + alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert.showAndWait(); return; } @@ -322,12 +323,16 @@ private void uploadGameLog(JFXButton uploadButton) { Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ putString(result.getUrl()); }}); - uploadButton.setText(i18n("logwindow.upload_game_crash_logs.copied") + " " + result.getUrl()); + alert = new Alert(Alert.AlertType.INFORMATION, i18n("logwindow.upload_game_crash_logs.copied") + "\n" + result.getUrl()); + alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert.showAndWait(); return; }else{ LOG.warning("Failed to upload game logs"); - uploadButton.setText(i18n("logwindow.upload_game_crash_logs.failed")); uploadButton.setDisable(false); + alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_crash_logs.failed")); + alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert.showAndWait(); return; } } @@ -465,13 +470,15 @@ private final class View extends VBox { helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); - JFXButton uploadButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); - uploadButton.setOnMouseClicked(e -> uploadGameLog(uploadButton)); + JFXButton uploadLogButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); + uploadLogButton.setOnMouseClicked(e -> uploadGameLog(uploadLogButton)); + + toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); toolBar.getStyleClass().add("jfx-tool-bar"); - toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadButton); + toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadLogButton); } getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java index 66bb40170b..077991f7d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java @@ -5,6 +5,7 @@ import org.jackhuang.hmcl.util.logging.Logger; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -70,6 +71,10 @@ public String getDocuments() { } public static UploadResult upload(HostingPlatform platform, String content) { + return upload(platform, content.getBytes(StandardCharsets.UTF_8)); + } + + public static UploadResult upload(HostingPlatform platform, byte[] content) { Gson gson = new Gson(); try { switch (platform) { @@ -77,7 +82,8 @@ public static UploadResult upload(HostingPlatform platform, String content) { HttpRequest.HttpPostRequest request = HttpRequest.POST("https://api.mclo.gs/1/log"); request.header("Content-Type", "application/x-www-form-urlencoded"); HashMap payload = new HashMap<>(); - payload.put("content", content); + //编码 + payload.put("content", new String(content, StandardCharsets.UTF_8)); request.form(payload); String response = request.getString(); From 9ea7c3088621f28983c36bad42b33a13fa97b5eb Mon Sep 17 00:00:00 2001 From: YELANDAOKONG Date: Sun, 23 Jun 2024 06:37:34 +0800 Subject: [PATCH 3/4] =?UTF-8?q?Support=20#2971:=20=E5=9C=A8=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E7=AA=97=E5=8F=A3=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=B4=A9=E6=BA=83=E4=BF=A1=E6=81=AF=E5=8C=85?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/GameCrashWindow.java | 90 +++++++++++++-- .../jackhuang/hmcl/util/GameLogUploader.java | 105 ++++++++++++++++-- .../resources/assets/lang/I18N.properties | 8 +- .../resources/assets/lang/I18N_zh.properties | 8 +- .../assets/lang/I18N_zh_CN.properties | 8 +- 5 files changed, 192 insertions(+), 27 deletions(-) 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 2b35e963cb..971b00d68c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui; import com.jfoenix.controls.JFXButton; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; @@ -44,7 +45,6 @@ import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.GameLogUploader; import org.jackhuang.hmcl.util.Log4jLevel; @@ -62,6 +62,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -311,32 +312,98 @@ private void uploadGameLog(JFXButton uploadButton) { log = FileUtils.readText(latestLog); } catch (IOException ex) { uploadButton.setDisable(false); - alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_crash_logs.failed") + "\n" + StringUtils.getStackTrace(ex)); - alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_logs.failed") + "\n" + StringUtils.getStackTrace(ex)); + alert.setTitle(i18n("logwindow.upload_game_logs")); alert.showAndWait(); return; } - GameLogUploader.UploadResult result = GameLogUploader.upload(GameLogUploader.HostingPlatform.MCLOGS, log); + GameLogUploader.UploadResult result = GameLogUploader.upload(GameLogUploader.HostingPlatform.MCLOGS, latestLog, log); if (result != null) { LOG.info("Uploaded game logs to " + result.getUrl()); Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ putString(result.getUrl()); }}); - alert = new Alert(Alert.AlertType.INFORMATION, i18n("logwindow.upload_game_crash_logs.copied") + "\n" + result.getUrl()); - alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert = new Alert(Alert.AlertType.INFORMATION, i18n("logwindow.upload_game_logs.copied") + "\n" + result.getUrl()); + alert.setTitle(i18n("logwindow.upload_game_logs")); alert.showAndWait(); return; }else{ LOG.warning("Failed to upload game logs"); uploadButton.setDisable(false); - alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_crash_logs.failed")); - alert.setTitle(i18n("logwindow.upload_game_crash_logs")); + alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_logs.failed")); + alert.setTitle(i18n("logwindow.upload_game_logs")); alert.showAndWait(); return; } } + private void uploadGameCrashInfo(JFXButton uploadCrashButton) { + uploadCrashButton.setDisable(true); + 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))) + .thenComposeAsync(logs -> + LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString())) + .handleAsync((result, exception) -> { + try { + if (exception == null) { + GameLogUploader.UploadResult uploadResult = GameLogUploader.upload( + GameLogUploader.HostingPlatform.FILE_IO, + logFile, + new byte[]{} + ); + if (uploadResult == null) { + LOG.warning("Failed to upload game crash info"); + Platform.runLater(() -> { + uploadCrashButton.setDisable(false); + Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); + alert.setTitle(i18n("settings.launcher.launcher_log.export")); + alert.showAndWait(); + }); + return null; + } else { + LOG.info("Uploaded game crash info to " + uploadResult.getUrl()); + Platform.runLater(() -> { + Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ + putString(uploadResult.getUrl()); + }}); + + Alert alert = new Alert( + Alert.AlertType.INFORMATION, + i18n("logwindow.upload_game_logs.copied") + "\n" + uploadResult.getUrl() + "\n" + + i18n("logwindow.upload_game_crash_logs.expires_time", uploadResult.getRaw()) + ); + alert.setTitle(i18n("settings.launcher.launcher_log.export")); + alert.showAndWait(); + }); + return null; + } + } else { + LOG.warning("Failed to export game crash info", exception); + Platform.runLater(() -> { + uploadCrashButton.setDisable(false); + Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); + alert.setTitle(i18n("settings.launcher.launcher_log.export")); + alert.showAndWait(); + }); + } + return null; + } + catch (Exception e) { + LOG.warning("Failed to upload game crash info", e); + Platform.runLater(() -> { + uploadCrashButton.setDisable(false); + Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); + alert.setTitle(i18n("settings.launcher.launcher_log.export")); + alert.showAndWait(); + }); + return null; + } + }); + } + private final class View extends VBox { View() { @@ -470,7 +537,10 @@ private final class View extends VBox { helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); - JFXButton uploadLogButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); + JFXButton uploadCrashButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); + uploadCrashButton.setOnMouseClicked(e -> uploadGameCrashInfo(uploadCrashButton)); + + JFXButton uploadLogButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_logs")); uploadLogButton.setOnMouseClicked(e -> uploadGameLog(uploadLogButton)); @@ -478,7 +548,7 @@ private final class View extends VBox { toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); toolBar.getStyleClass().add("jfx-tool-bar"); - toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadLogButton); + toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadCrashButton, uploadLogButton); } getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java index 077991f7d6..8183610607 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/GameLogUploader.java @@ -4,8 +4,13 @@ import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.logging.Logger; -import java.io.IOException; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; @@ -51,6 +56,8 @@ public static class UploadResult { private String id; private String raw; + + public UploadResult(String url, String id, String raw) { this.url = url; this.id = id; @@ -65,24 +72,24 @@ public String getId() { return id; } - public String getDocuments() { + public String getRaw() { return raw; } } - public static UploadResult upload(HostingPlatform platform, String content) { - return upload(platform, content.getBytes(StandardCharsets.UTF_8)); + public static UploadResult upload(HostingPlatform platform, Path filepath, String content) { + return upload(platform, filepath, content.getBytes(StandardCharsets.UTF_8)); } - public static UploadResult upload(HostingPlatform platform, byte[] content) { + public static UploadResult upload(HostingPlatform platform, Path filepath, byte[] content) { Gson gson = new Gson(); try { switch (platform) { - case MCLOGS: + case MCLOGS: { HttpRequest.HttpPostRequest request = HttpRequest.POST("https://api.mclo.gs/1/log"); request.header("Content-Type", "application/x-www-form-urlencoded"); HashMap payload = new HashMap<>(); - //编码 + payload.put("content", new String(content, StandardCharsets.UTF_8)); request.form(payload); @@ -99,10 +106,92 @@ public static UploadResult upload(HostingPlatform platform, byte[] content) { ); } return null; + } + case FILE_IO: { + String boundary = Long.toHexString(System.currentTimeMillis()); // 创建一个唯一的边界字符串 + String LINE_FEED = "\r\n"; + URL url = new URL("https://file.io/"); + + HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); + + httpConn.setDoOutput(true); + httpConn.setDoInput(true); + httpConn.setRequestMethod("POST"); + httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + httpConn.setRequestProperty("Accept", "application/json"); + + FileInputStream fileInputStream = new FileInputStream(filepath.toFile()); + OutputStream outputStream = httpConn.getOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8), true); + + writer.append("--").append(boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"file\"; filename=\"").append(filepath.getFileName().toString()).append("\"").append(LINE_FEED); + writer.append("Content-Type: application/octet-stream").append(LINE_FEED); + writer.append(LINE_FEED); + writer.flush(); + // outputStream.write(content); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = fileInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + writer.append(LINE_FEED); + + writer.append("--" + boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"autoDelete\"").append(LINE_FEED); + writer.append(LINE_FEED); + writer.append("true").append(LINE_FEED); + + writer.append("--" + boundary).append(LINE_FEED); + writer.append("Content-Disposition: form-data; name=\"expires\"").append(LINE_FEED); + writer.append(LINE_FEED); + writer.append("7d").append(LINE_FEED); + + writer.append("--").append(boundary).append("--").append(LINE_FEED).append(LINE_FEED); + writer.flush(); + + int responseCode = httpConn.getResponseCode(); + Logger.LOG.info("Http Response Code: " + responseCode); + if(responseCode != 200){ + BufferedReader err = new BufferedReader(new InputStreamReader(httpConn.getErrorStream())); + String errLine; + StringBuilder errResponse = new StringBuilder(); + while ((errLine = err.readLine()) != null) { + errResponse.append(errLine); + } + Logger.LOG.error("Error response from file.io: " + errResponse); + return null; + } + + + BufferedReader in = new BufferedReader(new InputStreamReader(httpConn.getInputStream())); + String inputLine; + StringBuilder response = new StringBuilder(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + Logger.LOG.info("Response from file.io: " + response); + in.close(); + + Map json = gson.fromJson(response.toString(), Map.class); + if (!json.containsKey("success")) { + return null; + } + if ((boolean) json.get("success")) { + return new UploadResult( + (String) json.get("link"), + (String) json.get("key"), + (String) json.get("expires") + ); + } + return null; + } default: return null; } - } catch (IOException ex) { + } catch (Exception ex) { Logger.LOG.error("Failed to upload game log", ex); return null; } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 58217574dc..e6882e1390 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -727,9 +727,11 @@ logwindow.terminate_game=Kill Game Process logwindow.title=Log logwindow.help=You can go to the HMCL community and find others for help logwindow.autoscroll=Auto-scroll -logwindow.upload_game_crash_logs=Upload Game Logs -logwindow.upload_game_crash_logs.failed=Upload Failed -logwindow.upload_game_crash_logs.copied=Copied URL to clipboard +logwindow.upload_game_logs=Upload Game Logs +logwindow.upload_game_logs.failed=Upload Failed +logwindow.upload_game_logs.copied=Copied URL to clipboard +logwindow.upload_game_crash_logs.expires_time=Expire: %s +logwindow.upload_game_crash_logs=Upload Crash Logs 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) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e7be5be7fb..6d8425dd38 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -606,9 +606,11 @@ logwindow.terminate_game=結束遊戲處理程式 logwindow.title=記錄 logwindow.help=你可以前往 HMCL 社區,尋找他人幫助 logwindow.autoscroll=自動滾動 -logwindow.upload_game_crash_logs=上傳遊戲日誌訊息 -logwindow.upload_game_crash_logs.failed=上傳遊戲日誌失敗 -logwindow.upload_game_crash_logs.copied=網址已複製到剪貼板 +logwindow.upload_game_logs=上傳遊戲日誌訊息 +logwindow.upload_game_logs.failed=上傳遊戲日誌失敗 +logwindow.upload_game_logs.copied=網址已複製到剪貼板 +logwindow.upload_game_crash_logs.expires_time=過期時間:%s +logwindow.upload_game_crash_logs=上傳遊戲崩潰訊息 logwindow.export_game_crash_logs=導出遊戲崩潰訊息 logwindow.export_dump.dependency_ok.button=導出遊戲運行棧 logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒) 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 291fd0ebd5..cf9d53c771 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -605,9 +605,11 @@ logwindow.terminate_game=结束游戏进程 logwindow.title=日志 logwindow.help=你可以前往 HMCL 社区,寻找他人帮助 logwindow.autoscroll=自动滚动 -logwindow.upload_game_crash_logs=上传游戏日志 -logwindow.upload_game_crash_logs.failed=上传游戏日志失败 -logwindow.upload_game_crash_logs.copied=链接已复制到剪切板 +logwindow.upload_game_logs=上传游戏日志 +logwindow.upload_game_logs.failed=上传游戏日志失败 +logwindow.upload_game_logs.copied=链接已复制到剪切板 +logwindow.upload_game_crash_logs.expires_time=过期时间:%s +logwindow.upload_game_crash_logs=上传游戏崩溃信息 logwindow.export_game_crash_logs=导出游戏崩溃信息 logwindow.export_dump.dependency_ok.button=导出游戏运行栈 logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒) From 667d953a2d0bb786bd01143dd3c94221fe6d5608 Mon Sep 17 00:00:00 2001 From: YELANDAOKONG Date: Mon, 24 Jun 2024 00:04:18 +0800 Subject: [PATCH 4/4] =?UTF-8?q?Support=20#2971=20=E5=9C=A8=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E7=AA=97=E5=8F=A3=E6=B7=BB=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E5=B4=A9=E6=BA=83=E6=97=A5=E5=BF=97=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/game/LogExporter.java | 63 ++++++++++ .../jackhuang/hmcl/ui/GameCrashWindow.java | 117 ++++-------------- 2 files changed, 88 insertions(+), 92 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java index 4474750e3d..4261f4b0f4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java @@ -23,9 +23,11 @@ import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import java.io.FileInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -85,6 +87,67 @@ public static CompletableFuture exportLogs(Path zipFile, DefaultGameReposi }); } + private static StringBuilder appendFileWithDivider(StringBuilder logsText, String fileName, String fileData) { + return logsText.append("\n") + .append("[00:00:00] $> FILE DATA START [ \"") + .append(fileName) + .append("\" ] ") + .append("=====================================================") + .append("=====================================================") + .append("\n\n\n\n\n") + .append(fileData) + .append("\n\n\n\n\n") + .append("[00:00:00] $> FILE DATA END [ \"") + .append(fileName) + .append("\" ] ") + .append("=====================================================") + .append("=====================================================") + .append("\n\n\n"); + } + + public static StringBuilder exportLogsText(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; + HashSet resolvedSoFar = new HashSet<>(); + while (true) { + if (resolvedSoFar.contains(currentVersionId)) break; + resolvedSoFar.add(currentVersionId); + Version currentVersion = gameRepository.getVersion(currentVersionId); + versions.add(currentVersionId); + + if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) { + currentVersionId = currentVersion.getInheritsFrom(); + } else { + break; + } + } + + StringBuilder logsText = new StringBuilder(); + appendFileWithDivider(logsText, "hmcl.log", LOG.getLogs()); + appendFileWithDivider(logsText, "minecraft.log", logs); + appendFileWithDivider(logsText, + OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh", Logger.filterForbiddenToken(launchScript) + ); + try{ + for (String id : versions) { + Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json"); + if (Files.exists(versionJson)) { + try(FileInputStream fis = new FileInputStream(versionJson.toFile())) { + byte[] bytes = new byte[fis.available()]; + fis.read(bytes); + appendFileWithDivider(logsText, id + ".json", new String(bytes, StandardCharsets.UTF_8)); + } + } + } + }catch (IOException e){ + throw new UncheckedIOException(e); + } + return logsText; + } + private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) { try (DirectoryStream stream = Files.newDirectoryStream(directory, fileExtension)) { long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); 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 971b00d68c..4b6274795a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -307,29 +307,31 @@ private void uploadGameLog(JFXButton uploadButton) { Alert alert; uploadButton.setDisable(true); Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log"); - String log = null; - try { - log = FileUtils.readText(latestLog); - } catch (IOException ex) { - uploadButton.setDisable(false); - alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_logs.failed") + "\n" + StringUtils.getStackTrace(ex)); - alert.setTitle(i18n("logwindow.upload_game_logs")); - alert.showAndWait(); - return; + try{ + String logs = FileUtils.readText(latestLog); + StringBuilder logAllInOne = LogExporter.exportLogsText(repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()); + + GameLogUploader.UploadResult result = GameLogUploader.upload(GameLogUploader.HostingPlatform.MCLOGS, latestLog, logAllInOne.toString()); + if (result != null) { + LOG.info("Uploaded game logs to " + result.getUrl()); + Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ + putString(result.getUrl()); + }}); + alert = new Alert(Alert.AlertType.INFORMATION, i18n("logwindow.upload_game_logs.copied") + "\n" + result.getUrl()); + alert.setTitle(i18n("logwindow.upload_game_logs")); + alert.showAndWait(); + return; + }else{ + LOG.warning("Failed to upload game logs"); + uploadButton.setDisable(false); + alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_logs.failed")); + alert.setTitle(i18n("logwindow.upload_game_logs")); + alert.showAndWait(); + return; + } } - - GameLogUploader.UploadResult result = GameLogUploader.upload(GameLogUploader.HostingPlatform.MCLOGS, latestLog, log); - if (result != null) { - LOG.info("Uploaded game logs to " + result.getUrl()); - Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ - putString(result.getUrl()); - }}); - alert = new Alert(Alert.AlertType.INFORMATION, i18n("logwindow.upload_game_logs.copied") + "\n" + result.getUrl()); - alert.setTitle(i18n("logwindow.upload_game_logs")); - alert.showAndWait(); - return; - }else{ - LOG.warning("Failed to upload game logs"); + catch (IOException ex){ + LOG.warning("Failed to upload game logs", ex); uploadButton.setDisable(false); alert = new Alert(Alert.AlertType.WARNING, i18n("logwindow.upload_game_logs.failed")); alert.setTitle(i18n("logwindow.upload_game_logs")); @@ -338,72 +340,6 @@ private void uploadGameLog(JFXButton uploadButton) { } } - private void uploadGameCrashInfo(JFXButton uploadCrashButton) { - uploadCrashButton.setDisable(true); - 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))) - .thenComposeAsync(logs -> - LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString())) - .handleAsync((result, exception) -> { - try { - if (exception == null) { - GameLogUploader.UploadResult uploadResult = GameLogUploader.upload( - GameLogUploader.HostingPlatform.FILE_IO, - logFile, - new byte[]{} - ); - if (uploadResult == null) { - LOG.warning("Failed to upload game crash info"); - Platform.runLater(() -> { - uploadCrashButton.setDisable(false); - Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); - }); - return null; - } else { - LOG.info("Uploaded game crash info to " + uploadResult.getUrl()); - Platform.runLater(() -> { - Clipboard.getSystemClipboard().setContent(new ClipboardContent() {{ - putString(uploadResult.getUrl()); - }}); - - Alert alert = new Alert( - Alert.AlertType.INFORMATION, - i18n("logwindow.upload_game_logs.copied") + "\n" + uploadResult.getUrl() + "\n" - + i18n("logwindow.upload_game_crash_logs.expires_time", uploadResult.getRaw()) - ); - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); - }); - return null; - } - } else { - LOG.warning("Failed to export game crash info", exception); - Platform.runLater(() -> { - uploadCrashButton.setDisable(false); - Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); - }); - } - return null; - } - catch (Exception e) { - LOG.warning("Failed to upload game crash info", e); - Platform.runLater(() -> { - uploadCrashButton.setDisable(false); - Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed")); - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); - }); - return null; - } - }); - } - private final class View extends VBox { View() { @@ -537,9 +473,6 @@ private final class View extends VBox { helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); - JFXButton uploadCrashButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_crash_logs")); - uploadCrashButton.setOnMouseClicked(e -> uploadGameCrashInfo(uploadCrashButton)); - JFXButton uploadLogButton = FXUtils.newRaisedButton(i18n("logwindow.upload_game_logs")); uploadLogButton.setOnMouseClicked(e -> uploadGameLog(uploadLogButton)); @@ -548,7 +481,7 @@ private final class View extends VBox { toolBar.setPadding(new Insets(8)); toolBar.setSpacing(8); toolBar.getStyleClass().add("jfx-tool-bar"); - toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadCrashButton, uploadLogButton); + toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton, uploadLogButton); } getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);