/******************************************************************************* * * Copyright (c) 2004-2012 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, Winston Prakash, Seiji Sogabe * *******************************************************************************/ package hudson.model; import hudson.BulkChange; import hudson.Extension; import hudson.ExtensionPoint; import hudson.Functions; import hudson.PluginManager; import hudson.PluginWrapper; import hudson.ProxyConfiguration; import hudson.Util; import hudson.XmlFile; import static hudson.init.InitMilestone.PLUGINS_STARTED; import hudson.init.Initializer; import hudson.lifecycle.Lifecycle; import hudson.model.UpdateSite.Data; import hudson.model.UpdateSite.Plugin; import hudson.model.listeners.SaveableListener; import hudson.util.DaemonThreadFactory; import hudson.util.IOException2; import hudson.util.IOUtils; import hudson.util.PersistedList; import hudson.util.XStream2; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.Vector; import java.util.concurrent.ExecutorService; 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 javax.net.ssl.SSLHandshakeException; import javax.servlet.ServletException; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.output.NullOutputStream; import org.eclipse.hudson.security.HudsonSecurityManager; import org.kohsuke.stapler.StaplerResponse; import org.springframework.security.core.Authentication; /** * Controls update center capability. * * <p> The main job of this class is to keep track of the latest update center * metadata file, and perform installations. Much of the UI about choosing * plugins to install is done in {@link PluginManager}. <p> The update center * can be configured to contact alternate servers for updates and plugins, and * to use alternate strategies for downloading, installing and updating * components. See the Javadocs for {@link UpdateCenterConfiguration} for more * information. * * @author Kohsuke Kawaguchi * @since 1.220 */ public class UpdateCenter extends AbstractModelObject implements Saveable { /** * {@link ExecutorService} that performs installation. */ private final ExecutorService installerService = Executors.newSingleThreadExecutor( new DaemonThreadFactory(new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("Update center installer thread"); return t; } })); /** * List of created {@link UpdateCenterJob}s. Access needs to be * synchronized. */ private final Vector<UpdateCenterJob> jobs = new Vector<UpdateCenterJob>(); /** * {@link UpdateSite}s from which we've already installed a plugin at least * once. This is used to skip network tests. */ private final Set<UpdateSite> sourcesUsed = new HashSet<UpdateSite>(); /** * List of {@link UpdateSite}s to be used. */ private final PersistedList<UpdateSite> sites = new PersistedList<UpdateSite>(this); /** * Update center configuration data */ private UpdateCenterConfiguration config; public UpdateCenter() { configure(new UpdateCenterConfiguration()); } /** * Configures update center to get plugins/updates from alternate servers, * and optionally using alternate strategies for downloading, installing and * upgrading. * * @param config Configuration data * @see UpdateCenterConfiguration */ public void configure(UpdateCenterConfiguration config) { if (config != null) { this.config = config; } } /** * Returns the list of {@link UpdateCenterJob} representing scheduled * installation attempts. * * @return can be empty but never null. Oldest entries first. */ public List<UpdateCenterJob> getJobs() { synchronized (jobs) { return new ArrayList<UpdateCenterJob>(jobs); } } /** * Returns latest install/upgrade job for the given plugin. * * @return InstallationJob or null if not found */ public InstallationJob getJob(Plugin plugin) { List<UpdateCenterJob> jobList = getJobs(); Collections.reverse(jobList); for (UpdateCenterJob job : jobList) { if (job instanceof InstallationJob) { InstallationJob ij = (InstallationJob) job; if (ij.plugin.name.equals(plugin.name) && ij.plugin.sourceId.equals(plugin.sourceId)) { return ij; } } } return null; } /** * Returns latest Hudson upgrade job. * * @return HudsonUpgradeJob or null if not found */ public HudsonUpgradeJob getHudsonJob() { List<UpdateCenterJob> jobList = getJobs(); Collections.reverse(jobList); for (UpdateCenterJob job : jobList) { if (job instanceof HudsonUpgradeJob) { return (HudsonUpgradeJob) job; } } return null; } /** * Returns the list of {@link UpdateSite}s to be used. This is a live list, * whose change will be persisted automatically. * * @return can be empty but never null. */ public PersistedList<UpdateSite> getSites() { return sites; } public UpdateSite getSite(String id) { for (UpdateSite site : sites) { if (site.getId().equals(id)) { return site; } } return null; } /** * Gets the string representing how long ago the data was obtained. Will be * the newest of all {@link UpdateSite}s. */ public String getLastUpdatedString() { long newestTs = -1; for (UpdateSite s : sites) { if (s.getDataTimestamp() > newestTs) { newestTs = s.getDataTimestamp(); } } if (newestTs < 0) { return "N/A"; } return Util.getPastTimeString(System.currentTimeMillis() - newestTs); } /** * Gets {@link UpdateSite} by its ID. Used to bind them to URL. */ public UpdateSite getById(String id) { for (UpdateSite s : sites) { if (s.getId().equals(id)) { return s; } } return null; } /** * Gets the {@link UpdateSite} from which we receive updates for * <tt>hudson.war</tt>. * * @return null if no such update center is provided. */ public UpdateSite getCoreSource() { for (UpdateSite s : sites) { Data data = s.getData(); if (data != null && data.core != null) { return s; } } return null; } /** * Gets the default base URL. * * @deprecated TODO: revisit tool update mechanism, as that should be * de-centralized, too. In the mean time, please try not to use this method, * and instead ping us to get this part completed. */ public String getDefaultBaseUrl() { return config.getUpdateServer(); } /** * Gets the plugin with the given name from the first {@link UpdateSite} to * contain it. */ public Plugin getPlugin(String artifactId) { for (UpdateSite s : sites) { Plugin p = s.getPlugin(artifactId); if (p != null) { return p; } } return null; } /** * Schedules a Hudson upgrade. */ public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException { requirePOST(); Hudson.getInstance().checkPermission(Hudson.ADMINISTER); HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), HudsonSecurityManager.getAuthentication()); if (!Lifecycle.get().canRewriteHudsonWar()) { sendError("Hudson upgrade not supported in this running mode"); return; } LOGGER.info("Scheduling the core upgrade"); addJob(job); 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(), HudsonSecurityManager.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<UpdateCenterJob> addJob(UpdateCenterJob job) { // the first job is always the connectivity check if (sourcesUsed.add(job.site)) { new ConnectionCheckJob(job.site).submit(); } return job.submit(); } public String getDisplayName() { return "Update center"; } public String getSearchUrl() { return "updateCenter"; } /** * Saves the configuration info to the disk. */ public synchronized void save() { if (BulkChange.contains(this)) { return; } try { getConfigFile().write(sites); SaveableListener.fireOnChange(this, getConfigFile()); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(), e); } } /** * Loads the data from the disk into this object. */ public synchronized void load() throws IOException { UpdateSite defaultSite = new UpdateSite("default", config.getUpdateCenterUrl()); XmlFile file = getConfigFile(); if (file.exists()) { try { sites.replaceBy(((PersistedList) file.unmarshal(sites)).toList()); } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to load " + file, e); } for (UpdateSite site : sites) { // replace the legacy site with the new site if (site.isLegacyDefault()) { sites.remove(site); sites.add(defaultSite); break; } } } else { if (sites.isEmpty()) { // If there aren't already any UpdateSources, add the default one. // to maintain compatibility with existing UpdateCenterConfiguration, create the default one as specified by UpdateCenterConfiguration sites.add(defaultSite); } } } private XmlFile getConfigFile() { return new XmlFile(XSTREAM, new File(Hudson.getInstance().root, UpdateCenter.class.getName() + ".xml")); } public List<Plugin> getAvailables() { List<Plugin> plugins = new ArrayList<Plugin>(); for (UpdateSite s : sites) { plugins.addAll(s.getAvailables()); } return plugins; } /** * Returns a list of plugins that should be shown in the "available" tab, * grouped by category. A plugin with multiple categories will appear * multiple times in the list. */ public PluginEntry[] getCategorizedAvailables() { TreeSet<PluginEntry> entries = new TreeSet<PluginEntry>(); for (Plugin p : getAvailables()) { if (p.categories == null || p.categories.length == 0) { entries.add(new PluginEntry(p, getCategoryDisplayName(null))); } else { for (String c : p.categories) { entries.add(new PluginEntry(p, getCategoryDisplayName(c))); } } } return entries.toArray(new PluginEntry[entries.size()]); } private static String getCategoryDisplayName(String category) { if (category == null) { return Messages.UpdateCenter_PluginCategory_misc(); } try { return (String) Messages.class.getMethod( "UpdateCenter_PluginCategory_" + category.replace('-', '_')).invoke(null); } catch (Exception ex) { return Messages.UpdateCenter_PluginCategory_unrecognized(category); } } public List<Plugin> getUpdates() { List<Plugin> plugins = new ArrayList<Plugin>(); for (UpdateSite s : sites) { plugins.addAll(s.getUpdates()); } return plugins; } /** * {@link AdministrativeMonitor} that checks if there's Hudson update. */ @Extension public static final class CoreUpdateMonitor extends AdministrativeMonitor { public boolean isActivated() { Data data = getData(); return data != null && data.hasCoreUpdates(); } public Data getData() { UpdateSite cs = Hudson.getInstance().getUpdateCenter().getCoreSource(); if (cs != null) { return cs.getData(); } return null; } } /** * Strategy object for controlling the update center's behaviors. * * <p> Until 1.333, this extension point used to control the configuration * of where to get updates (hence the name of this class), but with the * introduction of multiple update center sites capability, that * functionality is achieved by simply installing another * {@link UpdateSite}. * * <p> See {@link UpdateSite} for how to manipulate them programmatically. * * @since 1.266 */ @SuppressWarnings({"UnusedDeclaration"}) public static class UpdateCenterConfiguration implements ExtensionPoint { private final String updateServer = System.getProperty("updateServer", "http://hudson-ci.org/update-center3.3.2/"); /** * Creates default update center configuration - uses settings for * global update center. */ public UpdateCenterConfiguration() { } /** * Check network connectivity by trying to establish a connection to the * host in connectionCheckUrl. * * @param job The connection checker that is invoking this strategy. * @param connectionCheckUrl A string containing the URL of a domain * that is assumed to be always available. * @throws IOException if a connection can't be established */ public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException { testConnection(new URL(connectionCheckUrl)); } /** * Check connection to update center server. * * @param job The connection checker that is invoking this strategy. * @param updateCenterUrl A sting containing the URL of the update * center host. * @throws IOException if a connection to the update center server can't * be established. */ public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException { testConnection(new URL(updateCenterUrl + "?uctest")); } /** * Validate the URL of the resource before downloading it. * * @param job The download job that is invoking this strategy. This job * is responsible for managing the status of the download and * installation. * @param src The location of the resource on the network * @throws IOException if the validation fails */ public void preValidate(DownloadJob job, URL src) throws IOException { } /** * Validate the resource after it has been downloaded, before it is * installed. The default implementation does nothing. * * @param job The download job that is invoking this strategy. This job * is responsible for managing the status of the download and * installation. * @param src The location of the downloaded resource. * @throws IOException if the validation fails. */ public void postValidate(DownloadJob job, File src) throws IOException { } /** * Download a plugin or core upgrade in preparation for installing it * into its final location. Implementations will normally download the * resource into a temporary location and hand off a reference to this * location to the install or upgrade strategy to move into the final * location. * * @param job The download job that is invoking this strategy. This job * is responsible for managing the status of the download and * installation. * @param src The URL to the resource to be downloaded. * @return A File object that describes the downloaded resource. * @throws IOException if there were problems downloading the resource. * @see DownloadJob */ public File download(DownloadJob job, URL src) throws IOException { URLConnection con = connect(job, src); int total = con.getContentLength(); byte[] buf = new byte[8192]; int len; File dst = job.getDestination(); File tmp = new File(dst.getPath() + ".tmp"); OutputStream out = null; CountingInputStream in = null; LOGGER.info("Downloading " + job.getName()); try { in = new CountingInputStream(con.getInputStream()); out = new FileOutputStream(tmp); while ((len = in.read(buf)) >= 0) { out.write(buf, 0, len); job.status = job.new Installing(total == -1 ? -1 : in.getCount() * 100 / total); } } catch (IOException e) { throw new IOException2("Failed to load " + src + " to " + tmp, e); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } if (total != -1 && total != tmp.length()) { // don't know exactly how this happens, but report like // http://www.ashlux.com/wordpress/2009/08/14/hudson-and-the-sonar-plugin-fail-maveninstallation-nosuchmethoderror/ // indicates that this kind of inconsistency can happen. So let's be defensive throw new IOException("Inconsistent file length: expected " + total + " but only got " + tmp.length()); } return tmp; } /** * Connects to the given URL for downloading the binary. Useful for * tweaking how the connection gets established. */ protected URLConnection connect(DownloadJob job, URL src) throws IOException { return ProxyConfiguration.open(src); } /** * Called after a plugin has been downloaded to move it into its final * location. The default implementation is a file rename. * * @param job The install job that is invoking this strategy. * @param src The temporary location of the plugin. * @param dst The final destination to install the plugin to. * @throws IOException if there are problems installing the resource. */ public void install(DownloadJob job, File src, File dst) throws IOException { job.replace(dst, src); } /** * Called after an upgrade has been downloaded to move it into its final * location. The default implementation is a file rename. * * @param job The upgrade job that is invoking this strategy. * @param src The temporary location of the upgrade. * @param dst The final destination to install the upgrade to. * @throws IOException if there are problems installing the resource. */ public void upgrade(DownloadJob job, File src, File dst) throws IOException { job.replace(dst, src); } /** * Returns an "always up" server for Internet connectivity testing. * * @deprecated as of 1.333 With the introduction of multiple update * center capability, this information is now a part of the * <tt>update-center.json</tt> file. See * <tt>http://hudson-ci.org/update-center.json</tt> as an example. */ public String getConnectionCheckUrl() { return "http://www.google.com"; } /** * Get the Update Server name * * @return Name of the update server set to this configuration */ String getUpdateServer() { return updateServer; } /** * Returns the URL of the server that hosts the update-center.json file. * * @deprecated as of 1.333 With the introduction of multiple update * center capability, this information is now moved to * {@link UpdateSite}. * @return Absolute URL that ends with '/'. */ public String getUpdateCenterUrl() { return updateServer + "update-center.json"; } /** * Returns the URL of the server that hosts plugins and core updates. * * @deprecated as of 1.333 <tt>update-center.json</tt> is now signed, so * we don't have to further make sure that we aren't downloading from * anywhere unsecure. */ public String getPluginRepositoryBaseUrl() { return "http://hudson-ci.org/"; } private void testConnection(URL url) throws IOException { try { Util.copyStreamAndClose(ProxyConfiguration.open(url).getInputStream(), new NullOutputStream()); } catch (SSLHandshakeException e) { if (e.getMessage().contains("PKIX path building failed")) // fix up this illegible error message from JDK { throw new IOException2("Failed to validate the SSL certificate of " + url, e); } } } } /** * Things that {@link UpdateCenter#installerService} executes. * * This object will have the <tt>row.jelly</tt> which renders the job on UI. */ public abstract class UpdateCenterJob implements Runnable { /** * Which {@link UpdateSite} does this belong to? */ //TODO: review and check whether we can do it private public final UpdateSite site; protected UpdateCenterJob(UpdateSite site) { this.site = site; } public UpdateSite getSite() { return site; } /** * @deprecated as of 1.326 Use {@link #submit()} instead. */ public void schedule() { submit(); } /** * Schedules this job for an execution * * @return {@link Future} to keeps track of the status of the execution. */ public Future<UpdateCenterJob> submit() { LOGGER.fine("Scheduling " + this + " to installerService"); jobs.add(this); return installerService.submit(this, this); } } public void shutdown() { List<Runnable> running = installerService.shutdownNow(); if (!running.isEmpty()) { LOGGER.warning("shutdown with "+running.size()+" jobs pending"); } } /** * Tests the internet connectivity. */ public final class ConnectionCheckJob extends UpdateCenterJob { private final Vector<String> statuses = new Vector<String>(); public ConnectionCheckJob(UpdateSite site) { super(site); } public void run() { LOGGER.fine("Doing a connectivity check"); try { String connectionCheckUrl = site.getConnectionCheckUrl(); if (connectionCheckUrl != null) { statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); try { config.checkConnection(this, connectionCheckUrl); } catch (IOException e) { if (e.getMessage().contains("Connection timed out")) { // Google can't be down, so this is probably a proxy issue statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl)); return; } } } statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet()); config.checkUpdateCenter(this, site.getUrl()); statuses.add(Messages.UpdateCenter_Status_Success()); } catch (UnknownHostException e) { statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage())); addStatus(e); } catch (IOException e) { statuses.add(Functions.printThrowable(e)); } } private void addStatus(UnknownHostException e) { statuses.add("<pre>" + Functions.xmlEscape(Functions.printThrowable(e)) + "</pre>"); } public String[] getStatuses() { synchronized (statuses) { return statuses.toArray(new String[statuses.size()]); } } } /** * Base class for a job that downloads a file from the Hudson project. */ public abstract class DownloadJob extends UpdateCenterJob { /** * Unique ID that identifies this job. */ //TODO: review and check whether we can do it private public final int id = iota.incrementAndGet(); /** * Immutable object representing the current state of this job. */ //TODO: review and check whether we can do it private public volatile InstallationStatus status = new Pending(); /** * Where to download the file from. */ protected abstract URL getURL() throws MalformedURLException; /** * Where to download the file to. */ protected abstract File getDestination(); public abstract String getName(); /** * Called when the whole thing went successfully. */ protected abstract void onSuccess(); private Authentication authentication; /** * Get the user that initiated this job */ public Authentication getUser() { return this.authentication; } public int getId() { return id; } public InstallationStatus getStatus() { return status; } protected DownloadJob(UpdateSite site, Authentication authentication) { super(site); this.authentication = authentication; } public void run() { try { LOGGER.info("Starting the installation of " + getName() + " on behalf of " + getUser().getName()); _run(); LOGGER.info("Installation successful: " + getName()); status = new Success(); onSuccess(); } catch (Throwable e) { LOGGER.log(Level.SEVERE, "Failed to install " + getName(), e); status = new Failure(e); } } protected void _run() throws IOException { URL src = getURL(); config.preValidate(this, src); File dst = getDestination(); File tmp = config.download(this, src); config.postValidate(this, tmp); config.install(this, tmp, dst); } /** * Called when the download is completed to overwrite the old file with * the new file. */ protected void replace(File dst, File src) throws IOException { File bak = Util.changeExtension(dst, ".bak"); bak.delete(); dst.renameTo(bak); dst.delete(); // any failure up to here is no big deal if (!src.renameTo(dst)) { throw new IOException("Failed to rename " + src + " to " + dst); } } /** * Indicates the status or the result of a plugin installation. <p> * Instances of this class is immutable. */ public abstract class InstallationStatus { //TODO: review and check whether we can do it private public final int id = iota.incrementAndGet(); public int getId() { return id; } public boolean isSuccess() { return false; } } /** * Indicates that the installation of a plugin failed. */ public class Failure extends InstallationStatus { //TODO: review and check whether we can do it private public final Throwable problem; public Failure(Throwable problem) { this.problem = problem; } public Throwable getProblem() { return problem; } public String getStackTrace() { return Functions.printThrowable(problem); } } /** * Indicates that the plugin was successfully installed. */ public class Success extends InstallationStatus { @Override public boolean isSuccess() { return true; } } /** * Indicates that the plugin is waiting for its turn for installation. */ public class Pending extends InstallationStatus { } /** * Installation of a plugin is in progress. */ public class Installing extends InstallationStatus { /** * % completed download, or -1 if the percentage is not known. */ //TODO: review and check whether we can do it private public final int percentage; public int getPercentage() { return percentage; } public Installing(int percentage) { this.percentage = percentage; } } } /** * Represents the state of the installation activity of one plugin. */ public final class InstallationJob extends DownloadJob { /** * What plugin are we trying to install? */ //TODO: review and check whether we can do it private public final Plugin plugin; private final PluginManager pm = Hudson.getInstance().getPluginManager(); public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) { super(site, auth); this.plugin = plugin; } public Plugin getPlugin() { return 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"); } public String getName() { return plugin.getDisplayName(); } @Override public void _run() throws IOException { super._run(); // if this is a bundled plugin, make sure it won't get overwritten PluginWrapper pw = plugin.getInstalled(); if (pw != null && pw.isBundled()) { try { HudsonSecurityManager.grantFullControl(); pw.doPin(); } finally { HudsonSecurityManager.resetFullControl(); } } } protected void onSuccess() { pm.pluginUploaded = true; } @Override public String toString() { return super.toString() + "[plugin=" + plugin.title + "]"; } } /** * Represents the state of the downgrading activity of plugin. */ public final class PluginDowngradeJob extends DownloadJob { /** * What plugin are we trying to install? */ //TODO: review and check whether we can do it private 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(); } public Plugin getPlugin() { return plugin; } @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. */ public final class HudsonUpgradeJob extends DownloadJob { public HudsonUpgradeJob(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 protected void replace(File dst, File src) throws IOException { Lifecycle.get().rewriteHudsonWar(src); } } 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<PluginEntry> { //TODO: review and check whether we can do it private public Plugin plugin; public String category; private PluginEntry(Plugin p, String c) { plugin = p; category = c; } public Plugin getPlugin() { return plugin; } public String getCategory() { return category; } public int compareTo(PluginEntry o) { int r = category.compareTo(o.category); if (r == 0) { r = plugin.name.compareToIgnoreCase(o.plugin.name); } return r; } } /** * Adds the update center data retriever to HTML. */ @Extension public static class PageDecoratorImpl extends PageDecorator { public PageDecoratorImpl() { super(PageDecoratorImpl.class); } } /** * Initializes the update center. * * This has to wait until after all plugins load, to let custom * UpdateCenterConfiguration take effect first. */ @Initializer(after = PLUGINS_STARTED) public static void init(Hudson h) throws IOException { h.getUpdateCenter().load(); } /** * Sequence number generator. */ private static final AtomicInteger iota = new AtomicInteger(); private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName()); /** * @deprecated as of 1.333 Use {@link UpdateSite#neverUpdate} */ public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName() + ".never"); public static final XStream2 XSTREAM = new XStream2(); static { XSTREAM.alias("site", UpdateSite.class); XSTREAM.alias("sites", PersistedList.class); } }