Documented and prettified CrashReports

This commit is contained in:
OLEGSHA 2021-01-02 00:08:32 +03:00
parent bedc9fc729
commit 01645f5c6e
3 changed files with 141 additions and 57 deletions

View File

@ -690,6 +690,34 @@ public class StringUtil {
return padToRight(src, length, ' '); return padToRight(src, length, ' ');
} }
public static String center(String src, int length) {
return center(src, length, ' ');
}
public static String center(String src, int length, char filler) {
if (length <= 0) {
throw new IllegalArgumentException("length must be positive (" + length + ")");
}
if (src == null || length <= src.length()) {
return src;
}
char[] result = new char[length];
int leftPaddingLength = (length - src.length()) / 2;
Arrays.fill(result, 0, leftPaddingLength, filler);
for (int i = 0; i < src.length(); ++i) {
result[i + leftPaddingLength] = src.charAt(i);
}
Arrays.fill(result, leftPaddingLength + src.length(), result.length, filler);
return new String(result);
}
public static int countWords(String src) { public static int countWords(String src) {
int i = 0; int i = 0;
boolean isWord = false; boolean isWord = false;

View File

@ -18,11 +18,14 @@ import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
/**
* A utility for reporting critical problems, gathering system context and terminating the application consequently (crashing).
* Do not hesitate to use {@link #report(Throwable, String, Object...)} at every other line.
*
* @author serega404
*/
public class CrashReports { public class CrashReports {
private CrashReports() {
}
private static final Path CRASH_REPORTS_PATH = Paths.get("crash-reports"); private static final Path CRASH_REPORTS_PATH = Paths.get("crash-reports");
private static final Collection<ContextProvider> PROVIDERS = Collections.synchronizedCollection(new ArrayList<>()); private static final Collection<ContextProvider> PROVIDERS = Collections.synchronizedCollection(new ArrayList<>());
@ -31,31 +34,68 @@ public class CrashReports {
private static final Logger LOGGER = LogManager.getLogger("crash"); private static final Logger LOGGER = LogManager.getLogger("crash");
/**
* Creates a {@link ReportedException} that describes the provided problem so the program can crash later.
* This method is intended to be used like so:
*
* <pre>
* try {
* doSomethingDifficult(x);
* } catch (CouldntMakeItException e) {
* throw CrashReports.report(e, "We couldn't make it at x = %d", x);
* }
* </pre>
*
* <p>Such usage ensures that the report will be dealt with at the top of the call stack
* (at least in methods that have a properly set up
* {@linkplain #crash(Throwable, String, Object...) crash handler}). Not throwing the returned
* exception is pointless; using this in a thread without a crash handler will not produce a crash.
*
* <p>Avoid inserting variable information into {@code messageFormat} directly; use
* {@linkplain Formatter#summary format string} syntax and {@code args}. Different Strings
* in {@code messageFormat} may be interpreted as unrelated problems by {@linkplain Analyzer crash analyzers}.
*
* @param throwable a {@link Throwable} that caused the problem, if any; {@code null} otherwise
* @param messageFormat a human-readable description of the problem displayed in the crash report
* @param args an array of arguments for formatting {@code messageFormat}
* @return an exception containing the provided information that must be thrown
*/
public static ReportedException report(Throwable throwable, String messageFormat, Object... args) { public static ReportedException report(Throwable throwable, String messageFormat, Object... args) {
if (throwable instanceof ReportedException) return (ReportedException) throwable; if (throwable instanceof ReportedException) return (ReportedException) throwable;
return new ReportedException(throwable, messageFormat, args); return new ReportedException(throwable, messageFormat, args);
} }
public static RuntimeException crash(Throwable throwable, String messageFormat, Object... args) {
if (throwable instanceof ReportedException) {
throw crash((ReportedException) throwable);
} else {
throw crash(report(throwable, messageFormat, args));
}
}
/** /**
* <em>This method never returns.</em> * Crashes the program due to the supplied problem.
* <p>
* TODO document
* *
* @param reportException * <p><em>Use {@link #report(Throwable, String, Object...)} unless you are creating a catch-all handler for a
* thread.</em>
*
* <p>This method recovers information about the problem by casting {@code throwable} to {@link ReportedException},
* or, failing that, uses the provided arguments as the information instead. It then constructs a full crash
* report, exports it and terminates the program by invoking {@link System#exit(int)}.
*
* <p>Such behavior can be dangerous or lead to unwanted consequences in the middle of the call stack, so it is
* necessary to invoke this method as high on the call stack as possible, usually in a {@code catch} clause
* of a {@code try} statement enveloping the thread's main method(s).
*
* @param throwable a {@link ReportedException} or another {@link Throwable} that caused the problem, if any;
* {@code null} otherwise
* @param messageFormat a human-readable description of the problem used when {@code throwable} is not a
* {@link ReportedException}. See {@link #report(Throwable, String, Object...)} for details.
* @param args an array of arguments for formatting {@code messageFormat}
* @return {@code null}, although this method never returns normally. Provided for convenience.
*/ */
public static RuntimeException crash(ReportedException reportException) { public static RuntimeException crash(Throwable throwable, String messageFormat, Object... args) {
Throwable throwable = reportException.getCause(); if (throwable instanceof ReportedException) {
String messageFormat = reportException.getMessageFormat(); ReportedException reportedException = (ReportedException) throwable;
Object[] args = reportException.getArgs();
// Discard provided arguments
throwable = reportedException.getCause();
messageFormat = reportedException.getMessageFormat();
args = reportedException.getArgs();
}
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
@ -92,7 +132,7 @@ public class CrashReports {
export(output.toString()); export(output.toString());
System.exit(0); System.exit(0);
return reportException; return null;
} }
private static void appendContextProviders(StringBuilder output) { private static void appendContextProviders(StringBuilder output) {
@ -111,7 +151,7 @@ public class CrashReports {
provider.provideContext(buf); provider.provideContext(buf);
if (!buf.isEmpty()) { if (!buf.isEmpty()) {
output.append(StringUtilsTemp.center(provider.getName(), 80)).append("\n"); output.append(StringUtil.center(provider.getName(), 80)).append("\n");
for (Map.Entry<String, String> entry : buf.entrySet()) { for (Map.Entry<String, String> entry : buf.entrySet()) {
output.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); output.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
} }
@ -139,7 +179,7 @@ public class CrashReports {
Analyzer[] localAnalyzersCopy = ANALYZERS.toArray(new Analyzer[ANALYZERS.size()]); Analyzer[] localAnalyzersCopy = ANALYZERS.toArray(new Analyzer[ANALYZERS.size()]);
if (localAnalyzersCopy.length > 0) { if (localAnalyzersCopy.length > 0) {
output.append(StringUtilsTemp.center("Analyzers", 80)).append("\n"); output.append(StringUtil.center("Analyzers", 80)).append("\n");
} }
for (Analyzer analyzer : localAnalyzersCopy) { for (Analyzer analyzer : localAnalyzersCopy) {
@ -239,57 +279,73 @@ public class CrashReports {
} }
} }
public static void registerProvider(ContextProvider provider) {
PROVIDERS.add(provider);
}
public static void registerAnalyzer(Analyzer analyzer) {
ANALYZERS.add(analyzer);
}
private static void addSeparator(StringBuilder sb) { private static void addSeparator(StringBuilder sb) {
sb.append(StringUtil.sequence('-', 80)).append("\n"); sb.append(StringUtil.sequence('-', 80)).append("\n");
} }
/**
* Registers the provided {@link ContextProvider} so it is consulted in the case of a crash.
* @param provider the provider to register
*/
public static void registerProvider(ContextProvider provider) {
PROVIDERS.add(provider);
}
/**
* Registers the provided {@link Analyzer} so it is consulted in the case of a crash.
* @param analyzer the analyzer to register
*/
public static void registerAnalyzer(Analyzer analyzer) {
ANALYZERS.add(analyzer);
}
/**
* A wrapper used by {@link CrashReports} to transfer problem details from the place of
* occurrence to the handler at the top of the stack. Rethrow if caught
* (unless using {@link CrashReports#report(Throwable, String, Object...)}, which does
* so automatically).
*
* @author serega404
*/
public static class ReportedException extends RuntimeException { public static class ReportedException extends RuntimeException {
private String messageFormat; private static final long serialVersionUID = 223720835231091533L;
private Object[] args;
public ReportedException(Throwable throwable, String messageFormat, Object... args){ private final String messageFormat;
private final Object[] args;
/**
* Constructs a {@link ReportedException}.
* @param throwable the reported {@link Throwable} or {@code null}
* @param messageFormat the reported message format.
* <em>This is not the message of the constructed Exception</em>.
* @param args the reported message format arguments
*/
public ReportedException(Throwable throwable, String messageFormat, Object... args) {
super(throwable); super(throwable);
this.messageFormat = messageFormat; this.messageFormat = messageFormat;
this.args = args; this.args = args;
} }
/**
* Returns the reported message format.
* @return message format
*/
public String getMessageFormat() { public String getMessageFormat() {
return messageFormat; return messageFormat;
} }
/**
* Returns the reported message format arguments.
* @return message format arguments
*/
public Object[] getArgs() { public Object[] getArgs() {
return args; return args;
} }
} }
} private CrashReports() {
class StringUtilsTemp {
public static String center(String s, int size) {
return center(s, size, ' ');
} }
public static String center(String s, int size, char pad) {
if (s == null || size <= s.length())
return s;
StringBuilder sb = new StringBuilder(size);
for (int i = 0; i < (size - s.length()) / 2; i++) {
sb.append(pad);
}
sb.append(s);
while (sb.length() < size) {
sb.append(pad);
}
return sb.toString();
}
} }

View File

@ -11,7 +11,7 @@ public class StackTraceProvider implements ContextProvider {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("\n"); sb.append("\n");
for (int i = 4; i < stackTraceBuffer.length; i++) { for (int i = 4; i < stackTraceBuffer.length; i++) {
sb.append(stackTraceBuffer[i].toString()).append("\n"); sb.append('\t').append(stackTraceBuffer[i].toString()).append("\n");
} }
output.put("Reported from " + Thread.currentThread().getName(), sb.toString()); output.put("Reported from " + Thread.currentThread().getName(), sb.toString());