diff --git a/core/pom.xml b/core/pom.xml index d62566fd3c..045ba21ba8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -324,7 +324,7 @@ THE SOFTWARE. org.kohsuke.stapler stapler-jelly - 1.115 + 1.116 dom4j diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 3b88286c0a..2fa391e6b1 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -23,10 +23,23 @@ */ package hudson; -import hudson.model.*; +import hudson.model.AbstractModelObject; +import hudson.model.Failure; +import hudson.model.Hudson; +import hudson.model.UpdateCenter; import hudson.util.Service; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.LogFactory; +import org.kohsuke.stapler.HttpRedirect; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.WebApp; -import java.util.Enumeration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import java.io.File; @@ -34,27 +47,16 @@ import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.commons.logging.LogFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.io.FileUtils; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.WebApp; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.HttpRedirect; - /** * Manages {@link PluginWrapper}s. * @@ -110,7 +112,7 @@ public final class PluginManager extends AbstractModelObject { if(!rootDir.exists()) rootDir.mkdirs(); - loadBundledPlugins(); + Collection bundledPlugins = loadBundledPlugins(); File[] archives = rootDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { @@ -142,6 +144,7 @@ public final class PluginManager extends AbstractModelObject { for( File arc : archivesList ) { try { PluginWrapper p = strategy.createPluginWrapper(arc); + p.isBundled = bundledPlugins.contains(arc.getName()); plugins.add(p); if(p.isActive()) activePlugins.add(p); @@ -183,15 +186,19 @@ public final class PluginManager extends AbstractModelObject { /** * If the war file has any "/WEB-INF/plugins/*.hpi", extract them into the plugin directory. + * + * @return + * File names of the bundled plugins. Like {"ssh-slaves.hpi","subvesrion.hpi"} */ - private void loadBundledPlugins() { + private Collection loadBundledPlugins() { // this is used in tests, when we want to override the default bundled plugins with .hpl versions if (System.getProperty("hudson.bundled.plugins") != null) { - return; + return Collections.emptySet(); } - Set paths = context.getResourcePaths("/WEB-INF/plugins"); - if(paths==null) return; // crap - for( String path : (Set) paths) { + + Set names = new HashSet(); + + for( String path : Util.fixNull((Set)context.getResourcePaths("/WEB-INF/plugins"))) { String fileName = path.substring(path.lastIndexOf('/')+1); if(fileName.length()==0) { // see http://www.nabble.com/404-Not-Found-error-when-clicking-on-help-td24508544.html @@ -199,12 +206,17 @@ public final class PluginManager extends AbstractModelObject { continue; } try { + names.add(fileName); + URL url = context.getResource(path); long lastModified = url.openConnection().getLastModified(); File file = new File(rootDir, fileName); - // TODO: allow upgrade of bundled plugins; code below will overwrite the newly - // upgraded file with the bundled version since lastModified has changed. - if (!file.exists() || file.lastModified() != lastModified) { + File pinFile = new File(rootDir, fileName+".pinned"); + + // update file if: + // - no file exists today + // - bundled version and current version differs (by timestamp), and the file isn't pinned. + if (!file.exists() || (file.lastModified() != lastModified && !pinFile.exists())) { FileUtils.copyURLToFile(url, file); file.setLastModified(url.openConnection().getLastModified()); // lastModified is set for two reasons: @@ -216,6 +228,8 @@ public final class PluginManager extends AbstractModelObject { LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin "+fileName,e); } } + + return names; } /** diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 1a47d90b63..4a9c9a8119 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -38,7 +38,8 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import org.apache.commons.logging.LogFactory; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.HttpResponses; /** * Represents a Hudson plug-in and associated control information @@ -94,6 +95,15 @@ public final class PluginWrapper { */ 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 @@ -111,6 +121,11 @@ public final class PluginWrapper { private final List dependencies; private final List optionalDependencies; + /** + * Is this plugin bundled in hudson.war? + */ + /*package*/ boolean isBundled; + static final class Dependency { public final String shortName; public final String version; @@ -162,6 +177,7 @@ public final class PluginWrapper { 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; @@ -317,6 +333,10 @@ public final class PluginWrapper { return active; } + public boolean isBundled() { + return isBundled; + } + /** * If true, the plugin is going to be activated next time * Hudson runs. @@ -377,16 +397,28 @@ public final class PluginWrapper { // Action methods // // - public void doMakeEnabled(StaplerResponse rsp) throws IOException { + public HttpResponse doMakeEnabled() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); enable(); - rsp.setStatus(200); + return HttpResponses.ok(); } - public void doMakeDisabled(StaplerResponse rsp) throws IOException { + public HttpResponse doMakeDisabled() throws IOException { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); disable(); - rsp.setStatus(200); + 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(); } diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 0976dcdcd7..b9e4ec22b9 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -80,6 +80,8 @@ import java.util.ResourceBundle; import java.util.SimpleTimeZone; import java.util.StringTokenizer; import java.util.Arrays; +import java.util.Collections; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -852,6 +854,14 @@ public class Util { return fixEmpty(s.trim()); } + public static List fixNull(List l) { + return l!=null ? l : Collections.emptyList(); + } + + public static Set fixNull(Set l) { + return l!=null ? l : Collections.emptySet(); + } + /** * Cuts all the leading path portion and get just the file name. */