Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 15 additions & 31 deletions HMCL/src/main/java/org/jackhuang/hmcl/countly/CrashReport.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,13 @@
package org.jackhuang.hmcl.countly;

import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class CrashReport {

private final Thread thread;
private final Throwable throwable;
private final String stackTrace;

public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
}

public Throwable getThrowable() {
return this.throwable;
}

public record CrashReport(Thread thread, Throwable throwable, String stackTrace) {
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
Expand All @@ -52,20 +36,20 @@ public boolean shouldBeReport() {
}

public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
return "---- Hello Minecraft! Launcher Crash Report ----\n" +
"Version: " + Metadata.VERSION + "\n" +
"Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
"Thread: " + thread + "\n" +
"\nContent: \n" +
stackTrace + "\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
"Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION.getVersion() + "\n" +
"System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
"Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
"Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
"Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
"JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
"JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
"JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
}
71 changes: 55 additions & 16 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,80 @@
*/
package org.jackhuang.hmcl.ui;

import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.LauncherLogExporter;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;

import java.io.IOException;
import java.nio.file.Path;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/**
* @author huangyuhui
*/
public class CrashWindow extends Stage {
public final class CrashWindow extends Stage {
private static final Lazy<CrashWindow> instance = new Lazy<>(CrashWindow::new);

public static CrashWindow getInstance() {
return instance.get();
}

public CrashWindow(CrashReport report) {
private final TextArea textArea = new TextArea();

private CrashWindow() {
Label lblCrash = new Label();
if (report.getThrowable() instanceof InternalError)
lblCrash.setText(i18n("launcher.crash.java_internal_error"));
else if (UpdateChecker.isOutdated())
lblCrash.setWrapText(true);

if (UpdateChecker.isOutdated()) {
lblCrash.setText(i18n("launcher.crash.hmcl_out_dated"));
else
} else {
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
}

TextArea textArea = new TextArea();
textArea.setText(report.getDisplayText());
textArea.setEditable(false);
StackPane exportPane = new StackPane();
ProgressIndicator progressIndicator = new ProgressIndicator();
FXUtils.setLimitHeight(progressIndicator, 20);

Button btnExport = new Button();
exportPane.getChildren().setAll(btnExport);
btnExport.setText(i18n("settings.launcher.launcher_log.export"));
btnExport.setOnAction(event -> {
exportPane.getChildren().setAll(progressIndicator);
try {
Path path = LauncherLogExporter.exportLogsAsZip();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the log export off the JavaFX application thread

When retained logs are large, compressed, or stored on a slow filesystem, this call scans, decompresses, and recompresses up to five files and waits for Logger.exportLogs directly inside the button's JavaFX action handler. The FX pulse therefore cannot render the progress indicator, the crash window remains unresponsive, and subsequent queued crash reports cannot appear until the export finishes. Dispatch the export to Schedulers.io() and return to the JavaFX executor for the dialog and button update, as the settings-page export already does.

Useful? React with 👍 / 👎.

FXUtils.showFileInExplorer(path);
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", path));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
} catch (IOException e) {
LOG.warning("Failed to export launcher logs", e);
Alert alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e));
alert.setTitle(i18n("message.error"));
alert.setContentText(StringUtils.getStackTrace(e));
alert.showAndWait();
}
exportPane.getChildren().setAll(btnExport);
});

Button btnContact = new Button();
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
HBox box = new HBox();
HBox box = new HBox(8);
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
box.getChildren().setAll(exportPane, btnContact);
box.setAlignment(Pos.CENTER_RIGHT);

BorderPane pane = new BorderPane();
Expand All @@ -72,7 +106,12 @@ else if (UpdateChecker.isOutdated())
FXUtils.setIcon(this);
setTitle(i18n("message.error"));

setOnCloseRequest(e -> javafx.application.Platform.exit());
setOnCloseRequest(e -> Platform.exit());
}

public void addCrashReport(CrashReport report) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ERROR] [com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck]

'METHOD_DEF' has more than 1 empty lines before.

textArea.setText(textArea.getText() + "\n\n" + report.getDisplayText());
textArea.setEditable(false);
this.requestFocus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
import javafx.scene.text.TextFlow;
import javafx.util.Duration;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.ComponentVersionList;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
Expand Down
145 changes: 3 additions & 142 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
Expand All @@ -42,27 +41,13 @@
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.LauncherLogExporter;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.tukaani.xz.XZInputStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;

import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
Expand Down Expand Up @@ -221,7 +206,7 @@ else if (locale.isSameLanguage(currentLocale))
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
CompletableFuture.supplyAsync(Lang.wrap(LauncherLogExporter::exportLogsAsZip), Schedulers.io()).whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
Expand Down Expand Up @@ -262,128 +247,4 @@ private void onUpdate() {
}
UpdateHandler.updateFrom(target);
}

private static String getEntryName(Set<String> entryNames, String name) {
if (entryNames.add(name)) {
return name;
}

for (long i = 1; ; i++) {
String newName = name + "." + i;
if (entryNames.add(newName)) {
return newName;
}
}
}

/// This method guarantees to close both `input` and the current zip entry.
///
/// If no exception occurs, this method returns `true`;
/// If an exception occurs while reading from `input`, this method returns `false`;
/// If an exception occurs while writing to `output`, this method will throw it as is.
private static boolean exportLogFile(ZipOutputStream output,
Path file, // For logging
String entryName,
InputStream input,
byte[] buffer) throws IOException {
//noinspection TryFinallyCanBeTryWithResources
try {
output.putNextEntry(new ZipEntry(entryName));
int read;
while (true) {
try {
read = input.read(buffer);
if (read <= 0)
return true;
} catch (Throwable ex) {
LOG.warning("Failed to decompress log file " + file, ex);
return false;
}

output.write(buffer, 0, read);
}
} finally {
try {
input.close();
} catch (Throwable ex) {
LOG.warning("Failed to close log file " + file, ex);
}
output.closeEntry();
}
}

private CompletableFuture<Path> onExportLogs() {
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);

Path outputFile;
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);

byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {

Set<String> entryNames = new HashSet<>();

for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');

if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.

InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
}

String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
continue;
}

// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.

InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}

exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
}

zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
}

return outputFile;
}), Schedulers.io());
}
}
Loading