All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hudson.model.UpdateCenter Maven / Gradle / Ivy

package hudson.model;

import hudson.Functions;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.Util;
import hudson.ProxyConfiguration;
import hudson.util.DaemonThreadFactory;
import hudson.util.TextFile;
import hudson.util.VersionNumber;
import static hudson.util.TimeUnit2.DAYS;
import net.sf.json.JSONObject;
import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.IOUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Controls update center capability.
 *
 * 

* 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}. * * @author Kohsuke Kawaguchi * @since 1.220 */ public class UpdateCenter implements ModelObject { /** * What's the time stamp of data file? */ private long dataTimestamp = -1; /** * When was the last time we asked a browser to check the data for us? * *

* There's normally some delay between when we send HTML that includes the check code, * until we get the data back, so this variable is used to avoid asking too many browseres * all at once. */ private volatile long lastAttempt = -1; /** * {@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 jobs = new Vector(); /** * Returns true if it's time for us to check for new version. */ public boolean isDue() { if(neverUpdate) return false; if(dataTimestamp==-1) dataTimestamp = getDataFile().file.lastModified(); long now = System.currentTimeMillis(); boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000; if(due) lastAttempt = now; return due; } /** * Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts. * * @return * can be empty but never null. Oldest entries first. */ public List getJobs() { synchronized (jobs) { return new ArrayList(jobs); } } /** * Gets the string representing how long ago the data was obtained. */ public String getLastUpdatedString() { if(dataTimestamp<0) return "N/A"; return Util.getPastTimeString(System.currentTimeMillis()-dataTimestamp); } /** * This is the endpoint that receives the update center data file from the browser. */ public void doPostBack(StaplerRequest req) throws IOException { dataTimestamp = System.currentTimeMillis(); String p = req.getParameter("json"); JSONObject o = JSONObject.fromObject(p); int v = o.getInt("updateCenterVersion"); if(v !=1) { LOGGER.warning("Unrecognized update center version: "+v); return; } LOGGER.info("Obtained the latest update center data file"); getDataFile().write(p); } /** * Loads the update center data, if any. * * @return null if no data is available. */ public Data getData() { TextFile df = getDataFile(); if(df.exists()) { try { return new Data(JSONObject.fromObject(df.read())); } catch (IOException e) { LOGGER.log(Level.SEVERE,"Failed to parse "+df,e); df.delete(); // if we keep this file, it will cause repeated failures return null; } } else { return null; } } /** * Returns a list of plugins that should be shown in the "available" tab. * These are "all plugins - installed plugins". */ public List getAvailables() { List r = new ArrayList(); Data data = getData(); if(data ==null) return Collections.emptyList(); for (Plugin p : data.plugins.values()) { if(p.getInstalled()==null) r.add(p); } return r; } /** * Gets the information about a specific plugin. * * @param artifactId * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}. * * @return * null if no such information is found. */ public Plugin getPlugin(String artifactId) { Data dt = getData(); if(dt==null) return null; return dt.plugins.get(artifactId); } /** * This is where we store the update center data. */ private TextFile getDataFile() { return new TextFile(new File(Hudson.getInstance().root,"update-center.json")); } /** * Returns the list of plugins that are updates to currently installed ones. * * @return * can be empty but never null. */ public List getUpdates() { Data data = getData(); if(data==null) return Collections.emptyList(); // fail to determine List r = new ArrayList(); for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { Plugin p = pw.getUpdateInfo(); if(p!=null) r.add(p); } return r; } /** * Does any of the plugin has updates? */ public boolean hasUpdates() { Data data = getData(); if(data==null) return false; for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) { if(pw.getUpdateInfo() !=null) return true; } return false; } public String getDisplayName() { return "Update center"; } /** * In-memory representation of the update center data. */ public final class Data { /** * The latest hudson.war. */ public final Entry core; /** * Plugins in the official repository, keyed by their artifact IDs. */ public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER); Data(JSONObject o) { core = new Entry(o.getJSONObject("core")); for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) { plugins.put(e.getKey(),new Plugin(e.getValue())); } } /** * Is there a new version of the core? */ public boolean hasCoreUpdates() { return core.isNewerThan(Hudson.VERSION); } } public static class Entry { /** * Artifact ID. */ public final String name; /** * The version. */ public final String version; /** * Download URL. */ public final String url; public Entry(JSONObject o) { this.name = o.getString("name"); this.version = o.getString("version"); this.url = o.getString("url"); } /** * Checks if the specified "current version" is older than the version of this entry. * * @param currentVersion * The string that represents the version number to be compared. * @return * true if the version listed in this entry is newer. * false otherwise, including the situation where the strings couldn't be parsed as version numbers. */ public boolean isNewerThan(String currentVersion) { try { return new VersionNumber(currentVersion).compareTo(new VersionNumber(version)) < 0; } catch (IllegalArgumentException e) { // couldn't parse as the version number. return false; } } } public final class Plugin extends Entry { /** * Optional URL to the Wiki page that discusses this plugin. */ public final String wiki; /** * Human readable title of the plugin, taken from Wiki page. * Can be null. * *

* beware of XSS vulnerability since this data comes from Wiki */ public final String title; /** * Optional excerpt string. */ public final String excerpt; @DataBoundConstructor public Plugin(JSONObject o) { super(o); this.wiki = get(o,"wiki"); this.title = get(o,"title"); this.excerpt = get(o,"excerpt"); } private String get(JSONObject o, String prop) { if(o.has(prop)) return o.getString(prop); else return null; } public String getDisplayName() { if(title!=null) return title; return name; } /** * If some version of this plugin is currently installed, return {@link PluginWrapper}. * Otherwise null. */ public PluginWrapper getInstalled() { PluginManager pm = Hudson.getInstance().getPluginManager(); return pm.getPlugin(name); } /** * Schedules the installation of this plugin. * *

* This is mainly intended to be called from the UI. The actual installation work happens * asynchronously in another thread. */ public void install() { Hudson.getInstance().checkPermission(Hudson.ADMINISTER); // the first job is always the connectivity check if(jobs.size()==0) new ConnectionCheckJob().schedule(); LOGGER.info("Scheduling the installation of "+getDisplayName()); UpdateCenter.InstallationJob job = new InstallationJob(this); job.schedule(); } /** * Making the installation web bound. */ public void doInstall(StaplerResponse rsp) throws IOException { install(); rsp.sendRedirect2("../.."); } } /** * Things that {@link UpdateCenter#installerService} executes. * * This object will have the row.jelly which renders the job on UI. */ public abstract class UpdateCenterJob implements Runnable { public void schedule() { jobs.add(this); installerService.submit(this); } } /** * Tests the internet connectivity. */ public final class ConnectionCheckJob extends UpdateCenterJob { private final Vector statuses= new Vector(); public void run() { try { statuses.add(Messages.UpdateCenter_Status_CheckingInternet()); testConnection(new URL("http://www.google.com/")); statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet()); testConnection(new URL("https://hudson.dev.java.net/?uctest")); 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("

"+ Functions.xmlEscape(Functions.printThrowable(e))+"
"); } public String[] getStatuses() { synchronized (statuses) { return statuses.toArray(new String[statuses.size()]); } } private void testConnection(URL url) throws IOException { InputStream in = ProxyConfiguration.open(url).getInputStream(); IOUtils.copy(in,new ByteArrayOutputStream()); in.close(); } } /** * Represents the state of the installation activity of one plugin. */ public final class InstallationJob extends UpdateCenterJob { /** * What plugin are we trying to install? */ public final Plugin plugin; /** * Unique ID that identifies this job. */ public final int id = iota.incrementAndGet(); /** * Immutable object representing the current state of this job. */ public volatile InstallationStatus status = new Pending(); public InstallationJob(Plugin plugin) { this.plugin = plugin; } public void run() { try { LOGGER.info("Starting the installation of "+plugin.name); // for security reasons, only install from hudson.dev.java.net for now, which is also conveniently // https to guarantee transport level security. if(!plugin.url.startsWith("https://hudson.dev.java.net/")) { throw new IOException("Installation from non-official repository at "+plugin.url+" is not support yet"); } // In the future if we are to open up update center to 3rd party, we need more elaborate scheme // like signing to ensure the safety of the bits. URLConnection con = ProxyConfiguration.open(new URL(plugin.url)); int total = con.getContentLength(); CountingInputStream in = new CountingInputStream(con.getInputStream()); byte[] buf = new byte[8192]; int len; PluginManager pm = Hudson.getInstance().getPluginManager(); File baseDir = pm.rootDir; File target = new File(baseDir, plugin.name + ".tmp"); OutputStream out = new FileOutputStream(target); LOGGER.info("Downloading "+plugin.name); while((len=in.read(buf))>=0) { out.write(buf,0,len); status = new Installing(total==-1 ? -1 : in.getCount()*100/total); } in.close(); out.close(); File hpi = new File(baseDir, plugin.name + ".hpi"); hpi.delete(); if(!target.renameTo(hpi)) { throw new IOException("Failed to rename "+target+" to "+hpi); } LOGGER.info("Installation successful: "+plugin.name); pm.pluginUploaded = true; status = new Success(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to install "+plugin.name,e); status = new Failure(e); } } /** * Indicates the status or the result of a plugin installation. *

* Instances of this class is immutable. */ public abstract class InstallationStatus { public final int id = iota.incrementAndGet(); } /** * Indicates that the installation of a plugin failed. */ public class Failure extends InstallationStatus { public final Throwable problem; public Failure(Throwable problem) { this.problem = problem; } public String getStackTrace() { return Functions.printThrowable(problem); } } /** * Indicates that the plugin was successfully installed. */ public class Success extends InstallationStatus { } /** * 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. */ public final int percentage; public Installing(int percentage) { this.percentage = percentage; } } } /** * Adds the update center data retriever to HTML. */ public static class PageDecoratorImpl extends PageDecorator { public PageDecoratorImpl() { super(PageDecoratorImpl.class); } } static { PageDecorator.ALL.add(new PageDecoratorImpl()); } /** * Sequence number generator. */ private static final AtomicInteger iota = new AtomicInteger(); private static final long DAY = DAYS.toMillis(1); private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName()); public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never"); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy