+ * This method is conservative in the sense that if the version number is incomprehensible, + * it always returns false. + */ + public boolean hasUpdate() { + return getUpdateInfo()!=null; + } + /** * Explodes the plugin into a directory, if necessary. */ diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 7270344fba..c12820c347 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -1,21 +1,42 @@ package hudson.model; +import hudson.PluginManager; +import hudson.PluginWrapper; +import hudson.util.DaemonThreadFactory; import hudson.util.TextFile; import static hudson.util.TimeUnit2.DAYS; import net.sf.json.JSONObject; -import org.kohsuke.stapler.StaplerRequest; +import org.apache.commons.io.input.CountingInputStream; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.StaplerRequest; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; -import java.util.logging.Logger; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; -import java.util.HashMap; import java.util.Set; +import java.util.TreeMap; +import java.util.Vector; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +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
*/
@@ -32,6 +53,31 @@ public class UpdateCenter {
*/
private volatile long lastAttempt = -1;
+ /**
+ * {@link ExecutorService} that performs installation.
+ */
+ private final ThreadPoolExecutor installerService = new ThreadPoolExecutor(1, 1,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue
+ * beware of XSS vulnerability since this data comes from Wiki
*/
public final String title;
@@ -146,6 +236,108 @@ public class UpdateCenter {
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);
+
+ LOGGER.info("Scheduling the installation of "+getDisplayName());
+ installerService.submit(new Runnable() {
+ public void run() {
+ try {
+ // for security reasons, only install from hudson.dev.java.net for now, which is also conveniently
+ // https to guarantee transport level security.
+ if(!url.startsWith("https://hudson.dev.java.net/")) {
+ throw new IOException("Installation from non-official repository at "+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.
+ CountingInputStream in = new CountingInputStream(new URL(url).openStream());
+ byte[] buf = new byte[8192];
+ int len;
+
+ File baseDir = Hudson.getInstance().getPluginManager().rootDir;
+ File target = new File(baseDir, name + ".tmp");
+ OutputStream out = new FileOutputStream(target);
+
+ while((len=in.read(buf))>=0) {
+ out.write(buf,0,len);
+ }
+
+ in.close();
+ out.close();
+
+ File hpi = new File(baseDir, name + ".hpi");
+ hpi.delete();
+ if(!target.renameTo(hpi)) {
+ throw new IOException("Failed to rename "+target+" to "+hpi);
+ }
+
+ installationStatuses.add(new Success(Plugin.this));
+
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Failed to install "+name,e);
+ installationStatuses.add(new Failure(Plugin.this,e));
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Indicates the status or the result of a plugin installation.
+ */
+ public abstract class InstallationStatus {
+ /**
+ * Plugin that is installing, installed, or attempted to be installed.
+ */
+ public final Plugin plugin;
+
+ protected InstallationStatus(Plugin plugin) {
+ this.plugin = plugin;
+ }
+ }
+
+ /**
+ * Indicates that the installation of a plugin failed.
+ */
+ public class Failure extends InstallationStatus {
+ public final Throwable problem;
+
+ public Failure(Plugin plugin, Throwable problem) {
+ super(plugin);
+ this.problem = problem;
+ }
+ }
+
+ /**
+ * Indicates that the plugin was successfully installed.
+ */
+ public class Success extends InstallationStatus {
+ public Success(Plugin plugin) {
+ super(plugin);
+ }
}
private static final long DAY = DAYS.toMillis(1);
diff --git a/core/src/main/resources/hudson/PluginManager/NO-TRANSLATION-PLEASE-FOR-NOW b/core/src/main/resources/hudson/PluginManager/NO-TRANSLATION-PLEASE-FOR-NOW
new file mode 100644
index 0000000000..0975831c05
--- /dev/null
+++ b/core/src/main/resources/hudson/PluginManager/NO-TRANSLATION-PLEASE-FOR-NOW
@@ -0,0 +1,2 @@
+This is still a work in progress, so don't waste your time translating this. Things may change.
+Please give this a bit more time for it to stablize.
\ No newline at end of file
diff --git a/core/src/main/resources/hudson/PluginManager/available.jelly b/core/src/main/resources/hudson/PluginManager/available.jelly
new file mode 100644
index 0000000000..b8cb32dfa4
--- /dev/null
+++ b/core/src/main/resources/hudson/PluginManager/available.jelly
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Install
+ Name
+ Version
+
+
+
+
+
+
+
+
+
+
+