add crash report generation, add error handling, separate system properties

This commit is contained in:
moehreag 2024-06-03 19:38:31 +02:00
parent e688242ef5
commit 98848bafee
9 changed files with 336 additions and 51 deletions

View file

@ -15,8 +15,11 @@ import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.env.Env; import dev.frogmc.frogloader.api.env.Env;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.NonsensePlugin; import dev.frogmc.frogloader.api.plugin.NonsensePlugin;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.launch.MixinClassLoader; import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
import dev.frogmc.frogloader.impl.mod.ModUtil; import dev.frogmc.frogloader.impl.mod.ModUtil;
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import lombok.Getter; import lombok.Getter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -26,7 +29,7 @@ public class FrogLoaderImpl implements FrogLoader {
public static final String MOD_FILE_EXTENSION = ".frogmod"; public static final String MOD_FILE_EXTENSION = ".frogmod";
@Getter @Getter
private static FrogLoaderImpl instance; private static FrogLoaderImpl instance;
private final boolean DEV_ENV = Boolean.getBoolean("frogmc.development"); private final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
@Getter @Getter
private final String[] args; private final String[] args;
@Getter @Getter
@ -45,8 +48,8 @@ public class FrogLoaderImpl implements FrogLoader {
@Getter @Getter
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final Map<String, ModProperties> mods; private Map<String, ModProperties> mods;
private final Collection<String> modIds; private Collection<String> modIds;
private FrogLoaderImpl(String[] args, Env env) { private FrogLoaderImpl(String[] args, Env env) {
@ -67,6 +70,7 @@ public class FrogLoaderImpl implements FrogLoader {
LOGGER.warn("Failed to create essential directories ", e); LOGGER.warn("Failed to create essential directories ", e);
} }
try {
discoverPlugins(); discoverPlugins();
advanceMixinState(); advanceMixinState();
mods = collectMods(); mods = collectMods();
@ -74,6 +78,9 @@ public class FrogLoaderImpl implements FrogLoader {
LOGGER.info(ModUtil.getModList(mods.values())); LOGGER.info(ModUtil.getModList(mods.values()));
LOGGER.info("Launching..."); LOGGER.info("Launching...");
plugins.forEach(NonsensePlugin::run); plugins.forEach(NonsensePlugin::run);
} catch (Throwable t){
LoaderGui.builder().setContent(LoaderGui.ContentType.GENERIC_ERROR, t).addReport(CrashReportGenerator.writeReport(t, collectMods().values())).build().show();
}
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -157,4 +164,8 @@ public class FrogLoaderImpl implements FrogLoader {
private Collection<String> collectModIds() { private Collection<String> collectModIds() {
return mods.keySet(); return mods.keySet();
} }
public Collection<ModProperties> getMods(){
return mods.values();
}
} }

View file

@ -4,17 +4,24 @@ import javax.swing.*;
import javax.swing.plaf.basic.BasicBorders; import javax.swing.plaf.basic.BasicBorders;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI; import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import dev.frogmc.frogloader.impl.FrogLoaderImpl; import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver; import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
import dev.frogmc.frogloader.impl.mod.ModUtil; import dev.frogmc.frogloader.impl.mod.ModUtil;
import dev.frogmc.frogloader.impl.util.URLUtil; import dev.frogmc.frogloader.impl.util.URLUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class LoaderGui { public class LoaderGui {
public static Builder builder(){ public static Builder builder(){
@ -23,39 +30,96 @@ public class LoaderGui {
public static class Builder { public static class Builder {
private final JFrame frame = getFrame(); private Path report;
private Consumer<JFrame> contentFunc;
private boolean keepRunning = false;
public <T> Builder setContent(ContentType<T> type, T argument){ public <T> Builder setContent(ContentType<T> type, T argument){
type.contentSetter.accept(frame, argument); contentFunc = f -> type.contentSetter.accept(f, argument);
return this;
}
public Builder addReport(Path report){
this.report = report;
return this;
}
public Builder keepRunning(){
keepRunning = true;
return this; return this;
} }
public LoaderGui build(){ public LoaderGui build(){
return new LoaderGui(frame); JFrame frame = getFrame(report);
if (contentFunc != null) {
contentFunc.accept(frame);
}
return new LoaderGui(frame, keepRunning);
} }
} }
private final JFrame frame; private final JFrame frame;
private LoaderGui(JFrame frame){ private final boolean keepRunning;
this.frame = frame;
}
private static JFrame getFrame(){ private static JFrame getFrame(Path report){
var frame = new JFrame(); var frame = new JFrame();
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion()); frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
frame.setSize(952, 560); frame.setSize(952, 560);
frame.setLocationRelativeTo(null); frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
if (report != null) {
actions.add(new JButton(new AbstractAction("Open Report") {
@Override
public void actionPerformed(ActionEvent e) {
URLUtil.open(report.toUri());
}
}));
actions.add(new JButton(new AbstractAction("Copy Report") {
@Override
public void actionPerformed(ActionEvent e) {
URLUtil.copyStringContent(report.toUri());
}
}));
}
actions.add(new JButton(new AbstractAction("Join Discord") {
@Override
public void actionPerformed(ActionEvent e) {
}
}));
actions.add(new JButton(new AbstractAction("Exit") {
@Override
public void actionPerformed(ActionEvent e) {
frame.dispose();
}
}));
frame.add(actions, BorderLayout.SOUTH);
return frame; return frame;
} }
@SuppressWarnings("BusyWait")
public void show(){ public void show(){
frame.setVisible(true); frame.setVisible(true);
while (frame.isVisible()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
if (!keepRunning) {
System.exit(0);
}
} }
@AllArgsConstructor @AllArgsConstructor
public static class ContentType<T> { public static class ContentType<T> {
public static ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> { public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
JPanel pane = new JPanel(new BorderLayout()); JPanel pane = new JPanel(new BorderLayout());
JTextPane title = new JTextPane(); JTextPane title = new JTextPane();
title.setContentType("text/html"); title.setContentType("text/html");
@ -67,13 +131,18 @@ public class LoaderGui {
pane.add(title, BorderLayout.NORTH); pane.add(title, BorderLayout.NORTH);
JPanel list = new JPanel(); JPanel list = new JPanel();
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
ex.getDependencies().forEach(e -> { ex.getDependencies().forEach(e -> {
String description; StringBuilder description = new StringBuilder();
if (e.presentVersion() != null){ if (e.presentVersion() != null){
description = String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s, but a different version is present or provided: %s", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range()), e.presentVersion()); description.append("Mod %s (%s) depends on a Mod with id %s with a version matching %s, but a different version is present or provided: %s".formatted(e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range()), e.presentVersion()));
} else { } else {
description = String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s. \nNo version is currently available.", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range())); description.append(String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s. \nNo version is currently available.", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range())));
} }
description.append("\nSuggested Solution: Install ")
.append(e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>"))
.append(" of Mod with id ").append(e.dependency());
List<JButton> actions = new ArrayList<>(); List<JButton> actions = new ArrayList<>();
if (e.link() != null) { if (e.link() != null) {
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION); boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
@ -90,14 +159,13 @@ public class LoaderGui {
}); });
actions.add(urlButton); actions.add(urlButton);
} }
JPanel entry = getEntry(description, list.getBackground(), actions); JPanel entry = getEntry(description.toString(), list.getBackground(), actions);
entry.setSize(list.getWidth(), entry.getHeight());
list.add(entry); list.add(entry);
}); });
pane.add(new JScrollPane(list)); pane.add(new JScrollPane(list));
frame.add(pane); frame.add(pane);
}); });
public static ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> { public static final ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
JPanel pane = new JPanel(new BorderLayout()); JPanel pane = new JPanel(new BorderLayout());
JTextPane title = new JTextPane(); JTextPane title = new JTextPane();
title.setContentType("text/html"); title.setContentType("text/html");
@ -109,8 +177,13 @@ public class LoaderGui {
pane.add(title, BorderLayout.NORTH); pane.add(title, BorderLayout.NORTH);
JPanel list = new JPanel(); JPanel list = new JPanel();
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
ex.getBreaks().forEach(e -> { ex.getBreaks().forEach(e -> {
String description = String.format("Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)", e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version()); String description = "Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)".formatted(e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version()) +
"\nSuggested Solution: Install " +
e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>") +
" of Mod %s (%s) ".formatted(e.broken().id(), e.broken().name());
List<JButton> actions = new ArrayList<>(); List<JButton> actions = new ArrayList<>();
JPanel entry = getEntry(description, list.getBackground(), actions); JPanel entry = getEntry(description, list.getBackground(), actions);
@ -120,6 +193,27 @@ public class LoaderGui {
pane.add(new JScrollPane(list)); pane.add(new JScrollPane(list));
frame.add(pane); frame.add(pane);
}); });
public static final ContentType<Throwable> GENERIC_ERROR = new ContentType<>((frame, throwable) -> {
JPanel pane = new JPanel(new BorderLayout());
JTextPane title = new JTextPane();
title.setContentType("text/html");
title.setBackground(pane.getBackground());
title.setEditable(false);
title.setText("<html><h3>Caught Fatal Error during game startup:</h3></html>");
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
pane.add(title, BorderLayout.NORTH);
JTextPane error = new JTextPane();
title.setEditable(false);
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
error.setText(writer.toString());
pane.add(error);
frame.add(pane);
});
private final BiConsumer<JFrame, T> contentSetter; private final BiConsumer<JFrame, T> contentSetter;
} }
@ -135,7 +229,7 @@ public class LoaderGui {
text.setContentType("text/html"); text.setContentType("text/html");
text.setEditable(false); text.setEditable(false);
text.setBackground(background); text.setBackground(background);
text.setText("<html>" + description.replace("\n", "<br>") + "</html>"); text.setText("<html>" + description.replace("<", "&lt;").replace("\n", "<br>") + "</html>");
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
actions.forEach(actionPanel::add); actions.forEach(actionPanel::add);
entry.add(text); entry.add(text);

View file

@ -139,7 +139,8 @@ public class ModDependencyResolver {
return result; return result;
} }
private record ProvidedMod(String modId, SemVer version, ModProperties source){} private record ProvidedMod(String modId, SemVer version, ModProperties source) {
}
@AllArgsConstructor @AllArgsConstructor
private enum DependencyType { private enum DependencyType {
@ -162,22 +163,42 @@ public class ModDependencyResolver {
} }
} }
@AllArgsConstructor
@Getter @Getter
public static class BreakingModException extends Exception { public static class BreakingModException extends Exception {
private final Collection<Entry> breaks; private final Collection<Entry> breaks;
public BreakingModException(Collection<Entry> breaks) {
super(breaks.stream()
.map(e -> "Mod %s (%s) breaks with mod %s (%s) for versions matching %s".formatted(e.source().id(),
e.source().name(), e.broken().id(), e.broken().name(), e.range()))
.collect(Collectors.joining("\n")));
this.breaks = breaks;
}
public record Entry(ModProperties source, ModProperties broken, VersionRange range) { public record Entry(ModProperties source, ModProperties broken, VersionRange range) {
} }
} }
@AllArgsConstructor
@Getter @Getter
public static class UnfulfilledDependencyException extends Exception { public static class UnfulfilledDependencyException extends Exception {
private final Collection<Entry> dependencies; private final Collection<Entry> dependencies;
public record Entry(ModProperties source, String dependency, VersionRange range, SemVer presentVersion, @Nullable String link){} public UnfulfilledDependencyException(Collection<Entry> dependencies) {
super(dependencies.stream().map(e -> {
if (e.presentVersion() == null) {
return "Mod %s (%s) depends on mod %s with a version matching %s (No version available)".formatted(e.source().id(), e.source().name(), e.dependency(), e.range());
} else {
return "Mod %s (%s) depends on mod %s with a version matching %s, but a different version is present or provided: %s".formatted(e.source().id(), e.source().name(), e.dependency(), e.range(), e.presentVersion());
}
}).collect(Collectors.joining("\n")));
this.dependencies = dependencies;
}
public record Entry(ModProperties source, String dependency, VersionRange range, SemVer presentVersion,
@Nullable String link) {
}
} }
public static class ResolverException extends Exception { public static class ResolverException extends Exception {
@ -226,6 +247,46 @@ public class ModDependencyResolver {
public String toString() { public String toString() {
return type.prefix + version; return type.prefix + version;
} }
public SemVer maxCompatible() {
return switch (type) {
case GT -> null;
case LT -> {
if (version.patch() > 0) {
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
}
if (version.minor() > 0) {
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
}
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
}
default -> version;
};
}
public SemVer minCompatible() {
return switch (type) {
case LT -> {
if (version.patch() > 0) {
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
}
if (version.minor() > 0) {
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
}
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
}
case GT -> {
if (version.patch() > 0) {
yield new SemVerImpl(version.major(), version().minor(), version().patch() + 1, version.prerelease(), version.build());
}
if (version.minor() > 0) {
yield new SemVerImpl(version.major(), version().minor() + 1, version().patch(), version.prerelease(), version.build());
}
yield new SemVerImpl(version.major() + 1, version().minor(), version().patch(), version.prerelease(), version.build());
}
default -> version;
};
}
} }
private record ComparatorSet(Collection<Comparator> comparators) { private record ComparatorSet(Collection<Comparator> comparators) {
@ -243,6 +304,14 @@ public class ModDependencyResolver {
public String toString() { public String toString() {
return comparators.stream().map(Objects::toString).collect(Collectors.joining(" ")); return comparators.stream().map(Objects::toString).collect(Collectors.joining(" "));
} }
public SemVer maxCompatible() {
return comparators.stream().map(Comparator::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo).orElse(null);
}
public SemVer minCompatible() {
return comparators.stream().map(Comparator::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo).orElse(null);
}
} }
public record VersionRange(Collection<ComparatorSet> sets) { public record VersionRange(Collection<ComparatorSet> sets) {
@ -483,5 +552,13 @@ public class ModDependencyResolver {
public String toString() { public String toString() {
return sets.stream().map(Objects::toString).collect(Collectors.joining(" || ")); return sets.stream().map(Objects::toString).collect(Collectors.joining(" || "));
} }
public Optional<SemVer> maxCompatible() {
return sets.stream().map(ComparatorSet::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo);
}
public Optional<SemVer> minCompatible() {
return sets.stream().map(ComparatorSet::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo);
}
} }
} }

View file

@ -7,12 +7,20 @@ import java.util.Collection;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ModUtil { public class ModUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ModUtil.class);
public static String getModList(Collection<ModProperties> mods){ public static String getModList(Collection<ModProperties> mods){
StringBuilder builder = new StringBuilder();
int size = mods.size(); int size = mods.size();
if (size == 0){
return "No mods loaded.";
}
StringBuilder builder = new StringBuilder();
builder.append("Loaded ").append(size).append(" mod"); builder.append("Loaded ").append(size).append(" mod");
if (size > 1){ if (size > 1){
builder.append("s"); builder.append("s");
@ -36,8 +44,7 @@ public class ModUtil {
try { try {
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1)))); Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); LOGGER.error("Failed to install mod:", e);
// TODO handle better than this
} }
} }
} }

View file

@ -74,6 +74,10 @@ public record SemVerImpl(int major, int minor, int patch, String prerelease, Str
} }
} }
if (prerelease == null){
return 0;
}
String[] self = prerelease.split("\\."); String[] self = prerelease.split("\\.");
String[] other = o.prerelease().split("\\."); String[] other = o.prerelease().split("\\.");

View file

@ -22,6 +22,8 @@ import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.gui.LoaderGui; import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.mixin.AWProcessor; import dev.frogmc.frogloader.impl.mixin.AWProcessor;
import dev.frogmc.frogloader.impl.mod.*; import dev.frogmc.frogloader.impl.mod.*;
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import dev.frogmc.thyroxine.Thyroxine; import dev.frogmc.thyroxine.Thyroxine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -82,13 +84,9 @@ public class Minecraft implements NonsensePlugin {
try { try {
modProperties.retainAll(new ModDependencyResolver(modProperties).solve()); modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
} catch (ModDependencyResolver.BreakingModException e) { } catch (ModDependencyResolver.BreakingModException e) {
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).build().show(); LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
throw e;
// TODO handle
} catch (ModDependencyResolver.UnfulfilledDependencyException e) { } catch (ModDependencyResolver.UnfulfilledDependencyException e) {
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).build().show(); LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
throw e;
// TODO handle (and display)
} }
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> { mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
@ -147,7 +145,7 @@ public class Minecraft implements NonsensePlugin {
protected Path findGame() { protected Path findGame() {
LOGGER.info("Locating game.."); LOGGER.info("Locating game..");
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar"); String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
if (jar != null) { if (jar != null) {
Path p = Paths.get(jar); Path p = Paths.get(jar);
if (checkLocation(p)) { if (checkLocation(p)) {

View file

@ -0,0 +1,62 @@
package dev.frogmc.frogloader.impl.util;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.mod.ModUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CrashReportGenerator {
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss_SSSS");
private static final Logger LOGGER = LoggerFactory.getLogger(CrashReportGenerator.class);
public static Path writeReport(Throwable t) {
return writeReport(t, FrogLoaderImpl.getInstance().getMods());
}
public static Path writeReport(Throwable t, Collection<ModProperties> mods) {
String fileName = "crash-"+dateFormat.format(ZonedDateTime.now())+"_frogloader.log";
Path out = FrogLoaderImpl.getInstance().getGameDir().resolve("crash-reports").resolve(fileName);
try {
String report = getForException(t, mods);
Files.createDirectories(out.getParent());
Files.writeString(out, report);
LOGGER.info("Saved crash report to {}!", out.toAbsolutePath().toUri());
System.out.println(report);
} catch (IOException e) {
LOGGER.error("Failed to save crash report!", e);
}
return out;
}
public static String getForException(Throwable t){
return getForException(t, FrogLoaderImpl.getInstance().getMods());
}
public static String getForException(Throwable t, Collection<ModProperties> mods) {
StringBuilder builder = new StringBuilder("--- FrogLoader Crash Report ---");
builder.append("\n\n").append("Time: ").append(ZonedDateTime.now()).append(" (").append(ZonedDateTime.now(ZoneOffset.UTC)).append(" UTC)");
builder.append("\n");
builder.append("\n").append("Exception:");
builder.append("\n");
StringWriter writer = new StringWriter();
t.printStackTrace(new PrintWriter(writer));
builder.append(writer);
builder.append("\n").append(ModUtil.getModList(mods));
builder.append("\n\n").append("--- Report End ---");
return builder.toString();
}
}

View file

@ -0,0 +1,10 @@
package dev.frogmc.frogloader.impl.util;
import lombok.experimental.UtilityClass;
@UtilityClass
public class SystemProperties {
public final String MINECRAFT_GAME_JAR = "frogmc.plugin.minecraft.gameJar";
public final String DEVELOPMENT = "frogmc.development";
}

View file

@ -1,8 +1,14 @@
package dev.frogmc.frogloader.impl.util; package dev.frogmc.frogloader.impl.util;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.util.Locale;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -19,17 +25,33 @@ public class URLUtil {
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(url); Desktop.getDesktop().browse(url);
} else { } else {
throw new UnsupportedOperationException("Browse action is not supported");
}
} catch (IOException | UnsupportedOperationException e) {
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString()); ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
try {
builder.start(); builder.start();
} catch (IOException ex) {
ex.addSuppressed(e);
LOGGER.error("Failed to open url: ", ex);
} }
} catch (IOException e) {
LOGGER.error("Failed to open url: ", e);
} }
} }
public static void copyStringContent(URI url) {
if (System.getenv().getOrDefault("DESKTOP_SESSION", "").toLowerCase(Locale.ROOT)
.contains("wayland")) {
try {
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + url);
builder.start();
} catch (IOException e) {
LOGGER.error("Failed to copy contents of {}:", url, e);
}
} else {
try (InputStream in = url.toURL().openStream();
InputStreamReader inReader = new InputStreamReader(in);
BufferedReader reader = new BufferedReader(inReader)) {
String data = reader.lines().collect(Collectors.joining("\n"));
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
} catch (IOException e) {
LOGGER.error("Failed to copy contents of {}:", url, e);
}
}
}
} }