diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java index f2cc8f60c9..ec38aa346c 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandler.java @@ -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 { } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandlerTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandlerTests.java index 632467e4c5..df70c9272a 100644 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandlerTests.java +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/SilentExitExceptionHandlerTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.devtools.restart; +import java.util.concurrent.CountDownLatch; + import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -55,6 +57,24 @@ public class SilentExitExceptionHandlerTests { assertThat(testThread.getThrown().getMessage()).isEqualTo("Expected"); } + @Test + public void preventsNonZeroExitCodeWhenAllOtherThreadsAreDaemonThreads() { + try { + SilentExitExceptionHandler.exitCurrentThread(); + } + catch (Exception ex) { + TestSilentExitExceptionHandler silentExitExceptionHandler = new TestSilentExitExceptionHandler(); + silentExitExceptionHandler.uncaughtException(Thread.currentThread(), ex); + try { + assertThat(silentExitExceptionHandler.nonZeroExitCodePrevented).isTrue(); + } + finally { + silentExitExceptionHandler.cleanUp(); + } + } + + } + private static abstract class TestThread extends Thread { private Throwable thrown; @@ -79,4 +99,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(); + } + } + + } + }