/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.itest.springboot.arquillian.container; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.Callable; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import org.apache.camel.itest.springboot.arquillian.container.jmx.CustomJMXProtocol; import org.apache.camel.itest.springboot.arquillian.container.util.Await; import org.apache.camel.itest.springboot.arquillian.container.util.FileDeploymentUtils; import org.jboss.arquillian.container.se.api.ClassPath; import org.jboss.arquillian.container.se.api.ClassPathDirectory; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; import org.jboss.arquillian.container.spi.client.container.DeploymentException; import org.jboss.arquillian.container.spi.client.container.LifecycleException; import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; import org.jboss.arquillian.container.spi.client.protocol.metadata.JMXContext; import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; import org.jboss.arquillian.protocol.jmx.JMXTestRunnerMBean; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.Node; import org.jboss.shrinkwrap.api.asset.ArchiveAsset; import org.jboss.shrinkwrap.api.asset.Asset; import org.jboss.shrinkwrap.api.asset.ClassAsset; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.descriptor.api.Descriptor; public class ManagedSEDeployableContainer implements DeployableContainer<ManagedSEContainerConfiguration> { private static final Logger LOGGER = Logger.getLogger(ManagedSEDeployableContainer.class.getName()); private static final String SYSPROP_KEY_JAVA_HOME = "java.home"; private static final String DEBUG_AGENT_STRING = "-agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=y"; private static final String TARGET = "target"; private static final String SERVER_MAIN_CLASS_FQN = "org.jboss.arquillian.container.se.server.Main"; private static final String SYSTEM_PROPERTY_SWITCH = "-D"; private static final String EQUALS = "="; private boolean debugModeEnabled; private boolean keepDeploymentArchives; private Process process; private List<File> materializedFiles; private List<File> classpathDependencies; private String host; private int port; private String librariesPath; private List<String> additionalJavaOpts; private int waitTime; @Override public Class<ManagedSEContainerConfiguration> getConfigurationClass() { return ManagedSEContainerConfiguration.class; } public void setup(ManagedSEContainerConfiguration configuration) { debugModeEnabled = configuration.isDebug(); host = configuration.getHost(); port = configuration.getPort(); materializedFiles = new ArrayList<>(); classpathDependencies = new ArrayList<>(); librariesPath = configuration.getLibrariesPath(); keepDeploymentArchives = configuration.isKeepDeploymentArchives(); additionalJavaOpts = initAdditionalJavaOpts(configuration.getAdditionalJavaOpts()); configureLogging(configuration); waitTime = configuration.getWaitTime() > 0 ? configuration.getWaitTime() : 10; } private List<String> initAdditionalJavaOpts(String opts) { if (opts == null || opts.isEmpty()) { return Collections.emptyList(); } List<String> additionalOpts = new ArrayList<>(); for (String option : opts.split("\\s+")) { additionalOpts.add(option); } return additionalOpts; } private void configureLogging(ManagedSEContainerConfiguration configuration) { ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setLevel(configuration.getLogLevel()); LOGGER.setUseParentHandlers(false); LOGGER.addHandler(consoleHandler); LOGGER.setLevel(configuration.getLogLevel()); } @Override public void start() throws LifecycleException { } @Override public void stop() throws LifecycleException { } @Override public ProtocolDescription getDefaultProtocol() { return new ProtocolDescription(CustomJMXProtocol.NAME); } @Override public void deploy(Descriptor descriptor) throws DeploymentException { throw new UnsupportedOperationException(); } @Override public void undeploy(Descriptor descriptor) throws DeploymentException { throw new UnsupportedOperationException(); } @Override public void undeploy(Archive<?> archive) throws DeploymentException { LOGGER.info("Undeploying " + archive.getName()); if (!keepDeploymentArchives) { for (File materializedFile : materializedFiles) { if (materializedFile.isDirectory()) { try { FileDeploymentUtils.deleteRecursively(materializedFile.toPath()); } catch (IOException e) { LOGGER.warning("Could not delete materialized directory: " + materializedFile); } } else { materializedFile.delete(); } } } // Kill the subprocess (test JVM) if (process != null) { process.destroy(); try { process.waitFor(); } catch (final InterruptedException e) { Thread.interrupted(); throw new RuntimeException("Interrupted while awaiting server daemon process termination", e); } } } @Override public ProtocolMetaData deploy(final Archive<?> archive) throws DeploymentException { LOGGER.info("Deploying " + archive.getName()); // First of all clear the list of previously materialized deployments - otherwise the class path would grow indefinitely materializedFiles.clear(); // Create a new classpath classpathDependencies.clear(); if (ClassPath.isRepresentedBy(archive)) { for (Node child : archive.get(ClassPath.ROOT_ARCHIVE_PATH).getChildren()) { Asset asset = child.getAsset(); if (asset instanceof ArchiveAsset) { Archive<?> assetArchive = ((ArchiveAsset) asset).getArchive(); if (ClassPathDirectory.isRepresentedBy(assetArchive)) { materializeDirectory(assetArchive); } else { materializeArchive(assetArchive); } } } } else { materializeArchive(archive); } Properties systemProperties = getSystemProperties(archive); readJarFilesFromDirectory(); addTestResourcesDirectory(systemProperties); List<String> processCommand = buildProcessCommand(systemProperties); logExecutedCommand(processCommand); // Launch the process final ProcessBuilder processBuilder = new ProcessBuilder(processCommand); String path = systemProperties.getProperty("container.user.dir"); if (path != null) { processBuilder.directory(new File(path)); } processBuilder.environment().put("JAVA_HOME", new File(System.getProperty(SYSPROP_KEY_JAVA_HOME)).getAbsolutePath()); processBuilder.redirectErrorStream(true); processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); try { process = processBuilder.start(); } catch (final IOException e) { throw new DeploymentException("Could not start process", e); } int finalWaitTime = debugModeEnabled ? (3 * waitTime) : waitTime; // Wait for socket connection if (!isServerStarted(host, port, finalWaitTime)) { throw new DeploymentException("Child JVM process failed to start within " + finalWaitTime + " seconds."); } if (!isJMXTestRunnerMBeanRegistered(host, port, finalWaitTime)) { throw new DeploymentException("JMXTestRunnerMBean not registered within " + finalWaitTime + " seconds."); } ProtocolMetaData protocolMetaData = new ProtocolMetaData(); protocolMetaData.addContext(new JMXContext(host, port)); return protocolMetaData; } private Properties getSystemProperties(final Archive<?> archive) throws DeploymentException { Node systemPropertiesNode = archive.get(ClassPath.SYSTEM_PROPERTIES_ARCHIVE_PATH); if (systemPropertiesNode != null) { try (InputStream in = systemPropertiesNode.getAsset().openStream()) { Properties systemProperties = new Properties(); systemProperties.load(in); return systemProperties; } catch (IOException e) { throw new DeploymentException("Could not load system properties", e); } } return null; } private boolean isServerStarted(final String host, final int port, int waitTime) { return new Await(waitTime, new Callable<Boolean>() { @Override public Boolean call() throws Exception { try (Socket ignored = new Socket(host, port)) { return true; } } }).start(); } private boolean isJMXTestRunnerMBeanRegistered(final String host, final int port, int waitTime) throws DeploymentException { // Taken from org.jboss.arquillian.container.spi.client.protocol.metadata.JMXContext final String jmxServiceUrl = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi"; try (JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxServiceUrl), null)) { final MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); return new Await(waitTime, new Callable<Boolean>() { @Override public Boolean call() throws Exception { mbsc.getObjectInstance(new ObjectName(JMXTestRunnerMBean.OBJECT_NAME)); LOGGER.fine("JMXTestRunnerMBean registered with the remote MBean server at: " + jmxServiceUrl); return true; } }).start(); } catch (IOException e) { throw new DeploymentException("Could not verify JMXTestRunnerMBean registration", e); } } private void materializeArchive(Archive<?> archive) { File deploymentFile = new File(TARGET.concat(File.separator).concat(archive.getName())); archive.as(ZipExporter.class).exportTo(deploymentFile, true); materializedFiles.add(deploymentFile); } private void materializeDirectory(Archive<?> archive) throws DeploymentException { if (archive.getContent().isEmpty()) { // Do not materialize an empty directory return; } File entryDirectory = new File(TARGET.concat(File.separator).concat(archive.getName())); try { if (entryDirectory.exists()) { // Always delete previous content FileDeploymentUtils.deleteContent(entryDirectory.toPath()); } else { if (!entryDirectory.mkdirs()) { throw new DeploymentException("Could not create class path directory: " + entryDirectory); } } for (Node child : archive.get(ClassPath.ROOT_ARCHIVE_PATH).getChildren()) { Asset asset = child.getAsset(); if (asset instanceof ClassAsset) { FileDeploymentUtils.materializeClass(entryDirectory, (ClassAsset) asset); } else if (asset == null) { FileDeploymentUtils.materializeSubdirectories(entryDirectory, child); } } } catch (IOException e) { throw new DeploymentException("Could not materialize class path directory: " + archive.getName(), e); } materializedFiles.add(entryDirectory); } private List<String> buildProcessCommand(Properties properties) { final List<String> command = new ArrayList<String>(); final File javaHome = new File(System.getProperty(SYSPROP_KEY_JAVA_HOME)); command.add(javaHome.getAbsolutePath() + File.separator + "bin" + File.separator + "java"); command.add("-cp"); StringBuilder builder = new StringBuilder(); Set<File> classPathEntries = new HashSet<>(materializedFiles); classPathEntries.addAll(classpathDependencies); for (Iterator<File> iterator = classPathEntries.iterator(); iterator.hasNext();) { builder.append(iterator.next().getPath()); if (iterator.hasNext()) { builder.append(File.pathSeparator); } } command.add(builder.toString()); command.add("-Dcom.sun.management.jmxremote"); command.add("-Dcom.sun.management.jmxremote.port=" + port); command.add("-Dcom.sun.management.jmxremote.authenticate=false"); command.add("-Dcom.sun.management.jmxremote.ssl=false"); if (debugModeEnabled) { command.add(DEBUG_AGENT_STRING); } for (String option : additionalJavaOpts) { command.add(option); } if (properties != null) { for (Map.Entry<Object, Object> entry : properties.entrySet()) { addSystemProperty(command, entry.getKey().toString(), entry.getValue().toString()); } } command.add(SERVER_MAIN_CLASS_FQN); return command; } private void addSystemProperty(List<String> command, String key, String value) { command.add(SYSTEM_PROPERTY_SWITCH + key + EQUALS + value); } private void readJarFilesFromDirectory() throws DeploymentException { if (librariesPath == null) { return; } File lib = new File(librariesPath); if (!lib.exists() || lib.isFile()) { throw new DeploymentException("Cannot read files from " + librariesPath); } File[] dep = lib.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); classpathDependencies.addAll(Arrays.asList(dep)); } private void addTestResourcesDirectory(Properties properties) throws DeploymentException { String testResources = properties.getProperty("container.test.resources.dir"); if (testResources != null) { File testDir = new File(testResources); if (testDir.exists() && testDir.isDirectory()) { classpathDependencies.add(testDir); } } } private void logExecutedCommand(List<String> processCommand) { if (LOGGER.isLoggable(Level.FINE)) { StringBuilder builder = new StringBuilder(); for (String s : processCommand) { builder.append(s); builder.append(" "); } LOGGER.log(Level.FINE, "Executing command: " + builder); } } }