PluginWrapper.java 17.1 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

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

    /**
83 84 85 86
     * Short name of the plugin. The artifact Id of the plugin.
     * This is also used in the URL within Hudson, so it needs
     * to remain stable even when the *.hpi file name is changed
     * (like Maven does.)
K
kohsuke 已提交
87 88 89
     */
    private final String shortName;

90 91 92 93 94 95 96 97 98
    /**
     * 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>();
99
    private final List<Dependency> optionalDependencies = new ArrayList<Dependency>();
100 101 102 103

    private static final class Dependency {
        public final String shortName;
        public final String version;
104
        public final boolean optional;
105 106 107

        public Dependency(String s) {
            int idx = s.indexOf(':');
K
kohsuke 已提交
108 109
            if(idx==-1)
                throw new IllegalArgumentException("Illegal dependency specifier "+s);
110 111
            this.shortName = s.substring(0,idx);
            this.version = s.substring(idx+1);
112 113 114 115 116 117 118 119 120 121
            
            boolean isOptional = false;
            String[] osgiProperties = s.split(";");
            for (int i = 1; i < osgiProperties.length; i++) {
                String osgiProperty = osgiProperties[i].trim();
                if (osgiProperty.equalsIgnoreCase("resolution:=optional")) {
                    isOptional = true;
                }
            }
            this.optional = isOptional;
122 123 124
        }
    }

K
kohsuke 已提交
125 126 127 128 129 130 131 132
    /**
     * @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 已提交
133
    public PluginWrapper(PluginManager owner, File archive) throws IOException {
K
kohsuke 已提交
134
        LOGGER.info("Loading plugin: "+archive);
135
        this.archive = archive;
K
kohsuke 已提交
136 137 138 139 140 141 142

        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
143 144 145 146
            String firstLine = new BufferedReader(new FileReader(archive)).readLine();
            if(firstLine.startsWith("Manifest-Version:")) {
                // this is the manifest already
            } else {
K
kohsuke 已提交
147
                // indirection
148 149
                archive = resolve(archive, firstLine);
            }
K
kohsuke 已提交
150 151 152 153 154 155 156 157 158 159
            // 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 {
160
            expandDir = new File(archive.getParentFile(), getBaseName(archive));
K
kohsuke 已提交
161 162 163 164 165 166 167 168 169 170 171 172 173 174
            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();
            }
        }

175 176
        this.shortName = computeShortName(manifest,archive);

K
kohsuke 已提交
177 178 179 180 181
        // TODO: define a mechanism to hide classes
        // String export = manifest.getMainAttributes().getValue("Export");

        List<URL> paths = new ArrayList<URL>();
        if(isLinked) {
182 183
            parseClassPath(archive, paths, "Libraries", ",");
            parseClassPath(archive, paths, "Class-Path", " +"); // backward compatibility
K
kohsuke 已提交
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199

            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 已提交
200
        ClassLoader dependencyLoader = new DependencyClassLoader(getClass().getClassLoader(),owner);
K
kohsuke 已提交
201
        this.classLoader = new URLClassLoader(paths.toArray(new URL[0]), dependencyLoader);
K
kohsuke 已提交
202 203 204 205

        disableFile = new File(archive.getPath()+".disabled");
        if(disableFile.exists()) {
            LOGGER.info("Plugin is disabled");
206 207 208
            this.active = false;
        } else {
            this.active = true;
K
kohsuke 已提交
209 210
        }

211
        // compute dependencies
K
kohsuke 已提交
212 213
        String v = manifest.getMainAttributes().getValue("Plugin-Dependencies");
        if(v!=null) {
214 215 216 217 218 219 220 221
            for(String s : v.split(",")) {
                Dependency d = new Dependency(s);
                if (d.optional) {
                    optionalDependencies.add(d);
                } else {
                    dependencies.add(d);
                }
            }
K
kohsuke 已提交
222
        }
223 224
    }

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    private String computeShortName(Manifest manifest, File archive) {
        // use the name captured in the manifest, as often plugins
        // depend on the specific short name in its URLs.
        String n = manifest.getMainAttributes().getValue("Short-Name");
        if(n!=null)     return n;

        // maven seems to put this automatically, so good fallback to check.
        n = manifest.getMainAttributes().getValue("Extension-Name");
        if(n!=null)     return n;

        // otherwise infer from the file name, since older plugins don't have
        // this entry.
        return getBaseName(archive);
    }

240 241 242 243 244 245 246 247
    /**
     * 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 已提交
248 249 250 251 252
        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 已提交
253 254 255 256 257
        // make sure dependencies exist
        for (Dependency d : dependencies) {
            if(owner.getPlugin(d.shortName)==null)
                throw new IOException("Dependency "+d.shortName+" doesn't exist");
        }
258 259 260 261 262 263
        
        // add the optional dependencies that exists
        for (Dependency d : optionalDependencies) {
            if(owner.getPlugin(d.shortName)!=null)
                dependencies.add(d);
        }
K
kohsuke 已提交
264

265 266 267
        if(!active)
            return;

268 269 270 271
        // 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 已提交
272
        try {
273 274 275 276 277 278 279 280 281
            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 已提交
282
                throw new IOException2("Unable to load " + className + " from " + archive,e);
283
            } catch (IllegalAccessException e) {
K
kohsuke 已提交
284
                throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
285
            } catch (InstantiationException e) {
K
kohsuke 已提交
286
                throw new IOException2("Unable to create instance of " + className + " from " + archive,e);
K
kohsuke 已提交
287 288
            }

289 290 291 292 293 294
            // initialize plugin
            try {
                plugin.setServletContext(owner.context);
                plugin.start();
            } catch(Throwable t) {
                // gracefully handle any error in plugin.
K
kohsuke 已提交
295
                throw new IOException2("Failed to initialize",t);
296 297 298
            }
        } finally {
            Thread.currentThread().setContextClassLoader(old);
K
kohsuke 已提交
299 300 301
        }
    }

302 303
    private void parseClassPath(File archive, List<URL> paths, String attributeName, String separator) throws IOException {
        String classPath = manifest.getMainAttributes().getValue(attributeName);
304
        if(classPath==null) return; // attribute not found
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
        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 已提交
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
    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;
    }

346 347 348 349 350 351 352
    /**
     * Gets the instance of {@link Plugin} contributed by this plugin.
     */
    public Plugin getPlugin() {
        return plugin;
    }

353 354 355 356 357
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
358 359 360 361
    /**
     * Returns a one-line descriptive name of this plugin.
     */
    public String getLongName() {
362
        String name = manifest.getMainAttributes().getValue("Long-PluginName");
K
kohsuke 已提交
363 364 365 366
        if(name!=null)      return name;
        return shortName;
    }

367 368 369 370 371 372
    /**
     * Returns the version number of this plugin
     */
    public String getVersion() {
        String v = manifest.getMainAttributes().getValue("Plugin-Version");
        if(v!=null)      return v;
K
kohsuke 已提交
373 374 375 376 377

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

378 379 380
        return "???";
    }

K
kohsuke 已提交
381 382 383
    /**
     * Gets the "abc" portion from "abc.ext".
     */
384
    private static String getBaseName(File archive) {
K
kohsuke 已提交
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
        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);
        }
403 404 405
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(classLoader);
K
kohsuke 已提交
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    }

    /**
     * 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() {
429
        return active;
K
kohsuke 已提交
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
    }

    /**
     * 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 已提交
454 455 456
        // delete the contents so that old files won't interfere with new files
        Util.deleteContentsRecursive(destDir);

K
kohsuke 已提交
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
        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 {
480
    Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
481 482 483 484
        enable();
        rsp.setStatus(200);
    }
    public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException {
485
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
K
kohsuke 已提交
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
        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 已提交
501 502 503 504 505

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

        public DependencyClassLoader(ClassLoader parent, PluginManager manager) {
K
kohsuke 已提交
509
            super(parent);
K
kohsuke 已提交
510
            this.manager = manager;
K
kohsuke 已提交
511 512 513 514
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            for (Dependency dep : dependencies) {
K
kohsuke 已提交
515
                PluginWrapper p = manager.getPlugin(dep.shortName);
K
kohsuke 已提交
516 517
                if(p!=null)
                    try {
K
kohsuke 已提交
518
                        return p.classLoader.loadClass(name);
K
kohsuke 已提交
519 520 521 522 523 524 525 526 527 528
                    } catch (ClassNotFoundException _) {
                        // try next
                    }
            }

            throw new ClassNotFoundException(name);
        }

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