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
This commit is contained in:
parent
6574feea87
commit
13635201ff
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 2012-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -17,11 +17,13 @@
|
|||
package org.springframework.boot.devtools.restart;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class SilentExitExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
|
|
@ -34,6 +36,9 @@ class SilentExitExceptionHandler implements UncaughtExceptionHandler {
|
|||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable exception) {
|
||||
if (exception instanceof SilentExitException) {
|
||||
if (jvmWillExit(thread)) {
|
||||
preventNonZeroExitCode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.delegate != null) {
|
||||
|
|
@ -53,6 +58,41 @@ class SilentExitExceptionHandler implements UncaughtExceptionHandler {
|
|||
throw new SilentExitException();
|
||||
}
|
||||
|
||||
private boolean jvmWillExit(Thread exceptionThread) {
|
||||
for (Thread thread : getAllThreads()) {
|
||||
if (thread != exceptionThread && thread.isAlive() && !thread.isDaemon()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void preventNonZeroExitCode() {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
protected Thread[] getAllThreads() {
|
||||
ThreadGroup rootThreadGroup = getRootThreadGroup();
|
||||
int size = 32;
|
||||
int threadCount;
|
||||
Thread[] threads;
|
||||
do {
|
||||
size *= 2;
|
||||
threads = new Thread[size];
|
||||
threadCount = rootThreadGroup.enumerate(threads);
|
||||
}
|
||||
while (threadCount == threads.length);
|
||||
return Arrays.copyOf(threads, threadCount);
|
||||
}
|
||||
|
||||
private ThreadGroup getRootThreadGroup() {
|
||||
ThreadGroup candidate = Thread.currentThread().getThreadGroup();
|
||||
while (candidate.getParent() != null) {
|
||||
candidate = candidate.getParent();
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static class SilentExitException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,14 @@
|
|||
|
||||
package org.springframework.boot.devtools.restart;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
|
|
@ -57,6 +60,24 @@ public class SilentExitExceptionHandlerTests {
|
|||
assertThat(testThread.getThrown().getMessage(), equalTo("Expected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preventsNonZeroExitCodeWhenAllOtherThreadsAreDaemonThreads() {
|
||||
try {
|
||||
SilentExitExceptionHandler.exitCurrentThread();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
TestSilentExitExceptionHandler silentExitExceptionHandler = new TestSilentExitExceptionHandler();
|
||||
silentExitExceptionHandler.uncaughtException(Thread.currentThread(), ex);
|
||||
try {
|
||||
assertTrue(silentExitExceptionHandler.nonZeroExitCodePrevented);
|
||||
}
|
||||
finally {
|
||||
silentExitExceptionHandler.cleanUp();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static abstract class TestThread extends Thread {
|
||||
|
||||
private Throwable thrown;
|
||||
|
|
@ -81,4 +102,58 @@ public class SilentExitExceptionHandlerTests {
|
|||
|
||||
}
|
||||
|
||||
private static class TestSilentExitExceptionHandler
|
||||
extends SilentExitExceptionHandler {
|
||||
|
||||
private boolean nonZeroExitCodePrevented;
|
||||
|
||||
private final Object monitor = new Object();
|
||||
|
||||
TestSilentExitExceptionHandler() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preventNonZeroExitCode() {
|
||||
this.nonZeroExitCodePrevented = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Thread[] getAllThreads() {
|
||||
final CountDownLatch threadRunning = new CountDownLatch(1);
|
||||
Thread daemonThread = new Thread(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (TestSilentExitExceptionHandler.this.monitor) {
|
||||
threadRunning.countDown();
|
||||
try {
|
||||
TestSilentExitExceptionHandler.this.monitor.wait();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
daemonThread.setDaemon(true);
|
||||
daemonThread.start();
|
||||
try {
|
||||
threadRunning.await();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return new Thread[] { Thread.currentThread(), daemonThread };
|
||||
}
|
||||
|
||||
private void cleanUp() {
|
||||
synchronized (this.monitor) {
|
||||
this.monitor.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue