add crash report generation, add error handling, separate system properties
This commit is contained in:
parent
e688242ef5
commit
98848bafee
|
@ -15,8 +15,11 @@ import dev.frogmc.frogloader.api.FrogLoader;
|
|||
import dev.frogmc.frogloader.api.env.Env;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
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.mod.ModUtil;
|
||||
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
||||
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -26,7 +29,7 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||
@Getter
|
||||
private static FrogLoaderImpl instance;
|
||||
private final boolean DEV_ENV = Boolean.getBoolean("frogmc.development");
|
||||
private final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
||||
@Getter
|
||||
private final String[] args;
|
||||
@Getter
|
||||
|
@ -45,8 +48,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
@Getter
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private final Map<String, ModProperties> mods;
|
||||
private final Collection<String> modIds;
|
||||
private Map<String, ModProperties> mods;
|
||||
private Collection<String> modIds;
|
||||
|
||||
|
||||
private FrogLoaderImpl(String[] args, Env env) {
|
||||
|
@ -67,6 +70,7 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
LOGGER.warn("Failed to create essential directories ", e);
|
||||
}
|
||||
|
||||
try {
|
||||
discoverPlugins();
|
||||
advanceMixinState();
|
||||
mods = collectMods();
|
||||
|
@ -74,6 +78,9 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||
LOGGER.info("Launching...");
|
||||
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")
|
||||
|
@ -157,4 +164,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
private Collection<String> collectModIds() {
|
||||
return mods.keySet();
|
||||
}
|
||||
|
||||
public Collection<ModProperties> getMods(){
|
||||
return mods.values();
|
||||
}
|
||||
}
|
|
@ -4,17 +4,24 @@ import javax.swing.*;
|
|||
import javax.swing.plaf.basic.BasicBorders;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||
import dev.frogmc.frogloader.impl.util.URLUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class LoaderGui {
|
||||
|
||||
public static Builder builder(){
|
||||
|
@ -23,39 +30,96 @@ public class LoaderGui {
|
|||
|
||||
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){
|
||||
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;
|
||||
}
|
||||
|
||||
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 LoaderGui(JFrame frame){
|
||||
this.frame = frame;
|
||||
}
|
||||
private final boolean keepRunning;
|
||||
|
||||
private static JFrame getFrame(){
|
||||
private static JFrame getFrame(Path report){
|
||||
var frame = new JFrame();
|
||||
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
||||
frame.setSize(952, 560);
|
||||
frame.setLocationRelativeTo(null);
|
||||
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;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
public void show(){
|
||||
frame.setVisible(true);
|
||||
|
||||
while (frame.isVisible()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepRunning) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
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());
|
||||
JTextPane title = new JTextPane();
|
||||
title.setContentType("text/html");
|
||||
|
@ -67,13 +131,18 @@ public class LoaderGui {
|
|||
pane.add(title, BorderLayout.NORTH);
|
||||
|
||||
JPanel list = new JPanel();
|
||||
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
|
||||
ex.getDependencies().forEach(e -> {
|
||||
String description;
|
||||
StringBuilder description = new StringBuilder();
|
||||
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 {
|
||||
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<>();
|
||||
if (e.link() != null) {
|
||||
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
||||
|
@ -90,14 +159,13 @@ public class LoaderGui {
|
|||
});
|
||||
actions.add(urlButton);
|
||||
}
|
||||
JPanel entry = getEntry(description, list.getBackground(), actions);
|
||||
entry.setSize(list.getWidth(), entry.getHeight());
|
||||
JPanel entry = getEntry(description.toString(), list.getBackground(), actions);
|
||||
list.add(entry);
|
||||
});
|
||||
pane.add(new JScrollPane(list));
|
||||
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());
|
||||
JTextPane title = new JTextPane();
|
||||
title.setContentType("text/html");
|
||||
|
@ -109,8 +177,13 @@ public class LoaderGui {
|
|||
pane.add(title, BorderLayout.NORTH);
|
||||
|
||||
JPanel list = new JPanel();
|
||||
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
|
||||
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<>();
|
||||
|
||||
JPanel entry = getEntry(description, list.getBackground(), actions);
|
||||
|
@ -120,6 +193,27 @@ public class LoaderGui {
|
|||
pane.add(new JScrollPane(list));
|
||||
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;
|
||||
}
|
||||
|
@ -135,7 +229,7 @@ public class LoaderGui {
|
|||
text.setContentType("text/html");
|
||||
text.setEditable(false);
|
||||
text.setBackground(background);
|
||||
text.setText("<html>" + description.replace("\n", "<br>") + "</html>");
|
||||
text.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
actions.forEach(actionPanel::add);
|
||||
entry.add(text);
|
||||
|
|
|
@ -80,7 +80,7 @@ public class ModDependencyResolver {
|
|||
}
|
||||
}
|
||||
|
||||
if (!breaks.isEmpty()){
|
||||
if (!breaks.isEmpty()) {
|
||||
breaks.forEach(e ->
|
||||
LOGGER.error("Mod {} ({}) breaks with mod {} ({}) for versions matching {}", e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), e.range()));
|
||||
throw new BreakingModException(breaks);
|
||||
|
@ -125,7 +125,7 @@ public class ModDependencyResolver {
|
|||
result.add(value.origin());
|
||||
}
|
||||
|
||||
if (!unfulfilled.isEmpty()){
|
||||
if (!unfulfilled.isEmpty()) {
|
||||
unfulfilled.forEach(e -> {
|
||||
if (e.presentVersion() == null) {
|
||||
LOGGER.error("Mod {} ({}) depends on mod {} with a version matching {} (No version available)", e.source().id(), e.source().name(), e.dependency(), e.range());
|
||||
|
@ -139,7 +139,8 @@ public class ModDependencyResolver {
|
|||
return result;
|
||||
}
|
||||
|
||||
private record ProvidedMod(String modId, SemVer version, ModProperties source){}
|
||||
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
private enum DependencyType {
|
||||
|
@ -162,22 +163,42 @@ public class ModDependencyResolver {
|
|||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
|
||||
@Getter
|
||||
public static class BreakingModException extends Exception {
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class UnfulfilledDependencyException extends Exception {
|
||||
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 {
|
||||
|
@ -226,6 +247,46 @@ public class ModDependencyResolver {
|
|||
public String toString() {
|
||||
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) {
|
||||
|
@ -243,6 +304,14 @@ public class ModDependencyResolver {
|
|||
public String toString() {
|
||||
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) {
|
||||
|
@ -483,5 +552,13 @@ public class ModDependencyResolver {
|
|||
public String toString() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,20 @@ import java.util.Collection;
|
|||
|
||||
import dev.frogmc.frogloader.api.FrogLoader;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ModUtil {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModUtil.class);
|
||||
|
||||
public static String getModList(Collection<ModProperties> mods){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int size = mods.size();
|
||||
if (size == 0){
|
||||
return "No mods loaded.";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Loaded ").append(size).append(" mod");
|
||||
if (size > 1){
|
||||
builder.append("s");
|
||||
|
@ -36,8 +44,7 @@ public class ModUtil {
|
|||
try {
|
||||
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
// TODO handle better than this
|
||||
LOGGER.error("Failed to install mod:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[] other = o.prerelease().split("\\.");
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
|||
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -82,13 +84,9 @@ public class Minecraft implements NonsensePlugin {
|
|||
try {
|
||||
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
||||
} catch (ModDependencyResolver.BreakingModException e) {
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).build().show();
|
||||
throw e;
|
||||
// TODO handle
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).build().show();
|
||||
throw e;
|
||||
// TODO handle (and display)
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||
}
|
||||
|
||||
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() {
|
||||
LOGGER.info("Locating game..");
|
||||
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar");
|
||||
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
|
||||
if (jar != null) {
|
||||
Path p = Paths.get(jar);
|
||||
if (checkLocation(p)) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
package dev.frogmc.frogloader.impl.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -11,7 +17,7 @@ public class URLUtil {
|
|||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
|
||||
|
||||
public static void open(URI url){
|
||||
public static void open(URI url) {
|
||||
|
||||
LOGGER.info("Opening: {}", url);
|
||||
|
||||
|
@ -19,17 +25,33 @@ public class URLUtil {
|
|||
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(url);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Browse action is not supported");
|
||||
}
|
||||
} catch (IOException | UnsupportedOperationException e) {
|
||||
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue