diff --git a/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java b/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java index d24cec2..d5e38a9 100644 --- a/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java +++ b/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java @@ -31,16 +31,19 @@ abstract class AbstractServerManager implements ServerManager { private static final Logger LOGGER = Logger.getLogger(AbstractServerManager.class); - protected final ProcessHandle process; + protected final ProcessHandle processHandle; + private final Process process; final T client; private final boolean shutdownOnClose; private final DeploymentManager deploymentManager; private final AtomicBoolean closed; private final AtomicBoolean shutdown; + private volatile Thread shutdownWaitThread; - protected AbstractServerManager(final ProcessHandle process, final T client, + protected AbstractServerManager(final Process process, final ProcessHandle processHandle, final T client, final boolean shutdownOnClose) { this.process = process; + this.processHandle = processHandle; this.client = client; this.shutdownOnClose = shutdownOnClose; deploymentManager = DeploymentManager.create(client); @@ -76,14 +79,14 @@ public String launchType() { @Override public CompletableFuture kill() { - if (process != null) { - if (process.isAlive()) { + if (processHandle != null) { + if (processHandle.isAlive()) { return CompletableFuture.supplyAsync(() -> { internalClose(false, false); - return process.destroyForcibly(); + return processHandle.destroyForcibly(); }).thenCompose((successfulRequest) -> { if (successfulRequest) { - return process.onExit().thenApply((processHandle) -> this); + return processHandle.onExit().thenApply((ph) -> this); } return CompletableFuture.completedFuture(this); }); @@ -102,15 +105,15 @@ public boolean waitFor(final long startupTimeout, final TimeUnit unit) throws In break; } timeout -= (System.currentTimeMillis() - before); - if (process != null && !process.isAlive()) { - throw new ServerManagerException("The process %d is no longer active.", process.pid()); + if (processHandle != null && !processHandle.isAlive()) { + throw new ServerManagerException("The process %d is no longer active.", processHandle.pid()); } TimeUnit.MILLISECONDS.sleep(sleep); timeout -= sleep; } if (timeout <= 0) { - if (process != null) { - process.destroy(); + if (processHandle != null) { + processHandle.destroy(); } return false; } @@ -169,7 +172,7 @@ public void shutdown(final long timeout) throws IOException { public CompletableFuture shutdownAsync(final long timeout) { checkState(); final ServerManager serverManager = this; - if (process != null) { + if (processHandle != null) { return CompletableFuture.supplyAsync(() -> { try { if (shutdown.compareAndSet(false, true)) { @@ -180,17 +183,17 @@ public CompletableFuture shutdownAsync(final long timeout) { } return null; }) - .thenCombine(process.onExit(), (outcome, processHandle) -> null) + .thenCombine(processHandle.onExit(), (outcome, processHandle) -> null) .handle((ignore, error) -> { - if (error != null && process.isAlive()) { - if (process.destroyForcibly()) { + if (error != null && processHandle.isAlive()) { + if (processHandle.destroyForcibly()) { LOGGER.warnf(error, "Failed to shutdown the server. An attempt to destroy the process %d has been made, but it may still temporarily run in the background.", - process.pid()); + processHandle.pid()); } else { LOGGER.warnf(error, "Failed to shutdown server and destroy the process %d. The server may still be running in a process.", - process.pid()); + processHandle.pid()); } } return serverManager; @@ -214,6 +217,29 @@ public CompletableFuture shutdownAsync(final long timeout) { }); } + @Override + public int waitForTermination() throws InterruptedException { + checkState(); + shutdownWaitThread = Thread.currentThread(); + try { + if (process != null) { + return process.waitFor(); + } + waitForShutdown(client); + return UNKNOWN_EXIT_STATUS; + } finally { + shutdownWaitThread = null; + } + } + + @Override + public int exitValue() throws IllegalStateException { + if (process != null) { + return process.exitValue(); + } + return UNKNOWN_EXIT_STATUS; + } + @Override public boolean isClosed() { return closed.get(); @@ -245,9 +271,18 @@ void internalClose(final boolean shutdownOnClose, final boolean waitForShutdown) } } try { - client.close(); - } catch (IOException e) { - LOGGER.error("Failed to close the client.", e); + // Interrupt the shutdown thread if it's still running + final Thread shutdownThread = shutdownWaitThread; + if (shutdownThread != null) { + LOGGER.debugf("Interrupting shutdown thread %s.", shutdownThread.getName()); + shutdownThread.interrupt(); + } + } finally { + try { + client.close(); + } catch (IOException e) { + LOGGER.error("Failed to close the client.", e); + } } } } @@ -255,16 +290,17 @@ void internalClose(final boolean shutdownOnClose, final boolean waitForShutdown) abstract void internalShutdown(ModelControllerClient client, long timeout) throws IOException; private void waitForShutdown(final ModelControllerClient client) { - if (process != null) { + if (processHandle != null) { try { - process.onExit() + processHandle.onExit() .get(); } catch (InterruptedException | ExecutionException e) { - throw new ServerManagerException(e, "Error waiting for process %d to exit.", process.pid()); + throw new ServerManagerException(String.format("Error waiting for process %d to exit.", processHandle.pid()), + e); } } else { // Wait for the server manager to finish shutting down - while (ServerManager.isRunning(client)) { + while (!closed.get() && ServerManager.isRunning(client)) { Thread.onSpinWait(); } } diff --git a/src/main/java/org/wildfly/plugin/tools/server/DomainManager.java b/src/main/java/org/wildfly/plugin/tools/server/DomainManager.java index 9e484a3..52926bd 100644 --- a/src/main/java/org/wildfly/plugin/tools/server/DomainManager.java +++ b/src/main/java/org/wildfly/plugin/tools/server/DomainManager.java @@ -29,9 +29,9 @@ public class DomainManager extends AbstractServerManager { private static final Logger LOGGER = Logger.getLogger(DomainManager.class); - DomainManager(final ProcessHandle process, final DomainClient client, + DomainManager(final Process process, final ProcessHandle processHandle, final DomainClient client, final boolean shutdownOnClose) { - super(process, client, shutdownOnClose); + super(process, processHandle, client, shutdownOnClose); } @Override @@ -67,8 +67,8 @@ public ModelNode determineHostAddress() throws OperationExecutionException, IOEx */ @Override public boolean isRunning() { - if (process != null) { - return process.isAlive() && CommonOperations.isDomainRunning(client(), false); + if (processHandle != null) { + return processHandle.isAlive() && CommonOperations.isDomainRunning(client(), false); } return CommonOperations.isDomainRunning(client(), false); } diff --git a/src/main/java/org/wildfly/plugin/tools/server/ServerManager.java b/src/main/java/org/wildfly/plugin/tools/server/ServerManager.java index 5bb4483..3441716 100644 --- a/src/main/java/org/wildfly/plugin/tools/server/ServerManager.java +++ b/src/main/java/org/wildfly/plugin/tools/server/ServerManager.java @@ -36,13 +36,15 @@ */ @SuppressWarnings("unused") public interface ServerManager extends AutoCloseable { + int UNKNOWN_EXIT_STATUS = -1; /** * A builder used to build a {@link ServerManager}. */ class Builder { private final Configuration configuration; - private ProcessHandle process; + private Process process; + private ProcessHandle processHandle; public Builder() { this(new StandaloneConfiguration(null)); @@ -75,7 +77,7 @@ public Builder client(final ModelControllerClient client) { * @return this builder */ public Builder process(final ProcessHandle process) { - this.process = process; + this.processHandle = process; return this; } @@ -90,7 +92,8 @@ public Builder process(final ProcessHandle process) { * @see #process(ProcessHandle) */ public Builder process(final Process process) { - this.process = process == null ? null : process.toHandle(); + this.process = process; + this.processHandle = process == null ? null : process.toHandle(); return this; } @@ -143,7 +146,7 @@ public Builder shutdownOnClose(final boolean shutdownOnClose) { * @return a new {@link StandaloneManager} */ public StandaloneManager standalone() { - return new StandaloneManager(process, configuration.client(), configuration.shutdownOnClose()); + return new StandaloneManager(process, processHandle, configuration.client(), configuration.shutdownOnClose()); } /** @@ -154,7 +157,7 @@ public StandaloneManager standalone() { * @return a new {@link DomainManager} */ public DomainManager domain() { - return new DomainManager(process, getOrCreateDomainClient(), configuration.shutdownOnClose()); + return new DomainManager(process, processHandle, getOrCreateDomainClient(), configuration.shutdownOnClose()); } /** @@ -175,7 +178,8 @@ public DomainManager domain() { public CompletableFuture build() { @SuppressWarnings("resource") final ModelControllerClient client = configuration.client(); - final ProcessHandle process = this.process; + final Process process = this.process; + final ProcessHandle processHandle = this.processHandle; return CompletableFuture.supplyAsync(() -> { // Wait until the server is running, then determine what type we need to return while (!isRunning(client)) { @@ -189,9 +193,10 @@ public CompletableFuture build() { final String launchType = launchType(client).orElseThrow(() -> new ServerManagerException( "Could not determine the type of the server. Verify the server is running.")); if ("STANDALONE".equals(launchType)) { - return new StandaloneManager(process, client, configuration.shutdownOnClose()); + return new StandaloneManager(process, processHandle, client, configuration.shutdownOnClose()); } else if ("DOMAIN".equals(launchType)) { - return new DomainManager(process, getOrCreateDomainClient(), configuration.shutdownOnClose()); + return new DomainManager(process, processHandle, getOrCreateDomainClient(), + configuration.shutdownOnClose()); } throw new ServerManagerException("Only standalone and domain servers are support. %s is not supported.", launchType); @@ -574,6 +579,44 @@ default CompletableFuture shutdownAsync(long timeout) { }); } + /** + * Waits for the server to be shutdown and the process, if defined, to terminate. This returns the exit status of + * the process if it was defined. Otherwise, {@link #UNKNOWN_EXIT_STATUS -1} will be returned. + *

+ * Note this is a blocking action and will block the current thread until the server has been exited or this server + * manager has been {@linkplain #close() closed}. If this server manager has been closed, the thread waiting for + * terminate will be interrupted. + *

+ * + * @return the exit status of the process, if defined. If not defined a value of {@code -1} will be returned + * + * @throws InterruptedException if the current thread is interrupted while waiting for the server to exit + * @since 1.2 + */ + default int waitForTermination() throws InterruptedException { + while (!isClosed() && isRunning()) { + Thread.onSpinWait(); + } + return UNKNOWN_EXIT_STATUS; + } + + /** + * Returns the exit status of the process if it's defined. If the {@link Builder#process(Process)} was used and the + * process has not yet terminated, an {@link IllegalStateException} will be thrown. + *

+ * If no process was set or the process is defined as a {@link ProcessHandle}, then {@link #UNKNOWN_EXIT_STATUS -1} + * will be returned. + *

+ * + * @return the exit status of the process, if defined. If not defined a value of {@code -1} will be returned + * + * @throws IllegalStateException if the process has not yet terminated + * @since 1.2 + */ + default int exitValue() throws IllegalStateException { + return UNKNOWN_EXIT_STATUS; + } + /** * Reloads the server and returns immediately. * diff --git a/src/main/java/org/wildfly/plugin/tools/server/StandaloneManager.java b/src/main/java/org/wildfly/plugin/tools/server/StandaloneManager.java index bbc9205..564c087 100644 --- a/src/main/java/org/wildfly/plugin/tools/server/StandaloneManager.java +++ b/src/main/java/org/wildfly/plugin/tools/server/StandaloneManager.java @@ -24,9 +24,9 @@ public class StandaloneManager extends AbstractServerManager { private static final Logger LOGGER = Logger.getLogger(StandaloneManager.class); - StandaloneManager(final ProcessHandle process, final ModelControllerClient client, + StandaloneManager(final Process process, final ProcessHandle processHandle, final ModelControllerClient client, final boolean shutdownOnClose) { - super(process, client, shutdownOnClose); + super(process, processHandle, client, shutdownOnClose); } @Override @@ -75,8 +75,8 @@ public void reloadIfRequired(final long timeout, final TimeUnit unit) throws IOE @Override public boolean isRunning() { - if (process != null) { - return process.isAlive() && CommonOperations.isStandaloneRunning(client()); + if (processHandle != null) { + return processHandle.isAlive() && CommonOperations.isStandaloneRunning(client()); } return CommonOperations.isStandaloneRunning(client()); } diff --git a/src/test/java/org/wildfly/plugin/tools/Environment.java b/src/test/java/org/wildfly/plugin/tools/Environment.java index c07adbd..47a6c1c 100644 --- a/src/test/java/org/wildfly/plugin/tools/Environment.java +++ b/src/test/java/org/wildfly/plugin/tools/Environment.java @@ -168,9 +168,13 @@ public static StandaloneManager launchStandalone(final boolean shutdownOnClose) } public static DomainManager launchDomain() { + return launchDomain(true); + } + + public static DomainManager launchDomain(final boolean shutdownOnClose) { final DomainManager serverManager = ServerManager .start(Configuration.create(DomainCommandBuilder.of(Environment.WILDFLY_HOME)) - .shutdownOnClose(true) + .shutdownOnClose(shutdownOnClose) .managementAddress(HOSTNAME) .managementPort(PORT)); try { diff --git a/src/test/java/org/wildfly/plugin/tools/server/ServerManagerIT.java b/src/test/java/org/wildfly/plugin/tools/server/ServerManagerIT.java index 8665d74..26fa2f0 100644 --- a/src/test/java/org/wildfly/plugin/tools/server/ServerManagerIT.java +++ b/src/test/java/org/wildfly/plugin/tools/server/ServerManagerIT.java @@ -7,7 +7,12 @@ import java.io.IOException; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.helpers.ClientConstants; @@ -95,7 +100,7 @@ public void checkStandaloneServerManagerClosed() throws Exception { ProcessHandle process; try (StandaloneManager serverManager = Environment.launchStandalone(false)) { checker = serverManager; - process = serverManager.process; + process = serverManager.processHandle; } Assertions.assertTrue(checker.isClosed(), String .format("Expected ServerManager %s to be closed, but the client did not close the server manager.", checker)); @@ -114,7 +119,7 @@ public void checkStandaloneShutdownOnClose() throws Exception { ProcessHandle process = null; try { try (StandaloneManager serverManager = Environment.launchStandalone()) { - process = serverManager.process; + process = serverManager.processHandle; serverManager.waitFor(Environment.TIMEOUT, TimeUnit.SECONDS); Assertions.assertTrue(serverManager.isRunning(), "The server does not appear to be running."); } @@ -181,6 +186,78 @@ public void checkManagedDomain() { } } + @Test + public void standaloneWaitForTermination() throws Exception { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final AtomicBoolean expectedTermination = new AtomicBoolean(false); + final AtomicInteger exitCode = new AtomicInteger(); + ProcessHandle process = null; + try { + try (StandaloneManager serverManager = Environment.launchStandalone(false)) { + process = serverManager.processHandle; + serverManager.waitFor(Environment.TIMEOUT, TimeUnit.SECONDS); + Assertions.assertTrue(serverManager.isRunning(), "The standalone server is no running."); + final CountDownLatch started = new CountDownLatch(1); + executor.execute(() -> { + try { + started.countDown(); + exitCode.set(serverManager.waitForTermination()); + } catch (InterruptedException expected) { + expectedTermination.set(true); + } + }); + // Wait until the thread has started + Assertions.assertTrue(started.await(Environment.TIMEOUT, TimeUnit.SECONDS), () -> String + .format("Failed to wait for monitoring thread to start within %d seconds.", Environment.TIMEOUT)); + } + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(Environment.TIMEOUT, TimeUnit.SECONDS), + "Failed to wait for the executor server to shutdown."); + Assertions.assertEquals(0, exitCode.get(), "Expected exit code to be 0."); + Assertions.assertTrue(expectedTermination.get(), "Expected the wait thread to be interrupted."); + } finally { + if (process != null) { + process.destroyForcibly(); + } + } + } + + @Test + public void domainWaitForTermination() throws Exception { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final AtomicBoolean expectedTermination = new AtomicBoolean(false); + final AtomicInteger exitCode = new AtomicInteger(Integer.MIN_VALUE); + ProcessHandle process = null; + try { + try (DomainManager serverManager = Environment.launchDomain(false)) { + process = serverManager.processHandle; + serverManager.waitFor(Environment.TIMEOUT, TimeUnit.SECONDS); + Assertions.assertTrue(serverManager.isRunning(), "The domain server is no running."); + final CountDownLatch started = new CountDownLatch(1); + executor.execute(() -> { + try { + started.countDown(); + exitCode.set(serverManager.waitForTermination()); + } catch (InterruptedException expected) { + expectedTermination.set(true); + } + }); + // Wait until the thread has started + Assertions.assertTrue(started.await(Environment.TIMEOUT, TimeUnit.SECONDS), () -> String + .format("Failed to wait for monitoring thread to start within %d seconds.", Environment.TIMEOUT)); + } + executor.shutdown(); + Assertions.assertTrue(executor.awaitTermination(Environment.TIMEOUT, TimeUnit.SECONDS), + "Failed to wait for the executor server to shutdown."); + Assertions.assertEquals(Integer.MIN_VALUE, exitCode.get(), "Expected exit code to be 0."); + Assertions.assertTrue(expectedTermination.get(), "Expected the wait thread to be interrupted."); + } finally { + if (process != null) { + process.destroyForcibly(); + } + } + } + private ModelNode executeCommand(final ModelControllerClient client, final ModelNode op) throws IOException { final ModelNode result = client.execute(op); if (!Operations.isSuccessfulOutcome(result)) {