Skip to content

Commit 8663b73

Browse files
committed
Merge pull request #179 from JLLeitschuh/chore/betterErrorAndExceptionHandling
Improves unexpected throwable handling
2 parents e70e2aa + f97b9fa commit 8663b73

File tree

7 files changed

+92
-66
lines changed

7 files changed

+92
-66
lines changed

core/src/main/java/edu/wpi/grip/core/events/FatalErrorEvent.java

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package edu.wpi.grip.core.events;
2+
3+
import static com.google.common.base.Preconditions.checkNotNull;
4+
5+
/**
6+
* Event should be thrown when Unexpected Throwable ends up in an {@link Thread#uncaughtExceptionHandler}.
7+
*/
8+
public final class UnexpectedThrowableEvent {
9+
private final Throwable throwable;
10+
private final boolean fatal;
11+
private final String message;
12+
13+
/**
14+
* @param throwable The throwable that was caught.
15+
* @param message Any additional information that should be displayed to the user. Nullable.
16+
* @param fatal True if this cause the application to quit forcibly.
17+
* If the throwable is an {@link Error} than this is automatically true. Defaults to false.
18+
*/
19+
public UnexpectedThrowableEvent(Throwable throwable, String message, boolean fatal) {
20+
this.throwable = checkNotNull(throwable, "Throwable can not be null");
21+
this.message = checkNotNull(message, "Message can not be null");
22+
this.fatal = (throwable instanceof Error) || fatal;
23+
}
24+
25+
public UnexpectedThrowableEvent(Throwable throwable, String message) {
26+
this(throwable, message, false);
27+
}
28+
29+
30+
public Throwable getThrowable() {
31+
return throwable;
32+
}
33+
34+
public String getMessage() {
35+
return message;
36+
}
37+
38+
/**
39+
* @return True if this should cause the program to shutdown after it is handled
40+
*/
41+
public boolean isFatal() {
42+
return fatal;
43+
}
44+
}

core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import edu.wpi.grip.core.SocketHint;
1010
import edu.wpi.grip.core.SocketHints;
1111
import edu.wpi.grip.core.Source;
12-
import edu.wpi.grip.core.events.FatalErrorEvent;
12+
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
1313
import edu.wpi.grip.core.events.SourceRemovedEvent;
1414
import org.bytedeco.javacpp.opencv_core.Mat;
1515
import org.bytedeco.javacv.*;
@@ -172,16 +172,11 @@ private synchronized void startVideo(FrameGrabber grabber) throws IOException {
172172
}, "Camera");
173173
frameExecutor.setUncaughtExceptionHandler(
174174
(thread, exception) -> {
175-
// TODO Pass Exception to the UI.
176-
System.err.println("Webcam Frame Grabber Thread crashed with uncaught exception:");
177-
exception.printStackTrace();
178-
eventBus.post(new FatalErrorEvent(exception));
175+
eventBus.post(new UnexpectedThrowableEvent(exception, "Webcam Frame Grabber Thread crashed with uncaught exception"));
179176
try {
180177
stopVideo();
181178
} catch (TimeoutException e) {
182-
System.err.println("Webcam Frame Grabber could not be stopped!");
183-
e.printStackTrace();
184-
eventBus.post(new FatalErrorEvent(e));
179+
eventBus.post(new UnexpectedThrowableEvent(e, "Webcam Frame Grabber could not be stopped!"));
185180
}
186181
}
187182
);

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Fri Nov 20 19:04:57 EST 2015
1+
#Tue Dec 08 15:02:34 EST 2015
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
44
zipStoreBase=GRADLE_USER_HOME

ui/src/main/java/edu/wpi/grip/ui/ExceptionAlert.java

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package edu.wpi.grip.ui;
22

3+
import com.google.common.base.Throwables;
34
import com.google.common.net.UrlEscapers;
45
import javafx.application.HostServices;
56
import javafx.application.Platform;
@@ -11,9 +12,6 @@
1112
import javafx.scene.input.ClipboardContent;
1213
import javafx.scene.layout.GridPane;
1314

14-
import java.io.PrintWriter;
15-
import java.io.StringWriter;
16-
1715
import static com.google.common.base.Preconditions.checkNotNull;
1816

1917
/**
@@ -49,11 +47,12 @@ public final class ExceptionAlert extends Alert {
4947

5048
private final String exceptionMessage;
5149
private final String systemInfoMessage;
50+
private final String additionalInfoMessage;
51+
private final String message;
5252
private final Throwable initialCause;
5353

5454
private final ButtonType openGitHubIssuesBtnType = new ButtonType("Open GitHub Issues");
5555
private final ButtonType copyToClipboardBtnType = new ButtonType("Copy To Clipboard");
56-
private final ButtonType closeBtnType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
5756
private final Node initialFocusElement;
5857

5958
/**
@@ -62,17 +61,22 @@ public final class ExceptionAlert extends Alert {
6261
* the issue website.
6362
* @see <a href="http://code.makery.ch/blog/javafx-dialogs-official/">Inspiration</a>
6463
*/
65-
public ExceptionAlert(final Parent root, final Throwable throwable, final HostServices services) {
64+
public ExceptionAlert(final Parent root, final Throwable throwable, final String message, boolean isFatal, final HostServices services) {
6665
super(AlertType.ERROR);
66+
checkNotNull(root, "The parent can not be null");
6767
checkNotNull(throwable, "The Throwable can not be null");
68+
this.message = checkNotNull(message, "The message can not be null");
6869
checkNotNull(services, "HostServices can not be null");
6970

71+
final ButtonType closeBtnType = new ButtonType(isFatal ? "Quit" : "Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
72+
7073
this.exceptionMessage = generateExceptionMessage(throwable);
7174
this.systemInfoMessage = generateSystemInfoMessage();
72-
this.initialCause = generateInitialCause(throwable);
75+
this.additionalInfoMessage = generateAdditionalInfoMessage();
76+
this.initialCause = getInitialCause(throwable);
7377

7478
this.setTitle(initialCause.getClass().getSimpleName());
75-
this.setHeaderText(initialCause.getMessage());
79+
this.setHeaderText((isFatal ? "FATAL: " : "") + message);
7680

7781
// Set stylesheet
7882
this.getDialogPane().styleProperty().bind(root.styleProperty());
@@ -91,7 +95,7 @@ public ExceptionAlert(final Parent root, final Throwable throwable, final HostSe
9195
final Label issuePasteLabel = new Label(ISSUE_PROMPT_TEXT);
9296
issuePasteLabel.setWrapText(true);
9397

94-
final TextArea issueText = new TextArea(stackTrace(throwable));
98+
final TextArea issueText = new TextArea(Throwables.getStackTraceAsString(throwable));
9599
issuePasteLabel.setLabelFor(issueText);
96100
issueText.setEditable(false);
97101
issueText.setWrapText(true);
@@ -152,22 +156,16 @@ public final void setInitialFocus() {
152156
* @param throwable The throwable to iterate through.
153157
* @return The initial throwable
154158
*/
155-
private Throwable generateInitialCause(Throwable throwable) {
159+
private Throwable getInitialCause(Throwable throwable) {
156160
if (throwable.getCause() == null) {
157161
return throwable;
158162
} else {
159-
return generateInitialCause(throwable.getCause());
163+
return getInitialCause(throwable.getCause());
160164
}
161165
}
162166

163-
/**
164-
* Generates the Throwable's stack trace as a string.
165-
*/
166-
private String stackTrace(Throwable throwable) {
167-
final StringWriter sw = new StringWriter();
168-
final PrintWriter pw = new PrintWriter(sw);
169-
throwable.printStackTrace(pw);
170-
return sw.toString();
167+
private String generateAdditionalInfoMessage() {
168+
return "Message: " + message + "\n";
171169
}
172170

173171
/**
@@ -177,7 +175,7 @@ private String stackTrace(Throwable throwable) {
177175
* @return The markdown for the exception.
178176
*/
179177
private String generateExceptionMessage(Throwable throwable) {
180-
return new StringBuilder(stackTrace(throwable)
178+
return new StringBuilder(Throwables.getStackTraceAsString(throwable)
181179
/* Allow users to maintain anonymity */
182180
.replace(System.getProperty("user.home"), "$HOME").replace(System.getProperty("user.name"), "$USER"))
183181
.insert(0, "## Stack Trace:\n```java\n").append("\n```").toString();
@@ -203,10 +201,12 @@ private String generateSystemInfoMessage() {
203201
* @return The fully constructed issue text.
204202
*/
205203
private String issueText() {
206-
return new StringBuilder(ISSUE_PROMPT_QUESTION)
207-
.append("\n\n\n\n")
208-
.append(systemInfoMessage)
209-
.append(exceptionMessage).toString();
204+
return ISSUE_PROMPT_QUESTION
205+
+ "\n\n\n\n"
206+
+ additionalInfoMessage
207+
+ "\n"
208+
+ systemInfoMessage
209+
+ exceptionMessage;
210210
}
211211

212212

ui/src/main/java/edu/wpi/grip/ui/Main.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.google.common.eventbus.EventBus;
44
import com.google.common.eventbus.Subscribe;
55
import com.sun.javafx.application.PlatformImpl;
6-
import edu.wpi.grip.core.events.FatalErrorEvent;
6+
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
77
import edu.wpi.grip.ui.util.DPIUtility;
88
import javafx.application.Application;
99
import javafx.scene.Parent;
@@ -13,7 +13,7 @@
1313

1414
public class Main extends Application {
1515
private final EventBus eventBus = new EventBus((exception, context) -> {
16-
this.onFatalErrorEvent(new FatalErrorEvent(exception));
16+
this.triggerUnexpectedThrowableEvent(new UnexpectedThrowableEvent(exception, "An Event Bus subscriber threw an uncaught exception"));
1717
});
1818

1919
private final Object dialogLock = new Object();
@@ -31,8 +31,7 @@ public void start(Stage stage) {
3131
* Any exceptions thrown by the UI will be caught here and an exception dialog will be displayed
3232
*/
3333
Thread.currentThread().setUncaughtExceptionHandler((thread, throwable) -> {
34-
this.eventBus.post(new FatalErrorEvent(throwable));
35-
34+
this.eventBus.post(new UnexpectedThrowableEvent(throwable, "The UI Thread threw an uncaught exception"));
3635
});
3736

3837
root.setStyle("-fx-font-size: " + DPIUtility.FONT_SIZE + "px");
@@ -43,24 +42,34 @@ public void start(Stage stage) {
4342
stage.show();
4443
}
4544

45+
private void triggerUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
46+
eventBus.post(event);
47+
}
48+
4649
@Subscribe
47-
public final void onFatalErrorEvent(FatalErrorEvent error) {
50+
public final void onUnexpectedThrowableEvent(UnexpectedThrowableEvent event) {
4851
// Print throwable before showing the exception so that errors are in order in the console
49-
error.getThrowable().printStackTrace();
52+
event.getThrowable().printStackTrace();
5053
PlatformImpl.runAndWait(() -> {
5154
synchronized (this.dialogLock) {
5255
try {
5356
// Don't create more than one exception dialog at the same time
54-
final ExceptionAlert exceptionAlert = new ExceptionAlert(root, error.getThrowable(), getHostServices());
57+
final ExceptionAlert exceptionAlert = new ExceptionAlert(root, event.getThrowable(), event.getMessage(), event.isFatal(), getHostServices());
5558
exceptionAlert.setInitialFocus();
5659
exceptionAlert.showAndWait();
57-
} catch (Exception e) {
60+
} catch (Throwable e) {
5861
// Well in this case something has gone very, very wrong
5962
// We don't want to create a feedback loop either.
6063
e.printStackTrace();
6164
System.exit(1); // Ensure we shut down the application if we get an exception
6265
}
6366
}
6467
});
68+
69+
if(event.isFatal()) {
70+
System.err.println("Original fatal exception");
71+
event.getThrowable().printStackTrace();
72+
System.exit(1);
73+
}
6574
}
6675
}

ui/src/main/java/edu/wpi/grip/ui/pipeline/AddSourceView.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package edu.wpi.grip.ui.pipeline;
22

33
import com.google.common.eventbus.EventBus;
4-
import edu.wpi.grip.core.events.FatalErrorEvent;
4+
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
55
import edu.wpi.grip.core.events.SourceAddedEvent;
66
import edu.wpi.grip.core.sources.CameraSource;
77
import edu.wpi.grip.core.sources.ImageFileSource;
@@ -51,8 +51,7 @@ public AddSourceView(EventBus eventBus) {
5151
try {
5252
eventBus.post(new SourceAddedEvent(new ImageFileSource(eventBus, file)));
5353
} catch (IOException e) {
54-
e.printStackTrace();
55-
eventBus.post(new FatalErrorEvent(e));
54+
eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source"));
5655
}
5756
});
5857
});
@@ -78,8 +77,7 @@ public AddSourceView(EventBus eventBus) {
7877
final CameraSource source = new CameraSource(eventBus, cameraIndex.getValue());
7978
eventBus.post(new SourceAddedEvent(source));
8079
} catch (IOException e) {
81-
eventBus.post(new FatalErrorEvent(e));
82-
e.printStackTrace();
80+
eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source"));
8381
}
8482
});
8583
});
@@ -120,8 +118,7 @@ public AddSourceView(EventBus eventBus) {
120118
final CameraSource source = new CameraSource(eventBus, cameraAddress.getText());
121119
eventBus.post(new SourceAddedEvent(source));
122120
} catch (IOException e) {
123-
eventBus.post(new FatalErrorEvent(e));
124-
e.printStackTrace();
121+
eventBus.post(new UnexpectedThrowableEvent(e, "Tried to create an invalid source"));
125122
}
126123
});
127124
});

0 commit comments

Comments
 (0)