All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.as.arquillian.container.CommonManagedDeployableContainer Maven / Gradle / Ivy

There is a newer version: 5.1.0.Beta6
Show newest version
/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.arquillian.container;

import static org.wildfly.core.launcher.ProcessHelper.addShutdownHook;
import static org.wildfly.core.launcher.ProcessHelper.destroyProcess;

import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.wildfly.core.launcher.CommandBuilder;
import org.wildfly.core.launcher.Launcher;
import org.wildfly.plugin.tools.server.ServerManager;
import org.wildfly.plugin.tools.server.StandaloneManager;

/**
 * A deployable container that manages a {@linkplain Process process}.
 *
 * @author [email protected]
 * @author James R. Perkins
 * @since 3.0.0
 */
@SuppressWarnings("MagicNumber")
public abstract class CommonManagedDeployableContainer
        extends CommonDeployableContainer {

    private static final int PORT_RANGE_MIN = 1;
    private static final int PORT_RANGE_MAX = 65535;
    private Thread shutdownThread = null;
    private Process process = null;
    private boolean timeoutSupported = false;

    @Override
    @SuppressWarnings("FeatureEnvy")
    protected void startInternal() throws LifecycleException {
        final T config = getContainerConfiguration();
        if (isServerRunning(config)) {
            if (config.isAllowConnectingToRunningServer()) {
                // Set up the server manager attempting to discover the process for monitoring purposes. We need the
                // server manager regardless of whether we are in charge of the lifecycle or not.
                final StandaloneManager serverManager = ServerManager.builder()
                        .client(getManagementClient().getControllerClient())
                        .process(ServerManager.findProcess().orElse(null))
                        .standalone();
                serverManagerProducer.set(serverManager.asManaged());
                return;
            } else {
                failDueToRunning(config);
            }
        }

        try {
            final CommandBuilder commandBuilder = createCommandBuilder(config);

            // Wait on ports before launching; AS7-4070
            this.waitOnPorts(config);

            getLogger().info("Starting container with: " + commandBuilder.build());
            final Process process = Launcher.of(commandBuilder).setRedirectErrorStream(true).launch();
            new Thread(new ConsoleConsumer(process, config.isOutputToConsole())).start();
            shutdownThread = addShutdownHook(process);
            final StandaloneManager serverManager = ServerManager.builder()
                    .client(getManagementClient().getControllerClient())
                    .process(process)
                    .standalone();

            long startupTimeout = config.getStartupTimeoutInSeconds();
            if (!serverManager.waitFor(startupTimeout, TimeUnit.SECONDS)) {
                destroyProcess(process);
                throw new TimeoutException(String.format("Managed server was not started within [%d] s", startupTimeout));
            }
            timeoutSupported = isOperationAttributeSupported("shutdown", "timeout");
            this.process = process;
            serverManagerProducer.set(serverManager.asManaged());

        } catch (LifecycleException e) {
            throw e;
        } catch (Exception e) {
            throw new LifecycleException("Could not start container", e);
        }
    }

    /**
     * Returns the command builder used to launch the server.
     *
     * @return the command builder
     */
    protected abstract CommandBuilder createCommandBuilder(T config);

    /**
     * The logger used for messages.
     *
     * @return the logger
     */
    protected abstract Logger getLogger();

    /**
     * If specified in the configuration, waits on the specified ports to become
     * available for the specified time, else throws a {@link PortAcquisitionTimeoutException}
     *
     * @throws PortAcquisitionTimeoutException if a timeout occurs
     */
    private void waitOnPorts(final T config) throws PortAcquisitionTimeoutException {
        // Get the config
        final Integer[] ports = config.getWaitForPorts();
        final int timeoutInSeconds = config.getWaitForPortsTimeoutInSeconds();

        // For all ports we'll wait on
        if (ports != null && ports.length > 0) {
            for (final int port : ports) {
                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.
                        TimeUnit.MILLISECONDS.sleep(500L);
                    } catch (final InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }

                    // Log that we're waiting
                    getLogger().warnf("Waiting on port %d to become available for %ds", port,
                            (timeoutInSeconds - elapsedSeconds));
                }
            }
        }
    }

    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);
        }

        try (ServerSocket ss = new ServerSocket(port); DatagramSocket ds = new DatagramSocket(port)) {
            // Attempt both TCP and UDP
            // 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 ignore) {
            // 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;
        }
        final Process process = this.process;
        this.process = null;
        try {
            if (process != null) {
                final Logger logger = getLogger();

                // 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 {
                    getLogger().error(String.format("Timeout is not supported for %s on the shutdown operation.",
                            getContainerDescription()));
                }

                // If the process is not alive there is no sense it invoking a shutdown operation.
                if (process.isAlive()) {
                    final ManagementClient client = getManagementClient();
                    if (client == null) {
                        logger.error("The management client does not seem to be active. Forcibly destroying the process.");
                        process.destroyForcibly();
                    } else {
                        String shutdownFailureMessage = null;
                        try {
                            final ModelNode result = client.getControllerClient().execute(op);
                            if (!Operations.isSuccessfulOutcome(result)) {
                                shutdownFailureMessage = Operations.getFailureDescription(result).asString();
                            }
                        } catch (CancellationException | IOException e) {
                            if (e instanceof CancellationException || e.getCause() instanceof CancellationException) {
                                shutdownFailureMessage = e.toString();
                            } else {
                                throw e;
                            }
                        }
                        if (shutdownFailureMessage != null) {
                            // Don't fail stopping, but we should log an error
                            logger.errorf("Failed to shutdown the server: %s", shutdownFailureMessage);
                            process.destroyForcibly();
                        }
                    }
                }

                final int timeoutSeconds = getContainerConfiguration().getStartupTimeoutInSeconds();
                if (!process.waitFor(timeoutSeconds, TimeUnit.SECONDS)) {
                    // Log a warning indicating the timeout happened
                    logger.warnf("The container process did not exit within %d seconds. Forcibly destroying the process.",
                            timeoutSeconds);
                    process.destroyForcibly();
                }
            }
        } catch (Exception e) {
            try {
                destroyProcess(process);
            } catch (Exception ignore) {
            }
            throw new LifecycleException("Could not stop container", e);
        }
    }

    private boolean isServerRunning(final T config) {
        Socket socket = null;
        try {
            socket = new Socket(
                    config.getManagementAddress(),
                    config.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(final T config) throws LifecycleException {
        final int managementPort = config.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 static class ConsoleConsumer implements Runnable {
        private final Process process;
        private final boolean writeOutput;

        private ConsoleConsumer(final Process process, final boolean writeOutput) {
            this.process = process;
            this.writeOutput = writeOutput;
        }

        @Override
        @SuppressWarnings("UseOfSystemOutOrSystemErr")
        public void run() {
            final InputStream stream = process.getInputStream();

            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 ignore) {
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy