package org.wildfly.core.testrunner;
import static org.jboss.as.controller.client.helpers.ClientConstants.NAME;
import static org.jboss.as.controller.client.helpers.ClientConstants.OP;
import static org.jboss.as.controller.client.helpers.ClientConstants.OP_ADDR;
import static org.jboss.as.controller.client.helpers.ClientConstants.READ_ATTRIBUTE_OPERATION;
import static org.jboss.as.controller.client.helpers.ClientConstants.RESULT;
import static org.jboss.as.controller.client.helpers.ClientConstants.SERVER_CONFIG;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.as.controller.client.helpers.DelegatingModelControllerClient;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.junit.Assert;
import org.wildfly.core.launcher.Launcher;
import org.wildfly.core.launcher.ProcessHelper;
import org.wildfly.core.launcher.StandaloneCommandBuilder;
/**
* encapsulation of a server process
*
* @author Stuart Douglas
* @author Tomaz Cerar
*/
public class Server {
public static final String LEGACY_JAVA_HOME = "legacy.java.home";
private final String jbossHome = System.getProperty("jboss.home", System.getenv("JBOSS_HOME"));
private final String modulePath = System.getProperty("module.path");
private final String jvmArgs = System.getProperty("jvm.args", "-Xmx512m -XX:MaxMetaspaceSize=256m");
private final String jbossArgs = System.getProperty("jboss.args");
private final String javaHome = System.getProperty("java.home", System.getenv("JAVA_HOME"));
//Use this when specifying an older java to be used for running the server
private final String legacyJavaHome = System.getProperty(LEGACY_JAVA_HOME);
private String serverConfig = System.getProperty("server.config", "standalone.xml");
private final int managementPort = Integer.getInteger("management.port", 9990);
private final String managementAddress = System.getProperty("management.address", "localhost");
private final String managementProtocol = System.getProperty("management.protocol", "remote+http");
private final String serverDebug = "wildfly.debug";
private final int serverDebugPort = Integer.getInteger("wildfly.debug.port", 8787);
private StartMode startMode = StartMode.NORMAL;
private final Logger log = Logger.getLogger(Server.class.getName());
private Thread shutdownThread;
private volatile Process process;
private final ManagementClient client = new ManagementClient(new DelegatingModelControllerClient(ServerClientProvider.INSTANCE), managementAddress, managementPort, managementProtocol);
private static boolean processHasDied(final Process process) {
try {
process.exitValue();
return true;
} catch (IllegalThreadStateException e) {
// good
return false;
}
}
/**
* Sets server config to use
* @param serverConfig
*/
public void setServerConfig(String serverConfig) {
this.serverConfig = serverConfig;
}
public void setStartMode(StartMode startMode) {
this.startMode = startMode;
}
protected void start() {
start(System.out);
}
protected void start(PrintStream out) {
try {
final Path jbossHomeDir = Paths.get(jbossHome);
if (Files.notExists(jbossHomeDir) && !Files.isDirectory(jbossHomeDir)) {
throw new IllegalStateException("Cannot find: " + jbossHomeDir);
}
final StandaloneCommandBuilder commandBuilder = StandaloneCommandBuilder.of(jbossHomeDir);
if (modulePath != null && !modulePath.isEmpty()) {
commandBuilder.setModuleDirs(modulePath.split(Pattern.quote(File.pathSeparator)));
}
commandBuilder.setJavaHome(legacyJavaHome == null ? javaHome : legacyJavaHome);
if (jvmArgs != null) {
commandBuilder.setJavaOptions(jvmArgs.split("\\s+"));
}
if(Boolean.getBoolean(serverDebug)) {
commandBuilder.setDebug(true, serverDebugPort);
}
if (this.startMode == StartMode.ADMIN_ONLY) {
commandBuilder.setAdminOnly();
} else if (this.startMode == StartMode.SUSPEND){
commandBuilder.setStartSuspended();
}
//we are testing, of course we want assertions and set-up some other defaults
commandBuilder.addJavaOption("-ea")
.setServerConfiguration(serverConfig)
.setBindAddressHint("management", managementAddress);
if (jbossArgs != null) {
commandBuilder.addServerArguments(jbossArgs.split("\\s+"));
}
StringBuilder builder = new StringBuilder("Starting container with: ");
for(String arg : commandBuilder.build()) {
builder.append(arg).append(" ");
}
log.info(builder.toString());
process = Launcher.of(commandBuilder)
// Redirect the output and error stream to a file
.setRedirectErrorStream(true)
.launch();
new Thread(new ConsoleConsumer(process.getInputStream(), out)).start();
final Process proc = process;
shutdownThread = ProcessHelper.addShutdownHook(proc);
createClient();
long startupTimeout = 30;
long timeout = startupTimeout * 1000;
boolean serverAvailable = false;
long sleep = 1000;
while (timeout > 0 && !serverAvailable) {
long before = System.currentTimeMillis();
serverAvailable = client.isServerInRunningState();
timeout -= (System.currentTimeMillis() - before);
if (!serverAvailable) {
if (processHasDied(proc))
break;
Thread.sleep(sleep);
timeout -= sleep;
sleep = Math.max(sleep / 2, 100);
}
}
if (!serverAvailable) {
destroyProcess();
throw new RuntimeException("Managed server was not started within 30s");
}
} catch (Exception e) {
safeCloseClient();
throw new RuntimeException("Could not start container", e);
}
}
protected void stop() {
if (shutdownThread != null) {
Runtime.getRuntime().removeShutdownHook(shutdownThread);
shutdownThread = null;
}
try {
if (process != null) {
Thread shutdown = new Thread(() -> {
long timeout = System.currentTimeMillis() + 10000;
while (process != null && System.currentTimeMillis() < timeout) {
try {
process.exitValue();
process = null;
} catch (IllegalThreadStateException e) {
}
}
// The process hasn't shutdown within 60 seconds. Terminate forcibly.
if (process != null) {
process.destroy();
}
});
shutdown.start();
try {
// AS7-6620: Create the shutdown operation and run it asynchronously and wait for process to terminate
client.getControllerClient().executeAsync(Operations.createOperation("shutdown"), null);
} catch (AssertionError | RuntimeException e) {
//ignore as this can only fail if shutdown is already in progress
}
if (process != null) {
process.waitFor();
process = null;
}
shutdown.interrupt();
}
} catch (Exception e) {
try {
if (process != null) {
process.destroy();
process.waitFor();
}
} catch (Exception ignore) {
}
throw new RuntimeException("Could not stop container", e);
} finally {
safeCloseClient();
}
}
private int destroyProcess() {
if (process == null)
return 0;
process.destroy();
try {
return process.waitFor();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public ManagementClient getClient() {
return client;
}
private void createClient() {
ServerClientProvider.INSTANCE.setClient(createModelControllerClient());
}
private ModelControllerClient createModelControllerClient() {
ModelControllerClient modelControllerClient = null;
try {
modelControllerClient = ModelControllerClient.Factory.create(
managementProtocol,
managementAddress,
managementPort,
Authentication.getCallbackHandler());
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
return modelControllerClient;
}
private void safeCloseClient() {
try {
if (client != null) {
client.close();
}
} catch (final Exception e) {
Logger.getLogger(this.getClass().getName()).warnf(e, "Caught exception closing ModelControllerClient");
}
}
/**
* Reload current server
* This method makes sure client on server is still operational after reload is done.
* @param adminOnly tells server to boot in admin only mode or normal
* @param timeout time in miliseconds to wait for server to come back after reload
*/
public void reload(boolean adminOnly, int timeout) {
reload(adminOnly ? StartMode.ADMIN_ONLY : StartMode.NORMAL, timeout);
}
/**
* Reload current server
* This method makes sure client on server is still operational after reload is done.
* @param startMode tells server to boot in admin only mode, suspended or normal
* @param timeout time in miliseconds to wait for server to come back after reload
*/
public void reload(StartMode startMode, int timeout) {
reload(startMode, timeout, null);
}
/**
* Reload current server
* This method makes sure client on server is still operational after reload is done.
* @param adminOnly tells server to boot in admin only mode or normal
* @param timeout time in miliseconds to wait for server to come back after reload
*/
public void reload(boolean adminOnly, int timeout, String serverConfig) {
executeReload(adminOnly ? StartMode.ADMIN_ONLY : StartMode.NORMAL, serverConfig);
waitForLiveServerToReload(timeout); //30 seconds
}
/**
* Reload current server
* This method makes sure client on server is still operational after reload is done.
* @param startMode tells server to boot in admin only mode, suspended or normal
* @param timeout time in miliseconds to wait for server to come back after reload
*/
public void reload(StartMode startMode, int timeout, String serverConfig) {
executeReload(startMode, serverConfig);
waitForLiveServerToReload(timeout); //30 seconds
}
private void executeReload(StartMode startMode, String serverConfig) {
ModelNode operation = new ModelNode();
operation.get(OP_ADDR).setEmptyList();
operation.get(OP).set("reload");
if(startMode == StartMode.ADMIN_ONLY) {
operation.get("admin-only").set(true);
} else if(startMode == StartMode.SUSPEND) {
operation.get("start-mode").set("suspend");
}
if (serverConfig != null) {
operation.get(SERVER_CONFIG).set(serverConfig);
}
try {
ModelNode result = client.getControllerClient().execute(operation);
Assert.assertEquals("success", result.get(ClientConstants.OUTCOME).asString());
} catch (IOException e) {
final Throwable cause = e.getCause();
if (!(cause instanceof ExecutionException) && !(cause instanceof CancellationException) && !(cause instanceof SocketException) ) {
throw new RuntimeException(e);
} // else ignore, this might happen if the channel gets closed before we got the response
}finally {
safeCloseClient();//close existing client
}
}
private void recreateClient(){
safeCloseClient();
ServerClientProvider.INSTANCE.setClient(createModelControllerClient());
}
void waitForLiveServerToReload(int timeout) {
long start = System.currentTimeMillis();
ModelNode operation = new ModelNode();
operation.get(OP_ADDR).setEmptyList();
operation.get(OP).set(READ_ATTRIBUTE_OPERATION);
operation.get(NAME).set("server-state");
while (System.currentTimeMillis() - start < timeout) {
recreateClient();
ModelControllerClient liveClient = client.getControllerClient();
try {
ModelNode result = liveClient.execute(operation);
if ("running".equals(result.get(RESULT).asString())) {
return;
}
} catch (IOException e) {
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
fail("Live Server did not reload in the imparted time.");
}
/**
* Runnable that consumes the output of the process. If nothing consumes the output the AS will hang on some
* platforms
*
* @author Stuart Douglas
*/
private class ConsoleConsumer implements Runnable {
private final InputStream source;
private final PrintStream target;
private ConsoleConsumer(final InputStream source, final PrintStream target) {
this.source = source;
this.target = target;
}
@Override
public void run() {
final InputStream source = this.source;
try {
byte[] buf = new byte[32];
int num;
// Do not try reading a line cos it considers '\r' end of line
while ((num = source.read(buf)) != -1) {
target.write(buf, 0, num);
}
} catch (IOException ignore) {
}
}
}
private static class ServerClientProvider implements DelegatingModelControllerClient.DelegateProvider {
static final ServerClientProvider INSTANCE = new ServerClientProvider();
private final AtomicReference<ModelControllerClient> client = new AtomicReference<>();
void setClient(final ModelControllerClient client) {
assert client != null;
this.client.set(client);
}
@Override
public ModelControllerClient getDelegate() {
final ModelControllerClient result = client.get();
if (result == null) {
throw new IllegalStateException("The client has been closed");
}
return result;
}
}
public enum StartMode {
NORMAL,
ADMIN_ONLY,
SUSPEND
}
}