/*******************************************************************************
*
* Copyright (c) 2004-2010 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Stephen Connolly, Tom Huybrechts, Winston Prakash
*
*******************************************************************************/
package hudson;
import hudson.PluginWrapper.Dependency;
import hudson.init.InitStrategy;
import hudson.init.InitializerFinder;
import hudson.model.AbstractModelObject;
import hudson.model.Failure;
import hudson.model.Hudson;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.PersistedList;
import hudson.util.Service;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.LogFactory;
import org.jvnet.hudson.reactor.Executable;
import org.jvnet.hudson.reactor.Reactor;
import org.jvnet.hudson.reactor.TaskBuilder;
import org.jvnet.hudson.reactor.TaskGraphBuilder;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import static hudson.init.InitMilestone.PLUGINS_LISTED;
import static hudson.init.InitMilestone.PLUGINS_PREPARED;
import static hudson.init.InitMilestone.PLUGINS_STARTED;
/**
* Manages {@link PluginWrapper}s.
*
* @author Kohsuke Kawaguchi
*/
public abstract class PluginManager extends AbstractModelObject {
/**
* All discovered plugins.
*/
protected final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>();
/**
* All active plugins.
*/
protected final List<PluginWrapper> activePlugins = new CopyOnWriteArrayList<PluginWrapper>();
protected final List<FailedPlugin> failedPlugins = new ArrayList<FailedPlugin>();
/**
* Plug-in root directory.
*/
//TODO: review and check whether we can do it private
public final File rootDir;
/**
* @deprecated as of 1.355 {@link PluginManager} can now live longer than
* {@link Hudson} instance, so use
* {@code Hudson.getInstance().servletContext} instead.
*/
public final ServletContext context;
/**
* {@link ClassLoader} that can load all the publicly visible classes from
* plugins (and including the classloader that loads Hudson itself.)
*
*/
// implementation is minimal --- just enough to run XStream
// and load plugin-contributed classes.
//TODO: review and check whether we can do it private
public final ClassLoader uberClassLoader = new UberClassLoader();
/**
* Once plugin is uploaded, this flag becomes true. This is used to report a
* message that Hudson needs to be restarted for new plugins to take effect.
*/
//TODO: review and check whether we can do it private
public volatile boolean pluginUploaded = false;
/**
* The initialization of {@link PluginManager} splits into two parts; one is
* the part about listing them, extracting them, and preparing classloader
* for them. The 2nd part is about creating instances. Once the former
* completes this flags become true, as the 2nd part can be repeated for
* each Hudson instance.
*/
private boolean pluginListed = false;
/**
* Strategy for creating and initializing plugins
*/
private final PluginStrategy strategy;
public PluginManager(ServletContext context, File rootDir) {
this.context = context;
this.rootDir = rootDir;
if (!rootDir.exists()) {
rootDir.mkdirs();
}
strategy = createPluginStrategy();
}
public List<PluginWrapper> getActivePlugins() {
return activePlugins;
}
public File getRootDir() {
return rootDir;
}
public ServletContext getContext() {
return context;
}
public ClassLoader getUberClassLoader() {
return uberClassLoader;
}
/**
* Called immediately after the construction. This is a separate method so
* that code executed from here will see a valid value in
* {@link Hudson#pluginManager}.
*/
public TaskBuilder initTasks(final InitStrategy initStrategy) {
TaskBuilder builder;
if (!pluginListed) {
builder = new TaskGraphBuilder() {
List<File> archives;
Collection<String> bundledPlugins;
{
Handle loadBundledPlugins = add("Loading bundled plugins", new Executable() {
@Override
public void run(Reactor session) throws Exception {
bundledPlugins = loadBundledPlugins();
}
});
Handle listUpPlugins = requires(loadBundledPlugins).add("Listing up plugins", new Executable() {
@Override
public void run(Reactor session) throws Exception {
archives = initStrategy.listPluginArchives(PluginManager.this);
}
});
requires(listUpPlugins).attains(PLUGINS_LISTED).add("Preparing plugins", new Executable() {
@Override
public void run(Reactor session) throws Exception {
// once we've listed plugins, we can fill in the reactor with plugin-specific initialization tasks
TaskGraphBuilder g = new TaskGraphBuilder();
final Map<String, File> inspectedShortNames = new HashMap<String, File>();
for (final File arc : archives) {
g.followedBy().notFatal().attains(PLUGINS_LISTED).add("Inspecting plugin " + arc, new Executable() {
@Override
public void run(Reactor session1) throws Exception {
try {
PluginWrapper p = strategy.createPluginWrapper(arc);
if (isDuplicate(p)) {
return;
}
p.isBundled = bundledPlugins.contains(arc.getName());
plugins.add(p);
if (p.isActive()) {
activePlugins.add(p);
}
} catch (IOException e) {
failedPlugins.add(new FailedPlugin(arc.getName(), e));
throw e;
}
}
/**
* Inspects duplication. this happens when
* you run hpi:run on a bundled plugin, as
* well as putting numbered hpi files, like
* "cobertura-1.0.hpi" and
* "cobertura-1.1.hpi"
*/
private boolean isDuplicate(PluginWrapper p) {
String shortName = p.getShortName();
if (inspectedShortNames.containsKey(shortName)) {
LOGGER.info("Ignoring " + arc + " because " + inspectedShortNames.get(shortName) + " is already loaded");
return true;
}
inspectedShortNames.put(shortName, arc);
return false;
}
});
}
g.requires(PLUGINS_PREPARED).add("Checking cyclic dependencies", new Executable() {
/**
* Makes sure there's no cycle in dependencies.
*/
@Override
public void run(Reactor reactor) throws Exception {
try {
new CyclicGraphDetector<PluginWrapper>() {
@Override
protected List<PluginWrapper> getEdges(PluginWrapper p) {
List<PluginWrapper> next = new ArrayList<PluginWrapper>();
addTo(p.getDependencies(), next);
addTo(p.getOptionalDependencies(), next);
return next;
}
private void addTo(List<Dependency> dependencies, List<PluginWrapper> r) {
for (Dependency d : dependencies) {
PluginWrapper p = getPlugin(d.shortName);
if (p != null) {
r.add(p);
}
}
}
}.run(getPlugins());
} catch (CycleDetectedException e) {
stop(); // disable all plugins since classloading from them can lead to StackOverflow
throw e; // let Hudson fail
}
Collections.sort(plugins);
}
});
session.addAll(g.discoverTasks(session));
pluginListed = true; // technically speaking this is still too early, as at this point tasks are merely scheduled, not necessarily executed.
}
});
}
};
} else {
builder = TaskBuilder.EMPTY_BUILDER;
}
final InitializerFinder initializerFinder = new InitializerFinder(uberClassLoader); // misc. stuff
// lists up initialization tasks about loading plugins.
return TaskBuilder.union(initializerFinder, // this scans @Initializer in the core once
builder, new TaskGraphBuilder() {
{
requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins", new Executable() {
/**
* Once the plugins are listed, schedule their
* initialization.
*/
public void run(Reactor session) throws Exception {
Hudson.getInstance().lookup.set(PluginInstanceStore.class, new PluginInstanceStore());
TaskGraphBuilder g = new TaskGraphBuilder();
// schedule execution of loading plugins
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
g.followedBy().notFatal().attains(PLUGINS_PREPARED).add("Loading plugin " + p.getShortName(), new Executable() {
@Override
public void run(Reactor session) throws Exception {
try {
LOGGER.info("Loading plugin - " + p.getShortName() + " " + p.getVersion());
p.resolvePluginDependencies();
strategy.load(p);
} catch (Exception e) {
LOGGER.info("Failed to load plugin - " + p.getShortName() + " because of error " + e.getLocalizedMessage());
failedPlugins.add(new FailedPlugin(p.getShortName(), e));
activePlugins.remove(p);
plugins.remove(p);
throw e;
}
}
});
}
// schedule execution of initializing plugins
for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
g.followedBy().notFatal().attains(PLUGINS_STARTED).add("Initializing plugin " + p.getShortName(), new Executable() {
@Override
public void run(Reactor session) throws Exception {
try {
p.getPlugin().postInitialize();
} catch (Exception e) {
failedPlugins.add(new FailedPlugin(p.getShortName(), e));
activePlugins.remove(p);
plugins.remove(p);
throw e;
}
}
});
}
g.followedBy().attains(PLUGINS_STARTED).add("Discovering plugin initialization tasks", new Executable() {
@Override
public void run(Reactor reactor) throws Exception {
// rescan to find plugin-contributed @Initializer
reactor.addAll(initializerFinder.discoverTasks(reactor));
}
});
// register them all
session.addAll(g.discoverTasks(session));
}
});
}
});
}
/**
* If the war file has any "/WEB-INF/plugins/*.hpi", extract them into the
* plugin directory.
*
* @return File names of the bundled plugins. Like
* {"ssh-slaves.hpi","subvesrion.hpi"}
* @throws Exception Any exception will be reported and halt the startup.
*/
protected abstract Collection<String> loadBundledPlugins() throws Exception;
/**
* Copies the bundled plugin from the given URL to the destination of the
* given file name (like 'abc.hpi'), with a reasonable up-to-date check. A
* convenience method to be used by the {@link #loadBundledPlugins()}.
*/
protected void copyBundledPlugin(URL src, String fileName) throws IOException {
File file = new File(rootDir, fileName);
// update file no file exists
if (!file.exists()) {
FileUtils.copyURLToFile(src, file);
}
}
public PluginWrapper whichPlugin(Class c) {
PluginWrapper oneAndOnly = null;
ClassLoader cl = c.getClassLoader();
for (PluginWrapper p : activePlugins) {
if (p.classLoader == cl) {
if (oneAndOnly != null) {
return null; // ambigious
}
oneAndOnly = p;
}
}
return oneAndOnly;
}
/**
* Creates a hudson.PluginStrategy, looking at the corresponding system
* property.
*/
private PluginStrategy createPluginStrategy() {
// Allow the user to specify a the plugin strategy to use with a system property
String strategyName = System.getProperty(PluginStrategy.class.getName());
if (strategyName == null) {
// Now let's check the default that's been set in the hudson-core.properties file
try {
Properties hudsonCoreProperties = new Properties();
hudsonCoreProperties.load(getClass().getResourceAsStream("/hudson/hudson-core.properties"));
strategyName = hudsonCoreProperties.getProperty("hudson.PluginStrategy");
} catch (IOException ex) {
LOGGER.info("Plugin Manager: " + ex.getLocalizedMessage());
}
}
if (strategyName != null) {
try {
Class<?> klazz = getClass().getClassLoader().loadClass(strategyName);
Object strategy = klazz.getConstructor(PluginManager.class).newInstance(this);
if (strategy instanceof PluginStrategy) {
LOGGER.info("Plugin strategy: " + strategyName);
return (PluginStrategy) strategy;
} else {
LOGGER.warning("Plugin strategy (" + strategyName
+ ") is not an instance of hudson.PluginStrategy");
}
} catch (ClassNotFoundException e) {
LOGGER.warning("Plugin strategy class not found: "
+ strategyName);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not instantiate plugin strategy: "
+ strategyName + ". Falling back to ClassicPluginStrategy", e);
}
LOGGER.info("Falling back to ClassicPluginStrategy");
}
// default and fallback
return new ClassicPluginStrategy(this);
}
public PluginStrategy getPluginStrategy() {
return strategy;
}
/**
* Returns true if any new plugin was added, which means a restart is
* required for the change to take effect.
*/
public boolean isPluginUploaded() {
return pluginUploaded;
}
public List<PluginWrapper> getPlugins() {
return plugins;
}
public List<FailedPlugin> getFailedPlugins() {
return failedPlugins;
}
public PluginWrapper getPlugin(String shortName) {
for (PluginWrapper p : plugins) {
if (p.getShortName().equals(shortName)) {
return p;
}
}
return null;
}
/**
* Get the plugin instance that implements a specific class, use to find
* your plugin singleton. Note: beware the classloader fun.
*
* @param pluginClazz The class that your plugin implements.
* @return The plugin singleton or <code>null</code> if for some reason the
* plugin is not loaded.
*/
public PluginWrapper getPlugin(Class<? extends Plugin> pluginClazz) {
for (PluginWrapper p : plugins) {
if (pluginClazz.isInstance(p.getPlugin())) {
return p;
}
}
return null;
}
/**
* Get the plugin instances that extend a specific class, use to find
* similar plugins. Note: beware the classloader fun.
*
* @param pluginSuperclass The class that your plugin is derived from.
* @return The list of plugins implementing the specified class.
*/
public List<PluginWrapper> getPlugins(Class<? extends Plugin> pluginSuperclass) {
List<PluginWrapper> result = new ArrayList<PluginWrapper>();
for (PluginWrapper p : plugins) {
if (pluginSuperclass.isInstance(p.getPlugin())) {
result.add(p);
}
}
return Collections.unmodifiableList(result);
}
public String getDisplayName() {
return Messages.PluginManager_DisplayName();
}
public String getSearchUrl() {
return "pluginManager";
}
/**
* Discover all the service provider implementations of the given class, via
* <tt>META-INF/services</tt>.
*/
public <T> Collection<Class<? extends T>> discover(Class<T> spi) {
Set<Class<? extends T>> result = new HashSet<Class<? extends T>>();
for (PluginWrapper p : activePlugins) {
Service.load(spi, p.classLoader, result);
}
return result;
}
/**
* Orderly terminates all the plugins.
*/
public void stop() {
for (PluginWrapper p : activePlugins) {
p.stop();
p.releaseClassLoader();
}
activePlugins.clear();
// Work around a bug in commons-logging.
// See http://www.szegedi.org/articles/memleak.html
LogFactory.release(uberClassLoader);
}
public HttpResponse doUpdateSources(StaplerRequest req) throws IOException {
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
if (req.hasParameter("remove")) {
UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
BulkChange bc = new BulkChange(uc);
try {
for (String id : req.getParameterValues("sources")) {
uc.getSites().remove(uc.getById(id));
}
} finally {
bc.commit();
}
} else if (req.hasParameter("add")) {
return new HttpRedirect("addSite");
}
return new HttpRedirect("./sites");
}
/**
* Performs the installation of the plugins.
*/
public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
Enumeration<String> en = req.getParameterNames();
while (en.hasMoreElements()) {
String n = en.nextElement();
if (n.startsWith("plugin.")) {
n = n.substring(7);
if (n.indexOf(".") > 0) {
String[] pluginInfo = n.split("\\.");
UpdateSite.Plugin p = Hudson.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]);
if (p == null) {
throw new Failure("No such plugin: " + n);
}
p.deploy();
}
}
}
rsp.sendRedirect("../updateCenter/");
}
/**
* Bare-minimum configuration mechanism to change the update center.
*/
public HttpResponse doSiteConfigure(@QueryParameter String site) throws IOException {
Hudson hudson = Hudson.getInstance();
hudson.checkPermission(Hudson.ADMINISTER);
UpdateCenter uc = hudson.getUpdateCenter();
PersistedList<UpdateSite> sites = uc.getSites();
for (UpdateSite s : sites) {
if (s.getId().equals("default")) {
sites.remove(s);
}
}
sites.add(new UpdateSite("default", site));
return HttpResponses.redirectToContextRoot();
}
public HttpResponse doProxyConfigure(
@QueryParameter("proxy.server") String server,
@QueryParameter("proxy.port") String port,
@QueryParameter("proxy.noProxyFor") String noProxyFor,
@QueryParameter("proxy.userName") String userName,
@QueryParameter("proxy.password") String password,
@QueryParameter("proxy.authNeeded") String authNeeded) throws IOException {
Hudson hudson = Hudson.getInstance();
hudson.checkPermission(Hudson.ADMINISTER);
server = Util.fixEmptyAndTrim(server);
if ((server != null) && !"".equals(server)) {
// If port is not specified assume it is port 80 (usual default for HTTP port)
int portNumber = 80;
if (!"".equals(Util.fixNull(port))) {
portNumber = Integer.parseInt(Util.fixNull(port));
}
boolean proxyAuthNeeded = "on".equals(Util.fixNull(authNeeded));
if (!proxyAuthNeeded) {
userName = "";
password = "";
}
hudson.proxy.configure(server, portNumber, Util.fixEmptyAndTrim(noProxyFor),
Util.fixEmptyAndTrim(userName), Util.fixEmptyAndTrim(password), "on".equals(Util.fixNull(authNeeded)));
hudson.proxy.save();
} else {
hudson.proxy.getXmlFile().delete();
hudson.proxy = null;
}
return new HttpRedirect("advanced");
}
/**
* Uploads a plugin.
*/
public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException {
try {
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
// Parse the request
FileItem fileItem = (FileItem) upload.parseRequest(req).get(0);
String fileName = Util.getFileName(fileItem.getName());
if ("".equals(fileName)) {
return new HttpRedirect("advanced");
}
if (!fileName.endsWith(".hpi")) {
throw new Failure(hudson.model.Messages.Hudson_NotAPlugin(fileName));
}
fileItem.write(new File(rootDir, fileName));
fileItem.delete();
pluginUploaded = true;
return new HttpRedirect(".");
} catch (IOException e) {
throw e;
} catch (Exception e) {// grrr. fileItem.write throws this
throw new ServletException(e);
}
}
/**
* {@link ClassLoader} that can see all plugins.
*/
public final class UberClassLoader extends ClassLoader {
/**
* Make generated types visible. Keyed by the generated class name.
*/
private ConcurrentMap<String, WeakReference<Class>> generatedClasses = new ConcurrentHashMap<String, WeakReference<Class>>();
public UberClassLoader() {
super(PluginManager.class.getClassLoader());
}
public void addNamedClass(String className, Class c) {
generatedClasses.put(className, new WeakReference<Class>(c));
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
WeakReference<Class> wc = generatedClasses.get(name);
if (wc != null) {
Class c = wc.get();
if (c != null) {
return c;
} else {
generatedClasses.remove(name, wc);
}
}
// first, use the context classloader so that plugins that are loading
// can use its own classloader first.
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl != null && cl != this) {
try {
return cl.loadClass(name);
} catch (ClassNotFoundException e) {
// not found. try next
}
}
for (PluginWrapper p : activePlugins) {
try {
return p.classLoader.loadClass(name);
} catch (ClassNotFoundException e) {
//not found. try next
}
}
// not found in any of the classloader. delegate.
throw new ClassNotFoundException(name);
}
@Override
protected URL findResource(String name) {
for (PluginWrapper p : activePlugins) {
URL url = p.classLoader.getResource(name);
if (url != null) {
return url;
}
}
return null;
}
@Override
protected Enumeration<URL> findResources(String name) throws IOException {
List<URL> resources = new ArrayList<URL>();
for (PluginWrapper p : activePlugins) {
resources.addAll(Collections.list(p.classLoader.getResources(name)));
}
return Collections.enumeration(resources);
}
@Override
public String toString() {
// only for debugging purpose
return "classLoader " + getClass().getName();
}
}
private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());
/**
* Remembers why a plugin failed to deploy.
*/
public static final class FailedPlugin {
public final String name;
public final Exception cause;
public FailedPlugin(String name, Exception cause) {
this.name = name;
this.cause = cause;
}
public String getExceptionString() {
return Functions.printThrowable(cause);
}
}
/**
* Stores {@link Plugin} instances.
*/
/*package*/ static final class PluginInstanceStore {
final Map<PluginWrapper, Plugin> store = new Hashtable<PluginWrapper, Plugin>();
}
}