Skip to content

Commit 5e794db

Browse files
committed
update
1 parent ac4c3a7 commit 5e794db

File tree

2 files changed

+158
-49
lines changed

2 files changed

+158
-49
lines changed

HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javafx.beans.binding.Bindings;
2424
import javafx.beans.property.ObjectProperty;
2525
import javafx.scene.control.ToggleGroup;
26+
import org.jackhuang.hmcl.Metadata;
2627
import org.jackhuang.hmcl.setting.Settings;
2728
import org.jackhuang.hmcl.ui.Controllers;
2829
import org.jackhuang.hmcl.ui.FXUtils;
@@ -40,10 +41,12 @@
4041
import java.io.OutputStream;
4142
import java.nio.file.Files;
4243
import java.nio.file.Path;
43-
import java.nio.file.Paths;
4444
import java.time.LocalDateTime;
4545
import java.time.format.DateTimeFormatter;
46+
import java.util.List;
4647
import java.util.Optional;
48+
import java.util.zip.ZipEntry;
49+
import java.util.zip.ZipOutputStream;
4750

4851
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
4952
import static org.jackhuang.hmcl.util.Lang.thread;
@@ -126,19 +129,45 @@ protected void onExportLogs() {
126129
// We cannot determine which file is JUL using.
127130
// So we write all the logs to a new file.
128131
thread(() -> {
129-
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
130-
131-
LOG.info("Exporting logs to " + logFile);
132-
try (OutputStream output = Files.newOutputStream(logFile)) {
133-
LOG.exportLogs(output);
132+
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
133+
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
134+
135+
Path outputFile;
136+
try {
137+
if (recentLogFiles.isEmpty()) {
138+
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
139+
140+
LOG.info("Exporting latest logs to " + outputFile);
141+
try (OutputStream output = Files.newOutputStream(outputFile)) {
142+
LOG.exportLogs(output);
143+
}
144+
} else {
145+
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
146+
147+
LOG.info("Exporting latest logs to " + outputFile);
148+
try (var os = Files.newOutputStream(outputFile);
149+
var zos = new ZipOutputStream(os)) {
150+
151+
for (Path path : recentLogFiles) {
152+
String zipEntryName = path.getFileName().toString();
153+
zos.putNextEntry(new ZipEntry(zipEntryName));
154+
Files.copy(path, zos);
155+
zos.closeEntry();
156+
}
157+
158+
zos.putNextEntry(new ZipEntry("latest.log"));
159+
LOG.exportLogs(zos);
160+
zos.closeEntry();
161+
}
162+
}
134163
} catch (IOException e) {
135-
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
136164
LOG.warning("Failed to export logs", e);
165+
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
137166
return;
138167
}
139168

140-
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
141-
FXUtils.showFileInExplorer(logFile);
169+
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
170+
FXUtils.showFileInExplorer(outputFile);
142171
});
143172
}
144173

HMCLCore/src/main/java/org/jackhuang/hmcl/util/logging/Logger.java

Lines changed: 120 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
118
package org.jackhuang.hmcl.util.logging;
219

3-
import org.jackhuang.hmcl.util.Pair;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
422
import org.tukaani.xz.LZMA2Options;
523
import org.tukaani.xz.XZOutputStream;
624

@@ -127,47 +145,10 @@ private void onExit() {
127145
String caller = CLASS_NAME + ".onExit";
128146

129147
if (logRetention > 0 && logFile != null) {
130-
List<Pair<Path, int[]>> list = new ArrayList<>();
131-
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))?");
132-
Path dir = logFile.getParent();
133-
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
134-
for (Path path : stream) {
135-
Matcher matcher = fileNamePattern.matcher(path.getFileName().toString());
136-
if (matcher.matches() && Files.isRegularFile(path)) {
137-
int year = Integer.parseInt(matcher.group("year"));
138-
int month = Integer.parseInt(matcher.group("month"));
139-
int day = Integer.parseInt(matcher.group("day"));
140-
int hour = Integer.parseInt(matcher.group("hour"));
141-
int minute = Integer.parseInt(matcher.group("minute"));
142-
int second = Integer.parseInt(matcher.group("second"));
143-
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
144-
145-
list.add(Pair.pair(path, new int[]{year, month, day, hour, minute, second, n}));
146-
}
147-
}
148-
} catch (IOException e) {
149-
log(Level.WARNING, caller, "Failed to list log files in " + dir, e);
150-
}
151-
148+
var list = findRecentLogFiles(Integer.MAX_VALUE);
152149
if (list.size() > logRetention) {
153-
list.sort((a, b) -> {
154-
int[] v1 = a.getValue();
155-
int[] v2 = b.getValue();
156-
157-
assert v1.length == v2.length;
158-
159-
for (int i = 0; i < v1.length; i++) {
160-
int c = Integer.compare(v1[i], v2[i]);
161-
if (c != 0)
162-
return c;
163-
}
164-
165-
return 0;
166-
});
167-
168150
for (int i = 0, end = list.size() - logRetention; i < end; i++) {
169-
Path file = list.get(i).getKey();
170-
151+
Path file = list.get(i);
171152
try {
172153
if (!Files.isSameFile(file, logFile)) {
173154
log(Level.INFO, caller, "Delete old log file " + file, null);
@@ -276,6 +257,40 @@ public Path getLogFile() {
276257
return logFile;
277258
}
278259

260+
public @NotNull List<Path> findRecentLogFiles(int n) {
261+
if (n <= 0 || logFile == null)
262+
return List.of();
263+
264+
var currentLogFile = LogFile.ofFile(logFile);
265+
266+
Path logDir = logFile.getParent();
267+
if (logDir == null || !Files.isDirectory(logDir))
268+
return List.of();
269+
270+
var logFiles = new ArrayList<LogFile>();
271+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(logDir)) {
272+
for (Path path : stream) {
273+
LogFile item = LogFile.ofFile(path);
274+
if (item != null && (currentLogFile == null || item.compareTo(currentLogFile) < 0)) {
275+
logFiles.add(item);
276+
}
277+
}
278+
} catch (IOException e) {
279+
log(Level.WARNING, CLASS_NAME + ".findRecentLogFiles", "Failed to list log files in " + logDir, e);
280+
return List.of();
281+
}
282+
logFiles.sort(null);
283+
284+
final int resultLength = Math.min(n, logFiles.size());
285+
final int offset = logFiles.size() - resultLength;
286+
287+
var result = new Path[resultLength];
288+
for (int i = 0; i < resultLength; i++) {
289+
result[i] = logFiles.get(i + offset).file;
290+
}
291+
return List.of(result);
292+
}
293+
279294
public void exportLogs(OutputStream output) throws IOException {
280295
Objects.requireNonNull(output);
281296
LogEvent.ExportLog event = new LogEvent.ExportLog(output);
@@ -352,4 +367,69 @@ public void trace(String msg) {
352367
public void trace(String msg, Throwable exception) {
353368
log(Level.TRACE, CallerFinder.getCaller(), msg, exception);
354369
}
370+
371+
private static final class LogFile implements Comparable<LogFile> {
372+
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))?");
373+
374+
private static @Nullable LogFile ofFile(Path file) {
375+
if (!Files.isRegularFile(file))
376+
return null;
377+
378+
Matcher matcher = FILE_NAME_PATTERN.matcher(file.getFileName().toString());
379+
if (!matcher.matches())
380+
return null;
381+
382+
int year = Integer.parseInt(matcher.group("year"));
383+
int month = Integer.parseInt(matcher.group("month"));
384+
int day = Integer.parseInt(matcher.group("day"));
385+
int hour = Integer.parseInt(matcher.group("hour"));
386+
int minute = Integer.parseInt(matcher.group("minute"));
387+
int second = Integer.parseInt(matcher.group("second"));
388+
int n = Optional.ofNullable(matcher.group("n")).map(Integer::parseInt).orElse(0);
389+
390+
return new LogFile(file, year, month, day, hour, minute, second, n);
391+
}
392+
393+
private final Path file;
394+
private final int year;
395+
private final int month;
396+
private final int day;
397+
private final int hour;
398+
private final int minute;
399+
private final int second;
400+
private final int n;
401+
402+
private LogFile(Path file, int year, int month, int day, int hour, int minute, int second, int n) {
403+
this.file = file;
404+
this.year = year;
405+
this.month = month;
406+
this.day = day;
407+
this.hour = hour;
408+
this.minute = minute;
409+
this.second = second;
410+
this.n = n;
411+
}
412+
413+
@Override
414+
public int compareTo(@NotNull Logger.LogFile that) {
415+
if (this.year != that.year) return Integer.compare(this.year, that.year);
416+
if (this.month != that.month) return Integer.compare(this.month, that.month);
417+
if (this.day != that.day) return Integer.compare(this.day, that.day);
418+
if (this.hour != that.hour) return Integer.compare(this.hour, that.hour);
419+
if (this.minute != that.minute) return Integer.compare(this.minute, that.minute);
420+
if (this.second != that.second) return Integer.compare(this.second, that.second);
421+
if (this.n != that.n) return Integer.compare(this.n, that.n);
422+
return 0;
423+
}
424+
425+
@Override
426+
public int hashCode() {
427+
return Objects.hash(year, month, day, hour, minute, second, n);
428+
}
429+
430+
@Override
431+
public boolean equals(Object obj) {
432+
return obj instanceof LogFile && compareTo((LogFile) obj) == 0;
433+
}
434+
}
355435
}

0 commit comments

Comments
 (0)