ClassicPluginStrategy.java 19.0 KB
Newer Older
K
kohsuke 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 * 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.
 */
24 25
package hudson;

26
import hudson.Plugin.DummyImpl;
27
import hudson.PluginWrapper.Dependency;
28
import hudson.model.Hudson;
29
import hudson.util.IOException2;
30
import hudson.util.IOUtils;
31 32
import hudson.util.MaskingClassLoader;
import hudson.util.VersionNumber;
33

34
import java.io.Closeable;
35 36 37 38 39
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
K
kohsuke 已提交
40
import java.net.URLClassLoader;
41
import java.util.ArrayList;
42 43
import java.util.Arrays;
import java.util.Collection;
44 45 46 47
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
48
import java.util.jar.Attributes;
49
import java.util.jar.Manifest;
S
Stuart McCulloch 已提交
50
import java.util.logging.Level;
51 52
import java.util.logging.Logger;

53
import org.apache.tools.ant.AntClassLoader;
54 55 56 57 58 59
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 {
L
lacostej 已提交
60 61

    private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
62 63 64 65 66 67 68 69 70 71 72 73

    /**
     * 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;

L
lacostej 已提交
74 75 76 77 78 79 80
    public ClassicPluginStrategy(PluginManager pluginManager) {
        this.pluginManager = pluginManager;
    }

    public PluginWrapper createPluginWrapper(File archive) throws IOException {
        final Manifest manifest;
        URL baseResourceURL;
81

L
lacostej 已提交
82 83
        File expandDir = null;
        // if .hpi, this is the directory where war is expanded
84

K
kohsuke 已提交
85
        boolean isLinked = archive.getName().endsWith(".hpl");
L
lacostej 已提交
86 87
        if (isLinked) {
            // resolve the .hpl file to the location of the manifest file
88
            final String firstLine = IOUtils.readFirstLine(new FileInputStream(archive), "UTF-8");
L
lacostej 已提交
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
            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 {
K
kohsuke 已提交
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
            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();
            }
L
lacostej 已提交
124
        }
125

126 127
        final Attributes atts = manifest.getMainAttributes();

L
lacostej 已提交
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
        // 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);
143
            if (libs != null)
144
                paths.addAll(Arrays.asList(libs));
145

L
lacostej 已提交
146 147
            baseResourceURL = expandDir.toURI().toURL();
        }
148 149 150 151
        File disableFile = new File(archive.getPath() + ".disabled");
        if (disableFile.exists()) {
            LOGGER.info("Plugin " + archive.getName() + " is disabled");
        }
152

L
lacostej 已提交
153 154 155 156 157 158 159 160 161 162 163 164 165 166
        // 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);
                }
            }
        }
167 168
        for (DetachedPlugin detached : DETACHED_LIST)
            detached.fix(atts,optionalDependencies);
169

170
        ClassLoader dependencyLoader = new DependencyClassLoader(getBaseClassLoader(atts), archive, Util.join(dependencies,optionalDependencies));
171

172
        return new PluginWrapper(pluginManager, archive, manifest, baseResourceURL,
173 174
                createClassLoader(paths, dependencyLoader, atts), disableFile, dependencies, optionalDependencies);
    }
175

176 177 178
    @Deprecated
    protected ClassLoader createClassLoader(List<File> paths, ClassLoader parent) throws IOException {
        return createClassLoader( paths, parent, null );
179 180 181 182 183
    }

    /**
     * Creates the classloader that can load all the specified jar files and delegate to the given parent.
     */
184 185 186 187 188 189 190 191 192 193 194
    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;
            }
        }
K
kohsuke 已提交
195 196
        if(useAntClassLoader) {
            // using AntClassLoader with Closeable so that we can predictably release jar files opened by URLClassLoader
197
            AntClassLoader2 classLoader = new AntClassLoader2(parent);
K
kohsuke 已提交
198
            classLoader.addPathFiles(paths);
199
            return classLoader;
K
kohsuke 已提交
200 201 202 203 204 205
        } 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());
206
            return new URLClassLoader(urls.toArray(new URL[urls.size()]),parent);
K
kohsuke 已提交
207
        }
L
lacostej 已提交
208
    }
209

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    /**
     * 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
226 227
            String yourName = atts.getValue("Short-Name");
            if (shortName.equals(yourName))   return;
228 229

            // some earlier versions of maven-hpi-plugin apparently puts "null" as a literal in Hudson-Version. watch out for them.
K
Kohsuke Kawaguchi 已提交
230 231 232 233
            String jenkinsVersion = atts.getValue("Jenkins-Version");
            if (jenkinsVersion==null)
                jenkinsVersion = atts.getValue("Hudson-Version");
            if (jenkinsVersion == null || jenkinsVersion.equals("null") || new VersionNumber(jenkinsVersion).compareTo(splitWhen) <= 0)
234 235 236 237 238 239
                optionalDependencies.add(new PluginWrapper.Dependency(shortName+':'+requireVersion));
        }
    }

    private static final List<DetachedPlugin> DETACHED_LIST = Arrays.asList(
        new DetachedPlugin("maven-plugin","1.296","1.296"),
K
kohsuke 已提交
240
        new DetachedPlugin("subversion","1.310","1.0"),
241
        new DetachedPlugin("cvs","1.340","0.1"),
242 243
        new DetachedPlugin("ant","1.431","1.0"),
        new DetachedPlugin("javadoc","1.431","1.0")
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    );

    /**
     * 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) {
L
lacostej 已提交
261
    }
262

263
    public <T> List<ExtensionComponent<T>> findComponents(Class<T> type, Hudson hudson) {
S
Stuart McCulloch 已提交
264 265 266 267 268 269 270 271 272 273

        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);
        }

        /**
274
         * See {@link ExtensionFinder#scout(Class, Hudson)} for the dead lock issue and what this does.
S
Stuart McCulloch 已提交
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
         */
        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;
    }

L
lacostej 已提交
295
    public void load(PluginWrapper wrapper) throws IOException {
296 297
        // override the context classloader. This no longer makes sense,
        // but it is left for the backward compatibility
298 299 300
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(wrapper.classLoader);
        try {
K
kohsuke 已提交
301 302 303
            String className = wrapper.getPluginClass();
            if(className==null) {
                // use the default dummy instance
304
                wrapper.setPlugin(new DummyImpl());
K
kohsuke 已提交
305 306
            } else {
                try {
307
                    Class<?> clazz = wrapper.classLoader.loadClass(className);
K
kohsuke 已提交
308 309 310 311 312
                    Object o = clazz.newInstance();
                    if(!(o instanceof Plugin)) {
                        throw new IOException(className+" doesn't extend from hudson.Plugin");
                    }
                    wrapper.setPlugin((Plugin) o);
313 314
                } catch (LinkageError e) {
                    throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(),e);
K
kohsuke 已提交
315 316 317 318 319 320
                } 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);
321 322 323 324 325
                }
            }

            // initialize plugin
            try {
L
lacostej 已提交
326
                Plugin plugin = wrapper.getPlugin();
327 328 329 330 331 332 333 334 335
                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);
        }
L
lacostej 已提交
336 337 338 339 340
    }

    public void startPlugin(PluginWrapper plugin) throws Exception {
        plugin.getPlugin().start();
    }
341 342 343 344 345 346 347 348 349

    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);
    }

350
    private static void parseClassPath(Manifest manifest, File archive, List<File> paths, String attributeName, String separator) throws IOException {
351 352 353 354 355 356 357 358 359 360 361
        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() ) {
362
                    paths.add(new File(dir,included));
363 364 365 366
                }
            } else {
                if(!file.exists())
                    throw new IOException("No such file: "+file);
367
                paths.add(file);
368 369 370 371 372 373 374 375 376 377 378 379 380
            }
        }
    }

    /**
     * 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");
381
        if(explodeTime.exists() && explodeTime.lastModified()==archive.lastModified())
382 383 384 385 386 387 388 389 390 391 392 393 394
            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) {
L
lacostej 已提交
395
            throw new IOException2("Failed to expand " + archive,x);
396 397
        }

398 399 400 401 402
        try {
            new FilePath(explodeTime).touch(archive.lastModified());
        } catch (InterruptedException e) {
            throw new AssertionError(e); // impossible
        }
403 404 405 406 407 408
    }

    /**
     * Used to load classes from dependency plugins.
     */
    final class DependencyClassLoader extends ClassLoader {
409 410 411 412 413
        /**
         * This classloader is created for this plugin. Useful during debugging.
         */
        private final File _for;

L
lacostej 已提交
414
        private List<Dependency> dependencies;
415

416
        public DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies) {
417
            super(parent);
418
            this._for = archive;
419 420 421
            this.dependencies = dependencies;
        }

422
        @Override
423 424 425 426 427 428 429 430 431 432 433 434 435 436
        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);
        }

437 438 439 440 441 442 443 444 445 446 447 448 449 450
        @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);
        }
451 452 453 454 455 456 457 458 459 460 461 462 463 464

        @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;
        }
465
    }
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

    /**
     * {@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();
        }
    }
K
kohsuke 已提交
484 485

    public static boolean useAntClassLoader = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader");
486
}