From 41fd38d8d1b905b42246be4db4759b751bd9880e Mon Sep 17 00:00:00 2001 From: Zkitefly <64117916+zkitefly@users.noreply.github.com> Date: Sun, 10 Aug 2025 21:55:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=97=A5=E5=BF=97=E6=97=B6?= =?UTF-8?q?=E9=99=84=E5=B8=A6=E6=9C=80=E8=BF=91=E6=97=A5=E5=BF=97=20(#4051?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Glavo --- .gitignore | 1 + .../jackhuang/hmcl/ui/main/SettingsPage.java | 47 +++-- .../jackhuang/hmcl/util/logging/Logger.java | 160 +++++++++++++----- 3 files changed, 158 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 58bd5e5e3..fc25cc840 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ hs_err_pid* /externalgames NVIDIA minecraft-exported-crash-info* +hmcl-exported-logs-* # gradle build /build/ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 90df9e610..a959d7421 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -23,6 +23,7 @@ import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.scene.control.ToggleGroup; +import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; @@ -40,10 +41,12 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.Lang.thread; @@ -123,22 +126,46 @@ public final class SettingsPage extends SettingsView { @Override protected void onExportLogs() { - // We cannot determine which file is JUL using. - // So we write all the logs to a new file. thread(() -> { - Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath(); + String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")); + List recentLogFiles = LOG.findRecentLogFiles(5); - LOG.info("Exporting logs to " + logFile); - try (OutputStream output = Files.newOutputStream(logFile)) { - LOG.exportLogs(output); + Path outputFile; + try { + if (recentLogFiles.isEmpty()) { + outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log"); + + LOG.info("Exporting latest logs to " + outputFile); + try (OutputStream output = Files.newOutputStream(outputFile)) { + LOG.exportLogs(output); + } + } else { + outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip"); + + LOG.info("Exporting latest logs to " + outputFile); + try (var os = Files.newOutputStream(outputFile); + var zos = new ZipOutputStream(os)) { + + for (Path path : recentLogFiles) { + String zipEntryName = path.getFileName().toString(); + zos.putNextEntry(new ZipEntry(zipEntryName)); + Files.copy(path, zos); + zos.closeEntry(); + } + + zos.putNextEntry(new ZipEntry("latest.log")); + LOG.exportLogs(zos); + zos.closeEntry(); + } + } } catch (IOException e) { - Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR)); LOG.warning("Failed to export logs", e); + Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR)); return; } - Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile))); - FXUtils.showFileInExplorer(logFile); + Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile))); + FXUtils.showFileInExplorer(outputFile); }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/Logger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/Logger.java index 3fbf1fbe6..a97449fa1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/Logger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/Logger.java @@ -1,6 +1,24 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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.util.logging; -import org.jackhuang.hmcl.util.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.XZOutputStream; @@ -127,47 +145,10 @@ public final class Logger { String caller = CLASS_NAME + ".onExit"; if (logRetention > 0 && logFile != null) { - List> list = new ArrayList<>(); - Pattern fileNamePattern = Pattern.compile("(?\\d{4})-(?\\d{2})-(?\\d{2})T(?\\d{2})-(?\\d{2})-(?\\d{2})(\\.(?\\d+))?\\.log(\\.(gz|xz))?"); - Path dir = logFile.getParent(); - try (DirectoryStream stream = Files.newDirectoryStream(dir)) { - for (Path path : stream) { - Matcher matcher = fileNamePattern.matcher(path.getFileName().toString()); - if (matcher.matches() && Files.isRegularFile(path)) { - int year = Integer.parseInt(matcher.group("year")); - int month = Integer.parseInt(matcher.group("month")); - int day = Integer.parseInt(matcher.group("day")); - int hour = Integer.parseInt(matcher.group("hour")); - int minute = Integer.parseInt(matcher.group("minute")); - int second = Integer.parseInt(matcher.group("second")); - int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0); - - list.add(Pair.pair(path, new int[]{year, month, day, hour, minute, second, n})); - } - } - } catch (IOException e) { - log(Level.WARNING, caller, "Failed to list log files in " + dir, e); - } - + var list = findRecentLogFiles(Integer.MAX_VALUE); if (list.size() > logRetention) { - list.sort((a, b) -> { - int[] v1 = a.getValue(); - int[] v2 = b.getValue(); - - assert v1.length == v2.length; - - for (int i = 0; i < v1.length; i++) { - int c = Integer.compare(v1[i], v2[i]); - if (c != 0) - return c; - } - - return 0; - }); - for (int i = 0, end = list.size() - logRetention; i < end; i++) { - Path file = list.get(i).getKey(); - + Path file = list.get(i); try { if (!Files.isSameFile(file, logFile)) { log(Level.INFO, caller, "Delete old log file " + file, null); @@ -276,6 +257,40 @@ public final class Logger { return logFile; } + public @NotNull List findRecentLogFiles(int n) { + if (n <= 0 || logFile == null) + return List.of(); + + var currentLogFile = LogFile.ofFile(logFile); + + Path logDir = logFile.getParent(); + if (logDir == null || !Files.isDirectory(logDir)) + return List.of(); + + var logFiles = new ArrayList(); + try (DirectoryStream stream = Files.newDirectoryStream(logDir)) { + for (Path path : stream) { + LogFile item = LogFile.ofFile(path); + if (item != null && (currentLogFile == null || item.compareTo(currentLogFile) < 0)) { + logFiles.add(item); + } + } + } catch (IOException e) { + log(Level.WARNING, CLASS_NAME + ".findRecentLogFiles", "Failed to list log files in " + logDir, e); + return List.of(); + } + logFiles.sort(Comparator.naturalOrder()); + + final int resultLength = Math.min(n, logFiles.size()); + final int offset = logFiles.size() - resultLength; + + var result = new Path[resultLength]; + for (int i = 0; i < resultLength; i++) { + result[i] = logFiles.get(i + offset).file; + } + return List.of(result); + } + public void exportLogs(OutputStream output) throws IOException { Objects.requireNonNull(output); LogEvent.ExportLog event = new LogEvent.ExportLog(output); @@ -352,4 +367,69 @@ public final class Logger { public void trace(String msg, Throwable exception) { log(Level.TRACE, CallerFinder.getCaller(), msg, exception); } + + private static final class LogFile implements Comparable { + private static final Pattern FILE_NAME_PATTERN = Pattern.compile("(?\\d{4})-(?\\d{2})-(?\\d{2})T(?\\d{2})-(?\\d{2})-(?\\d{2})(\\.(?\\d+))?\\.log(\\.(gz|xz))?"); + + private static @Nullable LogFile ofFile(Path file) { + if (!Files.isRegularFile(file)) + return null; + + Matcher matcher = FILE_NAME_PATTERN.matcher(file.getFileName().toString()); + if (!matcher.matches()) + return null; + + int year = Integer.parseInt(matcher.group("year")); + int month = Integer.parseInt(matcher.group("month")); + int day = Integer.parseInt(matcher.group("day")); + int hour = Integer.parseInt(matcher.group("hour")); + int minute = Integer.parseInt(matcher.group("minute")); + int second = Integer.parseInt(matcher.group("second")); + int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0); + + return new LogFile(file, year, month, day, hour, minute, second, n); + } + + private final Path file; + private final int year; + private final int month; + private final int day; + private final int hour; + private final int minute; + private final int second; + private final int n; + + private LogFile(Path file, int year, int month, int day, int hour, int minute, int second, int n) { + this.file = file; + this.year = year; + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = second; + this.n = n; + } + + @Override + public int compareTo(@NotNull Logger.LogFile that) { + if (this.year != that.year) return Integer.compare(this.year, that.year); + if (this.month != that.month) return Integer.compare(this.month, that.month); + if (this.day != that.day) return Integer.compare(this.day, that.day); + if (this.hour != that.hour) return Integer.compare(this.hour, that.hour); + if (this.minute != that.minute) return Integer.compare(this.minute, that.minute); + if (this.second != that.second) return Integer.compare(this.second, that.second); + if (this.n != that.n) return Integer.compare(this.n, that.n); + return 0; + } + + @Override + public int hashCode() { + return Objects.hash(year, month, day, hour, minute, second, n); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof LogFile && compareTo((LogFile) obj) == 0; + } + } }