1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@ hs_err_pid*
|
|||||||
/externalgames
|
/externalgames
|
||||||
NVIDIA
|
NVIDIA
|
||||||
minecraft-exported-crash-info*
|
minecraft-exported-crash-info*
|
||||||
|
hmcl-exported-logs-*
|
||||||
|
|
||||||
# gradle build
|
# gradle build
|
||||||
/build/
|
/build/
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import javafx.beans.WeakInvalidationListener;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.scene.control.ToggleGroup;
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import org.jackhuang.hmcl.Metadata;
|
||||||
import org.jackhuang.hmcl.setting.Settings;
|
import org.jackhuang.hmcl.setting.Settings;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
@@ -40,10 +41,12 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
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.setting.ConfigHolder.config;
|
||||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||||
@@ -123,22 +126,46 @@ public final class SettingsPage extends SettingsView {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onExportLogs() {
|
protected void onExportLogs() {
|
||||||
// We cannot determine which file is JUL using.
|
|
||||||
// So we write all the logs to a new file.
|
|
||||||
thread(() -> {
|
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<Path> recentLogFiles = LOG.findRecentLogFiles(5);
|
||||||
|
|
||||||
LOG.info("Exporting logs to " + logFile);
|
Path outputFile;
|
||||||
try (OutputStream output = Files.newOutputStream(logFile)) {
|
try {
|
||||||
LOG.exportLogs(output);
|
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) {
|
} 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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
|
||||||
FXUtils.showFileInExplorer(logFile);
|
FXUtils.showFileInExplorer(outputFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2025 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.util.logging;
|
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.LZMA2Options;
|
||||||
import org.tukaani.xz.XZOutputStream;
|
import org.tukaani.xz.XZOutputStream;
|
||||||
|
|
||||||
@@ -127,47 +145,10 @@ public final class Logger {
|
|||||||
String caller = CLASS_NAME + ".onExit";
|
String caller = CLASS_NAME + ".onExit";
|
||||||
|
|
||||||
if (logRetention > 0 && logFile != null) {
|
if (logRetention > 0 && logFile != null) {
|
||||||
List<Pair<Path, int[]>> list = new ArrayList<>();
|
var list = findRecentLogFiles(Integer.MAX_VALUE);
|
||||||
Pattern fileNamePattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\d+))?\\.log(\\.(gz|xz))?");
|
|
||||||
Path dir = logFile.getParent();
|
|
||||||
try (DirectoryStream<Path> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.size() > logRetention) {
|
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++) {
|
for (int i = 0, end = list.size() - logRetention; i < end; i++) {
|
||||||
Path file = list.get(i).getKey();
|
Path file = list.get(i);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!Files.isSameFile(file, logFile)) {
|
if (!Files.isSameFile(file, logFile)) {
|
||||||
log(Level.INFO, caller, "Delete old log file " + file, null);
|
log(Level.INFO, caller, "Delete old log file " + file, null);
|
||||||
@@ -276,6 +257,40 @@ public final class Logger {
|
|||||||
return logFile;
|
return logFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NotNull List<Path> 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<LogFile>();
|
||||||
|
try (DirectoryStream<Path> 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 {
|
public void exportLogs(OutputStream output) throws IOException {
|
||||||
Objects.requireNonNull(output);
|
Objects.requireNonNull(output);
|
||||||
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
|
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
|
||||||
@@ -352,4 +367,69 @@ public final class Logger {
|
|||||||
public void trace(String msg, Throwable exception) {
|
public void trace(String msg, Throwable exception) {
|
||||||
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
|
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class LogFile implements Comparable<LogFile> {
|
||||||
|
private static final Pattern FILE_NAME_PATTERN = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})T(?<hour>\\d{2})-(?<minute>\\d{2})-(?<second>\\d{2})(\\.(?<n>\\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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user