Better shared crash handling

This commit is contained in:
Anuken 2019-04-22 12:00:47 -04:00
parent 0382c35ac0
commit b7759c8151
6 changed files with 182 additions and 250 deletions

View File

@ -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<File> 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<String, String> 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();
}
}
}

View File

@ -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<String, String> 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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<Runnable> 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<FileHandle> cons, boolean open, String filter){
new FileChooser(text, file -> file.extension().equalsIgnoreCase(filter), open, cons).show();

View File

@ -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();
}
}
}

View File

@ -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;
}
}