Files
jenkins/core/src/main/java/hudson/ClassicPluginStrategy.java
T
kohsuke fd13eee6d5 Merged revisions 23666-23667,23670,23711,23782-23785,23788,23809-23810,23817,23859,23870,23879,23899-23904,23906-23907,23909,23913 via svnmerge from
https://www.dev.java.net/svn/hudson/branches/managed-startup

........
  r23666 | kohsuke | 2009-11-11 15:56:46 -0800 (Wed, 11 Nov 2009) | 1 line
  
  Reworked the initialization logic
........
  r23667 | kohsuke | 2009-11-11 16:01:56 -0800 (Wed, 11 Nov 2009) | 1 line
  
  fixed a failing test
........
  r23670 | kohsuke | 2009-11-11 16:42:32 -0800 (Wed, 11 Nov 2009) | 1 line
  
  use a non-snapshot
........
  r23711 | kohsuke | 2009-11-13 14:27:28 -0800 (Fri, 13 Nov 2009) | 1 line
  
  Session -> Reactor
........
  r23782 | kohsuke | 2009-11-17 10:35:14 -0800 (Tue, 17 Nov 2009) | 1 line
  
  Poorman's clone of JDK6 ServiceLoader.
........
  r23783 | kohsuke | 2009-11-17 10:56:25 -0800 (Tue, 17 Nov 2009) | 1 line
  
  Integrated the fatal support so that all errors are sent to the listener.
........
  r23784 | kohsuke | 2009-11-17 10:58:07 -0800 (Tue, 17 Nov 2009) | 1 line
  
  with the new reactor initialization, things are getting too verbose
........
  r23785 | kohsuke | 2009-11-17 11:18:45 -0800 (Tue, 17 Nov 2009) | 1 line
  
  failing to load jobs are non-fatal, too
........
  r23788 | kohsuke | 2009-11-17 13:18:59 -0800 (Tue, 17 Nov 2009) | 1 line
  
  logging the exception properly
........
  r23809 | kohsuke | 2009-11-17 18:45:47 -0800 (Tue, 17 Nov 2009) | 1 line
  
  releasing 1.1 as milestone
........
  r23810 | kohsuke | 2009-11-17 18:47:37 -0800 (Tue, 17 Nov 2009) | 2 lines
  
  - use a better idiom for building a sequence
  - use null display name to signify internal book-keeping tasks that's not worth displaying
........
  r23817 | kohsuke | 2009-11-18 07:54:17 -0800 (Wed, 18 Nov 2009) | 1 line
  
  removed accidental JDK6 dependency.
........
  r23859 | kohsuke | 2009-11-19 11:03:28 -0800 (Thu, 19 Nov 2009) | 1 line
  
  introducing the strategy pattern to hook into key decision making points during the start up
........
  r23870 | kohsuke | 2009-11-19 14:33:07 -0800 (Thu, 19 Nov 2009) | 1 line
  
  bug fix. These steps add additional tasks to the reactor, so they need to also prevent future milestones from getting attained.
........
  r23879 | kohsuke | 2009-11-19 16:54:59 -0800 (Thu, 19 Nov 2009) | 1 line
  
  moved the logic to the strategy.
........
  r23899 | kohsuke | 2009-11-20 09:32:13 -0800 (Fri, 20 Nov 2009) | 1 line
  
  handle duplicate plugins better
........
  r23900 | kohsuke | 2009-11-20 10:06:42 -0800 (Fri, 20 Nov 2009) | 1 line
  
  support pre-exploded plugin
........
  r23901 | kohsuke | 2009-11-20 10:49:04 -0800 (Fri, 20 Nov 2009) | 1 line
  
  making it a subtype
........
  r23902 | kohsuke | 2009-11-20 10:54:42 -0800 (Fri, 20 Nov 2009) | 1 line
  
  allow InitStrategy to skip some tasks
........
  r23903 | kohsuke | 2009-11-20 11:03:38 -0800 (Fri, 20 Nov 2009) | 1 line
  
  doc improvement.
........
  r23904 | kohsuke | 2009-11-20 11:03:49 -0800 (Fri, 20 Nov 2009) | 1 line
  
  no need to throw exceptions
........
  r23906 | kohsuke | 2009-11-20 11:17:34 -0800 (Fri, 20 Nov 2009) | 1 line
  
  debug switch to disable the automatic launch of slaves
........
  r23907 | kohsuke | 2009-11-20 11:25:15 -0800 (Fri, 20 Nov 2009) | 1 line
  
  added a magic object for cancelling all saves
........
  r23909 | kohsuke | 2009-11-20 11:29:53 -0800 (Fri, 20 Nov 2009) | 1 line
  
  moving initialization up
........
  r23913 | kohsuke | 2009-11-20 13:32:08 -0800 (Fri, 20 Nov 2009) | 1 line
  
  seeing heap space problem during tests
........


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@23992 71c3de6d-444a-0410-be80-ed276b4c234a
2009-11-23 19:33:33 +00:00

436 lines
16 KiB
Java

/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jean-Baptiste Quenot, Tom Huybrechts
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson;
import hudson.PluginWrapper.Dependency;
import hudson.util.IOException2;
import hudson.util.MaskingClassLoader;
import hudson.util.VersionNumber;
import hudson.Plugin.DummyImpl;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.Closeable;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.Collection;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.logging.Logger;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.AntClassLoader;
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 {
LOGGER.info("Preparing plugin: " + archive);
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
String firstLine = new BufferedReader(new FileReader(archive))
.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 {
in.close();
}
} 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 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);
}
}
}
for (DetachedPlugin detached : DETACHED_LIST)
detached.fix(atts,optionalDependencies);
ClassLoader dependencyLoader = new DependencyClassLoader(getBaseClassLoader(atts), Util.join(dependencies,optionalDependencies));
ClassLoader cl;
if(useAntClassLoader) {
// using AntClassLoader with Closeable so that we can predictably release jar files opened by URLClassLoader
AntClassLoader2 classLoader = new AntClassLoader2(dependencyLoader);
classLoader.addPathFiles(paths);
cl = 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());
cl = new URLClassLoader(urls.toArray(new URL[urls.size()]),dependencyLoader);
}
return new PluginWrapper(archive, manifest, baseResourceURL,
cl, disableFile, dependencies, optionalDependencies);
}
/**
* Information about plugins that were originally in the core.
*/
private static final class DetachedPlugin {
private final String shortName;
private final VersionNumber splitWhen;
private final String requireVersion;
private DetachedPlugin(String shortName, String splitWhen, String requireVersion) {
this.shortName = shortName;
this.splitWhen = new VersionNumber(splitWhen);
this.requireVersion = requireVersion;
}
private void fix(Attributes atts, List<PluginWrapper.Dependency> optionalDependencies) {
// don't fix the dependency for yourself, or else we'll have a cycle
String yourName = atts.getValue("Short-Name");
if (shortName.equals(yourName)) return;
// some earlier versions of maven-hpi-plugin apparently puts "null" as a literal in Hudson-Version. watch out for them.
String hudsonVersion = atts.getValue("Hudson-Version");
if (hudsonVersion == null || hudsonVersion.equals("null") || new VersionNumber(hudsonVersion).compareTo(splitWhen) <= 0)
optionalDependencies.add(new PluginWrapper.Dependency(shortName+':'+requireVersion));
}
}
private static final List<DetachedPlugin> DETACHED_LIST = Arrays.asList(
new DetachedPlugin("maven-plugin","1.296","1.296"),
new DetachedPlugin("subversion","1.310","1.0")
);
/**
* 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 void load(PluginWrapper wrapper) throws IOException {
loadPluginDependencies(wrapper.getDependencies(),
wrapper.getOptionalDependencies());
if (!wrapper.isActive())
return;
// 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) {
IOException ioe = new IOException("Failed to expand " + archive);
ioe.initCause(x);
throw ioe;
}
try {
new FilePath(explodeTime).touch(archive.lastModified());
} catch (InterruptedException e) {
throw new AssertionError(e); // impossible
}
}
/**
* Loads the dependencies to other plugins.
*
* @throws IOException
* thrown if one or several mandatory dependencies doesnt
* exists.
*/
private void loadPluginDependencies(List<Dependency> dependencies,
List<Dependency> optionalDependencies) throws IOException {
List<String> missingDependencies = new ArrayList<String>();
// make sure dependencies exist
for (Dependency d : dependencies) {
if (pluginManager.getPlugin(d.shortName) == null)
missingDependencies.add(d.toString());
}
if (!missingDependencies.isEmpty()) {
StringBuilder builder = new StringBuilder();
builder.append("Dependency ");
builder.append(Util.join(missingDependencies, ", "));
builder.append(" doesn't exist");
throw new IOException(builder.toString());
}
// add the optional dependencies that exists
for (Dependency d : optionalDependencies) {
if (pluginManager.getPlugin(d.shortName) != null)
dependencies.add(d);
}
}
/**
* Used to load classes from dependency plugins.
*/
final class DependencyClassLoader extends ClassLoader {
private List<Dependency> dependencies;
public DependencyClassLoader(ClassLoader parent, List<Dependency> dependencies) {
super(parent);
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);
}
// TODO: delegate resources? watch out for diamond dependencies
}
/**
* {@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");
}