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

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

K
kohsuke 已提交
11
import java.io.BufferedReader;
K
kohsuke 已提交
12 13 14
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
K
kohsuke 已提交
15
import java.io.FileReader;
K
kohsuke 已提交
16 17 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 61 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
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.
     */
    public final Plugin plugin;

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

    /**
     * @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.
     */
    public PluginWrapper(PluginManager owner, File archive) throws IOException {
        LOGGER.info("Loading plugin: "+archive);

        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
104 105 106 107 108 109 110
            String firstLine = new BufferedReader(new FileReader(archive)).readLine();
            if(firstLine.startsWith("Manifest-Version:")) {
                // this is the manifest already
            } else {
                // in direction
                archive = resolve(archive, firstLine);
            }
K
kohsuke 已提交
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
            // 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) {
141 142
            parseClassPath(archive, paths, "Libraries", ",");
            parseClassPath(archive, paths, "Class-Path", " +"); // backward compatibility
K
kohsuke 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172

            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();
        }
        this.classLoader = new URLClassLoader(paths.toArray(new URL[0]), getClass().getClassLoader());

        disableFile = new File(archive.getPath()+".disabled");
        if(disableFile.exists()) {
            LOGGER.info("Plugin is disabled");
            this.plugin = null;
            return;
        }

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

173 174 175 176
        // 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 已提交
177
        try {
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
            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) {
                IOException ioe = new IOException("Unable to load " + className + " from " + archive);
                ioe.initCause(e);
                throw ioe;
            } catch (IllegalAccessException e) {
                IOException ioe = new IOException("Unable to create instance of " + className + " from " + archive);
                ioe.initCause(e);
                throw ioe;
            } catch (InstantiationException e) {
                IOException ioe = new IOException("Unable to create instance of " + className + " from " + archive);
                ioe.initCause(e);
                throw ioe;
K
kohsuke 已提交
198 199
            }

200 201 202 203 204 205 206 207 208 209 210 211
            // initialize plugin
            try {
                plugin.setServletContext(owner.context);
                plugin.start();
            } catch(Throwable t) {
                // gracefully handle any error in plugin.
                IOException ioe = new IOException("Failed to initialize");
                ioe.initCause(t);
                throw ioe;
            }
        } finally {
            Thread.currentThread().setContextClassLoader(old);
K
kohsuke 已提交
212 213 214
        }
    }

215 216
    private void parseClassPath(File archive, List<URL> paths, String attributeName, String separator) throws IOException {
        String classPath = manifest.getMainAttributes().getValue(attributeName);
217
        if(classPath==null) return; // attribute not found
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
        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 已提交
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
    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;
    }

259 260 261 262 263
    @Override
    public String toString() {
        return "Plugin:" + getShortName();
    }

K
kohsuke 已提交
264 265 266 267
    /**
     * Returns a one-line descriptive name of this plugin.
     */
    public String getLongName() {
268
        String name = manifest.getMainAttributes().getValue("Long-PluginName");
K
kohsuke 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
        if(name!=null)      return name;
        return shortName;
    }

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

    /**
     * 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() {
        return plugin!=null;
    }

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

K
kohsuke 已提交
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
        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 {
        enable();
        rsp.setStatus(200);
    }
    public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException {
        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");
        }
    };
}