/* * RHQ Management Platform * Copyright (C) 2012 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.test.arquillian.impl; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import javax.xml.namespace.QName; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jboss.arquillian.container.spi.Container; 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.ProtocolMetaData; import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.test.spi.annotation.TestScoped; import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.ArchivePath; import org.jboss.shrinkwrap.api.ArchivePaths; import org.jboss.shrinkwrap.api.Filter; import org.jboss.shrinkwrap.api.Node; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.descriptor.api.Descriptor; import org.jboss.shrinkwrap.impl.base.path.BasicPath; import org.rhq.core.pc.PluginContainer; import org.rhq.core.pc.ServerServices; import org.rhq.core.pc.plugin.FileSystemPluginFinder; import org.rhq.core.pc.plugin.PluginEnvironment; import org.rhq.core.system.SystemInfoFactory; import org.rhq.core.util.file.FileUtil; import org.rhq.test.arquillian.impl.util.SigarInstaller; import org.rhq.test.shrinkwrap.FilteredView; import org.rhq.test.shrinkwrap.RhqAgentPluginArchive; /** * * @author Lukas Krejci */ public class RhqAgentPluginContainer implements DeployableContainer<RhqAgentPluginContainerConfiguration> { private static final AtomicInteger CONTAINER_COUNT = new AtomicInteger(0); private static final File DEPLOYMENT_ROOT; private static final File ROOT; private static final String PLUGINS_DIR_NAME = "plugins"; private static final String DATA_DIR_NAME = "data"; private static final String TMP_DIR_NAME = "tmp"; private static final ArchivePath PLUGIN_DESCRIPTOR_PATH = new BasicPath("META-INF", "rhq-plugin.xml"); private static final Map<String, Boolean> NATIVE_SYSTEM_INFO_ENABLEMENT_PER_PC = new HashMap<String, Boolean>(); static { File root; File deployments; File sigar; try { root = FileUtil.createTempDirectory("TEST_RHQ_PC_DEPLOYMENTS", null, null); deployments = new File(root, "pcs"); deployments.mkdir(); sigar = new File(root, "sigar"); sigar.mkdir(); } catch (IOException e) { throw new IllegalStateException( "Could not create the root directory for RHQ plugin container test deployments"); } ROOT = root; DEPLOYMENT_ROOT = deployments; //install sigar if available SigarInstaller installer = new SigarInstaller(sigar); if (installer.isSigarAvailable()) { installer.installSigarNativeLibraries(); } } private static class ExcludeDirectory implements Filter<ArchivePath> { private ArchivePath root; public ExcludeDirectory(ArchivePath root) { this.root = root; } @Override public boolean include(ArchivePath object) { return !object.get().startsWith(root.get()); } } private static final Log LOG = LogFactory.getLog(RhqAgentPluginContainer.class); private RhqAgentPluginContainerConfiguration configuration; private File deploymentDirectory; @Inject private Instance<Container> container; @Override public Class<RhqAgentPluginContainerConfiguration> getConfigurationClass() { return RhqAgentPluginContainerConfiguration.class; } public static void init() { //this is just a dummy method that other classes can call to force the static //initialization of this class. } public static PluginContainer switchPluginContainer(String deploymentName) throws Exception { PluginContainer oldInstance = PluginContainer.getInstance(); Method setInstance = PluginContainer.class.getMethod("setContainerInstance", String.class); setInstance.invoke(null, deploymentName); PluginContainer newInstance = PluginContainer.getInstance(); if (newInstance != oldInstance) { Boolean enableNativeInfo = NATIVE_SYSTEM_INFO_ENABLEMENT_PER_PC.get(deploymentName); if (enableNativeInfo == null || !enableNativeInfo.booleanValue()) { SystemInfoFactory.disableNativeSystemInfo(); } else { SystemInfoFactory.enableNativeSystemInfo(); } LOG.info("Switched PluginContainer to '" + deploymentName + "'."); } return newInstance; } public static PluginContainer getPluginContainer(String deploymentName) throws Exception { Method getInstance = PluginContainer.class.getMethod("getContainerInstance", String.class); return (PluginContainer) getInstance.invoke(null, deploymentName); } @Override public void setup(RhqAgentPluginContainerConfiguration configuration) { this.configuration = configuration; finalizeConfiguration(this.configuration); } @Override public void start() throws LifecycleException { CONTAINER_COUNT.incrementAndGet(); try { switchPcInstance(); } catch (Exception e) { throw new LifecycleException("Failed to switch plugin container.", e); } LOG.info("Starting PluginContainer " + container.get().getName()); startPc(); } @Override public void stop() throws LifecycleException { try { switchPcInstance(); } catch (Exception e) { throw new LifecycleException("Failed to switch plugin container.", e); } LOG.info("Stopping PluginContainer " + container.get().getName()); stopPc(); if (CONTAINER_COUNT.decrementAndGet() == 0) { purgePcDeployments(); } } @Override public ProtocolDescription getDefaultProtocol() { return new ProtocolDescription("Local"); } @Override public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException { LOG.info("Deploying " + archive + " to PluginContainer " + container.get().getName()); try { switchPcInstance(); } catch (Exception e) { throw new DeploymentException("Failed to switch to PluginContainer [" + container.get().getName() + "].", e); } RhqAgentPluginArchive pluginArchive = archive.as(RhqAgentPluginArchive.class); Node descriptor = pluginArchive.get(ArchivePaths.create("META-INF/rhq-plugin.xml")); if (descriptor == null) { throw new DeploymentException("Plugin archive [" + archive + "] doesn't specify an RHQ plugin descriptor."); } boolean wasStarted = stopPc(); deployPlugin(pluginArchive); if (wasStarted) { startPc(); } PluginContainer pc = PluginContainer.getInstance(); String pluginName = getPluginName(pluginArchive); PluginEnvironment plugin = pc.getPluginManager().getPlugin(pluginName); if (plugin == null) { throw new RuntimeException("Failed to deploy plugin '" + pluginName + "' (" + pluginArchive.getName() + ") - check the log above for an error (and big stack trace) from PluginManager.initialize()."); } LOG.info("Done deploying plugin '" + pluginName + "' (" + archive + ") to PluginContainer " + container.get().getName() +"."); return new ProtocolMetaData(); } @Override public void undeploy(Archive<?> archive) throws DeploymentException { LOG.info("Undeploying " + archive + " from PluginContainer " + container.get().getName()); try { switchPcInstance(); } catch (Exception e) { throw new DeploymentException("Failed to switch plugin container.", e); } RhqAgentPluginArchive plugin = archive.as(RhqAgentPluginArchive.class); boolean wasStarted = stopPc(); File pluginDeploymentPath = getDeploymentPath(plugin); if (pluginDeploymentPath.exists() && !pluginDeploymentPath.delete()) { if (File.separatorChar == '/') { // Unix throw new DeploymentException("Could not delete the RHQ plugin jar " + plugin.getName()); } else { // Windows // TODO: file locking, probably due to // http://management-platform.blogspot.com/2009/01/classloaders-keeping-jar-files-open.html, // is not allowing deletion. Perhaps this can be fixed at some point. } } if (wasStarted) { startPc(); } LOG.info("Done undeploying " + archive + " from PluginContainer " + container.get().getName()); } @Override public void deploy(Descriptor descriptor) throws DeploymentException { throw new UnsupportedOperationException(); } @Override public void undeploy(Descriptor descriptor) throws DeploymentException { throw new UnsupportedOperationException(); } public RhqAgentPluginContainerConfiguration getConfiguration() { return configuration; } private File getDeploymentPath(Archive<?> plugin) { return new File(configuration.getPluginDirectory(), plugin.getName()); } private void finalizeConfiguration(RhqAgentPluginContainerConfiguration config) { String arquillianContainerName = container.get().getName(); String pluginContainerName = (arquillianContainerName) != null ? arquillianContainerName : UUID.randomUUID() .toString(); config.setContainerName(pluginContainerName); deploymentDirectory = new File(DEPLOYMENT_ROOT, pluginContainerName); File pluginsDir = new File(deploymentDirectory, PLUGINS_DIR_NAME); pluginsDir.mkdirs(); File dataDir = new File(deploymentDirectory, DATA_DIR_NAME); dataDir.mkdirs(); File tmpDir = new File(deploymentDirectory, TMP_DIR_NAME); tmpDir.mkdirs(); config.setPluginDirectory(pluginsDir); config.setDataDirectory(dataDir); config.setTemporaryDirectory(tmpDir); NATIVE_SYSTEM_INFO_ENABLEMENT_PER_PC.put(arquillianContainerName, config.isNativeSystemInfoEnabled()); if (config.getServerServicesImplementationClassName() != null) { try { Class<?> serverServicesClass = Class.forName(config.getServerServicesImplementationClassName()); ServerServices serverServices = (ServerServices) serverServicesClass.newInstance(); config.setServerServices(serverServices); } catch (Exception e) { throw new IllegalArgumentException("The serverServicesImplementationClassName property is invalid", e); } } } private static void purgePcDeployments() { FileUtil.purge(ROOT, true); } /** * Starts the plugin container. * This method is package private so that other instances can start/stop the PC the same way as the container * itself even outside the default lifecycle of the container. * * @return true if the plugin container needed to be started (i.e. was not running before), false otherwise. */ boolean startPc() { LOG.debug("Starting PluginContainer on demand..."); PluginContainer pc = PluginContainer.getInstance(); if (pc.isStarted()) { return false; } //always refresh the plugin finder so that it reports all the plugins //each time (and doesn't remember the plugins from previous PC runs) configuration.setPluginFinder(new FileSystemPluginFinder(configuration.getPluginDirectory())); if (LOG.isDebugEnabled() && (configuration.getAdditionalPackagesForRootPluginClassLoaderToExclude() != null)) { LOG.debug("Using root plugin classloader regex [" + configuration.getRootPluginClassLoaderRegex() + "]..."); } pc.setConfiguration(configuration); pc.initialize(); return true; } /** * Stops the plugin container. * This method is package private so that other instances can start/stop the PC the same way as the container * itself even outside the default lifecycle of the container. * * @return true if PC was running before this call, false otherwise */ boolean stopPc() { LOG.debug("Stopping PluginContainer on demand..."); PluginContainer pc = PluginContainer.getInstance(); boolean wasStarted = pc.isStarted(); if (wasStarted) { boolean shutdownGracefully = pc.shutdown(); if (shutdownGracefully) { LOG.debug("Stopped PluginContainer gracefully."); } else { LOG.debug("Stopped PluginContainer."); } } FileUtil.purge(configuration.getTemporaryDirectory(), false); if (configuration.isClearDataOnShutdown()) { FileUtil.purge(configuration.getDataDirectory(), false); } return wasStarted; } private void deployPlugin(RhqAgentPluginArchive plugin) { if (plugin.getRequiredPlugins() != null) { for (Archive<?> a : plugin.getRequiredPlugins()) { RhqAgentPluginArchive p = a.as(RhqAgentPluginArchive.class); deployPlugin(p); } } File pluginDeploymentPath = getDeploymentPath(plugin); plugin.as(FilteredView.class).filterContents(new ExcludeDirectory(plugin.getRequiredPluginsPath())) .as(ZipExporter.class).exportTo(pluginDeploymentPath, true); } private PluginContainer switchPcInstance() throws Exception { return switchPluginContainer(container.get().getName()); } private static String getPluginName(Archive<?> archive) { InputStream is = archive.get(PLUGIN_DESCRIPTOR_PATH).getAsset().openStream(); XMLEventReader rdr = null; try { rdr = XMLInputFactory.newInstance().createXMLEventReader(is); XMLEvent event = null; while (rdr.hasNext()) { event = rdr.nextEvent(); if (event.getEventType() == XMLEvent.START_ELEMENT) { break; } } StartElement startElement = event.asStartElement(); String tagName = startElement.getName().getLocalPart(); if (!"plugin".equals(tagName)) { throw new IllegalArgumentException("Illegal start tag found in the plugin descriptor. Expected 'plugin' but found '" + tagName + "' in the plugin '" + archive + "'."); } Attribute nameAttr = startElement.getAttributeByName(new QName("name")); if (nameAttr == null) { throw new IllegalArgumentException("Couldn't find the name attribute on the plugin tag in the plugin descriptor of plugin '" + archive + "'."); } return nameAttr.getValue(); } catch (XMLStreamException e) { throw new IllegalArgumentException("Failed to extract the plugin name out of the RHQ plugin archive [" + archive + "]", e); } catch (FactoryConfigurationError e) { throw new IllegalArgumentException("Failed to extract the plugin name out of the RHQ plugin archive [" + archive + "]", e); } finally { closeReaderAndStream(rdr, is, archive); } } private static void closeReaderAndStream(XMLEventReader rdr, InputStream str, Archive<?> archive) { if (rdr != null) { try { rdr.close(); } catch (XMLStreamException e) { LOG.error("Failed to close the XML reader of the plugin descriptor in archive [" + archive + "]", e); } } try { str.close(); } catch (IOException e) { LOG.error("Failed to close the input stream of the plugin descriptor in archive [" + archive + "]", e); } } }