/*
* 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.managed;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.as.arquillian.container.CommonDeployableContainer;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.server.logging.ServerLogger;
import org.jboss.dmr.ModelNode;
import org.wildfly.core.launcher.Launcher;
import org.wildfly.core.launcher.StandaloneCommandBuilder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramSocket;
import java.net.ServerSocket;
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 static org.wildfly.core.launcher.ProcessHelper.addShutdownHook;
import static org.wildfly.core.launcher.ProcessHelper.destroyProcess;
import static org.wildfly.core.launcher.ProcessHelper.processHasDied;
/**
* The managed deployable container.
*
* @author Thomas.Diesler@jboss.com
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
* @since 17-Nov-2010
*/
public final class ManagedDeployableContainer extends CommonDeployableContainer<ManagedContainerConfiguration> {
static final String TEMP_CONTAINER_DIRECTORY = "arquillian-temp-container";
static final String CONFIG_DIR = "configuration";
static final String DATA_DIR = "data";
private static final int PORT_RANGE_MIN = 1;
private static final int PORT_RANGE_MAX = 65535;
private final Logger log = Logger.getLogger(ManagedDeployableContainer.class.getName());
private Thread shutdownThread;
private Process process;
private boolean timeoutSupported = false;
@Override
public Class<ManagedContainerConfiguration> getConfigurationClass() {
return ManagedContainerConfiguration.class;
}
@Override
protected void startInternal() throws LifecycleException {
ManagedContainerConfiguration config = getContainerConfiguration();
if (isServerRunning()) {
if (config.isAllowConnectingToRunningServer()) {
return;
} else {
failDueToRunning();
}
}
try {
final StandaloneCommandBuilder commandBuilder = StandaloneCommandBuilder.of(config.getJbossHome());
String modulesPath = config.getModulePath();
if (modulesPath != null && !modulesPath.isEmpty()) {
commandBuilder.addModuleDirs(modulesPath.split(Pattern.quote(File.pathSeparator)));
}
String bundlesPath = config.getBundlePath();
if (bundlesPath == null || !bundlesPath.isEmpty()) {
log.warning("Bundles path is deprecated and no longer used.");
}
final String javaOpts = config.getJavaVmArguments();
final String jbossArguments = config.getJbossArguments();
commandBuilder.setJavaHome(config.getJavaHome());
if (javaOpts != null && !javaOpts.trim().isEmpty()) {
commandBuilder.setJavaOptions(javaOpts.split("\\s+"));
}
if (config.isEnableAssertions()) {
commandBuilder.addJavaOption("-ea");
}
// Create a clean server base to run the container; ARQ-638
if (config.isSetupCleanServerBaseDir() || config.getCleanServerBaseDir() != null) {
setupCleanServerDirectories(commandBuilder, config.getCleanServerBaseDir());
}
if (config.isAdminOnly())
commandBuilder.setAdminOnly();
if (jbossArguments != null && !jbossArguments.trim().isEmpty()) {
commandBuilder.addServerArguments(jbossArguments.split("\\s+"));
}
if (config.getServerConfig() != null) {
commandBuilder.setServerConfiguration(config.getServerConfig());
}
// 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.addJavaOption("-Djboss.home.dir=" + commandBuilder.getWildFlyHome());
// Wait on ports before launching; AS7-4070
this.waitOnPorts();
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().isServerInRunningState();
timeout -= (System.currentTimeMillis() - before);
if (!serverAvailable) {
if (processHasDied(process)) {
final String msg = String.format("The java process starting the managed server exited unexpectedly with code [%d]", process.exitValue());
throw new LifecycleException(msg);
}
Thread.sleep(sleep);
timeout -= sleep;
sleep = Math.max(sleep / 2, 100);
}
}
if (!serverAvailable) {
destroyProcess(process);
throw new TimeoutException(String.format("Managed server was not started within [%d] s", getContainerConfiguration().getStartupTimeoutInSeconds()));
}
timeoutSupported = isOperationAttributeSupported("shutdown", "timeout");
} catch (LifecycleException e) {
throw e;
} catch (Exception e) {
throw new LifecycleException("Could not start container", e);
}
}
/**
* If specified in the configuration, waits on the specified ports to become
* available for the specified time, else throws a {@link PortAcquisitionTimeoutException}
*
* @throws PortAcquisitionTimeoutException
*/
private void waitOnPorts() throws PortAcquisitionTimeoutException {
// Get the config
final Integer[] ports = this.getContainerConfiguration().getWaitForPorts();
final int timeoutInSeconds = this.getContainerConfiguration().getWaitForPortsTimeoutInSeconds();
// For all ports we'll wait on
if (ports != null && ports.length > 0) {
for (int i = 0; i < ports.length; i++) {
final int port = ports[i];
final long start = System.currentTimeMillis();
// If not available
while (!this.isPortAvailable(port)) {
// Get time elapsed
final int elapsedSeconds = (int) ((System.currentTimeMillis() - start) / 1000);
// See that we haven't timed out
if (elapsedSeconds > timeoutInSeconds) {
throw new PortAcquisitionTimeoutException(port, timeoutInSeconds);
}
try {
// Wait a bit, then try again.
Thread.sleep(500);
} catch (final InterruptedException e) {
Thread.interrupted();
}
// Log that we're waiting
log.warning("Waiting on port " + port + " to become available for "
+ (timeoutInSeconds - elapsedSeconds) + "s");
}
}
}
}
private boolean isPortAvailable(final int port) {
// Precondition checks
if (port < PORT_RANGE_MIN || port > PORT_RANGE_MAX) {
throw new IllegalArgumentException("Port specified is out of range: " + port);
}
ServerSocket ss = null;
DatagramSocket ds = null;
try {
// Attempt both TCP and UDP
ss = new ServerSocket(port);
ds = new DatagramSocket(port);
// So we don't block from using this port while it's in a TIMEOUT state after we release it
ss.setReuseAddress(true);
ds.setReuseAddress(true);
// Could be acquired
return true;
} catch (final IOException e) {
// Swallow
} finally {
if (ds != null) {
ds.close();
}
if (ss != null) {
try {
ss.close();
} catch (final IOException e) {
// Swallow
}
}
}
// Couldn't be acquired
return false;
}
@Override
protected void stopInternal(final Integer timeout) 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();
// AS7-6620: Create the shutdown operation and run it asynchronously and wait for process to terminate
final ModelNode op = Operations.createOperation("shutdown");
if (timeoutSupported) {
if (timeout != null) {
op.get("timeout").set(timeout);
}
} else {
log.severe(String.format("Timeout is not supported for %s on the shutdown operation.", getContainerDescription()));
}
getManagementClient().getControllerClient().executeAsync(op, null);
process.waitFor();
process = null;
shutdown.interrupt();
}
} catch (Exception e) {
try {
destroyProcess(process);
}catch (Exception ignore) {}
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 {
final int managementPort = getContainerConfiguration().getManagementPort();
throw new LifecycleException(
String.format("The port %1$d is already in use. It means that either the server might be already running " +
"or there is another process using port %1$d.%n" +
"Managed containers do not support connecting to running server instances due to the " +
"possible harmful effect of connecting to the wrong server.%n" +
"Please stop server (or another process) before running, " +
"change to another type of container (e.g. remote) or use jboss.socket.binding.port-offset variable " +
"to change the default port.%n" +
"To disable this check and allow Arquillian to connect to a running server, " +
"set allowConnectingToRunningServer to true in the container configuration",
managementPort));
}
/**
* 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 StandaloneCommandBuilder 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 ServerLogger.ROOT_LOGGER.serverBaseDirectoryDoesNotExist(cleanBase.toFile());
}
if (!Files.isDirectory(cleanBase)) {
throw ServerLogger.ROOT_LOGGER.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);
}
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;
}
});
}
}