/*
* Copyright 2015 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.as.arquillian.container.domain.managed;
import static org.wildfly.core.launcher.ProcessHelper.addShutdownHook;
import static org.wildfly.core.launcher.ProcessHelper.destroyProcess;
import static org.wildfly.core.launcher.ProcessHelper.processHasDied;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.as.arquillian.container.domain.CommonDomainDeployableContainer;
import org.jboss.as.controller.client.helpers.ClientConstants;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.dmr.ModelNode;
import org.wildfly.core.launcher.DomainCommandBuilder;
import org.wildfly.core.launcher.Launcher;
/**
* @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
* @version $Revision: $
*/
public class ManagedDomainDeployableContainer extends CommonDomainDeployableContainer<ManagedDomainContainerConfiguration> {
static final String TEMP_CONTAINER_DIRECTORY = "arquillian-temp-container";
static final String CONFIG_DIR = "configuration";
static final String DATA_DIR = "data";
static final String SERVERS_DIR = "servers";
private final Logger log = Logger.getLogger(ManagedDomainDeployableContainer.class.getName());
private Thread shutdownThread;
private Process process;
@Override
public Class<ManagedDomainContainerConfiguration> getConfigurationClass() {
return ManagedDomainContainerConfiguration.class;
}
@Override
protected void startInternal() throws LifecycleException {
ManagedDomainContainerConfiguration config = getContainerConfiguration();
if (isServerRunning()) {
if (config.isAllowConnectingToRunningServer()) {
return;
} else {
failDueToRunning();
}
}
try {
final DomainCommandBuilder commandBuilder = DomainCommandBuilder.of(config.getJbossHome(), config.getJavaHome());
final String javaVmArguments = config.getJavaVmArguments();
if (javaVmArguments != null && !javaVmArguments.trim().isEmpty()) {
final String[] javaOpts = javaVmArguments.split("\\s+");
commandBuilder.setProcessControllerJavaOptions(javaOpts)
.setHostControllerJavaOptions(javaOpts);
}
if (config.isSetupCleanServerBaseDir() || config.getCleanServerBaseDir() != null) {
setupCleanServerDirectories(commandBuilder, config.getCleanServerBaseDir());
}
final String modulesPath = config.getModulePath();
if (modulesPath != null && !modulesPath.isEmpty()) {
commandBuilder.addModuleDirs(modulesPath.split(Pattern.quote(File.pathSeparator)));
}
if (config.isEnableAssertions()) {
commandBuilder.addHostControllerJavaOption("-ea")
.addProcessControllerJavaOption("-ea");
}
if (config.getDomainConfig() != null) {
commandBuilder.setDomainConfiguration(config.getDomainConfig());
}
if (config.getHostConfig() != null) {
commandBuilder.setHostConfiguration(config.getHostConfig());
}
// Set server arguments if not null
final String serverArgs = config.getJbossArguments();
if (serverArgs != null && !serverArgs.trim().isEmpty()) {
commandBuilder.addServerArguments(serverArgs.split("\\s+"));
}
// Previous versions of arquillian set the jboss.home.dir property in the JVM properties.
// Some tests may rely on this behavior, but could be considered to be removed as all the scripts add this
// property after the modules path (-mp) has been defined. The command builder will set the property after
// the module path has been defined as well.
commandBuilder.addProcessControllerJavaOption("-Djboss.home.dir=" + commandBuilder.getWildFlyHome());
log.info("Starting container with: " + commandBuilder.build());
process = Launcher.of(commandBuilder).setRedirectErrorStream(true).launch();
new Thread(new ConsoleConsumer()).start();
shutdownThread = addShutdownHook(process);
long startupTimeout = getContainerConfiguration().getStartupTimeoutInSeconds();
long timeout = startupTimeout * 1000;
boolean serverAvailable = false;
long sleep = 1000;
while (timeout > 0 && serverAvailable == false) {
long before = System.currentTimeMillis();
serverAvailable = getManagementClient().isDomainInRunningState();
timeout -= (System.currentTimeMillis() - before);
if (!serverAvailable) {
if (processHasDied(process))
break;
Thread.sleep(sleep);
timeout -= sleep;
sleep = Math.max(sleep / 2, 100);
}
}
if (!serverAvailable) {
destroyProcess(process);
throw new TimeoutException(String.format("Managed Domain server was not started within [%d] s",
config.getStartupTimeoutInSeconds()));
}
} catch (Exception e) {
throw new LifecycleException("Could not start container", e);
}
}
@Override
protected void stopInternal() throws LifecycleException {
if (shutdownThread != null) {
Runtime.getRuntime().removeShutdownHook(shutdownThread);
shutdownThread = null;
}
try {
if (process != null) {
Thread shutdown = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(getContainerConfiguration().getStopTimeoutInSeconds() * 1000);
} catch (InterruptedException e) {
return;
}
// The process hasn't shutdown within 60 seconds. Terminate forcibly.
if (process != null) {
process.destroy();
}
}
});
shutdown.start();
// Fetch the local-host-name attribute (e.g. "master")
ModelNode op = Operations.createReadAttributeOperation(new ModelNode().setEmptyList(), "local-host-name");
ModelNode result = getManagementClient().getControllerClient().execute(op, null);
if (Operations.isSuccessfulOutcome(result)) {
final String hostName = Operations.readResult(result).asString();
op = Operations.createOperation("shutdown", Operations.createAddress(ClientConstants.HOST, hostName));
getManagementClient().getControllerClient().executeAsync(op, null);
}
process.waitFor();
process = null;
shutdown.interrupt();
}
} catch (Exception e) {
if (process != null) {
process.destroyForcibly();
}
throw new LifecycleException("Could not stop container", e);
}
}
private boolean isServerRunning() {
Socket socket = null;
try {
socket = new Socket(getContainerConfiguration().getManagementAddress(), getContainerConfiguration()
.getManagementPort());
} catch (Exception ignored) { // nothing is running on defined ports
return false;
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
throw new RuntimeException("Could not close isServerStarted socket", e);
}
}
}
return true;
}
private void failDueToRunning() throws LifecycleException {
throw new LifecycleException("The server is already running! "
+ "Managed containers do not support connecting to running server instances due to the "
+ "possible harmful effect of connecting to the wrong server. Please stop server before running or "
+ "change to another type of container.\n"
+ "To disable this check and allow Arquillian to connect to a running server, "
+ "set allowConnectingToRunningServer to true in the container configuration");
}
/**
* 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 {
@Override
public void run() {
final InputStream stream = process.getInputStream();
final boolean writeOutput = getContainerConfiguration().isOutputToConsole();
try {
byte[] buf = new byte[32];
int num;
// Do not try reading a line cos it considers '\r' end of line
while ((num = stream.read(buf)) != -1) {
if (writeOutput)
System.out.write(buf, 0, num);
}
} catch (IOException e) {
}
}
}
/**
* Setup clean directories to run the container.
* @param cleanServerBaseDirPath the clean server base directory
*/
private static void setupCleanServerDirectories(final DomainCommandBuilder commandBuilder, final String cleanServerBaseDirPath) throws IOException {
final Path cleanBase;
if (cleanServerBaseDirPath != null) {
cleanBase = Paths.get(cleanServerBaseDirPath);
} else {
cleanBase = Files.createTempDirectory(TEMP_CONTAINER_DIRECTORY);
}
if (Files.notExists(cleanBase)) {
throw serverBaseDirectoryDoesNotExist(cleanBase.toFile());
}
if (!Files.isDirectory(cleanBase)) {
throw serverBaseDirectoryIsNotADirectory(cleanBase.toFile());
}
final Path currentConfigDir = commandBuilder.getConfigurationDirectory();
final Path configDir = cleanBase.resolve(CONFIG_DIR);
copyDir(currentConfigDir, configDir);
final Path currentDataDir = commandBuilder.getBaseDirectory().resolve(DATA_DIR);
if (Files.exists(currentDataDir)) {
copyDir(currentDataDir, cleanBase.resolve(DATA_DIR));
}
commandBuilder.setBaseDirectory(cleanBase);
commandBuilder.setConfigurationDirectory(configDir);
}
static IllegalStateException serverBaseDirectoryDoesNotExist(File f) {
return new IllegalStateException(String.format("Server base directory does not exist: %s", f));
}
static IllegalStateException serverBaseDirectoryIsNotADirectory(File file) {
return new IllegalStateException(String.format("Server base directory is not a directory: %s", file));
}
private static void copyDir(final Path from, final Path to) throws IOException {
Files.walkFileTree(from, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
Files.copy(dir, to.resolve(from.relativize(dir)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
Files.copy(file, to.resolve(from.relativize(file)));
return FileVisitResult.CONTINUE;
}
});
}
}