From 3e216ed0ac525e0f57f93367c546cd4b33dd8e9e Mon Sep 17 00:00:00 2001 From: JaniruTEC <52893617+JaniruTEC@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:41:54 +0200 Subject: [PATCH] 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: https://github.com/cryptomator/android/blob/3ae90ab521a4aa69f394c0490f27e9db6106ce0e/presentation/src/main/java/org/cryptomator/presentation/logging/GeneratedErrorCode.kt Co-authored-by: Sebastian Stenzel --- .../org/cryptomator/common/ErrorCode.java | 121 ++++++++++++++++ .../ui/common/ErrorController.java | 68 ++++++++- .../cryptomator/ui/common/ErrorModule.java | 6 + .../ui/controls/FontAwesome5Icon.java | 1 + src/main/resources/fxml/error.fxml | 35 ++++- src/main/resources/i18n/strings.properties | 7 +- .../org/cryptomator/common/ErrorCodeTest.java | 130 ++++++++++++++++++ 7 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/cryptomator/common/ErrorCode.java create mode 100644 src/test/java/org/cryptomator/common/ErrorCodeTest.java diff --git a/src/main/java/org/cryptomator/common/ErrorCode.java b/src/main/java/org/cryptomator/common/ErrorCode.java new file mode 100644 index 000000000..51fb355b6 --- /dev/null +++ b/src/main/java/org/cryptomator/common/ErrorCode.java @@ -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. + *

+ * 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 cause. + *

+ * The code consists of three parts separated by {@value DELIM}: + *

+ *

+ * 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 Stream reverseStream(T[] array) { + return IntStream.rangeClosed(1, array.length).mapToObj(i -> array[array.length - i]); + } + +} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/ui/common/ErrorController.java b/src/main/java/org/cryptomator/ui/common/ErrorController.java index 85b335b15..c75df26ce 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorController.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorController.java @@ -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 = """ + + + """; + + 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(); + } } diff --git a/src/main/java/org/cryptomator/ui/common/ErrorModule.java b/src/main/java/org/cryptomator/ui/common/ErrorModule.java index d2515e661..01b8790c1 100644 --- a/src/main/java/org/cryptomator/ui/common/ErrorModule.java +++ b/src/main/java/org/cryptomator/ui/common/ErrorModule.java @@ -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) diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 15b1718e1..66eda7556 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -12,6 +12,7 @@ public enum FontAwesome5Icon { CARET_RIGHT("\uF0Da"), // CHECK("\uF00C"), // CLOCK("\uF017"), // + CLIPBOARD("\uF328"), // COG("\uF013"), // COGS("\uF085"), // COPY("\uF0C5"), // diff --git a/src/main/resources/fxml/error.fxml b/src/main/resources/fxml/error.fxml index adcebdb67..4dbddc4c3 100644 --- a/src/main/resources/fxml/error.fxml +++ b/src/main/resources/fxml/error.fxml @@ -1,12 +1,15 @@ + + + @@ -15,7 +18,7 @@ fx:controller="org.cryptomator.ui.common.ErrorController" prefWidth="450" prefHeight="450" - spacing="12" + spacing="18" alignment="TOP_CENTER"> @@ -27,12 +30,38 @@ - -