Skip to content

Commit 1363520

Browse files
committed
Prevent JVM from exiting with 1 when main thread is only non-daemon
DevTools deliberately throws an uncaught exception on the main thread as a safe way of causing it to stop processing. This exception is caught and swallowed by an uncaught exception handler. Unfortunately, this has the unwanted side-effect of causing the JVM to exit with 1 once all running threads are daemons. Normally, this isn't a problem. Non-daemon threads, such as those started by an embedded servlet container, will keep the JVM alive and restarts of the application context will occur when the user makes to their application. However, if the user adds DevTools to an application that doesn't start any non-daemon threads, i.e. it starts, runs, and then exits, it will exit with 1. This causes both bootRun in Gradle and spring-boot:run in Maven to report that the build has failed. While there's no benefit to using DevTools with an application that behaves in this way, the side-effect of causing the JVM to exit with 1 is unwanted. This commit address the problem by updating the uncaught exception handler to call System.exit(0) if the JVM is going to exit as a result of the uncaught exception causing the main thread to die. In other words, if the main thread was the only non-daemon thread, its death as a result of the uncaught exception will now cause the JVM to exit with 1 rather than 0. If there are other non-daemon threads that will keep the JVM alive, the behaviour is unchanged. Closes gh-5968
1 parent 6574fee commit 1363520

File tree

2 files changed

+116
-1
lines changed

2 files changed

+116
-1
lines changed

spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2015 the original author or authors.
2+
* Copyright 2012-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,11 +17,13 @@
1717
package org.springframework.boot.devtools.restart;
1818

1919
import java.lang.Thread.UncaughtExceptionHandler;
20+
import java.util.Arrays;
2021

2122
/**
2223
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
2324
*
2425
* @author Phillip Webb
26+
* @author Andy Wilkinson
2527
*/
2628
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
2729

@@ -34,6 +36,9 @@ class SilentExitExceptionHandler implements UncaughtExceptionHandler {
3436
@Override
3537
public void uncaughtException(Thread thread, Throwable exception) {
3638
if (exception instanceof SilentExitException) {
39+
if (jvmWillExit(thread)) {
40+
preventNonZeroExitCode();
41+
}
3742
return;
3843
}
3944
if (this.delegate != null) {
@@ -53,6 +58,41 @@ public static void exitCurrentThread() {
5358
throw new SilentExitException();
5459
}
5560

61+
private boolean jvmWillExit(Thread exceptionThread) {
62+
for (Thread thread : getAllThreads()) {
63+
if (thread != exceptionThread && thread.isAlive() && !thread.isDaemon()) {
64+
return false;
65+
}
66+
}
67+
return true;
68+
}
69+
70+
protected void preventNonZeroExitCode() {
71+
System.exit(0);
72+
}
73+
74+
protected Thread[] getAllThreads() {
75+
ThreadGroup rootThreadGroup = getRootThreadGroup();
76+
int size = 32;
77+
int threadCount;
78+
Thread[] threads;
79+
do {
80+
size *= 2;
81+
threads = new Thread[size];
82+
threadCount = rootThreadGroup.enumerate(threads);
83+
}
84+
while (threadCount == threads.length);
85+
return Arrays.copyOf(threads, threadCount);
86+
}
87+
88+
private ThreadGroup getRootThreadGroup() {
89+
ThreadGroup candidate = Thread.currentThread().getThreadGroup();
90+
while (candidate.getParent() != null) {
91+
candidate = candidate.getParent();
92+
}
93+
return candidate;
94+
}
95+
5696
private static class SilentExitException extends RuntimeException {
5797

5898
}

spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandlerTests.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.boot.devtools.restart;
1818

19+
import java.util.concurrent.CountDownLatch;
20+
1921
import org.junit.Test;
2022

2123
import static org.hamcrest.Matchers.equalTo;
2224
import static org.hamcrest.Matchers.nullValue;
2325
import static org.junit.Assert.assertThat;
26+
import static org.junit.Assert.assertTrue;
2427
import static org.junit.Assert.fail;
2528

2629
/**
@@ -57,6 +60,24 @@ public void run() {
5760
assertThat(testThread.getThrown().getMessage(), equalTo("Expected"));
5861
}
5962

63+
@Test
64+
public void preventsNonZeroExitCodeWhenAllOtherThreadsAreDaemonThreads() {
65+
try {
66+
SilentExitExceptionHandler.exitCurrentThread();
67+
}
68+
catch (Exception ex) {
69+
TestSilentExitExceptionHandler silentExitExceptionHandler = new TestSilentExitExceptionHandler();
70+
silentExitExceptionHandler.uncaughtException(Thread.currentThread(), ex);
71+
try {
72+
assertTrue(silentExitExceptionHandler.nonZeroExitCodePrevented);
73+
}
74+
finally {
75+
silentExitExceptionHandler.cleanUp();
76+
}
77+
}
78+
79+
}
80+
6081
private static abstract class TestThread extends Thread {
6182

6283
private Throwable thrown;
@@ -81,4 +102,58 @@ public void startAndJoin() throws InterruptedException {
81102

82103
}
83104

105+
private static class TestSilentExitExceptionHandler
106+
extends SilentExitExceptionHandler {
107+
108+
private boolean nonZeroExitCodePrevented;
109+
110+
private final Object monitor = new Object();
111+
112+
TestSilentExitExceptionHandler() {
113+
super(null);
114+
}
115+
116+
@Override
117+
protected void preventNonZeroExitCode() {
118+
this.nonZeroExitCodePrevented = true;
119+
}
120+
121+
@Override
122+
protected Thread[] getAllThreads() {
123+
final CountDownLatch threadRunning = new CountDownLatch(1);
124+
Thread daemonThread = new Thread(new Runnable() {
125+
126+
@Override
127+
public void run() {
128+
synchronized (TestSilentExitExceptionHandler.this.monitor) {
129+
threadRunning.countDown();
130+
try {
131+
TestSilentExitExceptionHandler.this.monitor.wait();
132+
}
133+
catch (InterruptedException ex) {
134+
Thread.currentThread().interrupt();
135+
}
136+
}
137+
}
138+
139+
});
140+
daemonThread.setDaemon(true);
141+
daemonThread.start();
142+
try {
143+
threadRunning.await();
144+
}
145+
catch (InterruptedException ex) {
146+
Thread.currentThread().interrupt();
147+
}
148+
return new Thread[] { Thread.currentThread(), daemonThread };
149+
}
150+
151+
private void cleanUp() {
152+
synchronized (this.monitor) {
153+
this.monitor.notifyAll();
154+
}
155+
}
156+
157+
}
158+
84159
}

0 commit comments

Comments
 (0)