Skip to content

Commit 131b26a

Browse files
Add UncaughtExceptionHandler, improve README
1 parent 1ff3292 commit 131b26a

14 files changed

+374
-51
lines changed

README.md

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,117 @@ Maven (pom.xml):
2626
</dependency>
2727
```
2828

29-
## Usage
29+
To use the `GraphicalReportHandler` and other features of the package `com.opencastsoftware.yvette.handlers.graphical`, the [jansi](https://github.com/fusesource/jansi) library is also needed:
30+
31+
Gradle (build.gradle / build.gradle.kts):
32+
```groovy
33+
implementation("org.fusesource.jansi:jansi:2.4.0")
34+
```
35+
36+
Maven (pom.xml):
37+
```xml
38+
<dependency>
39+
<groupId>org.fusesource.jansi</groupId>
40+
<artifactId>jansi</artifactId>
41+
<version>2.4.0</version>
42+
</dependency>
43+
```
44+
45+
## Displaying diagnostics
46+
47+
In order to display diagnostics with *yvette*, your application's error messages must implement the abstract class [Diagnostic](./src/main/java/com/opencastsoftware/yvette/Diagnostic.java). All of the methods of this class may return `null`, but bear in mind that your diagnostics may not be very helpful without a `message`.
48+
49+
A [BasicDiagnostic](./src/main/java/com/opencastsoftware/yvette/BasicDiagnostic.java) implementation is provided, which can be used to wrap exceptions:
50+
51+
```java
52+
Diagnostic err = new BasicDiagnostic(e.getMessage(), e.getCause());
53+
```
54+
55+
You can then implement a [ReportHandler](./src/main/java/com/opencastsoftware/yvette/handlers/ReportHandler.java) to display your diagnostics using the [Appendable](https://docs.oracle.com/javase/8/docs/api/java/lang/Appendable.html) instance you wish to use for output, for example [System.out](https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#out), [System.err](https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#err) or a [StringBuilder](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html).
56+
57+
*yvette* provides a [GraphicalReportHandler](./src/main/java/com/opencastsoftware/yvette/handlers/graphical/GraphicalReportHandler.java) which produces output like the screenshot above. You can create one of these using the `GraphicalReportHandler.builder()` static method:
58+
59+
```java
60+
ReportHandler reportHandler = GraphicalReportHandler.builder()
61+
.withRgbColours(RgbColours.PREFERRED)
62+
.buildFor(System.err);
63+
```
64+
65+
There are some terminal feature detection features in *yvette*. If you wish to bypass these, use the `withColours`, `withTerminalWidth` and `withUnicode` methods of the builder to enable or disable those features explicitly. However, please bear in mind that using `withColours` to force enable colour output will override the [NO_COLOR](https://no-color.org/) detection implemented by this library.
66+
67+
Once you have a [ReportHandler](./src/main/java/com/opencastsoftware/yvette/handlers/ReportHandler.java), it can be used to output diagnostics:
68+
69+
```java
70+
Diagnostic diagnostic = ???; // A diagnostic from your application
71+
reportHandler.display(diagnostic, System.err);
72+
```
73+
74+
## Uncaught exception handler
75+
76+
*yvette* provides an implementation of [Thread.UncaughtExceptionHandler](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.UncaughtExceptionHandler.html) which can be used to replace the default handler for all threads.
77+
78+
```java
79+
import com.opencastsoftware.yvette.UncaughtExceptionHandler;
80+
81+
ReportHandler handler = ???; // Your report handler, obtained as described above
82+
83+
try {
84+
UncaughtExceptionHandler.install(handler, System.err); // Installs the new handler
85+
} finally {
86+
UncaughtExceptionHandler.uninstall(); // Restores the default handler
87+
}
88+
```
89+
90+
The setup above will print diagnostics to STDERR:
91+
92+
```java
93+
new Thread(() -> {
94+
Throwable exc = new FileNotFoundException("Couldn't find the file BadFile.java");
95+
exc.initCause(new AccessDeniedException("Access denied to file BadFile.java"));
96+
throw new RuntimeException("Whoops!", exc);
97+
}).start();
98+
99+
/*
100+
Uncaught exception in thread Thread-5:
101+
x Whoops!
102+
|-> Couldn't find the file BadFile.java
103+
`-> Access denied to file BadFile.java
104+
105+
java.lang.RuntimeException: Whoops!
106+
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:87)
107+
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
108+
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
109+
at java.lang.Thread.run(Thread.java:750)
110+
Caused by: java.io.FileNotFoundException: Couldn't find the file BadFile.java
111+
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:85)
112+
... 3 more
113+
Caused by: java.nio.file.AccessDeniedException: Access denied to file BadFile.java
114+
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:86)
115+
... 3 more
116+
*/
117+
```
118+
119+
It can also be set as the uncaught exception handler for new threads in a thread pool by using a [ThreadFactory](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadFactory.html):
120+
121+
```java
122+
Thread.UncaughtExceptionHandler excHandler = UncaughtExceptionHandler.create(handler, System.err);
123+
124+
ThreadFactory threadFactory = runnable -> {
125+
Thread thread = new Thread(runnable);
126+
thread.setUncaughtExceptionHandler(excHandler);
127+
thread.setDaemon(true);
128+
return newThread;
129+
};
130+
```
131+
132+
## Deviations and Limitations
133+
134+
This is not an exact port of *miette* - there are some differences and unported features:
135+
136+
* Where *miette* and the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)'s definitions deviate, we have erred on the side of alignment with LSP. This is for ease of integrating *yvette* with language server applications. For example, *miette* uses `SourceSpan` to keep track of the byte offset and length of a span within a source file. However, *yvette*'s equivalent of `SourceSpan` is called `Range`, and specifies a start and end `Position`, each of which refers to a zero-indexed line and character position within a document. The upside of this is alignment with LSP, but the downside is that we can no longer efficiently read arbitrary offsets of a source file in order to get the span contents.
137+
* Only the `GraphicalReportHandler` and `ToStringReportHandler` are currently implemented, there is no `NarratableReportHandler` or `JSONReportHandler` as yet.
138+
* There is no special handling of tab characters or [unicode character width](https://crates.io/crates/unicode-width) in *yvette* yet, which may mean that your highlights are misaligned with the source code they are supposed to underline.
139+
* Related diagnostics are not currently implemented in *yvette*, since the definition of `DiagnosticRelatedInformation` is sufficiently different in the Language Server Protocol that we haven't decided how to implement it yet.
30140

31141
## Acknowlegements
32142

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ nexusPublishing {
106106
}
107107

108108
tasks.named<Test>("test") {
109-
useJUnitPlatform() {
109+
useJUnitPlatform {
110110
includeEngines("junit-jupiter", "jqwik")
111111
}
112112
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.opencastsoftware.yvette;
2+
3+
import java.net.URI;
4+
import java.util.Collection;
5+
6+
public class BasicDiagnostic extends Diagnostic {
7+
public String code;
8+
public Severity severity;
9+
public String help;
10+
public URI url;
11+
public SourceCode sourceCode;
12+
public Collection<LabelledRange> labels;
13+
14+
public BasicDiagnostic(String message) {
15+
this(null, Severity.Error, message, null, null, null, null);
16+
}
17+
18+
public BasicDiagnostic(
19+
String message,
20+
Throwable cause) {
21+
this(null, Severity.Error, message, cause, null, null, null, null);
22+
}
23+
24+
public BasicDiagnostic(
25+
String message,
26+
Throwable cause,
27+
URI url) {
28+
this(null, Severity.Error, message, cause, null, url, null, null);
29+
}
30+
31+
public BasicDiagnostic(
32+
String code,
33+
Severity severity,
34+
String message,
35+
String help,
36+
URI url,
37+
SourceCode sourceCode,
38+
Collection<LabelledRange> labels) {
39+
super(message);
40+
}
41+
42+
public BasicDiagnostic(
43+
String code,
44+
Severity severity,
45+
String message, Throwable cause,
46+
String help,
47+
URI url,
48+
SourceCode sourceCode,
49+
Collection<LabelledRange> labels) {
50+
super(message, cause);
51+
}
52+
53+
@Override
54+
public String code() {
55+
return code;
56+
}
57+
58+
@Override
59+
public Severity severity() {
60+
return severity;
61+
}
62+
63+
@Override
64+
public String message() {
65+
return getMessage();
66+
}
67+
68+
@Override
69+
public String help() {
70+
return help;
71+
}
72+
73+
@Override
74+
public URI url() {
75+
return url;
76+
}
77+
78+
@Override
79+
public SourceCode sourceCode() {
80+
return sourceCode;
81+
}
82+
83+
@Override
84+
public Collection<LabelledRange> labels() {
85+
return labels;
86+
}
87+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.opencastsoftware.yvette;
2+
3+
import java.io.IOException;
4+
import java.io.PrintStream;
5+
6+
import com.opencastsoftware.yvette.handlers.ReportHandler;
7+
8+
public class UncaughtExceptionHandler {
9+
private static volatile Thread.UncaughtExceptionHandler previous;
10+
11+
private UncaughtExceptionHandler() {
12+
}
13+
14+
static {
15+
previous = Thread.getDefaultUncaughtExceptionHandler();
16+
}
17+
18+
public static Thread.UncaughtExceptionHandler create(ReportHandler handler, PrintStream output) {
19+
return (thread, exc) -> {
20+
Diagnostic diagnostic = new BasicDiagnostic(exc.getMessage(), exc.getCause());
21+
22+
output.format("Uncaught exception in thread %s: ", thread.getName());
23+
24+
try {
25+
handler.display(diagnostic, output);
26+
output.println();
27+
exc.printStackTrace(output);
28+
} catch (IOException ioe) {
29+
ioe.addSuppressed(exc);
30+
ioe.printStackTrace(output);
31+
}
32+
};
33+
}
34+
35+
public static void install(ReportHandler handler, PrintStream output) {
36+
previous = Thread.getDefaultUncaughtExceptionHandler();
37+
Thread.setDefaultUncaughtExceptionHandler(create(handler, output));
38+
}
39+
40+
public static void uninstall() {
41+
Thread.setDefaultUncaughtExceptionHandler(previous);
42+
}
43+
}

src/main/java/com/opencastsoftware/yvette/handlers/graphical/ColourSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public enum ColourSupport {
88

99
private final int level;
1010

11-
private ColourSupport(int level) {
11+
ColourSupport(int level) {
1212
this.level = level;
1313
}
1414

src/main/java/com/opencastsoftware/yvette/handlers/graphical/GraphicalReportHandler.java

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class GraphicalReportHandler implements ReportHandler {
2727
private final int contextLines;
2828
private final boolean renderCauseChain;
2929

30-
private final Pattern ansiEscapePattern = Pattern.compile("\\u001B\\[[;\\d]*m");
30+
private static final Pattern ansiEscapePattern = Pattern.compile("\\u001B\\[[;\\d]*m");
3131

3232
public GraphicalReportHandler(LinkStyle linkStyle, int terminalWidth, GraphicalTheme theme, String footer,
3333
int contextLines, boolean renderCauseChain) {
@@ -557,7 +557,7 @@ void renderHelp(Ansi ansi, Diagnostic diagnostic) throws IOException {
557557
}
558558

559559
void renderRelated(Ansi ansi, Diagnostic diagnostic, SourceCode source) {
560-
560+
// TODO: Figure out how to align this with LSP
561561
}
562562

563563
void renderFooter(Ansi ansi) throws IOException {
@@ -604,7 +604,6 @@ public int hashCode() {
604604
result = prime * result + ((footer == null) ? 0 : footer.hashCode());
605605
result = prime * result + contextLines;
606606
result = prime * result + (renderCauseChain ? 1231 : 1237);
607-
result = prime * result + ((ansiEscapePattern == null) ? 0 : ansiEscapePattern.hashCode());
608607
return result;
609608
}
610609

@@ -635,11 +634,6 @@ public boolean equals(Object obj) {
635634
return false;
636635
if (renderCauseChain != other.renderCauseChain)
637636
return false;
638-
if (ansiEscapePattern == null) {
639-
if (other.ansiEscapePattern != null)
640-
return false;
641-
} else if (!ansiEscapePattern.equals(other.ansiEscapePattern))
642-
return false;
643637
return true;
644638
}
645639

@@ -711,13 +705,7 @@ public Builder withContextLines(int contextLines) {
711705
}
712706

713707
public GraphicalReportHandler buildFor(Appendable output) {
714-
int width;
715-
716-
if (terminalWidth.isPresent()) {
717-
width = terminalWidth.get();
718-
} else {
719-
width = TerminalSupport.terminalWidth(output);
720-
}
708+
int width = terminalWidth.orElseGet(() -> TerminalSupport.terminalWidth(output));
721709

722710
LinkStyle linkStyle;
723711
if (enableTerminalLinks.isPresent()) {

src/main/java/com/opencastsoftware/yvette/handlers/graphical/RgbColours.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
public enum RgbColours {
44
NEVER,
55
PREFERRED,
6-
ALWAYS;
6+
ALWAYS
77
}

src/main/java/com/opencastsoftware/yvette/handlers/graphical/RgbThemeStyles.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public UnaryOperator<Ansi> lineNumber() {
4949

5050
@Override
5151
public UnaryOperator<Ansi> reset() {
52-
return ansi -> ansi.reset();
52+
return Ansi::reset;
5353
}
5454

5555
@Override

src/main/java/com/opencastsoftware/yvette/handlers/graphical/TerminalSupport.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,12 @@ static boolean envVarSet(String varName) {
204204

205205
static boolean envVarIn(String varName, String... values) {
206206
String varValue = System.getenv(varName);
207-
return varValue != null && Arrays.stream(values).anyMatch(v -> v.equals(varValue));
207+
return varValue != null && Arrays.asList(values).contains(varValue);
208208
}
209209

210210
static boolean envVarNotIn(String varName, String... values) {
211211
String varValue = System.getenv(varName);
212-
return varValue != null && Arrays.stream(values).noneMatch(v -> v.equals(varValue));
212+
return varValue != null && !Arrays.asList(values).contains(varValue);
213213
}
214214

215215
static boolean envVarMatches(String varName, Predicate<String> valueFn) {
@@ -219,7 +219,7 @@ static boolean envVarMatches(String varName, Predicate<String> valueFn) {
219219

220220
static boolean envVarEquals(String varName, String onValue) {
221221
String varValue = System.getenv(varName);
222-
return varValue != null && onValue.equals(varValue);
222+
return onValue.equals(varValue);
223223
}
224224

225225
static boolean envVarNotEquals(String varName, String offValue) {

src/main/java/com/opencastsoftware/yvette/handlers/graphical/ThemeStyles.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,6 @@ default UnaryOperator<Ansi> forSeverity(Severity severity) {
4747
return style;
4848
}
4949

50-
static ThemeStyles forColourLevel(ColourSupport level) {
51-
ThemeStyles styles = null;
52-
53-
switch (level) {
54-
case COLOUR_16:
55-
styles = ansi();
56-
case COLOUR_256:
57-
styles = ansi();
58-
case COLOUR_16M:
59-
styles = rgb();
60-
case NONE:
61-
styles = none();
62-
}
63-
64-
return styles;
65-
}
66-
6750
static ThemeStyles ansi() {
6851
return new AnsiThemeStyles();
6952
}

0 commit comments

Comments
 (0)