diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 23f19f683fb28352547f774c47d9df15d1bad3cf..c742d5808ee92d371951843b5b0338b09d109f57 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -494,6 +494,7 @@ public abstract class PluginManager extends AbstractModelObject { } rsp.sendRedirect("../updateCenter/"); } + /** * Bare-minimum configuration mechanism to change the update center. diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index b196eff2dab8fff86d747927ae90de90d05041e6..a1090dea80218c03ab96b4f16ed14ff924cce7a7 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -37,7 +37,6 @@ import java.io.OutputStream; import java.io.Closeable; import java.net.URL; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import java.util.jar.Manifest; import java.util.logging.Logger; @@ -47,6 +46,9 @@ 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}. @@ -464,6 +466,37 @@ public class PluginWrapper implements Comparable { 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()) { + try { + JarFile 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; + } + } else { + return null; + } + } // // // Action methods diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index cd2d747abafba852dad159544928a2812d3531bc..e138a565ffcebe4cb98baeea192a8c14dbf982a5 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -139,7 +139,17 @@ public abstract class Lifecycle implements ExtensionPoint { // but let's be defensive if(dest==null) throw new IOException("hudson.war location is not known."); + // backing up the old hudson.war before it gets lost due to upgrading + // (newly downloaded hudson.war and 'backup' (hudson.war.tmp) are the same files + // unless we are trying to rewrite hudson.war by a backup itself + File bak = new File(dest.getPath() + ".bak"); + if (!by.equals(bak)) + FileUtils.copyFile(dest, bak); + FileUtils.copyFile(by, dest); + // we don't want to keep backup if we are downgrading + if (by.equals(bak)&&bak.exists()) + bak.delete(); } /** diff --git a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java index dfbd7ef68869092416afea276fb285284d201b1a..beaf1206c59df343778e6f7beea71b4e76f25e51 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java @@ -87,6 +87,17 @@ public class WindowsServiceLifecycle extends Lifecycle { */ @Override public void rewriteHudsonWar(File by) throws IOException { + File dest = getHudsonWar(); + // this should be impossible given the canRewriteHudsonWar method, + // but let's be defensive + if(dest==null) throw new IOException("hudson.war location is not known."); + + // backing up the old hudson.war before its lost due to upgrading + // unless we are trying to rewrite hudson.war by a backup itself + File bak = new File(dest.getPath() + ".bak"); + if (!by.equals(bak)) + FileUtils.copyFile(dest, bak); + File rootDir = Hudson.getInstance().getRootDir(); File copyFiles = new File(rootDir,"hudson.copies"); diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 58c69d7cda039551c09f19492b0101ca4d384adf..b97ef60589d5ecf67723fa033b6218cc3fea34b1 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -70,10 +70,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import org.acegisecurity.context.SecurityContextHolder; + /** * Controls update center capability. * @@ -282,6 +284,45 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { rsp.sendRedirect2("."); } + /** + * Returns true if backup of hudson.war exists on the hard drive + */ + public boolean isDowngradable() { + return new File(Lifecycle.get().getHudsonWar() + ".bak").exists(); + } + + /** + * Performs hudson downgrade. + */ + public void doDowngrade(StaplerResponse rsp) throws IOException, ServletException { + requirePOST(); + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + if(!isDowngradable()) { + sendError("Hudson downgrade is not possible, probably backup does not exist"); + return; + } + + HudsonDowngradeJob job = new HudsonDowngradeJob(getCoreSource(), Hudson.getAuthentication()); + LOGGER.info("Scheduling the core downgrade"); + addJob(job); + rsp.sendRedirect2("."); + } + + /** + * Returns String with version of backup .war file, + * if the file does not exists returns null + */ + public String getBackupVersion() + { + try { + JarFile backupWar = new JarFile(new File(Lifecycle.get().getHudsonWar().getParentFile(), "hudson.war.bak")); + return backupWar.getManifest().getMainAttributes().getValue("Hudson-Version"); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to read backup version ", e); + return null;} + + } + /*package*/ synchronized Future addJob(UpdateCenterJob job) { // the first job is always the connectivity check if (sourcesUsed.add(job.site)) @@ -892,6 +933,87 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + /** + * Represents the state of the downgrading activity of plugin. + */ + public final class PluginDowngradeJob extends DownloadJob { + /** + * What plugin are we trying to install? + */ + public final Plugin plugin; + + private final PluginManager pm = Hudson.getInstance().getPluginManager(); + + public PluginDowngradeJob(Plugin plugin, UpdateSite site, Authentication auth) { + super(site, auth); + this.plugin = plugin; + } + + protected URL getURL() throws MalformedURLException { + return new URL(plugin.url); + } + + protected File getDestination() { + File baseDir = pm.rootDir; + return new File(baseDir, plugin.name + ".hpi"); + } + + protected File getBackup() + { + File baseDir = pm.rootDir; + return new File(baseDir, plugin.name + ".bak"); + } + + public String getName() { + return plugin.getDisplayName(); + } + + @Override + public void run() { + try { + LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName()); + + _run(); + + LOGGER.info("Downgrade successful: "+getName()); + status = new Success(); + onSuccess(); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e); + status = new Failure(e); + } + } + + @Override + protected void _run() throws IOException { + File dst = getDestination(); + File backup = getBackup(); + + config.install(this, backup, dst); + } + + /** + * Called to overwrite + * current version with backup file + */ + @Override + protected void replace(File dst, File backup) throws IOException { + dst.delete(); // any failure up to here is no big deal + if(!backup.renameTo(dst)) { + throw new IOException("Failed to rename "+backup+" to "+dst); + } + } + + protected void onSuccess() { + pm.pluginUploaded = true; + } + + @Override + public String toString() { + return super.toString()+"[plugin="+plugin.title+"]"; + } + } + /** * Represents the state of the upgrade activity of Hudson core. */ @@ -922,6 +1044,56 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + public final class HudsonDowngradeJob extends DownloadJob { + public HudsonDowngradeJob(UpdateSite site, Authentication auth) { + super(site, auth); + } + + protected URL getURL() throws MalformedURLException { + return new URL(site.getData().core.url); + } + + protected File getDestination() { + return Lifecycle.get().getHudsonWar(); + } + + public String getName() { + return "hudson.war"; + } + protected void onSuccess() { + status = new Success(); + } + @Override + public void run() { + try { + LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName()); + + _run(); + + LOGGER.info("Downgrading successful: "+getName()); + status = new Success(); + onSuccess(); + } catch (Throwable e) { + LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e); + status = new Failure(e); + } + } + + @Override + protected void _run() throws IOException { + + File backup = new File(Lifecycle.get().getHudsonWar() + ".bak"); + File dst = getDestination(); + + config.install(this, backup, dst); + } + + @Override + protected void replace(File dst, File src) throws IOException { + Lifecycle.get().rewriteHudsonWar(src); + } + } + public static final class PluginEntry implements Comparable { public Plugin plugin; public String category; diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index dd3b5135c75b05574fa6ef1088f1c2f3b6ac04bb..97094819937cfe929886d9b1d4209204835f603d 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -35,6 +35,7 @@ import hudson.util.VersionNumber; import static hudson.util.TimeUnit2.DAYS; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.jvnet.hudson.crypto.CertificateUtil; @@ -67,6 +68,7 @@ import java.security.cert.TrustAnchor; import com.trilead.ssh2.crypto.Base64; import javax.servlet.ServletContext; +import javax.servlet.ServletException; /** @@ -601,6 +603,14 @@ public class UpdateSite { return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Hudson.getAuthentication())); } + /** + * Schedules the downgrade of this plugin. + */ + public Future deployBackup() { + Hudson.getInstance().checkPermission(Hudson.ADMINISTER); + UpdateCenter uc = Hudson.getInstance().getUpdateCenter(); + return uc.addJob(uc.new PluginDowngradeJob(this, UpdateSite.this, Hudson.getAuthentication())); + } /** * Making the installation web bound. */ @@ -608,6 +618,14 @@ public class UpdateSite { deploy(); rsp.sendRedirect2("../.."); } + + /** + * Performs the downgrade of the plugin. + */ + public void doDowngrade(StaplerResponse rsp) throws IOException { + deployBackup(); + rsp.sendRedirect("../updateCenter/"); + } } private static final long DAY = DAYS.toMillis(1); diff --git a/core/src/main/resources/hudson/PluginManager/installed.jelly b/core/src/main/resources/hudson/PluginManager/installed.jelly index 4564f188fc33dffa36d4c12c3d3d00ea6818914c..5e30cedcfb7ea06635484882312b83718b167d04 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.jelly +++ b/core/src/main/resources/hudson/PluginManager/installed.jelly @@ -42,6 +42,7 @@ THE SOFTWARE. ${%Enabled} ${%Name} ${%Version} + ${%Previously installed version} ${%Pinned} @@ -68,6 +69,13 @@ THE SOFTWARE. ${p.version} + + +
+ + +
+ diff --git a/core/src/main/resources/hudson/PluginManager/installed.properties b/core/src/main/resources/hudson/PluginManager/installed.properties index 0bb76aeaee2a29e716e0a2a6da63c1ce227b6031..b901fab6c5860fcaebef00a157b7fbaeabc4471a 100644 --- a/core/src/main/resources/hudson/PluginManager/installed.properties +++ b/core/src/main/resources/hudson/PluginManager/installed.properties @@ -20,4 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. wiki.url=http://wiki.hudson-ci.org/display/HUDSON/Pinned+Plugins - +downgradeTo=Downgrade to {0} \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Hudson/downgrade.jelly b/core/src/main/resources/hudson/model/Hudson/downgrade.jelly new file mode 100644 index 0000000000000000000000000000000000000000..6a76ae2f60887cfd08175f9528e3fe5f2061bc7f --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/downgrade.jelly @@ -0,0 +1,34 @@ + + + +
+ +
+
${%Restore the previous version of Hudson} + + +
+
+
diff --git a/core/src/main/resources/hudson/model/Hudson/downgrade.properties b/core/src/main/resources/hudson/model/Hudson/downgrade.properties new file mode 100644 index 0000000000000000000000000000000000000000..055767e4be9762855b8478441806f76bbe41e750 --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/downgrade.properties @@ -0,0 +1 @@ +buttonText=Downgrade to {0} diff --git a/core/src/main/resources/hudson/model/Hudson/manage.jelly b/core/src/main/resources/hudson/model/Hudson/manage.jelly index 45d6f1dc05833990d0c77cbba81662e2d3f28131..b8546a16f69ffb7797125a5ceec5433138d94162 100644 --- a/core/src/main/resources/hudson/model/Hudson/manage.jelly +++ b/core/src/main/resources/hudson/model/Hudson/manage.jelly @@ -63,6 +63,8 @@ THE SOFTWARE.
+ + ${%Configure global settings and paths.}