From 7b30d150010c94affcf77626fc4529a4540fd489 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Wed, 24 Jul 2024 14:19:04 -0700 Subject: [PATCH] [61] Add an exitValue() and waitForTermination() method which both return an int for the exit code. This will use the process' exit if one was set. Otherwise, an unknown exit code of -1 is returned. Signed-off-by: James R. Perkins --- .../tools/server/AbstractServerManager.java | 79 +++++++++++++----- .../plugin/tools/server/DomainManager.java | 8 +- .../plugin/tools/server/ServerManager.java | 59 ++++++++++++-- .../tools/server/StandaloneManager.java | 8 +- .../org/wildfly/plugin/tools/Environment.java | 6 +- .../plugin/tools/server/ServerManagerIT.java | 81 ++++++++++++++++++- 6 files changed, 200 insertions(+), 41 deletions(-) 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 8abafd2..d729dd1 100644 --- a/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java +++ b/src/main/java/org/wildfly/plugin/tools/server/AbstractServerManager.java @@ -31,15 +31,18 @@ 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 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); @@ -75,10 +78,10 @@ public String launchType() { @Override public CompletableFuture kill() { final CompletableFuture cf = new CompletableFuture<>(); - if (process != null && process.isAlive()) { + if (processHandle != null && processHandle.isAlive()) { internalClose(false, false); - if (process.destroyForcibly()) { - cf.thenCombine(process.onExit(), (serverManager, processHandle) -> serverManager); + if (processHandle.destroyForcibly()) { + cf.thenCombine(processHandle.onExit(), (serverManager, processHandle) -> serverManager); } } else { cf.complete(this); @@ -96,16 +99,16 @@ public boolean waitFor(final long startupTimeout, final TimeUnit unit) throws In break; } timeout -= (System.currentTimeMillis() - before); - if (process != null && !process.isAlive()) { + if (processHandle != null && !processHandle.isAlive()) { throw new RuntimeException( - String.format("The process %d is no longer active.", process.pid())); + String.format("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; } @@ -162,7 +165,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 { internalShutdown(client, timeout); @@ -171,17 +174,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; @@ -203,6 +206,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(); @@ -232,9 +258,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); + } } } } @@ -242,16 +277,16 @@ 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 RuntimeException(String.format("Error waiting for process %d to exit.", process.pid()), e); + throw new RuntimeException(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 c7a4e3b..0a58396 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 88fd168..4afcb40 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; } @@ -141,7 +144,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()); } /** @@ -152,7 +155,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()); } /** @@ -173,7 +176,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)) { @@ -182,9 +186,10 @@ public CompletableFuture build() { final String launchType = launchType(client).orElseThrow(() -> new IllegalStateException( "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 IllegalStateException( String.format("Only standalone and domain servers are support. %s is not supported.", launchType)); @@ -558,6 +563,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 0abc356..65638cf 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)) {