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

org.jivesoftware.openfire.container.PluginManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
 *
 * 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.jivesoftware.openfire.container;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.admin.AdminConsole;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.JavaSpecVersion;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.jar.JarFile;
import java.util.zip.ZipException;

/**
 * Manages plugins.
 *
 * The plugins directory is monitored for any new plugins, and they are dynamically loaded.
 *
 * An instance of this class can be obtained using: XMPPServer.getInstance().getPluginManager()
 *
 * These states are defined for plugin management:
 * 
    *
  • installed - the plugin archive file is present in the plugins directory.
  • *
  • extracted - the plugin archive file has been extracted.
  • *
  • loaded - the plugin has (successfully) been initialized.
  • *
* * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @author Matt Tucker * @see Plugin * @see org.jivesoftware.openfire.XMPPServer#getPluginManager() */ public class PluginManager { private static final Logger Log = LoggerFactory.getLogger( PluginManager.class ); private final Path pluginDirectory; /** * Plugins that are loaded, mapped by their canonical name. */ private final Map pluginsLoaded = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); /** * The plugin classloader for each loaded plugin. */ private final Map classloaders = new HashMap<>(); /** * The directory in which a plugin is extracted, mapped by canonical name. This collection contains loaded plugins, * as well as extracted (but not loaded) plugins. * * Note that typically these directories are subdirectories of plugins, but a 'dev-plugin' could live * elsewhere. */ private final Map pluginDirs = new HashMap<>(); /** * Plugin metadata for all extracted plugins, mapped by canonical name. */ private final Map pluginMetadata = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); private final Map pluginDevelopment = new HashMap<>(); private final Map> parentPluginMap = new HashMap<>(); private final Map childPluginMap = new HashMap<>(); private final Set pluginListeners = new CopyOnWriteArraySet<>(); private final Set pluginManagerListeners = new CopyOnWriteArraySet<>(); private final Map failureToLoadCount = new HashMap<>(); private final PluginMonitor pluginMonitor; private boolean executed = false; /** * Constructs a new plugin manager. * * @param pluginDir the directory containing all Openfire plugins, typically OPENFIRE_HOME/plugins/ */ public PluginManager( File pluginDir ) { this.pluginDirectory = pluginDir.toPath(); pluginMonitor = new PluginMonitor( this ); } /** * Starts plugins and the plugin monitoring service. */ public synchronized void start() { pluginMonitor.start(); } /** * Shuts down all running plugins. */ public synchronized void shutdown() { Log.info( "Shutting down. Unloading all loaded plugins..." ); // Stop the plugin monitoring service. pluginMonitor.stop(); // Shutdown all loaded plugins. for ( Map.Entry plugin : pluginsLoaded.entrySet() ) { try { plugin.getValue().destroyPlugin(); Log.info( "Unloaded plugin '{}'.", plugin.getKey() ); } catch ( Exception e ) { Log.error( "An exception occurred while trying to unload plugin '{}':", plugin.getKey(), e ); } } pluginsLoaded.clear(); pluginDirs.clear(); pluginMetadata.clear(); classloaders.clear(); pluginDevelopment.clear(); childPluginMap.clear(); failureToLoadCount.clear(); } /** * Returns the directory that contains all plugins. This typically is OPENFIRE_HOME/plugins. * * @return The directory that contains all plugins. */ public Path getPluginsDirectory() { return pluginDirectory; } /** * Installs or updates an existing plugin. * * @param in the input stream that contains the new plugin definition. * @param pluginFilename the filename of the plugin to create or update. * @return true if the plugin was successfully installed or updated. */ public boolean installPlugin( InputStream in, String pluginFilename ) { if ( pluginFilename == null || pluginFilename.isEmpty() ) { Log.error( "Error installing plugin: pluginFilename was null or empty." ); return false; } if ( in == null ) { Log.error( "Error installing plugin '{}': Input stream was null.", pluginFilename ); return false; } try { // If pluginFilename is a path instead of a simple file name, we only want the file name pluginFilename = Paths.get(pluginFilename).getFileName().toString(); // Absolute path to the plugin file Path absolutePath = pluginDirectory.resolve( pluginFilename ); Path partFile = pluginDirectory.resolve( pluginFilename + ".part" ); // Save input stream contents to a temp file Files.copy( in, partFile, StandardCopyOption.REPLACE_EXISTING ); // Check if zip file, else ZipException caught below. try (JarFile file = new JarFile(partFile.toFile())) { } catch (ZipException e) { Files.deleteIfExists(partFile); throw e; }; // Rename temp file to .jar Files.move( partFile, absolutePath, StandardCopyOption.REPLACE_EXISTING ); // Ask the plugin monitor to update the plugin immediately. pluginMonitor.runNow( true ); } catch ( IOException e ) { Log.error( "An exception occurred while installing new version of plugin '{}':", pluginFilename, e ); return false; } return true; } /** * Returns true if the plugin by the specified name is installed. Specifically, this checks if the plugin * archive file is present in the plugins directory. * * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @param canonicalName the canonical filename of the plugin (cannot be null). * @return true if the plugin is installed, otherwise false. */ public boolean isInstalled( final String canonicalName ) { final DirectoryStream.Filter filter = new DirectoryStream.Filter() { @Override public boolean accept( Path entry ) throws IOException { final String name = entry.getFileName().toString(); return Files.exists( entry ) && !Files.isDirectory( entry ) && ( name.equalsIgnoreCase( canonicalName + ".jar" ) || name.equalsIgnoreCase( canonicalName + ".war" ) ); } }; try ( final DirectoryStream paths = Files.newDirectoryStream( pluginDirectory, filter ) ) { return paths.iterator().hasNext(); } catch ( IOException e ) { Log.error( "Unable to determine if plugin '{}' is installed.", canonicalName, e ); // return the next best guess return pluginsLoaded.containsKey( canonicalName ); } } /** * Returns true if the plugin by the specified name is extracted. Specifically, this checks if the plugins * directory contains a subdirectory that matches the canonical name of the plugin. * * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @param canonicalName the canonical filename of the plugin (cannot be null). * @return true if the plugin is extracted, otherwise false. */ public boolean isExtracted( final String canonicalName ) { return pluginMetadata.containsKey( canonicalName ); } /** * Returns true if the plugin by the specified name is loaded. Specifically, this checks if an instance was created * for the plugin class file. * * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @param canonicalName the canonical filename of the plugin (cannot be null). * @return true if the plugin is extracted, otherwise false. */ public boolean isLoaded( final String canonicalName ) { return pluginsLoaded.containsKey( canonicalName ); } /** * Returns metadata for all extracted plugins, mapped by their canonical name. * * The collection is alphabetically sorted, by plugin name. * * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @return A collection of metadata (possibly empty, never null). */ public Map getMetadataExtractedPlugins() { return Collections.unmodifiableMap( this.pluginMetadata ); } /** * Returns metadata for an extracted plugin, or null when the plugin is extracted nor loaded. * * Note that an installed plugin is not per definition an extracted plugin, and an extracted * plugin is not per definition a loaded plugin. A plugin that's extracted might, for instance, fail to * load, due to restrictions imposed by its minServerVersion definition. * * @return A collection of metadata (possibly empty, never null). */ public PluginMetadata getMetadata( String canonicalName ) { return this.pluginMetadata.get( canonicalName ); } /** * Returns a Collection of all loaded plugins. * * The returned collection will not include plugins that have been downloaded, but not loaded. * * @return a Collection of all loaded plugins. */ public Collection getPlugins() { return Collections.unmodifiableCollection( Arrays.asList( pluginsLoaded.values().toArray( new Plugin[ pluginsLoaded.size() ] ) ) ); } /** * Returns the canonical name for a loaded plugin. * * @param plugin A plugin (cannot be null). * @return The canonical name for the plugin (never null). */ public String getCanonicalName( Plugin plugin ) { // TODO consider using a bimap for a more efficient lookup. for ( Map.Entry entry : pluginsLoaded.entrySet() ) { if ( entry.getValue().equals( plugin ) ) { return entry.getKey(); } } return null; } /** * Returns an loaded plugin by its canonical name or null if a plugin with that name does not exist. The * canonical name is the lowercase-name of the plugin archive, without the file extension. For example: "broadcast". * * @param canonicalName the name of the plugin. * @return the plugin. */ public Plugin getPlugin( String canonicalName ) { return pluginsLoaded.get( canonicalName.toLowerCase() ); } /** * @deprecated Use #getPluginPath() instead. */ @Deprecated public File getPluginDirectory( Plugin plugin ) { return getPluginPath( plugin ).toFile(); } /** * Returns the plugin's directory. * * @param plugin the plugin. * @return the plugin's directory. */ public Path getPluginPath( Plugin plugin ) { final String canonicalName = getCanonicalName( plugin ); if ( canonicalName != null ) { return pluginDirs.get( canonicalName ); } return null; } /** * Returns true if at least one attempt to load plugins has been done. A true value does not mean * that available plugins have been loaded nor that plugins to be added in the future are already * loaded. :)

* * @return true if at least one attempt to load plugins has been done. */ public boolean isExecuted() { return executed; } /** * Loads a plugin. * * @param pluginDir the plugin directory. */ boolean loadPlugin( String canonicalName, Path pluginDir ) { final PluginMetadata metadata = PluginMetadata.getInstance( pluginDir ); pluginMetadata.put( canonicalName, metadata ); // Only load the admin plugin during setup mode. if ( XMPPServer.getInstance().isSetupMode() && !( canonicalName.equals( "admin" ) ) ) { return false; } if ( failureToLoadCount.containsKey( canonicalName ) && failureToLoadCount.get( canonicalName ) > JiveGlobals.getIntProperty( "plugins.loading.retries", 5 ) ) { Log.debug( "The unloaded file for plugin '{}' is silently ignored, as it has failed to load repeatedly.", canonicalName ); return false; } Log.debug( "Loading plugin '{}'...", canonicalName ); try { final Path pluginConfig = pluginDir.resolve( "plugin.xml" ); if ( !Files.exists( pluginConfig ) ) { Log.warn( "Plugin '{}' could not be loaded: no plugin.xml file found.", canonicalName ); failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from. return false; } final Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion(); // See if the plugin specifies a minimum version of Openfire required to run. if ( metadata.getMinServerVersion() != null ) { // OF-1338: Ignore release status when comparing minimum server version requirement. final Version compareVersion = new Version( currentServerVersion.getMajor(), currentServerVersion.getMinor(), currentServerVersion.getMicro(), null, -1 ); if ( metadata.getMinServerVersion().isNewerThan( compareVersion ) ) { Log.warn( "Ignoring plugin '{}': requires server version {}. Current server version is {}.", canonicalName, metadata.getMinServerVersion(), currentServerVersion ); failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from. return false; } } // See if the plugin specifies a maximum version of Openfire required to run. if ( metadata.getPriorToServerVersion() != null ) { // OF-1338: Ignore release status when comparing maximum server version requirement. final Version compareVersion = new Version( currentServerVersion.getMajor(), currentServerVersion.getMinor(), currentServerVersion.getMicro(), null, -1 ); if ( !metadata.getPriorToServerVersion().isNewerThan( compareVersion ) ) { Log.warn( "Ignoring plugin '{}': compatible with server versions up to but excluding {}. Current server version is {}.", canonicalName, metadata.getPriorToServerVersion(), currentServerVersion ); failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from. return false; } } // See if the plugin specifies a minimum version of Java required to run. if ( metadata.getMinJavaVersion() != null ) { final JavaSpecVersion runtimeVersion = new JavaSpecVersion( System.getProperty( "java.specification.version" ) ); if ( metadata.getMinJavaVersion().isNewerThan( runtimeVersion ) ) { Log.warn( "Ignoring plugin '{}': requires Java specification version {}. Openfire is currently running in Java {}.", canonicalName, metadata.getMinJavaVersion(), System.getProperty( "java.specification.version" ) ); failureToLoadCount.put( canonicalName, Integer.MAX_VALUE ); // Don't retry - this cannot be recovered from. return false; } } // Properties to be used to load external resources. When set, plugin is considered to run in DEV mode. final String devModeClassesDir = System.getProperty( canonicalName + ".classes" ); final String devModewebRoot = System.getProperty( canonicalName + ".webRoot" ); final boolean devMode = devModewebRoot != null || devModeClassesDir != null; final PluginDevEnvironment dev = ( devMode ? configurePluginDevEnvironment( pluginDir, devModeClassesDir, devModewebRoot ) : null ); // Initialize the plugin class loader, which is either a new instance, or a the loader from a parent plugin. final PluginClassLoader pluginLoader; // Check to see if this is a child plugin of another plugin. If it is, we re-use the parent plugin's class // loader so that the plugins can interact. String parentPluginName = null; Plugin parentPlugin = null; final String parentCanonicalName = PluginMetadataHelper.getParentPlugin( pluginDir ); if ( parentCanonicalName != null ) { // The name of the parent plugin as specified in plugin.xml might have incorrect casing. Lookup the correct name. for ( final Map.Entry entry : pluginsLoaded.entrySet() ) { if ( entry.getKey().equalsIgnoreCase( parentCanonicalName ) ) { parentPluginName = entry.getKey(); parentPlugin = entry.getValue(); break; } } // See if the parent is loaded. if ( parentPlugin == null ) { Log.info( "Unable to load plugin '{}': parent plugin '{}' has not been loaded.", canonicalName, parentCanonicalName ); Integer count = failureToLoadCount.get( canonicalName ); if ( count == null ) { count = 0; } failureToLoadCount.put( canonicalName, ++count ); return false; } pluginLoader = classloaders.get( parentPlugin ); } else { // This is not a child plugin, so create a new class loader. pluginLoader = new PluginClassLoader(); } // Add the plugin sources to the classloaded. pluginLoader.addDirectory( pluginDir.toFile(), devMode ); // When running in DEV mode, add optional other sources too. if ( dev != null && dev.getClassesDir() != null ) { pluginLoader.addURLFile( dev.getClassesDir().toURI().toURL() ); } // Instantiate the plugin! final SAXReader saxReader = new SAXReader(); saxReader.setEncoding( "UTF-8" ); final Document pluginXML = saxReader.read( pluginConfig.toFile() ); final String className = pluginXML.selectSingleNode( "/plugin/class" ).getText().trim(); final Plugin plugin = (Plugin) pluginLoader.loadClass( className ).newInstance(); // Bookkeeping! classloaders.put( plugin, pluginLoader ); pluginsLoaded.put( canonicalName, plugin ); pluginDirs.put( canonicalName, pluginDir ); if ( dev != null ) { pluginDevelopment.put( plugin, dev ); } // If this is a child plugin, register it as such. if ( parentPlugin != null ) { List childrenPlugins = parentPluginMap.get( parentPlugin ); if ( childrenPlugins == null ) { childrenPlugins = new ArrayList<>(); parentPluginMap.put( parentPlugin, childrenPlugins ); } childrenPlugins.add( canonicalName ); // Also register child to parent relationship. childPluginMap.put( plugin, parentPluginName ); } // Check the plugin's database schema (if it requires one). if ( !DbConnectionManager.getSchemaManager().checkPluginSchema( plugin ) ) { // The schema was not there and auto-upgrade failed. Log.error( "Error while loading plugin '{}': {}", canonicalName, LocaleUtils.getLocalizedString( "upgrade.database.failure" ) ); } // Load any JSP's defined by the plugin. final Path webXML = pluginDir.resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" ); if ( Files.exists( webXML ) ) { PluginServlet.registerServlets( this, plugin, webXML.toFile() ); } // Load any custom-defined servlets. final Path customWebXML = pluginDir.resolve( "web" ).resolve( "WEB-INF" ).resolve( "web-custom.xml" ); if ( Files.exists( customWebXML ) ) { PluginServlet.registerServlets( this, plugin, customWebXML.toFile() ); } // Configure caches of the plugin configureCaches( pluginDir, canonicalName ); // Initialze the plugin. final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader( pluginLoader ); plugin.initializePlugin( this, pluginDir.toFile() ); Log.debug( "Initialized plugin '{}'.", canonicalName ); Thread.currentThread().setContextClassLoader( oldLoader ); // If there a section defined, register it. final Element adminElement = (Element) pluginXML.selectSingleNode( "/plugin/adminconsole" ); if ( adminElement != null ) { final Element appName = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/appname" ); if ( appName != null ) { // Set the plugin name so that the proper i18n String can be loaded. appName.addAttribute( "plugin", canonicalName ); } // If global images are specified, override their URL. Element imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/logo-image" ); if ( imageEl != null ) { imageEl.setText( "plugins/" + canonicalName + "/" + imageEl.getText() ); imageEl.addAttribute( "plugin", canonicalName ); // Set the plugin name so that the proper i18n String can be loaded. } imageEl = (Element) adminElement.selectSingleNode( "/plugin/adminconsole/global/login-image" ); if ( imageEl != null ) { imageEl.setText( "plugins/" + canonicalName + "/" + imageEl.getText() ); imageEl.addAttribute( "plugin", canonicalName ); // Set the plugin name so that the proper i18n String can be loaded. } // Modify all the URL's in the XML so that they are passed through the plugin servlet correctly. final List urls = adminElement.selectNodes( "//@url" ); for ( final Object url : urls ) { final Attribute attr = (Attribute) url; attr.setValue( "plugins/" + canonicalName + "/" + attr.getValue() ); } // In order to internationalize the names and descriptions in the model, we add a "plugin" attribute to // each tab, sidebar, and item so that the the renderer knows where to load the i18n Strings from. final String[] elementNames = new String[]{ "tab", "sidebar", "item" }; for ( final String elementName : elementNames ) { final List values = adminElement.selectNodes( "//" + elementName ); for ( final Object value : values ) { final Element element = (Element) value; // Make sure there's a name or description. Otherwise, no need to i18n settings. if ( element.attribute( "name" ) != null || element.attribute( "value" ) != null ) { element.addAttribute( "plugin", canonicalName ); } } } AdminConsole.addModel( canonicalName, adminElement ); } firePluginCreatedEvent( canonicalName, plugin ); Log.info( "Successfully loaded plugin '{}'.", canonicalName ); return true; } catch ( Throwable e ) { Log.error( "An exception occurred while loading plugin '{}':", canonicalName, e ); Integer count = failureToLoadCount.get( canonicalName ); if ( count == null ) { count = 0; } failureToLoadCount.put( canonicalName, ++count ); return false; } } private PluginDevEnvironment configurePluginDevEnvironment( final Path pluginDir, String classesDir, String webRoot ) throws IOException { final String pluginName = pluginDir.getFileName().toString(); final Path compilationClassesDir = pluginDir.resolve( "classes" ); if ( Files.notExists( compilationClassesDir ) ) { Files.createDirectory( compilationClassesDir ); } compilationClassesDir.toFile().deleteOnExit(); final PluginDevEnvironment dev = new PluginDevEnvironment(); Log.info( "Plugin '{}' is running in development mode.", pluginName ); if ( webRoot != null ) { Path webRootDir = Paths.get( webRoot ); if ( Files.notExists( webRootDir ) ) { // Ok, let's try it relative from this plugin dir? webRootDir = pluginDir.resolve( webRoot ); } if ( Files.exists( webRootDir ) ) { dev.setWebRoot( webRootDir.toFile() ); } } if ( classesDir != null ) { Path classes = Paths.get( classesDir ); if ( Files.notExists( classes ) ) { // ok, let's try it relative from this plugin dir? classes = pluginDir.resolve( classesDir ); } if ( Files.exists( classes ) ) { dev.setClassesDir( classes.toFile() ); } } return dev; } private void configureCaches( Path pluginDir, String pluginName ) { Path cacheConfig = pluginDir.resolve( "cache-config.xml" ); if ( Files.exists( cacheConfig ) ) { PluginCacheConfigurator configurator = new PluginCacheConfigurator(); try { configurator.setInputStream( new BufferedInputStream( Files.newInputStream( cacheConfig ) ) ); configurator.configure( pluginName ); } catch ( Exception e ) { Log.error( "An exception occurred while trying to configure caches for plugin '{}':", pluginName, e ); } } } /** * Delete a plugin, which removes the plugin.jar/war file after which the plugin is unloaded. */ public void deletePlugin( final String pluginName ) { Log.debug( "Deleting plugin '{}'...", pluginName ); try ( final DirectoryStream ds = Files.newDirectoryStream( getPluginsDirectory(), new DirectoryStream.Filter() { @Override public boolean accept( final Path path ) throws IOException { if ( Files.isDirectory( path ) ) { return false; } final String fileName = path.getFileName().toString().toLowerCase(); return ( fileName.equals( pluginName + ".jar" ) || fileName.equals( pluginName + ".war" ) ); } } ) ) { for ( final Path pluginFile : ds ) { try { Files.delete( pluginFile ); pluginMonitor.runNow( true ); // trigger unload by running the monitor (which is more thread-safe than calling unloadPlugin directly). } catch ( IOException ex ) { Log.warn( "Unable to delete plugin '{}', as the plugin jar/war file cannot be deleted. File path: {}", pluginName, pluginFile, ex ); } } } catch ( Throwable e ) { Log.error( "An unexpected exception occurred while deleting plugin '{}'.", pluginName, e ); } } public boolean reloadPlugin( String pluginName ) { Log.debug( "Reloading plugin '{}'..." ); final Plugin plugin = getPlugin( pluginName ); if ( plugin == null ) { Log.warn( "Unable to reload plugin '{}'. No such plugin loaded.", pluginName ); return false; } final Path path = getPluginPath( plugin ); if ( path == null ) { // When there's a plugin, there should be a path. If there isn't, our code is buggy. throw new IllegalStateException( "Unable to determine installation path of plugin: " + pluginName ); } try { Files.setLastModifiedTime( path, FileTime.fromMillis( 0 ) ); } catch ( IOException e ) { Log.warn( "Unable to reload plugin '{}'. Unable to reset the 'last modified time' of the plugin path. Try removing and restoring the plugin jar file manually." ); return false; } pluginMonitor.runNow( false ); return true; } /** * Unloads a plugin. The {@link Plugin#destroyPlugin()} method will be called and then any resources will be * released. The name should be the canonical name of the plugin (based on the plugin directory name) and not the * human readable name as given by the plugin meta-data. * * This method only removes the plugin but does not delete the plugin JAR file. Therefore, if the plugin JAR still * exists after this method is called, the plugin will be started again the next time the plugin monitor process * runs. This is useful for "restarting" plugins. To completely remove the plugin, use {@link #deletePlugin(String)} * instead. * * This method is called automatically when a plugin's JAR file is deleted. * * @param canonicalName the canonical name of the plugin to unload. */ void unloadPlugin( String canonicalName ) { Log.debug( "Unloading plugin '{}'...", canonicalName ); failureToLoadCount.remove( canonicalName ); Plugin plugin = pluginsLoaded.get( canonicalName ); if ( plugin != null ) { // Remove from dev mode if it exists. pluginDevelopment.remove( plugin ); // See if any child plugins are defined. if ( parentPluginMap.containsKey( plugin ) ) { String[] childPlugins = parentPluginMap.get( plugin ).toArray( new String[ parentPluginMap.get( plugin ).size() ] ); for ( String childPlugin : childPlugins ) { Log.debug( "Unloading child plugin: '{}'.", childPlugin ); childPluginMap.remove( pluginsLoaded.get( childPlugin ) ); unloadPlugin( childPlugin ); } parentPluginMap.remove( plugin ); } Path webXML = pluginDirectory.resolve( canonicalName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web.xml" ); if ( Files.exists( webXML ) ) { AdminConsole.removeModel( canonicalName ); PluginServlet.unregisterServlets( webXML.toFile() ); } Path customWebXML = pluginDirectory.resolve( canonicalName ).resolve( "web" ).resolve( "WEB-INF" ).resolve( "web-custom.xml" ); if ( Files.exists( customWebXML ) ) { PluginServlet.unregisterServlets( customWebXML.toFile() ); } // Wrap destroying the plugin in a try/catch block. Otherwise, an exception raised // in the destroy plugin process will disrupt the whole unloading process. It's still // possible that classloader destruction won't work in the case that destroying the plugin // fails. In that case, Openfire may need to be restarted to fully cleanup the plugin // resources. try { plugin.destroyPlugin(); Log.debug( "Destroyed plugin '{}'.", canonicalName ); } catch ( Exception e ) { Log.error( "An exception occurred while unloading plugin '{}':", canonicalName, e ); } } // Remove references to the plugin so it can be unloaded from memory // If plugin still fails to be removed then we will add references back // Anyway, for a few seconds admins may not see the plugin in the admin console // and in a subsequent refresh it will appear if failed to be removed pluginsLoaded.remove( canonicalName ); Path pluginFile = pluginDirs.remove( canonicalName ); PluginClassLoader pluginLoader = classloaders.remove( plugin ); PluginMetadata metadata = pluginMetadata.remove( canonicalName ); // try to close the cached jar files from the plugin class loader if ( pluginLoader != null ) { pluginLoader.unloadJarFiles(); } else { Log.warn( "No plugin loader found for '{}'.", canonicalName ); } // Try to remove the folder where the plugin was exploded. If this works then // the plugin was successfully removed. Otherwise, some objects created by the // plugin are still in memory. Path dir = pluginDirectory.resolve( canonicalName ); // Give the plugin 2 seconds to unload. try { Thread.sleep( 2000 ); // Ask the system to clean up references. System.gc(); int count = 0; while ( !deleteDir( dir ) && count++ < 5 ) { Log.warn( "Error unloading plugin '{}'. Will attempt again momentarily.", canonicalName ); Thread.sleep( 8000 ); // Ask the system to clean up references. System.gc(); } } catch ( InterruptedException e ) { Log.debug( "Stopped waiting for plugin '{}' to be fully unloaded.", canonicalName, e ); } if ( plugin != null && Files.notExists( dir ) ) { // Unregister plugin caches PluginCacheRegistry.getInstance().unregisterCaches( canonicalName ); // See if this is a child plugin. If it is, we should unload // the parent plugin as well. if ( childPluginMap.containsKey( plugin ) ) { String parentPluginName = childPluginMap.get( plugin ); Plugin parentPlugin = pluginsLoaded.get( parentPluginName ); List childrenPlugins = parentPluginMap.get( parentPlugin ); childrenPlugins.remove( canonicalName ); childPluginMap.remove( plugin ); // When the parent plugin implements PluginListener, its pluginDestroyed() method // isn't called if it dies first before its child. Athough the parent will die anyway, // it's proper if the parent "gets informed first" about the dying child when the // child is the one being killed first. if ( parentPlugin instanceof PluginListener ) { PluginListener listener; listener = (PluginListener) parentPlugin; listener.pluginDestroyed( canonicalName, plugin ); } unloadPlugin( parentPluginName ); } firePluginDestroyedEvent( canonicalName, plugin ); Log.info( "Successfully unloaded plugin '{}'.", canonicalName ); } else if ( plugin != null ) { Log.info( "Restore references since we failed to remove the plugin '{}'.", canonicalName ); pluginsLoaded.put( canonicalName, plugin ); pluginDirs.put( canonicalName, pluginFile ); classloaders.put( plugin, pluginLoader ); pluginMetadata.put( canonicalName, metadata ); } } /** * Loads a class from the classloader of a plugin. * * @param plugin the plugin. * @param className the name of the class to load. * @return the class. * @throws ClassNotFoundException if the class was not found. * @throws IllegalAccessException if not allowed to access the class. * @throws InstantiationException if the class could not be created. */ public Class loadClass( Plugin plugin, String className ) throws ClassNotFoundException, IllegalAccessException, InstantiationException { PluginClassLoader loader = classloaders.get( plugin ); return loader.loadClass( className ); } /** * Returns a plugin's dev environment if development mode is enabled for * the plugin. * * @param plugin the plugin. * @return the plugin dev environment, or null if development * mode is not enabled for the plugin. */ public PluginDevEnvironment getDevEnvironment( Plugin plugin ) { return pluginDevelopment.get( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getName(Plugin)}. */ @Deprecated public String getName( Plugin plugin ) { return PluginMetadataHelper.getName( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getDescription(Plugin)}. */ @Deprecated public String getDescription( Plugin plugin ) { return PluginMetadataHelper.getDescription( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getAuthor(Plugin)}. */ @Deprecated public String getAuthor( Plugin plugin ) { return PluginMetadataHelper.getAuthor( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getVersion(Plugin)}. */ @Deprecated public String getVersion( Plugin plugin ) { return PluginMetadataHelper.getVersion( plugin ).getVersionString(); } /** * @deprecated Moved to {@link PluginMetadataHelper#getMinServerVersion(Plugin)}. */ @Deprecated public String getMinServerVersion( Plugin plugin ) { return PluginMetadataHelper.getMinServerVersion( plugin ).getVersionString(); } /** * @deprecated Moved to {@link PluginMetadataHelper#getDatabaseKey(Plugin)}. */ @Deprecated public String getDatabaseKey( Plugin plugin ) { return PluginMetadataHelper.getDatabaseKey( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getDatabaseVersion(Plugin)}. */ @Deprecated public int getDatabaseVersion( Plugin plugin ) { return PluginMetadataHelper.getDatabaseVersion( plugin ); } /** * @deprecated Moved to {@link PluginMetadataHelper#getLicense(Plugin)}. */ @Deprecated public String getLicense( Plugin plugin ) { return PluginMetadataHelper.getLicense( plugin ); } /** * Returns the classloader of a plugin. * * @param plugin the plugin. * @return the classloader of the plugin. */ public PluginClassLoader getPluginClassloader( Plugin plugin ) { return classloaders.get( plugin ); } /** * Deletes a directory. * * @param dir the directory to delete. * @return true if the directory was deleted. */ static boolean deleteDir( Path dir ) { try { if ( Files.isDirectory( dir ) ) { Files.walkFileTree( dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) throws IOException { try { Files.deleteIfExists( file ); } catch ( IOException e ) { Log.debug( "Plugin removal: could not delete: {}", file ); throw e; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory( Path dir, IOException exc ) throws IOException { try { Files.deleteIfExists( dir ); } catch ( IOException e ) { Log.debug( "Plugin removal: could not delete: {}", dir ); throw e; } return FileVisitResult.CONTINUE; } } ); } return Files.notExists( dir ) || Files.deleteIfExists( dir ); } catch ( IOException e ) { return Files.notExists( dir ); } } /** * Registers a PluginListener, which will now start receiving events regarding plugin creation and destruction. * * When the listener was already registered, this method will have no effect. * * @param listener the listener to be notified (cannot be null). */ public void addPluginListener( PluginListener listener ) { pluginListeners.add( listener ); } /** * Deregisters a PluginListener, which will no longer receive events. * * When the listener was never added, this method will have no effect. * * @param listener the listener to be removed (cannot be null). */ public void removePluginListener( PluginListener listener ) { pluginListeners.remove( listener ); } /** * Registers a PluginManagerListener, which will now start receiving events regarding plugin management. * * @param listener the listener to be notified (cannot be null). */ public void addPluginManagerListener( PluginManagerListener listener ) { pluginManagerListeners.add( listener ); if ( isExecuted() ) { firePluginsMonitored(); } } /** * Deregisters a PluginManagerListener, which will no longer receive events. * * When the listener was never added, this method will have no effect. * * @param listener the listener to be notified (cannot be null). */ public void removePluginManagerListener( PluginManagerListener listener ) { pluginManagerListeners.remove( listener ); } /** * Notifies all registered PluginListener instances that a new plugin was created. * * @param name The name of the plugin * @param plugin the plugin. */ void firePluginCreatedEvent( String name, Plugin plugin ) { for ( final PluginListener listener : pluginListeners ) { try { listener.pluginCreated( name, plugin ); } catch ( Exception ex ) { Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'created' event for plugin '{}'!", name, ex ); } } } /** * Notifies all registered PluginListener instances that a plugin was destroyed. * * @param name The name of the plugin * @param plugin the plugin. */ void firePluginDestroyedEvent( String name, Plugin plugin ) { for ( final PluginListener listener : pluginListeners ) { try { listener.pluginDestroyed( name, plugin ); } catch ( Exception ex ) { Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'destroyed' event for plugin '{}'!", name, ex ); } } } /** * Notifies all registered PluginManagerListener instances that the service monitoring for plugin changes completed a * periodic check. */ void firePluginsMonitored() { // Set that at least one iteration was done. That means that "all available" plugins // have been loaded by now. if ( !XMPPServer.getInstance().isSetupMode() ) { executed = true; } for ( final PluginManagerListener listener : pluginManagerListeners ) { try { listener.pluginsMonitored(); } catch ( Exception ex ) { Log.warn( "An exception was thrown when one of the pluginManagerListeners was notified of a 'monitored' event!", ex ); } } } public boolean isMonitorTaskRunning() { return pluginMonitor.isTaskRunning(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy