diff --git a/core/src/io/anuke/mindustry/net/CrashSender.java b/core/src/io/anuke/mindustry/net/CrashSender.java new file mode 100644 index 0000000000..50c2ad059d --- /dev/null +++ b/core/src/io/anuke/mindustry/net/CrashSender.java @@ -0,0 +1,140 @@ +package io.anuke.mindustry.net; + +import io.anuke.arc.Core; +import io.anuke.arc.collection.ObjectMap; +import io.anuke.arc.function.Consumer; +import io.anuke.arc.util.Log; +import io.anuke.arc.util.OS; +import io.anuke.arc.util.Strings; +import io.anuke.arc.util.io.PropertiesUtils; +import io.anuke.arc.util.serialization.JsonValue; +import io.anuke.arc.util.serialization.JsonValue.ValueType; +import io.anuke.arc.util.serialization.JsonWriter.OutputType; +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.game.Version; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class CrashSender{ + + public static void send(Throwable exception, Consumer writeListener){ + try{ + exception.printStackTrace(); + + //don't create crash logs for me (anuke) or custom builds, as it's expected + //TODO maybe custom builds such as bleeding edge in certain cases + if(System.getProperty("user.name").equals("anuke") || Version.build == -1) return; + + //attempt to load version regardless + if(Version.number == 0){ + try{ + ObjectMap map = new ObjectMap<>(); + PropertiesUtils.load(map, new InputStreamReader(CrashSender.class.getResourceAsStream("version.properties"))); + + Version.type = map.get("type"); + Version.number = Integer.parseInt(map.get("number")); + Version.modifier = map.get("modifier"); + if(map.get("build").contains(".")){ + String[] split = map.get("build").split("\\."); + Version.build = Integer.parseInt(split[0]); + Version.revision = Integer.parseInt(split[1]); + }else{ + Version.build = Strings.canParseInt(map.get("build")) ? Integer.parseInt(map.get("build")) : -1; + } + }catch(Throwable ignored){ + Log.err("Failed to parse version."); + } + } + + try{ + //check crash report setting + if(!Core.settings.getBool("crashreport", true)){ + return; + } + }catch(Throwable ignored){ + //if there's no settings init we don't know what the user wants but chances are it's an important crash, so send it anyway + } + + //do not send exceptions that occur for versions that can't be parsed + if(Version.number == 0){ + return; + } + + try{ + File file = new File(OS.getAppDataDirectoryString(Vars.appName), "crashes/crash-report-" + DateTimeFormatter.ofPattern("MM_dd_yyyy_HH_mm_ss").format(LocalDateTime.now()) + ".txt"); + new File(OS.getAppDataDirectoryString(Vars.appName)).mkdir(); + new BufferedOutputStream(new FileOutputStream(file), 2048).write(parseException(exception).getBytes()); + Files.createDirectories(Paths.get(OS.getAppDataDirectoryString(Vars.appName), "crashes")); + + writeListener.accept(file); + }catch(Throwable ignored){ + Log.err("Failed to save local crash report."); + } + + boolean netActive = false, netServer = false; + + //attempt to close connections, if applicable + try{ + netActive = Net.active(); + netServer = Net.server(); + Net.dispose(); + }catch(Throwable ignored){ + } + + JsonValue value = new JsonValue(ValueType.object); + + boolean fn = netActive, fs = netServer; + + //add all relevant info, ignoring exceptions + ex(() -> value.addChild("versionType", new JsonValue(Version.type))); + ex(() -> value.addChild("versionNumber", new JsonValue(Version.number))); + ex(() -> value.addChild("versionModifier", new JsonValue(Version.modifier))); + ex(() -> value.addChild("build", new JsonValue(Version.build))); + ex(() -> value.addChild("net", new JsonValue(fn))); + ex(() -> value.addChild("server", new JsonValue(fs))); + ex(() -> value.addChild("players", new JsonValue(Vars.playerGroup.size()))); + ex(() -> value.addChild("state", new JsonValue(Vars.state.getState().name()))); + ex(() -> value.addChild("os", new JsonValue(System.getProperty("os.name")))); + ex(() -> value.addChild("trace", new JsonValue(parseException(exception)))); + + Log.info("Sending crash report."); + //post to crash report URL + Net.http(Vars.crashReportURL, "POST", value.toJson(OutputType.json), r -> { + Log.info("Crash sent successfully."); + System.exit(1); + }, t -> { + t.printStackTrace(); + System.exit(1); + }); + + //sleep for 10 seconds or until crash report is sent + try{ + Thread.sleep(10000); + }catch(InterruptedException ignored){ + } + }catch(Throwable death){ + death.printStackTrace(); + System.exit(1); + } + } + + + private static String parseException(Throwable e){ + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + return sw.toString(); + } + + private static void ex(Runnable r){ + try{ + r.run(); + }catch(Throwable t){ + t.printStackTrace(); + } + } +} diff --git a/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java b/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java deleted file mode 100644 index 24a0fa4529..0000000000 --- a/desktop/src/io/anuke/mindustry/desktop/CrashHandler.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.anuke.mindustry.desktop; - -import io.anuke.arc.Core; -import io.anuke.arc.collection.ObjectMap; -import io.anuke.arc.util.*; -import io.anuke.arc.util.io.PropertiesUtils; -import io.anuke.arc.util.serialization.JsonValue; -import io.anuke.arc.util.serialization.JsonValue.ValueType; -import io.anuke.arc.util.serialization.JsonWriter.OutputType; -import io.anuke.mindustry.Vars; -import io.anuke.mindustry.game.Version; -import io.anuke.mindustry.net.Net; -import org.lwjgl.util.tinyfd.TinyFileDialogs; - -import java.io.*; -import java.nio.file.*; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -public class CrashHandler{ - - public static void handle(Throwable e){ - e.printStackTrace(); - if(Version.number == 0){ - try{ - ObjectMap map = new ObjectMap<>(); - PropertiesUtils.load(map, new InputStreamReader(CrashHandler.class.getResourceAsStream("/version.properties"))); - - Version.type = map.get("type"); - Version.number = Integer.parseInt(map.get("number")); - Version.modifier = map.get("modifier"); - if(map.get("build").contains(".")){ - String[] split = map.get("build").split("\\."); - Version.build = Integer.parseInt(split[0]); - Version.revision = Integer.parseInt(split[1]); - }else{ - Version.build = Strings.canParseInt(map.get("build")) ? Integer.parseInt(map.get("build")) : -1; - } - }catch(Throwable ignored){ - - } - } - - try{ - //check crash report setting - if(!Core.settings.getBool("crashreport", true)){ - return; - } - }catch(Throwable ignored){ - //if there's no settings init we don't know what the user wants but chances are it's an important crash, so send it anyway - } - - boolean badGPU = false; - - if(e.getMessage() != null && (e.getMessage().contains("Couldn't create window") || e.getMessage().contains("OpenGL 2.0 or higher"))){ - - dialog(() -> TinyFileDialogs.tinyfd_messageBox("oh no", - e.getMessage().contains("Couldn't create window") ? "A graphics initialization error has occured! Try to update your graphics drivers.\nReport this to the developer." : - "Your graphics card does not support OpenGL 2.0!\n" + - "Try to update your graphics drivers.\n\n" + - "(If that doesn't work, your computer just doesn't support Mindustry.)", "ok", "error", true)); - badGPU = true; - } - - //don't create crash logs for me (anuke) or custom builds, as it's expected - if(System.getProperty("user.name").equals("anuke") || Version.build == -1) return; - - boolean netActive = false, netServer = false; - - //attempt to close connections, if applicable - try{ - netActive = Net.active(); - netServer = Net.server(); - Net.dispose(); - }catch(Throwable p){ - p.printStackTrace(); - } - - JsonValue value = new JsonValue(ValueType.object); - - boolean fn = netActive, fs = netServer; - - //add all relevant info, ignoring exceptions - ex(() -> value.addChild("versionType", new JsonValue(Version.type))); - ex(() -> value.addChild("versionNumber", new JsonValue(Version.number))); - ex(() -> value.addChild("versionModifier", new JsonValue(Version.modifier))); - ex(() -> value.addChild("build", new JsonValue(Version.build))); - ex(() -> value.addChild("net", new JsonValue(fn))); - ex(() -> value.addChild("server", new JsonValue(fs))); - ex(() -> value.addChild("state", new JsonValue(Vars.state.getState().name()))); - ex(() -> value.addChild("os", new JsonValue(System.getProperty("os.name")))); - ex(() -> value.addChild("trace", new JsonValue(parseException(e)))); - - try{ - Path path = Paths.get(OS.getAppDataDirectoryString(Vars.appName), "crashes", - "crash-report-" + DateTimeFormatter.ofPattern("MM_dd_yyyy_HH_mm_ss").format(LocalDateTime.now()) + ".txt"); - Files.createDirectories(Paths.get(OS.getAppDataDirectoryString(Vars.appName), "crashes")); - Files.write(path, parseException(e).getBytes()); - - if(!badGPU){ - dialog(() -> TinyFileDialogs.tinyfd_messageBox("oh no", "A crash has occured. It has been saved in:\n" + path.toAbsolutePath().toString(), "ok", "error", true)); - } - }catch(Throwable t){ - Log.err("Failed to save local crash report."); - t.printStackTrace(); - } - - Log.info("Sending crash report."); - //post to crash report URL - Net.http(Vars.crashReportURL, "POST", value.toJson(OutputType.json), r -> { - Log.info("Crash sent successfully."); - System.exit(1); - }, t -> { - t.printStackTrace(); - System.exit(1); - }); - - //sleep for 10 seconds or until crash report is sent - try{ - Thread.sleep(10000); - }catch(InterruptedException ignored){ - } - } - - private static void dialog(Runnable r){ - new Thread(r).start(); - } - - private static String parseException(Throwable e){ - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - return sw.toString(); - } - - private static void ex(Runnable r){ - try{ - r.run(); - }catch(Throwable t){ - t.printStackTrace(); - } - } -} diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java index 7dfc9ce5f6..28011094a0 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopLauncher.java @@ -49,7 +49,7 @@ public class DesktopLauncher extends Lwjgl3Application{ Net.setServerProvider(new ArcNetServer()); new DesktopLauncher(new Mindustry(), config); }catch(Throwable e){ - CrashHandler.handle(e); + DesktopPlatform.handleCrash(e); } } } diff --git a/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java index 8c4559ea60..f4d79f6b09 100644 --- a/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java +++ b/desktop/src/io/anuke/mindustry/desktop/DesktopPlatform.java @@ -1,16 +1,21 @@ package io.anuke.mindustry.desktop; -import club.minnced.discord.rpc.*; +import club.minnced.discord.rpc.DiscordEventHandlers; +import club.minnced.discord.rpc.DiscordRPC; +import club.minnced.discord.rpc.DiscordRichPresence; import io.anuke.arc.collection.Array; import io.anuke.arc.files.FileHandle; import io.anuke.arc.function.Consumer; +import io.anuke.arc.util.Log; import io.anuke.arc.util.OS; import io.anuke.arc.util.Strings; import io.anuke.arc.util.serialization.Base64Coder; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.core.Platform; +import io.anuke.mindustry.net.CrashSender; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.ui.dialogs.FileChooser; +import org.lwjgl.util.tinyfd.TinyFileDialogs; import java.net.NetworkInterface; import java.util.Enumeration; @@ -18,7 +23,7 @@ import java.util.Enumeration; import static io.anuke.mindustry.Vars.*; public class DesktopPlatform extends Platform{ - final static boolean useDiscord = OS.is64Bit; + static boolean useDiscord = OS.is64Bit; final static String applicationId = "398246104468291591"; String[] args; @@ -28,13 +33,41 @@ public class DesktopPlatform extends Platform{ testMobile = Array.with(args).contains("-testMobile"); if(useDiscord){ - DiscordEventHandlers handlers = new DiscordEventHandlers(); - DiscordRPC.INSTANCE.Discord_Initialize(applicationId, handlers, true, ""); + try{ + DiscordEventHandlers handlers = new DiscordEventHandlers(); + DiscordRPC.INSTANCE.Discord_Initialize(applicationId, handlers, true, ""); - Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC.INSTANCE::Discord_Shutdown)); + Runtime.getRuntime().addShutdownHook(new Thread(DiscordRPC.INSTANCE::Discord_Shutdown)); + }catch(Throwable t){ + useDiscord = false; + Log.err("Failed to initialize discord.", t); + } } } + static void handleCrash(Throwable e){ + Consumer dialog = r -> new Thread(r).start(); + boolean badGPU = false; + + if(e.getMessage() != null && (e.getMessage().contains("Couldn't create window") || e.getMessage().contains("OpenGL 2.0 or higher"))){ + + dialog.accept(() -> TinyFileDialogs.tinyfd_messageBox("oh no", + e.getMessage().contains("Couldn't create window") ? "A graphics initialization error has occured! Try to update your graphics drivers.\nReport this to the developer." : + "Your graphics card does not support OpenGL 2.0!\n" + + "Try to update your graphics drivers.\n\n" + + "(If that doesn't work, your computer just doesn't support Mindustry.)", "ok", "error", true)); + badGPU = true; + } + + boolean fbgp = badGPU; + + CrashSender.send(e, file -> { + if(!fbgp){ + dialog.accept(() -> TinyFileDialogs.tinyfd_messageBox("oh no", "A crash has occured. It has been saved in:\n" + file.getAbsolutePath(), "ok", "error", true)); + } + }); + } + @Override public void showFileChooser(String text, String content, Consumer cons, boolean open, String filter){ new FileChooser(text, file -> file.extension().equalsIgnoreCase(filter), open, cons).show(); diff --git a/server/src/io/anuke/mindustry/server/CrashHandler.java b/server/src/io/anuke/mindustry/server/CrashHandler.java deleted file mode 100644 index 3e65ffb84c..0000000000 --- a/server/src/io/anuke/mindustry/server/CrashHandler.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.anuke.mindustry.server; - -import io.anuke.arc.Core; -import io.anuke.arc.util.Log; -import io.anuke.arc.util.OS; -import io.anuke.arc.util.serialization.JsonValue; -import io.anuke.arc.util.serialization.JsonValue.ValueType; -import io.anuke.arc.util.serialization.JsonWriter.OutputType; -import io.anuke.mindustry.Vars; -import io.anuke.mindustry.game.Version; -import io.anuke.mindustry.net.Net; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.*; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -public class CrashHandler{ - - public static void handle(Throwable e){ - e.printStackTrace(); - - //don't create crash logs for me (anuke), as it's expected - //also don't create logs for custom builds - if(System.getProperty("user.name").equals("anuke") || Version.build == -1) return; - - //if getting the crash report property failed, OR if it set to false... don't send it - try{ - if(!Core.settings.getBool("crashreport")) return; - }catch(Throwable ignored){ - return; - } - - //attempt to close connections, if applicable - try{ - Net.dispose(); - }catch(Throwable p){ - p.printStackTrace(); - } - - JsonValue value = new JsonValue(ValueType.object); - - //add all relevant info, ignoring exceptions - ex(() -> value.addChild("versionType", new JsonValue(Version.type))); - ex(() -> value.addChild("versionNumber", new JsonValue(Version.number))); - ex(() -> value.addChild("versionModifier", new JsonValue(Version.modifier))); - ex(() -> value.addChild("build", new JsonValue(Version.build))); - ex(() -> value.addChild("state", new JsonValue(Vars.state.getState().name()))); - ex(() -> value.addChild("players", new JsonValue(Vars.playerGroup.size()))); - ex(() -> value.addChild("os", new JsonValue(System.getProperty("os.name")))); - ex(() -> value.addChild("trace", new JsonValue(parseException(e)))); - - try{ - Path path = Paths.get(OS.getAppDataDirectoryString(Vars.appName), "crashes", - "crash-report-" + DateTimeFormatter.ofPattern("MM-dd-yyyy-HH:mm:ss").format(LocalDateTime.now()) + ".txt"); - Files.createDirectories(Paths.get(OS.getAppDataDirectoryString(Vars.appName), "crashes")); - Files.write(path, parseException(e).getBytes()); - - Log.info("Saved crash report at {0}", path.toAbsolutePath().toString()); - }catch(Throwable t){ - Log.err("Failure saving crash report: "); - t.printStackTrace(); - } - - Log.info("&lcSending crash report."); - //post to crash report URL - Net.http(Vars.crashReportURL, "POST", value.toJson(OutputType.json), r -> System.exit(1), t -> System.exit(1)); - - //sleep forever - try{ - Thread.sleep(Long.MAX_VALUE); - }catch(InterruptedException ignored){ - } - } - - private static String parseException(Throwable e){ - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - return sw.toString(); - } - - private static void ex(Runnable r){ - try{ - r.run(); - }catch(Throwable t){ - t.printStackTrace(); - } - } -} diff --git a/server/src/io/anuke/mindustry/server/ServerLauncher.java b/server/src/io/anuke/mindustry/server/ServerLauncher.java index 818545e9ce..a6ed80333c 100644 --- a/server/src/io/anuke/mindustry/server/ServerLauncher.java +++ b/server/src/io/anuke/mindustry/server/ServerLauncher.java @@ -4,6 +4,7 @@ package io.anuke.mindustry.server; import io.anuke.arc.ApplicationListener; import io.anuke.arc.backends.headless.HeadlessApplication; import io.anuke.arc.backends.headless.HeadlessApplicationConfiguration; +import io.anuke.mindustry.net.CrashSender; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.ArcNetClient; import io.anuke.mindustry.net.ArcNetServer; @@ -23,21 +24,13 @@ public class ServerLauncher extends HeadlessApplication{ HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); new ServerLauncher(new MindustryServer(args), config); }catch(Throwable t){ - CrashHandler.handle(t); + CrashSender.send(t, f -> {}); } //find and handle uncaught exceptions in libGDX thread for(Thread thread : Thread.getAllStackTraces().keySet()){ if(thread.getName().equals("HeadlessApplication")){ - thread.setUncaughtExceptionHandler((t, throwable) -> { - try{ - CrashHandler.handle(throwable); - System.exit(-1); - }catch(Throwable crashCrash){ - crashCrash.printStackTrace(); - System.exit(-1); - } - }); + thread.setUncaughtExceptionHandler((t, throwable) -> CrashSender.send(throwable, f -> {})); break; } }