/******************************************************************************* * * Copyright (c) 2004-2010 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Yahoo! Inc., Erik Ramfelt, Tom Huybrechts * * *******************************************************************************/ package hudson; import hudson.PluginManager.PluginInstanceStore; import hudson.model.Hudson; import hudson.model.UpdateCenter; import hudson.model.UpdateSite; import hudson.util.VersionNumber; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Closeable; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.jar.Manifest; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import org.apache.commons.logging.LogFactory; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import java.util.Enumeration; import java.util.jar.JarFile; /** * 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 class PluginWrapper implements Comparable<PluginWrapper> { private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); /** * {@link PluginManager} to which this belongs to. */ //TODO: review and check whether we can do it private public final PluginManager parent; /** * Plugin manifest. Contains description of the plugin. */ private final Manifest manifest; /** * {@link ClassLoader} for loading classes from this plugin. Null if * disabled. */ //TODO: review and check whether we can do it private 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>. */ //TODO: review and check whether we can do it private 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; /** * Used to control the unpacking of the bundled plugin. If a pin file * exists, Hudson assumes that the user wants to pin down a particular * version of a plugin, and will not try to overwrite it. Otherwise, it'll * be overwritten by a bundled copy, to ensure consistency across * upgrade/downgrade. * * @since 1.325 */ private final File pinFile; /** * 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.) */ private final String shortName; /** * 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 List<Dependency> dependencies; private final List<Dependency> optionalDependencies; /** * Is this plugin bundled in hudson.war? */ /*package*/ boolean isBundled; public static final class Dependency { //TODO: review and check whether we can do it private public final String shortName; public final String version; public final boolean optional; public Dependency(String s) { int idx = s.indexOf(':'); if (idx == -1) { throw new IllegalArgumentException("Illegal dependency specifier " + s); } this.shortName = s.substring(0, idx); this.version = s.substring(idx + 1); 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; } public String getShortName() { return shortName; } public String getVersion() { return version; } public boolean isOptional() { return optional; } @Override public String toString() { return shortName + " (" + version + ")"; } } /** * @param archive A .hpi archive file jar file, or a .hpl linked plugin. * @param manifest The manifest for the plugin * @param baseResourceURL A URL pointing to the resources for this plugin * @param classLoader a classloader that loads classes from this plugin and * its dependencies * @param disableFile if this file exists on startup, the plugin will not be * activated * @param dependencies a list of mandatory dependencies * @param optionalDependencies a list of optional dependencies */ public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL, ClassLoader classLoader, File disableFile, List<Dependency> dependencies, List<Dependency> optionalDependencies) { this.parent = parent; this.manifest = manifest; this.shortName = computeShortName(manifest, archive); this.baseResourceURL = baseResourceURL; this.classLoader = classLoader; this.disableFile = disableFile; this.pinFile = new File(archive.getPath() + ".pinned"); this.active = !disableFile.exists(); this.dependencies = dependencies; this.optionalDependencies = optionalDependencies; } public PluginManager getParent() { return parent; } public ClassLoader getClassLoader() { return classLoader; } public URL getBaseResourceURL() { return baseResourceURL; } /** * Returns the URL of the index page jelly script. */ public URL getIndexPage() { // In the current impl dependencies are checked first, so the plugin itself // will add the last entry in the getResources result. URL idx = null; try { Enumeration<URL> en = classLoader.getResources("index.jelly"); while (en.hasMoreElements()) { idx = en.nextElement(); } } catch (IOException ignore) {/*ignore*/ } // In case plugin has dependencies but is missing its own index.jelly, // check that result has this plugin's artifactId in it: return idx != null && idx.toString().contains(shortName) ? idx : null; } 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); } /** * Gets the "abc" portion from "abc.ext". */ static String getBaseName(File archive) { String n = archive.getName(); int idx = n.lastIndexOf('.'); if (idx >= 0) { n = n.substring(0, idx); } return n; } public List<Dependency> getDependencies() { return dependencies; } public List<Dependency> getOptionalDependencies() { return optionalDependencies; } /** * Returns the short name suitable for URL. */ public String getShortName() { return shortName; } /** * Gets the instance of {@link Plugin} contributed by this plugin. */ public Plugin getPlugin() { return Hudson.lookup(PluginInstanceStore.class).store.get(this); } /** * Gets the URL that shows more information about this plugin. * * @return null if this information is unavailable. * @since 1.283 */ public String getUrl() { // first look for the manifest entry. This is new in maven-hpi-plugin 1.30 String url = manifest.getMainAttributes().getValue("Url"); if (url != null) { return url; } // fallback to update center metadata UpdateSite.Plugin ui = getInfo(); if (ui != null) { return ui.wiki; } return null; } @Override public String toString() { return "Plugin:" + getShortName(); } /** * Returns a one-line descriptive name of this plugin. */ public String getLongName() { String name = manifest.getMainAttributes().getValue("Long-Name"); if (name != null) { return name; } return shortName; } /** * Returns the version number of this plugin */ public String getVersion() { String v = manifest.getMainAttributes().getValue("Plugin-Version"); if (v != null) { return v; } // plugins generated before maven-hpi-plugin 1.3 should still have this attribute v = manifest.getMainAttributes().getValue("Implementation-Version"); if (v != null) { return v; } return "???"; } /** * Returns the version number of this plugin */ public VersionNumber getVersionNumber() { return new VersionNumber(getVersion()); } /** * Returns true if the version of this plugin is older than the given * version. */ public boolean isOlderThan(VersionNumber v) { try { return getVersionNumber().compareTo(v) < 0; } catch (IllegalArgumentException e) { // if we can't figure out our current version, it probably means it's very old, // since the version information is missing only from the very old plugins return true; } } /** * Terminates the plugin. */ public void stop() { LOGGER.info("Stopping " + shortName); try { getPlugin().stop(); } catch (Throwable t) { LOGGER.log(WARNING, "Failed to shut down " + shortName, t); } // Work around a bug in commons-logging. // See http://www.szegedi.org/articles/memleak.html LogFactory.release(classLoader); } public void releaseClassLoader() { if (classLoader instanceof Closeable) { try { ((Closeable) classLoader).close(); } catch (IOException e) { LOGGER.log(WARNING, "Failed to shut down classloader", e); } } } /** * 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 active; } public boolean isBundled() { return isBundled; } /** * If true, the plugin is going to be activated next time Hudson runs. */ public boolean isEnabled() { return !disableFile.exists(); } public Manifest getManifest() { return manifest; } public void setPlugin(Plugin plugin) { Hudson.lookup(PluginInstanceStore.class).store.put(this, plugin); plugin.wrapper = this; } public String getPluginClass() { return manifest.getMainAttributes().getValue("Plugin-Class"); } /** * Makes sure that all the dependencies exist, and then accept optional * dependencies as real dependencies. * * @throws IOException thrown if one or several mandatory dependencies * doesn't exists. */ /*package*/ void resolvePluginDependencies() throws IOException { List<String> missingDependencies = new ArrayList<String>(); // make sure dependencies exist for (Dependency d : dependencies) { if (parent.getPlugin(d.shortName) == null) { missingDependencies.add(d.toString()); } } if (!missingDependencies.isEmpty()) { throw new IOException("Dependency " + Util.join(missingDependencies, ", ") + " doesn't exist"); } // add the optional dependencies that exists for (Dependency d : optionalDependencies) { if (parent.getPlugin(d.shortName) != null) { dependencies.add(d); } } } /** * If the plugin has {@link #getUpdateInfo() an update}, returns the * {@link UpdateSite.Plugin} object. * * @return This method may return null — for example, the user may * have installed a plugin locally developed. */ public UpdateSite.Plugin getUpdateInfo() { UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); UpdateSite.Plugin p = uc.getPlugin(getShortName()); if (p != null && p.isNewerThan(getVersion())) { return p; } return null; } /** * returns the {@link UpdateSite.Plugin} object, or null. */ public UpdateSite.Plugin getInfo() { UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); return uc.getPlugin(getShortName()); } /** * Returns true if this plugin has update in the update center. * * <p> This method is conservative in the sense that if the version number * is incomprehensible, it always returns false. */ public boolean hasUpdate() { return getUpdateInfo() != null; } public boolean isPinned() { return pinFile.exists(); } /** * Sort by short name. */ @Override public int compareTo(PluginWrapper pw) { return shortName.compareToIgnoreCase(pw.shortName); } /** * returns true if backup of previous version of plugin exists */ public boolean isDowngradable() { return getBackupFile().exists(); } /** * Where is the backup file? */ public File getBackupFile() { return new File(Hudson.getInstance().getRootDir(), "plugins/" + getShortName() + ".bak"); } /** * returns the version of the backed up plugin, or null if there's no back * up. */ public String getBackupVersion() { if (getBackupFile().exists()) { JarFile backupPlugin = null; try { backupPlugin = new JarFile(getBackupFile()); return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version"); } catch (IOException e) { LOGGER.log(WARNING, "Failed to get backup version ", e); return null; } finally { if (backupPlugin != null) { try { backupPlugin.close(); } catch (IOException ex) { /* ignore */ } } } } else { return null; } } // // // Action methods // // public HttpResponse doMakeEnabled() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); enable(); return HttpResponses.ok(); } public HttpResponse doMakeDisabled() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); disable(); return HttpResponses.ok(); } public HttpResponse doPin() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); new FileOutputStream(pinFile).close(); return HttpResponses.ok(); } public HttpResponse doUnpin() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); pinFile.delete(); return HttpResponses.ok(); } }