diff --git a/src/main/java/org/buildobjects/process/Proc.java b/src/main/java/org/buildobjects/process/Proc.java index b3706db..9857379 100644 --- a/src/main/java/org/buildobjects/process/Proc.java +++ b/src/main/java/org/buildobjects/process/Proc.java @@ -77,28 +77,35 @@ public Proc(String command, throw new StartupException("Could not startup process '" + toString() + "'.", e); } - try { startControlThread(); do { - ExecutionEvent nextEvent = timeout == null ? eventQueue.poll(MAX_VALUE, HOURS) : eventQueue.poll(timeout, MILLISECONDS); + try { + ExecutionEvent nextEvent = timeout == null ? eventQueue.poll(MAX_VALUE, HOURS) : eventQueue.poll(timeout, MILLISECONDS); - if (nextEvent == null) { - killCleanUpAndThrowTimeoutException(); - } + if (nextEvent == null) { + killCleanUpAndThrowTimeoutException(); + } - if (nextEvent == PROCESS_EXITED) { - break; - } + if (nextEvent == PROCESS_EXITED) { + break; + } - if (nextEvent == EXCEPTION_IN_STREAM_HANDLING) { + if (nextEvent == EXCEPTION_IN_STREAM_HANDLING) { + killProcessCleanup(); + break; + } + } catch (InterruptedException e) { + // if this thread is interrupted, the process is destroyed + // the process gets an exit code, so the next event is PROCESS_EXITED killProcessCleanup(); - break; + continue; } throw new RuntimeException("Felix reckons we should never reach this point"); } while (true); + try { List exceptions = ioHandler.joinConsumption(); if (!exceptions.isEmpty()) { throw new IllegalStateException("Exception in stream consumption", exceptions.get(0)); @@ -150,14 +157,31 @@ public void run() { } private void killCleanUpAndThrowTimeoutException() { - process.destroy(); - ioHandler.cancelConsumption(); + killProcessCleanup(); throw new TimeoutException(toString(), timeout); } private void killProcessCleanup() { - process.destroy(); + List throwables = new ArrayList<>(); + + // destroy this process and all sub processes + ProcessHandle.of(process.pid()) + .ifPresentOrElse(rootHandle -> { + rootHandle.descendants() + .forEach(subHandle -> { + try { + subHandle.destroy(); + } + catch (Throwable t) { + throwables.add(new IllegalStateException("Could not destroy process " + subHandle.pid(), t)); + } + }); + rootHandle.destroy(); + }, process::destroy); ioHandler.cancelConsumption(); + + if(!throwables.isEmpty()) + throw new RuntimeException(throwables.get(0)); } public void dispatch(ExecutionEvent event) { diff --git a/src/test/java/org/buildobjects/process/ProcBuilderTest.java b/src/test/java/org/buildobjects/process/ProcBuilderTest.java index 00e519e..27a6bed 100644 --- a/src/test/java/org/buildobjects/process/ProcBuilderTest.java +++ b/src/test/java/org/buildobjects/process/ProcBuilderTest.java @@ -5,6 +5,9 @@ import java.io.*; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; @@ -262,6 +265,41 @@ public void testDisablingTimeout() { assertEquals(result.getExecutionTime(), 7000, 500); } + /** + * Tests if the process stops the execution when the thread is interrupted + */ + @Test + public void testInterruptingProcess() throws InterruptedException { + AtomicBoolean finished = new AtomicBoolean(false); + AtomicReference resultRef = new AtomicReference<>(null); + + Thread executorThread = new Thread(new Runnable() { + @Override + public void run() { + resultRef.set(new ProcBuilder("sleep") + .withArg("10") + .ignoreExitStatus() + .withNoTimeout() + .run()); + finished.set(true); + } + }); + executorThread.start(); + + // wait some time + Thread.sleep(2000); + + // stop the thread + executorThread.interrupt(); + + // wait shortly + Thread.sleep(500); + + assertTrue(finished.get()); + assertNotNull(resultRef.get()); + assertEquals(2000, resultRef.get().getExecutionTime(), 300); + } + /** * Exit Status * ----------- @@ -384,6 +422,37 @@ public void testHonorsOnlyDefinedExitStatuses() { } } + /** + * If the process is interrupted, the exit code should be 1 + */ + @Test + public void testExitStatusWhenProcessIsInterrupted() throws InterruptedException { + AtomicReference resultRef = new AtomicReference<>(null); + Thread executorThread = new Thread(new Runnable() { + @Override + public void run() { + resultRef.set(new ProcBuilder("sleep") + .withArg("10") + .withExpectedExitStatuses(1) + .withNoTimeout() + .run()); + } + }); + executorThread.start(); + + // wait some time + Thread.sleep(2000); + + // stop the thread + executorThread.interrupt(); + + // wait shortly + Thread.sleep(500); + + assertNotNull(resultRef.get()); + assertEquals(1, resultRef.get().getExitValue()); + } + /** * * Good to Know