Added error codes to error screen (#1741)

Added error codes based on a translation of GeneratedErrorCode.kt (Cryptomator Android) into Java:

Source of GeneratedErrorCode.kt: 3ae90ab521/presentation/src/main/java/org/cryptomator/presentation/logging/GeneratedErrorCode.kt

Co-authored-by: Sebastian Stenzel <sebastian.stenzel@gmail.com>
This commit is contained in:
JaniruTEC
2021-09-21 17:41:54 +02:00
committed by GitHub
parent efc77e4997
commit 3e216ed0ac
7 changed files with 362 additions and 6 deletions

View File

@@ -0,0 +1,121 @@
package org.cryptomator.common;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* Holds a throwable and provides a human-readable {@link #toString() three-component string representation}
* aiming to allow documentation and lookup of same or similar errors.
*/
public class ErrorCode {
private final static int A_PRIME = 31;
private final static int SEED = 0xdeadbeef;
public final static String DELIM = ":";
private final static int LATEST_FRAME = 1;
private final static int ALL_FRAMES = Integer.MAX_VALUE;
private final Throwable throwable;
private final Throwable rootCause;
private final int rootCauseSpecificFrames;
private ErrorCode(Throwable throwable, Throwable rootCause, int rootCauseSpecificFrames) {
this.throwable = Objects.requireNonNull(throwable);
this.rootCause = Objects.requireNonNull(rootCause);
this.rootCauseSpecificFrames = rootCauseSpecificFrames;
}
// visible for testing
String methodCode() {
return format(traceCode(rootCause, LATEST_FRAME));
}
// visible for testing
String rootCauseCode() {
return format(traceCode(rootCause, rootCauseSpecificFrames));
}
// visible for testing
String throwableCode() {
return format(traceCode(throwable, ALL_FRAMES));
}
/**
* Produces an error code consisting of three {@value DELIM}-separated components.
* <p>
* A full match of the error code indicates the exact same throwable (to the extent possible
* without hash collisions). A partial match of the first or second component indicates related problems
* with the same root cause.
*
* @return A three-part error code
*/
@Override
public String toString() {
return methodCode() + DELIM + rootCauseCode() + DELIM + throwableCode();
}
/**
* Deterministically creates an error code from the stack trace of the given <code>cause</code>.
* <p>
* The code consists of three parts separated by {@value DELIM}:
* <ul>
* <li>The first part depends on the root cause and the method that threw it</li>
* <li>The second part depends on the root cause and its stack trace</li>
* <li>The third part depends on all the cause hierarchy</li>
* </ul>
* <p>
* Parts may be identical if the cause is the root cause or the root cause has just one single item in its stack trace.
*
* @param throwable The exception
* @return A three-part error code
*/
public static ErrorCode of(Throwable throwable) {
var causalChain = Throwables.getCausalChain(throwable);
if (causalChain.size() > 1) {
var rootCause = causalChain.get(causalChain.size() - 1);
var parentOfRootCause = causalChain.get(causalChain.size() - 2);
var rootSpecificFrames = nonOverlappingFrames(parentOfRootCause.getStackTrace(), rootCause.getStackTrace());
return new ErrorCode(throwable, rootCause, rootSpecificFrames);
} else {
return new ErrorCode(throwable, throwable, ALL_FRAMES);
}
}
private String format(int value) {
// Cut off highest 12 bits (only leave 20 least significant bits) and XOR rest with cutoff
value = (value & 0xfffff) ^ (value >>> 20);
return Strings.padStart(Integer.toString(value, 32).toUpperCase(Locale.ROOT), 4, '0');
}
private int traceCode(Throwable e, int frameCount) {
int result = SEED;
if (e.getCause() != null) {
result = traceCode(e.getCause(), frameCount);
}
result = result * A_PRIME + e.getClass().getName().hashCode();
var stack = e.getStackTrace();
for (int i = 0; i < Math.min(stack.length, frameCount); i++) {
result = result * A_PRIME + stack[i].getClassName().hashCode();
result = result * A_PRIME + stack[i].getMethodName().hashCode();
}
return result;
}
private static int nonOverlappingFrames(StackTraceElement[] frames, StackTraceElement[] enclosingFrames) {
// Compute the number of elements in `frames` not contained in `enclosingFrames` by iterating backwards
// Result should usually be equal to the difference in size of both traces
var i = reverseStream(enclosingFrames).iterator();
return (int) reverseStream(frames).dropWhile(f -> i.hasNext() && i.next().equals(f)).count();
}
private static <T> Stream<T> reverseStream(T[] array) {
return IntStream.rangeClosed(1, array.length).mapToObj(i -> array[array.length - i]);
}
}

View File

@@ -1,22 +1,47 @@
package org.cryptomator.ui.common;
import org.cryptomator.common.ErrorCode;
import org.cryptomator.common.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.stage.Stage;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class ErrorController implements FxController {
private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s";
private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
private static final String SEARCH_ERRORCODE_DELIM = " OR ";
private static final String REPORT_BODY_TEMPLATE = """
<!-- ✏️ Please describe what happened as accurately as possible. -->
<!-- 📋 Please also copy and paste the detail text from the error window. -->
""";
private final Application application;
private final String stackTrace;
private final ErrorCode errorCode;
private final Scene previousScene;
private final Stage window;
private BooleanProperty copiedDetails = new SimpleBooleanProperty();
@Inject
ErrorController(@Named("stackTrace") String stackTrace, @Nullable Scene previousScene, Stage window) {
ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window) {
this.application = application;
this.stackTrace = stackTrace;
this.errorCode = errorCode;
this.previousScene = previousScene;
this.window = window;
}
@@ -33,6 +58,31 @@ public class ErrorController implements FxController {
window.close();
}
@FXML
public void searchError() {
var searchTerm = URLEncoder.encode(getErrorCode().replace(ErrorCode.DELIM, SEARCH_ERRORCODE_DELIM), StandardCharsets.UTF_8);
application.getHostServices().showDocument(SEARCH_URL_FORMAT.formatted(searchTerm));
}
@FXML
public void reportError() {
var title = URLEncoder.encode(getErrorCode(), StandardCharsets.UTF_8);
var body = URLEncoder.encode(REPORT_BODY_TEMPLATE, StandardCharsets.UTF_8);
application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body));
}
@FXML
public void copyDetails() {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(getDetailText());
Clipboard.getSystemClipboard().setContent(clipboardContent);
copiedDetails.set(true);
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
copiedDetails.set(false);
});
}
/* Getter/Setter */
public boolean isPreviousScenePresent() {
@@ -42,4 +92,20 @@ public class ErrorController implements FxController {
public String getStackTrace() {
return stackTrace;
}
public String getErrorCode() {
return errorCode.toString();
}
public String getDetailText() {
return "```\nError Code " + getErrorCode() + "\n" + getStackTrace() + "\n```";
}
public BooleanProperty copiedDetailsProperty() {
return copiedDetails;
}
public boolean getCopiedDetails() {
return copiedDetails.get();
}
}

View File

@@ -4,6 +4,7 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.ErrorCode;
import javax.inject.Named;
import javax.inject.Provider;
@@ -31,6 +32,11 @@ abstract class ErrorModule {
return baos.toString(StandardCharsets.UTF_8);
}
@Provides
static ErrorCode provideErrorCode(Throwable cause) {
return ErrorCode.of(cause);
}
@Binds
@IntoMap
@FxControllerKey(ErrorController.class)

View File

@@ -12,6 +12,7 @@ public enum FontAwesome5Icon {
CARET_RIGHT("\uF0Da"), //
CHECK("\uF00C"), //
CLOCK("\uF017"), //
CLIPBOARD("\uF328"), //
COG("\uF013"), //
COGS("\uF085"), //
COPY("\uF0C5"), //