/******************************************************************************* * * Copyright (c) 2004-2009 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, Jean-Baptiste Quenot, Tom Huybrechts * * *******************************************************************************/ package hudson; import hudson.Plugin.DummyImpl; import hudson.PluginWrapper.Dependency; import hudson.model.Hudson; import hudson.util.IOException2; import hudson.util.MaskingClassLoader; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.Expand; import org.apache.tools.ant.types.FileSet; public class ClassicPluginStrategy implements PluginStrategy { private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName()); /** * Filter for jar files. */ private static final FilenameFilter JAR_FILTER = new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }; private PluginManager pluginManager; public ClassicPluginStrategy(PluginManager pluginManager) { this.pluginManager = pluginManager; } public PluginWrapper createPluginWrapper(File archive) throws IOException { final Manifest manifest; URL baseResourceURL; File expandDir = null; // if .hpi, this is the directory where war is expanded boolean isLinked = archive.getName().endsWith(".hpl"); if (isLinked) { // resolve the .hpl file to the location of the manifest file BufferedReader br = new BufferedReader(new FileReader(archive)); String firstLine = br.readLine(); if (firstLine.startsWith("Manifest-Version:")) { // this is the manifest already } else { // indirection archive = resolve(archive, firstLine); } // then parse manifest FileInputStream in = new FileInputStream(archive); try { manifest = new Manifest(in); } catch (IOException e) { throw new IOException2("Failed to load " + archive, e); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(br); } } else { if (archive.isDirectory()) {// already expanded expandDir = archive; } else { expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive)); explode(archive, expandDir); } File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF"); if (!manifestFile.exists()) { throw new IOException( "Plugin installation failed. No manifest at " + manifestFile); } FileInputStream fin = new FileInputStream(manifestFile); try { manifest = new Manifest(fin); } finally { fin.close(); } } final Attributes atts = manifest.getMainAttributes(); // TODO: define a mechanism to hide classes // String export = manifest.getMainAttributes().getValue("Export"); List<File> paths = new ArrayList<File>(); if (isLinked) { parseClassPath(manifest, archive, paths, "Libraries", ","); parseClassPath(manifest, archive, paths, "Class-Path", " +"); // backward compatibility baseResourceURL = resolve(archive, atts.getValue("Resource-Path")).toURI().toURL(); } else { File classes = new File(expandDir, "WEB-INF/classes"); if (classes.exists()) { paths.add(classes); } File lib = new File(expandDir, "WEB-INF/lib"); File[] libs = lib.listFiles(JAR_FILTER); if (libs != null) { paths.addAll(Arrays.asList(libs)); } baseResourceURL = expandDir.toURI().toURL(); } File disableFile = new File(archive.getPath() + ".disabled"); if (disableFile.exists()) { LOGGER.info("Plugin " + archive.getName() + " is disabled"); } // compute dependencies List<PluginWrapper.Dependency> dependencies = new ArrayList<PluginWrapper.Dependency>(); List<PluginWrapper.Dependency> optionalDependencies = new ArrayList<PluginWrapper.Dependency>(); String v = atts.getValue("Plugin-Dependencies"); if (v != null) { for (String s : v.split(",")) { PluginWrapper.Dependency d = new PluginWrapper.Dependency(s); if (d.optional) { optionalDependencies.add(d); } else { dependencies.add(d); } } } ClassLoader dependencyLoader = new DependencyClassLoader(getBaseClassLoader(atts), archive, Util.join(dependencies, optionalDependencies)); return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL, createClassLoader(paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies); } @Deprecated protected ClassLoader createClassLoader(List<File> paths, ClassLoader parent) throws IOException { return createClassLoader(paths, parent, null); } /** * Creates the classloader that can load all the specified jar files and * delegate to the given parent. */ protected ClassLoader createClassLoader(List<File> paths, ClassLoader parent, Attributes atts) throws IOException { if (atts != null) { String usePluginFirstClassLoader = atts.getValue("PluginFirstClassLoader"); if (Boolean.valueOf(usePluginFirstClassLoader)) { PluginFirstClassLoader classLoader = new PluginFirstClassLoader(); classLoader.setParentFirst(false); classLoader.setParent(parent); classLoader.addPathFiles(paths); return classLoader; } } if (useAntClassLoader) { // using AntClassLoader with Closeable so that we can predictably release jar files opened by URLClassLoader AntClassLoader2 classLoader = new AntClassLoader2(parent); classLoader.addPathFiles(paths); return classLoader; } else { // Tom reported that AntClassLoader has a performance issue when Hudson keeps trying to load a class that doesn't exist, // so providing a legacy URLClassLoader support, too List<URL> urls = new ArrayList<URL>(); for (File path : paths) { urls.add(path.toURI().toURL()); } return new URLClassLoader(urls.toArray(new URL[urls.size()]), parent); } } /** * Computes the classloader that takes the class masking into account. * * <p> This mechanism allows plugins to have their own verions for libraries * that core bundles. */ private ClassLoader getBaseClassLoader(Attributes atts) { ClassLoader base = getClass().getClassLoader(); String masked = atts.getValue("Mask-Classes"); if (masked != null) { base = new MaskingClassLoader(base, masked.trim().split("[ \t\r\n]+")); } return base; } public void initializeComponents(PluginWrapper plugin) { } public <T> List<ExtensionComponent<T>> findComponents(Class<T> type, Hudson hudson) { List<ExtensionFinder> finders; if (type == ExtensionFinder.class) { // Avoid infinite recursion of using ExtensionFinders to find ExtensionFinders finders = Collections.<ExtensionFinder>singletonList(new ExtensionFinder.Sezpoz()); } else { finders = hudson.getExtensionList(ExtensionFinder.class); } /** * See {@link ExtensionFinder#scout(Class, Hudson)} for the dead lock * issue and what this does. */ if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Scout-loading ExtensionList: " + type, new Throwable()); } for (ExtensionFinder finder : finders) { finder.scout(type, hudson); } List<ExtensionComponent<T>> r = new ArrayList<ExtensionComponent<T>>(); for (ExtensionFinder finder : finders) { try { r.addAll(finder._find(type, hudson)); } catch (AbstractMethodError e) { // backward compatibility for (T t : finder.findExtensions(type, hudson)) { r.add(new ExtensionComponent<T>(t)); } } } return r; } public void load(PluginWrapper wrapper) throws IOException { // override the context classloader so that XStream activity in plugin.start() // will be able to resolve classes in this plugin ClassLoader old = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(wrapper.classLoader); try { String className = wrapper.getPluginClass(); if (className == null) { // use the default dummy instance wrapper.setPlugin(new DummyImpl()); } else { try { Class clazz = wrapper.classLoader.loadClass(className); Object o = clazz.newInstance(); if (!(o instanceof Plugin)) { throw new IOException(className + " doesn't extend from hudson.Plugin"); } wrapper.setPlugin((Plugin) o); } catch (LinkageError e) { throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(), e); } catch (ClassNotFoundException e) { throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(), e); } catch (IllegalAccessException e) { throw new IOException2("Unable to create instance of " + className + " from " + wrapper.getShortName(), e); } catch (InstantiationException e) { throw new IOException2("Unable to create instance of " + className + " from " + wrapper.getShortName(), e); } } // initialize plugin try { Plugin plugin = wrapper.getPlugin(); plugin.setServletContext(pluginManager.context); startPlugin(wrapper); } catch (Throwable t) { // gracefully handle any error in plugin. throw new IOException2("Failed to initialize", t); } } finally { Thread.currentThread().setContextClassLoader(old); } } public void startPlugin(PluginWrapper plugin) throws Exception { plugin.getPlugin().start(); } private static File resolve(File base, String relative) { File rel = new File(relative); if (rel.isAbsolute()) { return rel; } else { return new File(base.getParentFile(), relative); } } private static void parseClassPath(Manifest manifest, File archive, List<File> paths, String attributeName, String separator) throws IOException { String classPath = manifest.getMainAttributes().getValue(attributeName); if (classPath == null) { return; // attribute not found } for (String s : classPath.split(separator)) { File file = resolve(archive, s); if (file.getName().contains("*")) { // handle wildcard FileSet fs = new FileSet(); File dir = file.getParentFile(); fs.setDir(dir); fs.setIncludes(file.getName()); for (String included : fs.getDirectoryScanner(new Project()).getIncludedFiles()) { paths.add(new File(dir, included)); } } else { if (!file.exists()) { throw new IOException("No such file: " + file); } paths.add(file); } } } /** * Explodes the plugin into a directory, if necessary. */ private static void explode(File archive, File destDir) throws IOException { if (!destDir.exists()) { destDir.mkdirs(); } // timestamp check File explodeTime = new File(destDir, ".timestamp"); if (explodeTime.exists() && explodeTime.lastModified() == archive.lastModified()) { return; // no need to expand } // delete the contents so that old files won't interfere with new files Util.deleteContentsRecursive(destDir); try { Expand e = new Expand(); e.setProject(new Project()); e.setTaskType("unzip"); e.setSrc(archive); e.setDest(destDir); e.execute(); } catch (BuildException x) { throw new IOException2("Failed to expand " + archive, x); } try { new FilePath(explodeTime).touch(archive.lastModified()); } catch (InterruptedException e) { throw new AssertionError(e); // impossible } } /** * Used to load classes from dependency plugins. */ final class DependencyClassLoader extends ClassLoader { /** * This classloader is created for this plugin. Useful during debugging. */ private final File _for; private List<Dependency> dependencies; public DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies) { super(parent); this._for = archive; this.dependencies = dependencies; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); if (p != null) { try { return p.classLoader.loadClass(name); } catch (ClassNotFoundException _) { // try next } } } throw new ClassNotFoundException(name); } @Override protected Enumeration<URL> findResources(String name) throws IOException { HashSet<URL> result = new HashSet<URL>(); for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); if (p != null) { Enumeration<URL> urls = p.classLoader.getResources(name); while (urls != null && urls.hasMoreElements()) { result.add(urls.nextElement()); } } } return Collections.enumeration(result); } @Override protected URL findResource(String name) { for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); if (p != null) { URL url = p.classLoader.getResource(name); if (url != null) { return url; } } } return null; } } /** * {@link AntClassLoader} with a few methods exposed and {@link Closeable} * support. */ private static final class AntClassLoader2 extends AntClassLoader implements Closeable { private AntClassLoader2(ClassLoader parent) { super(parent, true); } public void addPathFiles(Collection<File> paths) throws IOException { for (File f : paths) { addPathFile(f); } } public void close() throws IOException { cleanup(); } } public static boolean useAntClassLoader = Boolean.getBoolean(ClassicPluginStrategy.class.getName() + ".useAntClassLoader"); }