ClassicPluginStrategy.java 16.2 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 26 27
package hudson;

import hudson.PluginWrapper.Dependency;
import hudson.util.IOException2;
28 29
import hudson.util.MaskingClassLoader;
import hudson.util.VersionNumber;
30
import hudson.Plugin.DummyImpl;
31 32 33 34 35 36 37

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
38
import java.io.Closeable;
39
import java.net.URL;
K
kohsuke 已提交
40
import java.net.URLClassLoader;
41 42
import java.util.ArrayList;
import java.util.List;
43 44
import java.util.Arrays;
import java.util.Collection;
45
import java.util.jar.Manifest;
46
import java.util.jar.Attributes;
47 48 49 50
import java.util.logging.Logger;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
51
import org.apache.tools.ant.AntClassLoader;
52 53 54 55
import org.apache.tools.ant.taskdefs.Expand;
import org.apache.tools.ant.types.FileSet;

public class ClassicPluginStrategy implements PluginStrategy {
L
lacostej 已提交
56 57

    private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName());
58 59 60 61 62 63 64 65 66 67 68 69

    /**
     * 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 已提交
70 71 72 73 74 75 76
    public ClassicPluginStrategy(PluginManager pluginManager) {
        this.pluginManager = pluginManager;
    }

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

L
lacostej 已提交
78 79
        File expandDir = null;
        // if .hpi, this is the directory where war is expanded
80

K
kohsuke 已提交
81
        boolean isLinked = archive.getName().endsWith(".hpl");
L
lacostej 已提交
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
        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 {
K
kohsuke 已提交
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
            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 已提交
121
        }
122

123 124
        final Attributes atts = manifest.getMainAttributes();

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

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

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

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

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

    /**
     * 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) throws IOException {
K
kohsuke 已提交
177 178
        if(useAntClassLoader) {
            // using AntClassLoader with Closeable so that we can predictably release jar files opened by URLClassLoader
179
            AntClassLoader2 classLoader = new AntClassLoader2(parent);
K
kohsuke 已提交
180
            classLoader.addPathFiles(paths);
181
            return classLoader;
K
kohsuke 已提交
182 183 184 185 186 187
        } 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());
188
            return new URLClassLoader(urls.toArray(new URL[urls.size()]),parent);
K
kohsuke 已提交
189
        }
L
lacostej 已提交
190
    }
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    /**
     * 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
208 209
            String yourName = atts.getValue("Short-Name");
            if (shortName.equals(yourName))   return;
210 211 212 213 214 215 216 217 218 219

            // 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"),
K
kohsuke 已提交
220 221
        new DetachedPlugin("subversion","1.310","1.0"),
        new DetachedPlugin("cvs","1.340","0.1")
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
    );

    /**
     * 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 已提交
239
    }
240

L
lacostej 已提交
241
    public void load(PluginWrapper wrapper) throws IOException {
242 243 244 245 246
        // 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 {
K
kohsuke 已提交
247 248 249
            String className = wrapper.getPluginClass();
            if(className==null) {
                // use the default dummy instance
250
                wrapper.setPlugin(new DummyImpl());
K
kohsuke 已提交
251 252 253 254 255 256 257 258
            } 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);
259 260
                } catch (LinkageError e) {
                    throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(),e);
K
kohsuke 已提交
261 262 263 264 265 266
                } 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);
267 268 269 270 271
                }
            }

            // initialize plugin
            try {
L
lacostej 已提交
272
                Plugin plugin = wrapper.getPlugin();
273 274 275 276 277 278 279 280 281
                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 已提交
282 283 284 285 286
    }

    public void startPlugin(PluginWrapper plugin) throws Exception {
        plugin.getPlugin().start();
    }
287 288 289 290 291 292 293 294 295

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

296
    private static void parseClassPath(Manifest manifest, File archive, List<File> paths, String attributeName, String separator) throws IOException {
297 298 299 300 301 302 303 304 305 306 307
        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() ) {
308
                    paths.add(new File(dir,included));
309 310 311 312
                }
            } else {
                if(!file.exists())
                    throw new IOException("No such file: "+file);
313
                paths.add(file);
314 315 316 317 318 319 320 321 322 323 324 325 326
            }
        }
    }

    /**
     * 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");
327
        if(explodeTime.exists() && explodeTime.lastModified()==archive.lastModified())
328 329 330 331 332 333 334 335 336 337 338 339 340
            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 已提交
341
            throw new IOException2("Failed to expand " + archive,x);
342 343
        }

344 345 346 347 348
        try {
            new FilePath(explodeTime).touch(archive.lastModified());
        } catch (InterruptedException e) {
            throw new AssertionError(e); // impossible
        }
349 350 351 352 353 354
    }

    /**
     * Used to load classes from dependency plugins.
     */
    final class DependencyClassLoader extends ClassLoader {
355 356 357 358 359
        /**
         * This classloader is created for this plugin. Useful during debugging.
         */
        private final File _for;

L
lacostej 已提交
360
        private List<Dependency> dependencies;
361

362
        public DependencyClassLoader(ClassLoader parent, File archive, List<Dependency> dependencies) {
363
            super(parent);
364
            this._for = archive;
365 366 367
            this.dependencies = dependencies;
        }

368
        @Override
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
        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
384 385 386 387 388 389 390 391 392 393 394 395 396 397

        @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;
        }
398
    }
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

    /**
     * {@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 已提交
417 418

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