/* * RHQ Management Platform * Copyright (C) 2005-2014 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.core.pc.plugin; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.Nullable; import org.rhq.core.clientapi.agent.PluginContainerException; import org.rhq.core.clientapi.agent.metadata.PluginDependencyGraph; import org.rhq.core.clientapi.agent.metadata.PluginMetadataManager; import org.rhq.core.clientapi.descriptor.AgentPluginDescriptorUtil; import org.rhq.core.clientapi.descriptor.PluginTransformer; import org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor; import org.rhq.core.domain.plugin.Plugin; import org.rhq.core.pc.ContainerService; import org.rhq.core.pc.PluginContainerConfiguration; import org.rhq.core.pluginapi.plugin.PluginContext; import org.rhq.core.pluginapi.plugin.PluginLifecycleListener; import org.rhq.core.system.SystemInfo; import org.rhq.core.system.SystemInfoFactory; import org.rhq.core.util.exception.ThrowableUtil; /** * This container service will load in all plugins that can be found and will maintain the complete set of * {@link #getMetadataManager() metadata} found in all plugin descriptors from all loaded plugins. You can obtain a * loaded plugin's {@link PluginEnvironment environment}, including its classloader, from this object as well - see * {@link #getPlugin(String)}. * * @author Greg Hinkle * @author Jason Dobies * @author John Mazzitelli */ public class PluginManager implements ContainerService { /** * A callback interface for updating the loadedPlugins list. When running inside of an agent, loadedPlugins gets * updated with Plugin objects received from the RHQ server. When running in embedded mode, we have to create the * Plugin objects ourselves. */ private static interface UpdateLoadedPlugins { void execute(PluginDescriptor pluginDescriptor, URL pluginURL); } private static final Log log = LogFactory.getLog(PluginManager.class); /** * The map of all plugins keyed on plugin name. */ private final Map<String, PluginEnvironment> loadedPluginEnvironments; /** * A list of loaded plugins in the order in which they were loaded. */ private final List<Plugin> loadedPlugins; private final PluginMetadataManager metadataManager; private final ClassLoaderManager classLoaderManager; private final PluginContainerConfiguration pluginContainerConfiguration; private final PluginLifecycleListenerManager pluginLifecycleListenerMgr; private final UpdateLoadedPlugins updateLoadedPlugins; /** * Finds all plugins using the plugin finder defined in the <code>pluginContainerConfiguration</code> and * {@link #loadPlugin(java.net.URL, ClassLoader, org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor) loads} * each plugin found. */ public PluginManager(PluginContainerConfiguration pluginContainerConfiguration, PluginLifecycleListenerManager pluginLifecycleListenerManager) { if (pluginContainerConfiguration == null) { throw new NullPointerException("pluginContainerConfiguration is null"); } if (pluginLifecycleListenerManager == null) { throw new NullPointerException("pluginLifecycleListenerManager is null"); } this.pluginContainerConfiguration = pluginContainerConfiguration; this.pluginLifecycleListenerMgr = pluginLifecycleListenerManager; loadedPluginEnvironments = new HashMap<String, PluginEnvironment>(); loadedPlugins = new ArrayList<Plugin>(); metadataManager = new PluginMetadataManager(); metadataManager.setDisabledResourceTypes(pluginContainerConfiguration.getDisabledResourceTypes()); updateLoadedPlugins = new UpdateLoadedPlugins() { PluginTransformer transformer = new PluginTransformer(); public void execute(PluginDescriptor pluginDescriptor, URL pluginURL) { Plugin plugin = transformer.toPlugin(pluginDescriptor, pluginURL); loadedPlugins.add(plugin); } }; PluginFinder finder = pluginContainerConfiguration.getPluginFinder(); File tmpDir = pluginContainerConfiguration.getTemporaryDirectory(); List<String> disabledPlugins = pluginContainerConfiguration.getDisabledPlugins(); // The root classloader for all plugins will have all classes hidden except for those configured in the regex. // Notice this root classloader has no jar URLs - it will provide no classes except for what it will allow // the parent to expose as dictated by the regex. ClassLoader thisClassLoader = this.getClass().getClassLoader(); String rootPluginClassLoaderRegex = pluginContainerConfiguration.getRootPluginClassLoaderRegex(); ClassLoader rootCL = new RootPluginClassLoader(new URL[] {}, thisClassLoader, rootPluginClassLoaderRegex); // build our empty class loader manager - we use it to create and manage our plugin's classloaders Map<String, URL> pluginNamesUrls = new HashMap<String, URL>(); // Save descriptors for later use, so we don't need to parse them twice. Map<URL, PluginDescriptor> descriptors = new HashMap<URL, PluginDescriptor>(); PluginDependencyGraph graph = new PluginDependencyGraph(); boolean createResourceCL = pluginContainerConfiguration.isCreateResourceClassloaders(); this.classLoaderManager = new ClassLoaderManager(pluginNamesUrls, graph, rootCL, tmpDir, createResourceCL); if (finder == null) { log.warn("No plugin finder was specified in the plugin container configuration - this should only occur within test environments."); return; } try { Collection<URL> pluginUrls = finder.findPlugins(); // first, we need to parse all descriptors so we can build the dependency graph for (URL url : pluginUrls) { log.debug("Plugin found at: " + url); try { PluginDescriptor descriptor = AgentPluginDescriptorUtil.loadPluginDescriptorFromUrl(url); if (!disabledPlugins.contains(descriptor.getName())) { AgentPluginDescriptorUtil.addPluginToDependencyGraph(graph, descriptor); pluginNamesUrls.put(descriptor.getName(), url); descriptors.put(url, descriptor); } else { log.info("Not loading disabled plugin: " + url); } } catch (Throwable t) { // probably due to invalid XML syntax in the deployment descriptor - the plugin will be ignored log.error("Plugin at [" + url + "] could not be loaded and will therefore not be deployed.", t); continue; } } // our graph is complete, get the order that we have to deploy the plugins List<String> deploymentOrder = graph.getDeploymentOrder(); // now deploy the plugins in the proper order, making sure we build the proper classloaders for (String nextPlugin : deploymentOrder) { URL pluginUrl = pluginNamesUrls.get(nextPlugin); try { ClassLoader pluginClassLoader = this.classLoaderManager.obtainPluginClassLoader(nextPlugin); PluginDescriptor descriptor = descriptors.get(pluginUrl); loadPlugin(pluginUrl, pluginClassLoader, descriptor); } catch (Throwable t) { // for some reason, the plugin failed to load - it will be ignored, and its depending plugins will also fail later log.error("Plugin [" + nextPlugin + "] at [" + pluginUrl + "] could not be loaded and will therefore not be deployed.", t); continue; } } log.info("Deployed plugins: " + this.loadedPlugins); metadataManager.cleanupDescriptors(); } catch (Exception e) { shutdown(); // have to clean up the environments (e.g. unpacked jars) we might have already created log.error("Error initializing plugin container", e); throw new RuntimeException("Cannot initialize the plugin container", e); } } /** * @see ContainerService#shutdown() */ public void shutdown() { // Inform the plugins we are shutting them down. // We want to shut them down in the reverse order that we initialized them. Collections.reverse(this.loadedPlugins); for (Plugin plugin : loadedPlugins) { PluginLifecycleListener listener = pluginLifecycleListenerMgr.getListener(plugin.getName()); if (listener != null) { try { ClassLoader originalCL = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader( this.classLoaderManager.obtainPluginClassLoader(plugin.getName())); try { listener.shutdown(); } finally { Thread.currentThread().setContextClassLoader(originalCL); } } catch (Throwable t) { log.warn("Failed to get lifecycle listener to shutdown [" + plugin.getName() + "]. Cause: " + ThrowableUtil.getAllMessages(t)); } } } // Clean up the plugin environment and the temp dirs that were used by the plugin classloaders. for (PluginEnvironment pluginEnvironment : this.loadedPluginEnvironments.values()) { pluginEnvironment.destroy(); } this.classLoaderManager.destroy(); this.loadedPluginEnvironments.clear(); this.loadedPlugins.clear(); pluginLifecycleListenerMgr.shutdown(); } /** * Returns the {@link PluginEnvironment}s for every plugin this manager found and loaded. * * @return environments for all the plugins */ public Collection<PluginEnvironment> getPlugins() { return this.loadedPluginEnvironments.values(); } /** * Returns the {@link PluginEnvironment} for the specific plugin with the given name. * * <p>The plugin's name is defined in its plugin descriptor - specifically the XML root node's "name" attribute * (e.g. <plugin name="thePluginName").</p> * * @param name plugin name as defined in the plugin's descriptor * * @return the environment of the loaded plugin with the given name (<code>null</code> if there is no loaded plugin * with the given name) */ @Nullable public PluginEnvironment getPlugin(String name) { return this.loadedPluginEnvironments.get(name); } /** * An object that can be used to process and store all metadata from all plugins. This object will contain all the * metadata found in all loaded plugins. * * @return object to retrieve plugin metadata from */ public PluginMetadataManager getMetadataManager() { return metadataManager; } /** * Returns the manager of all classloaders created for the plugin manager. * * @return the classloader manager for all plugins */ public ClassLoaderManager getClassLoaderManager() { return this.classLoaderManager; } public String getAmpsVersion(String pluginName) { for (Plugin plugin : loadedPlugins) { if (plugin.getName().equals(pluginName)) { return plugin.getAmpsVersion(); } } return null; } /** * This will create a {@link PluginEnvironment} for the plugin at the given URL. The plugin's descriptor is parsed. * Once this method returns, the plugin's components are ready to be created and used. * * @param pluginUrl the new plugin's jar location * @param classLoader the new plugin's classloader * @param pluginDescriptor the already parsed plugin descriptor for this plugin * @throws PluginContainerException if the plugin fails to load */ private void loadPlugin(URL pluginUrl, ClassLoader classLoader, PluginDescriptor pluginDescriptor) throws PluginContainerException { if (log.isDebugEnabled()) { log.debug("Loading plugin from [" + pluginUrl + "] in classloader [" + classLoader + "]..."); } PluginDescriptorLoader pluginDescriptorLoader = new PluginDescriptorLoader(pluginUrl, classLoader); PluginEnvironment pluginEnvironment = new PluginEnvironment(pluginDescriptor.getName(), pluginDescriptorLoader); String pluginName = pluginEnvironment.getPluginName(); // tell the plugin we have loaded it PluginLifecycleListener overseer = getPluginLifecycleListener(pluginName, pluginEnvironment, pluginDescriptor); if (overseer != null) { PluginContext context = createPluginContext(pluginName); ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(classLoader); overseer.initialize(context); } catch (Throwable t) { throw new PluginContainerException("Plugin Lifecycle Listener failed to initialize plugin", t); } finally { Thread.currentThread().setContextClassLoader(originalContextClassLoader); } } // everything is loaded and initialized this.loadedPluginEnvironments.put(pluginName, pluginEnvironment); this.metadataManager.loadPlugin(pluginDescriptor); pluginLifecycleListenerMgr.setListener(pluginDescriptor.getName(), overseer); updateLoadedPlugins.execute(pluginDescriptor, pluginUrl); } /** * This will create a new {@link PluginLifecycleListener} instance for that is used to * initialize and shutdown a particular plugin. If there is no plugin lifecycle listener * configured for the given plugin, <code>null</code> is returned. * * The new object will be loaded in the plugin's specific classloader. * * @param pluginName the name of the plugin whose {@link PluginLifecycleListener} is to be retrieved * @param pluginEnvironment the environment in which the plugin will execute * @param pluginDescriptor the plugin's descriptor * * @return a new object loaded in the proper plugin classloader that can initialize/shutdown the plugin, * or <code>null</code> if there is no plugin lifecycle listener to be associated with the given plugin * * @throws PluginContainerException if failed to create the instance */ private PluginLifecycleListener getPluginLifecycleListener(String pluginName, PluginEnvironment pluginEnvironment, PluginDescriptor pluginDescriptor) throws PluginContainerException { return pluginLifecycleListenerMgr.loadListener(pluginDescriptor, pluginEnvironment); } private PluginContext createPluginContext(String pluginName) { SystemInfo sysInfo = SystemInfoFactory.createSystemInfo(); File dataDir = new File(pluginContainerConfiguration.getDataDirectory(), pluginName); File tmpDir = pluginContainerConfiguration.getTemporaryDirectory(); String pcName = pluginContainerConfiguration.getContainerName(); PluginContext context = new PluginContext(pluginName, sysInfo, tmpDir, dataDir, pcName); return context; } /** * Given a plugin name, this will get all of its dependencies' plugin URLs and add them to <code>allUrls</code>. * This recursively travels N levels deep in the dependency graph. * * @param pluginName the name of the plugin to obtain dependency URLs for * @param pluginNamesUrls map of all known plugin names and their plugin jar URLs * @param allUrls where the results will be stored * * TODO: Use it or lose it - this used to be needed, keeping this around just in case it needs to be resurrected * This could be useful if we want to somehow implement multiple plugin classloader inheritance. */ private void getDependentUrls(String pluginName, Map<String, URL> pluginNamesUrls, Set<URL> allUrls) { List<String> deps = this.classLoaderManager.getPluginDependencyGraph().getPluginDependencies(pluginName); for (String dep : deps) { getDependentUrls(dep, pluginNamesUrls, allUrls); allUrls.add(pluginNamesUrls.get(dep)); } return; } }