PluginWrapper.java 15.4 KB
Newer Older
K
kohsuke 已提交
1 2
package hudson;

K
kohsuke 已提交
3
import hudson.util.IOException2;
K
kohsuke 已提交
4
import hudson.model.Hudson;
K
kohsuke 已提交
5 6 7
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Expand;
K
kohsuke 已提交
8
import org.apache.tools.ant.types.FileSet;
9
import org.apache.commons.logging.LogFactory;
K
kohsuke 已提交
10 11 12
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

K
kohsuke 已提交
13
import java.io.BufferedReader;
K
kohsuke 已提交
14 15 16
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
K
kohsuke 已提交
17
import java.io.FileReader;
K
kohsuke 已提交
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Manifest;
import java.util.logging.Logger;

/**
 * Represents a Hudson plug-in and associated control information
 * for Hudson to control {@link Plugin}.
 *
 * <p>
 * A plug-in is packaged into a jar file whose extension is <tt>".hpi"</tt>,
 * A plugin needs to have a special manifest entry to identify what it is.
 *
 * <p>
 * At the runtime, a plugin has two distinct state axis.
 * <ol>
 *  <li>Enabled/Disabled. If enabled, Hudson is going to use it
 *      next time Hudson runs. Otherwise the next run will ignore it.
 *  <li>Activated/Deactivated. If activated, that means Hudson is using
 *      the plugin in this session. Otherwise it's not.
 * </ol>
 * <p>
 * For example, an activated but disabled plugin is still running but the next
 * time it won't.
 *
 * @author Kohsuke Kawaguchi
 */
public final class PluginWrapper {
    /**
     * Plugin manifest.
     * Contains description of the plugin.
     */
    private final Manifest manifest;

    /**
     * Loaded plugin instance.
     * Null if disabled.
     */
61
    private Plugin plugin;
K
kohsuke 已提交
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86

    /**
     * {@link ClassLoader} for loading classes from this plugin.
     * Null if disabled.
     */
    public final ClassLoader classLoader;

    /**
     * Base URL for loading static resources from this plugin.
     * Null if disabled. The static resources are mapped under
     * <tt>hudson/plugin/SHORTNAME/</tt>.
     */
    public final URL baseResourceURL;

    /**
     * Used to control enable/disable setting of the plugin.
     * If this file exists, plugin will be disabled.
     */
    private final File disableFile;

    /**
     * Short name of the plugin. The "abc" portion of "abc.hpl".
     */
    private final String shortName;

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    /**
     * True if this plugin is activated for this session.
     * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
     */
    private final boolean active;

    private final File archive;

    private final List<Dependency> dependencies = new ArrayList<Dependency>();

    private static final class Dependency {
        public final String shortName;
        public final String version;

        public Dependency(String s) {
            int idx = s.indexOf(':');
K
kohsuke 已提交
103 104
            if(idx==-1)
                throw new IllegalArgumentException("Illegal dependency specifier "+s);
105 106 107 108 109
            this.shortName = s.substring(0,idx);
            this.version = s.substring(idx+1);
        }
    }

K
kohsuke 已提交
110 111 112 113 114 115 116 117
    /**
     * @param archive
     *      A .hpi archive file jar file, or a .hpl linked plugin.
     *
     * @throws IOException
     *      if an installation of this plugin failed. The caller should
     *      proceed to work with other plugins.
     */
K
kohsuke 已提交
118
    public PluginWrapper(PluginManager owner, File archive) throws IOException {
K
kohsuke 已提交
119
        LOGGER.info("Loading plugin: "+archive);
120
        this.archive = archive;
K
kohsuke 已提交
121 122 123 124 125 126 127 128 129

        this.shortName = getShortName(archive);

        boolean isLinked = archive.getName().endsWith(".hpl");

        File expandDir = null;  // if .hpi, this is the directory where war is expanded

        if(isLinked) {
            // resolve the .hpl file to the location of the manifest file
130 131 132 133
            String firstLine = new BufferedReader(new FileReader(archive)).readLine();
            if(firstLine.startsWith("Manifest-Version:")) {
                // this is the manifest already
            } else {
K
kohsuke 已提交
134
                // indirection
135 136
                archive = resolve(archive, firstLine);
            }
K
kohsuke 已提交
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
            // 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 {
            expandDir = new File(archive.getParentFile(), shortName);
            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();
            }
        }

        // TODO: define a mechanism to hide classes
        // String export = manifest.getMainAttributes().getValue("Export");

        List<URL> paths = new ArrayList<URL>();
        if(isLinked) {
167 168
            parseClassPath(archive, paths, "Libraries", ",");
            parseClassPath(archive, paths, "Class-Path", " +"); // backward compatibility
K
kohsuke 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

            this.baseResourceURL = resolve(archive,
                manifest.getMainAttributes().getValue("Resource-Path")).toURL();
        } else {
            File classes = new File(expandDir,"WEB-INF/classes");
            if(classes.exists())
                paths.add(classes.toURL());
            File lib = new File(expandDir,"WEB-INF/lib");
            File[] libs = lib.listFiles(JAR_FILTER);
            if(libs!=null) {
                for (File jar : libs)
                    paths.add(jar.toURL());
            }

            this.baseResourceURL = expandDir.toURL();
        }
K
kohsuke 已提交
185
        ClassLoader dependencyLoader = new DependencyClassLoader(getClass().getClassLoader(),owner);
K
kohsuke 已提交
186
        this.classLoader = new URLClassLoader(paths.toArray(new URL[0]), dependencyLoader);
K
kohsuke 已提交
187 188 189 190

        disableFile = new File(archive.getPath()+".disabled");
        if(disableFile.exists()) {
            LOGGER.info("Plugin is disabled");
191 192 193
            this.active = false;
        } else {
            this.active = true;
K
kohsuke 已提交
194 195
        }

196
        // compute dependencies
K
kohsuke 已提交
197 198 199 200 201
        String v = manifest.getMainAttributes().getValue("Plugin-Dependencies");
        if(v!=null) {
            for(String s : v.split(","))
                dependencies.add(new Dependency(s));
        }
202 203 204 205 206 207 208 209 210 211
    }

    /**
     * Loads the plugin and starts it.
     *
     * <p>
     * This should be done after all the classloaders are constructed for
     * all the plugins, so that dependencies can be properly loaded by plugins.
     */
    /*package*/ void load(PluginManager owner) throws IOException {
K
kohsuke 已提交
212 213 214 215 216
        String className = manifest.getMainAttributes().getValue("Plugin-Class");
        if(className ==null) {
            throw new IOException("Plugin installation failed. No 'Plugin-Class' entry in the manifest of "+archive);
        }

K
kohsuke 已提交
217 218 219 220 221 222
        // make sure dependencies exist
        for (Dependency d : dependencies) {
            if(owner.getPlugin(d.shortName)==null)
                throw new IOException("Dependency "+d.shortName+" doesn't exist");
        }

223 224 225
        if(!active)
            return;

226 227 228 229
        // 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(classLoader);
K
kohsuke 已提交
230
        try {
231 232 233 234 235 236 237 238 239
            try {
                Class clazz = classLoader.loadClass(className);
                Object plugin = clazz.newInstance();
                if(!(plugin instanceof Plugin)) {
                    throw new IOException(className+" doesn't extend from hudson.Plugin");
                }
                this.plugin = (Plugin)plugin;
                this.plugin.wrapper = this;
            } catch (ClassNotFoundException e) {
K
kohsuke 已提交
240
                throw new IOException2("Unable to load " + className + " from " + archive,e);
241
            } catch (IllegalAccessException e) {
K
kohsuke 已提交
242
                throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
243
            } catch (InstantiationException e) {
K
kohsuke 已提交
244
                throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
K
kohsuke 已提交
245 246
            }

247 248 249 250 251 252
            // initialize plugin
            try {
                plugin.setServletContext(owner.context);
                plugin.start();
            } catch(Throwable t) {
                // gracefully handle any error in plugin.
K
kohsuke 已提交
253
                throw new IOException2("Failed to initialize",t);
254 255 256
            }
        } finally {
            Thread.currentThread().setContextClassLoader(old);
K
kohsuke 已提交
257 258 259
        }
    }

260 261
    private void parseClassPath(File archive, List<URL> paths, String attributeName, String separator) throws IOException {
        String classPath = manifest.getMainAttributes().getValue(attributeName);
262
        if(classPath==null) return; // attribute not found
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        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).toURL());
                }
            } else {
                if(!file.exists())
                    throw new IOException("No such file: "+file);
                paths.add(file.toURL());
            }
        }
    }

K
kohsuke 已提交
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    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);
    }

    /**
     * Returns the URL of the index page jelly script.
     */
    public URL getIndexPage() {
        return classLoader.getResource("index.jelly");
    }

    /**
     * Returns the short name suitable for URL.
     */
    public String getShortName() {
        return shortName;
    }

304 305 306 307 308 309 310
    /**
     * Gets the instance of {@link Plugin} contributed by this plugin.
     */
    public Plugin getPlugin() {
        return plugin;
    }

311 312 313 314 315
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
316 317 318 319
    /**
     * Returns a one-line descriptive name of this plugin.
     */
    public String getLongName() {
320
        String name = manifest.getMainAttributes().getValue("Long-PluginName");
K
kohsuke 已提交
321 322 323 324
        if(name!=null)      return name;
        return shortName;
    }

325 326 327 328 329 330
    /**
     * Returns the version number of this plugin
     */
    public String getVersion() {
        String v = manifest.getMainAttributes().getValue("Plugin-Version");
        if(v!=null)      return v;
K
kohsuke 已提交
331 332 333 334 335

        // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
        v = manifest.getMainAttributes().getValue("Implementation-Version");
        if(v!=null)      return v;

336 337 338
        return "???";
    }

K
kohsuke 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    /**
     * Gets the "abc" portion from "abc.ext".
     */
    private static String getShortName(File archive) {
        String n = archive.getName();
        int idx = n.lastIndexOf('.');
        if(idx>=0)
            n = n.substring(0,idx);
        return n;
    }

    /**
     * Terminates the plugin.
     */
    void stop() {
        LOGGER.info("Stopping "+shortName);
        try {
            plugin.stop();
        } catch(Throwable t) {
            System.err.println("Failed to shut down "+shortName);
            System.err.println(t);
        }
361 362 363
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(classLoader);
K
kohsuke 已提交
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
    }

    /**
     * Enables this plugin next time Hudson runs.
     */
    public void enable() throws IOException {
        if(!disableFile.delete())
            throw new IOException("Failed to delete "+disableFile);
    }

    /**
     * Disables this plugin next time Hudson runs.
     */
    public void disable() throws IOException {
        // creates an empty file
        OutputStream os = new FileOutputStream(disableFile);
        os.close();
    }

    /**
     * Returns true if this plugin is enabled for this session.
     */
    public boolean isActive() {
387
        return active;
K
kohsuke 已提交
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    }

    /**
     * If true, the plugin is going to be activated next time
     * Hudson runs.
     */
    public boolean isEnabled() {
        return !disableFile.exists();
    }

    /**
     * Explodes the plugin into a directory, if necessary.
     */
    private 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

        LOGGER.info("Extracting "+archive);

K
kohsuke 已提交
412 413 414
        // delete the contents so that old files won't interfere with new files
        Util.deleteContentsRecursive(destDir);

K
kohsuke 已提交
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
        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;
        }

        Util.touch(explodeTime);
    }


//
//
// Action methods
//
//
    public void doMakeEnabled(StaplerRequest req, StaplerResponse rsp) throws IOException {
438
    Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
439 440 441 442
        enable();
        rsp.setStatus(200);
    }
    public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException {
443
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
        disable();
        rsp.setStatus(200);
    }


    private static final Logger LOGGER = Logger.getLogger(PluginWrapper.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");
        }
    };
K
kohsuke 已提交
459 460 461 462 463

    /**
     * Used to load classes from dependency plugins.
     */
    final class DependencyClassLoader extends ClassLoader {
K
kohsuke 已提交
464 465 466
        private final PluginManager manager;

        public DependencyClassLoader(ClassLoader parent, PluginManager manager) {
K
kohsuke 已提交
467
            super(parent);
K
kohsuke 已提交
468
            this.manager = manager;
K
kohsuke 已提交
469 470 471 472
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            for (Dependency dep : dependencies) {
K
kohsuke 已提交
473
                PluginWrapper p = manager.getPlugin(dep.shortName);
K
kohsuke 已提交
474 475
                if(p!=null)
                    try {
K
kohsuke 已提交
476
                        return p.classLoader.loadClass(name);
K
kohsuke 已提交
477 478 479 480 481 482 483 484 485 486
                    } catch (ClassNotFoundException _) {
                        // try next
                    }
            }

            throw new ClassNotFoundException(name);
        }

        // TODO: delegate resources? watch out for diamond dependencies
    }
K
kohsuke 已提交
487
}