From c6d81ef2e26e55dd3341a6eacd113a1f58023f73 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Tue, 15 Nov 2011 23:09:47 -0800 Subject: [PATCH] hooking up the dynamic loading logic into the UI --- core/src/main/java/hudson/PluginManager.java | 32 +++++++++--- core/src/main/java/hudson/PluginWrapper.java | 10 ++++ .../java/hudson/cli/InstallPluginCommand.java | 23 +++++++-- .../main/java/hudson/model/UpdateCenter.java | 51 ++++++++++++++++++- .../main/java/hudson/model/UpdateSite.java | 30 ++++++++--- .../jenkins/RestartRequiredException.java | 44 ++++++++++++++++ .../main/resources/hudson/Messages.properties | 2 + .../hudson/PluginManager/table.jelly | 4 +- .../SuccessButRequiresRestart/status.groovy | 27 ++++++++++ 9 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 core/src/main/java/jenkins/RestartRequiredException.java create mode 100644 core/src/main/resources/hudson/model/UpdateCenter/DownloadJob/SuccessButRequiresRestart/status.groovy diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index cc49662890..372dc3969f 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -34,10 +34,13 @@ import hudson.model.UpdateSite; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; import hudson.util.FormValidation; +import hudson.util.IOException2; import hudson.util.PersistedList; import hudson.util.Service; import jenkins.ClassLoaderReflectionToolkit; import jenkins.InitReactorRunner; +import jenkins.RestartRequiredException; +import jenkins.YesNoMaybe; import jenkins.model.Jenkins; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; @@ -47,6 +50,7 @@ import org.apache.commons.io.FilenameUtils; import org.apache.commons.logging.LogFactory; import org.jvnet.hudson.reactor.Executable; import org.jvnet.hudson.reactor.Reactor; +import org.jvnet.hudson.reactor.ReactorException; import org.jvnet.hudson.reactor.TaskBuilder; import org.jvnet.hudson.reactor.TaskGraphBuilder; import org.kohsuke.stapler.HttpRedirect; @@ -124,7 +128,7 @@ public abstract class PluginManager extends AbstractModelObject { /** * Once plugin is uploaded, this flag becomes true. - * This is used to report a message that Hudson needs to be restarted + * This is used to report a message that Jenkins needs to be restarted * for new plugins to take effect. */ public volatile boolean pluginUploaded = false; @@ -330,10 +334,15 @@ public abstract class PluginManager extends AbstractModelObject { /** * TODO: revisit where/how to expose this. This is an experiment. */ - public void dynamicLoad(File arc) throws Exception { + public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException { + LOGGER.info("Attempting to dynamic load "+arc); final PluginWrapper p = strategy.createPluginWrapper(arc); - if (getPlugin(p.getShortName())!=null) - throw new IllegalArgumentException("Dynamic reloading isn't possible"); + String sn = p.getShortName(); + if (getPlugin(sn)!=null) + throw new RestartRequiredException(Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn)); + + if (p.supportsDynamicLoad()== YesNoMaybe.NO) + throw new RestartRequiredException(Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn)); // there's no need to do cyclic dependency check, because we are deploying one at a time, // so existing plugins can't be depending on this newly deployed one. @@ -349,10 +358,10 @@ public abstract class PluginManager extends AbstractModelObject { p.getPlugin().postInitialize(); } catch (Exception e) { - failedPlugins.add(new FailedPlugin(p.getShortName(), e)); + failedPlugins.add(new FailedPlugin(sn, e)); activePlugins.remove(p); plugins.remove(p); - throw e; + throw new IOException2("Failed to install "+ sn +" plugin",e); } // run initializers in the added plugin @@ -363,7 +372,12 @@ public abstract class PluginManager extends AbstractModelObject { return e.getDeclaringClass().getClassLoader()!=p.classLoader || super.filter(e); } }.discoverTasks(r)); - new InitReactorRunner().run(r); + try { + new InitReactorRunner().run(r); + } catch (ReactorException e) { + throw new IOException2("Failed to initialize "+ sn +" plugin",e); + } + LOGGER.info("Plugin " + sn + " dynamically installed"); } /** @@ -559,6 +573,8 @@ public abstract class PluginManager extends AbstractModelObject { * Performs the installation of the plugins. */ public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + boolean dynamicLoad = req.getParameter("dynamicLoad")!=null; + Enumeration en = req.getParameterNames(); while (en.hasMoreElements()) { String n = en.nextElement(); @@ -569,7 +585,7 @@ public abstract class PluginManager extends AbstractModelObject { UpdateSite.Plugin p = Jenkins.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]); if(p==null) throw new Failure("No such plugin: "+n); - p.deploy(); + p.deploy(dynamicLoad); } } } diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 9db9bbeb5d..16f158bc89 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -25,6 +25,7 @@ package hudson; import hudson.PluginManager.PluginInstanceStore; +import jenkins.YesNoMaybe; import jenkins.model.Jenkins; import hudson.model.UpdateCenter; import hudson.model.UpdateSite; @@ -290,6 +291,15 @@ public class PluginWrapper implements Comparable { return shortName; } + /** + * Does this plugin supports dynamic loading? + */ + public YesNoMaybe supportsDynamicLoad() { + String v = manifest.getMainAttributes().getValue("Support-Dynamic-Loading"); + if (v==null) return YesNoMaybe.MAYBE; + return Boolean.parseBoolean(v) ? YesNoMaybe.YES : YesNoMaybe.NO; + } + /** * Returns the version number of this plugin */ diff --git a/core/src/main/java/hudson/cli/InstallPluginCommand.java b/core/src/main/java/hudson/cli/InstallPluginCommand.java index a71b7dc243..cfa00ef570 100644 --- a/core/src/main/java/hudson/cli/InstallPluginCommand.java +++ b/core/src/main/java/hudson/cli/InstallPluginCommand.java @@ -25,6 +25,7 @@ package hudson.cli; import hudson.Extension; import hudson.FilePath; +import hudson.PluginManager; import hudson.util.IOException2; import jenkins.model.Jenkins; import hudson.model.UpdateSite; @@ -65,9 +66,13 @@ public class InstallPluginCommand extends CLICommand { @Option(name="-restart",usage="Restart Jenkins upon successful installation") public boolean restart; + @Option(name="-deploy",usage="Deploy plugins right away without postponing them until the reboot.") + public boolean dynamicLoad; + protected int run() throws Exception { Jenkins h = Jenkins.getInstance(); h.checkPermission(Jenkins.ADMINISTER); + PluginManager pm = h.getPluginManager(); for (String source : sources) { // is this a file? @@ -76,7 +81,9 @@ public class InstallPluginCommand extends CLICommand { stdout.println(Messages.InstallPluginCommand_InstallingPluginFromLocalFile(f)); if (name==null) name = f.getBaseName(); - f.copyTo(getTargetFile()); + f.copyTo(getTargetFilePath()); + if (dynamicLoad) + pm.dynamicLoad(getTargetFile()); continue; } @@ -91,7 +98,9 @@ public class InstallPluginCommand extends CLICommand { int idx = name.lastIndexOf('.'); if (idx>0) name = name.substring(0,idx); } - getTargetFile().copyFrom(u); + getTargetFilePath().copyFrom(u); + if (dynamicLoad) + pm.dynamicLoad(getTargetFile()); continue; } catch (MalformedURLException e) { // not an URL @@ -101,7 +110,7 @@ public class InstallPluginCommand extends CLICommand { UpdateSite.Plugin p = h.getUpdateCenter().getPlugin(source); if (p!=null) { stdout.println(Messages.InstallPluginCommand_InstallingFromUpdateCenter(source)); - Throwable e = p.deploy().get().getError(); + Throwable e = p.deploy(dynamicLoad).get().getError(); if (e!=null) throw new IOException2("Failed to install plugin "+source,e); continue; @@ -134,7 +143,11 @@ public class InstallPluginCommand extends CLICommand { return 0; // all success } - private FilePath getTargetFile() { - return new FilePath(new File(Jenkins.getInstance().getPluginManager().rootDir,name+".hpi")); + private FilePath getTargetFilePath() { + return new FilePath(getTargetFile()); + } + + private File getTargetFile() { + return new File(Jenkins.getInstance().getPluginManager().rootDir,name+".hpi"); } } diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 21a8974b91..2aa42a635b 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -36,6 +36,7 @@ import static hudson.init.InitMilestone.PLUGINS_STARTED; import hudson.init.Initializer; import hudson.lifecycle.Lifecycle; import hudson.lifecycle.RestartNotSupportedException; +import hudson.model.UpdateCenter.DownloadJob; import hudson.model.UpdateSite.Data; import hudson.model.UpdateSite.Plugin; import hudson.model.listeners.SaveableListener; @@ -45,10 +46,12 @@ import hudson.util.HttpResponses; import hudson.util.IOException2; import hudson.util.PersistedList; import hudson.util.XStream2; +import jenkins.RestartRequiredException; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.io.output.NullOutputStream; +import org.jvnet.localizer.Localizable; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -951,6 +954,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { LOGGER.info("Installation successful: "+getName()); status = new Success(); onSuccess(); + } catch (RestartRequiredException e) { + status = new SuccessButRequiresRestart(e.message); + LOGGER.log(Level.INFO, "Installation successful but restart required: "+getName(), e); + onSuccess(); } catch (Throwable e) { LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e); status = new Failure(e); @@ -958,7 +965,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } - protected void _run() throws IOException { + protected void _run() throws IOException, RestartRequiredException { URL src = getURL(); config.preValidate(this, src); @@ -1011,6 +1018,23 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + /** + * Indicates that the installation was successful but a restart is needed. + * + * @see + */ + public class SuccessButRequiresRestart extends InstallationStatus { + private final Localizable message; + + public SuccessButRequiresRestart(Localizable message) { + this.message = message; + } + + public String getMessage() { + return message.toString(); + } + } + /** * Indicates that the plugin was successfully installed. */ @@ -1052,9 +1076,22 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { private final PluginManager pm = Jenkins.getInstance().getPluginManager(); + /** + * True to load the plugin into this Jenkins, false to wait until restart. + */ + private final boolean dynamicLoad; + + /** + * @deprecated as of 1.DynamicExtensionFinder + */ public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) { + this(plugin,site,auth,false); + } + + public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth, boolean dynamicLoad) { super(site, auth); this.plugin = plugin; + this.dynamicLoad = dynamicLoad; } protected URL getURL() throws MalformedURLException { @@ -1071,7 +1108,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } @Override - public void _run() throws IOException { + public void _run() throws IOException, RestartRequiredException { super._run(); // if this is a bundled plugin, make sure it won't get overwritten @@ -1083,6 +1120,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } finally { SecurityContextHolder.clearContext(); } + + if (dynamicLoad) { + try { + pm.dynamicLoad(getDestination()); + } catch (RestartRequiredException e) { + throw e; // pass through + } catch (Exception e) { + throw new IOException2("Failed to dynamically deploy this plugin",e); + } + } } protected void onSuccess() { diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 8389fcbffb..74bccfb147 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -45,6 +45,7 @@ import org.jvnet.hudson.crypto.CertificateUtil; import org.jvnet.hudson.crypto.SignatureOutputStream; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -689,21 +690,29 @@ public class UpdateSite { deploy(); } + public Future deploy() { + return deploy(false); + } + /** * 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. + * + * @param dynamicLoad + * If true, the plugin will be dynamically loaded into this Jenkins. If false, + * the plugin will only take effect after the reboot. */ - public Future deploy() { + public Future deploy(boolean dynamicLoad) { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); UpdateCenter uc = Jenkins.getInstance().getUpdateCenter(); for (Plugin dep : getNeededDependencies()) { LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name); - dep.deploy(); + dep.deploy(dynamicLoad); } - return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication())); + return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad)); } /** @@ -717,17 +726,22 @@ public class UpdateSite { /** * Making the installation web bound. */ - public void doInstall(StaplerResponse rsp) throws IOException { - deploy(); - rsp.sendRedirect2("../.."); + public HttpResponse doInstall() throws IOException { + deploy(false); + return HttpResponses.redirectTo("../.."); + } + + public HttpResponse doInstallNow() throws IOException { + deploy(true); + return HttpResponses.redirectTo("../.."); } /** * Performs the downgrade of the plugin. */ - public void doDowngrade(StaplerResponse rsp) throws IOException { + public HttpResponse doDowngrade() throws IOException { deployBackup(); - rsp.sendRedirect2("../.."); + return HttpResponses.redirectTo("../.."); } } diff --git a/core/src/main/java/jenkins/RestartRequiredException.java b/core/src/main/java/jenkins/RestartRequiredException.java new file mode 100644 index 0000000000..2cadc2f20e --- /dev/null +++ b/core/src/main/java/jenkins/RestartRequiredException.java @@ -0,0 +1,44 @@ +/* + * The MIT License + * + * Copyright (c) 2011, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package jenkins; + +import org.jvnet.localizer.Localizable; + +/** + * Indicates that the plugin cannot be deployed without a restart. + * + * @author Kohsuke Kawaguchi + */ +public class RestartRequiredException extends Exception { + public final Localizable message; + + public RestartRequiredException(Localizable message) { + this.message = message; + } + + public RestartRequiredException(Localizable message, Throwable cause) { + super(cause); + this.message = message; + } +} diff --git a/core/src/main/resources/hudson/Messages.properties b/core/src/main/resources/hudson/Messages.properties index fb82bc8266..0bb1216d96 100644 --- a/core/src/main/resources/hudson/Messages.properties +++ b/core/src/main/resources/hudson/Messages.properties @@ -35,6 +35,8 @@ FilePath.validateRelativePath.notDirectory=''{0}'' is not a directory FilePath.validateRelativePath.noSuchFile=No such file: ''{0}'' FilePath.validateRelativePath.noSuchDirectory=No such directory: ''{0}'' +PluginManager.PluginDoesntSupportDynamicLoad.RestartRequired={0} plugin doesn''t support dynamic loading. Jenkins needs to be restarted for the update to take effect +PluginManager.PluginIsAlreadyInstalled.RestartRequired={0} plugin is already installed. Jenkins needs to be restarted for the update to take effect Util.millisecond={0} ms Util.second={0} sec Util.minute={0} min diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly index 714ff283c9..a16746b0b6 100644 --- a/core/src/main/resources/hudson/PluginManager/table.jelly +++ b/core/src/main/resources/hudson/PluginManager/table.jelly @@ -137,7 +137,9 @@ THE SOFTWARE.

- + + +
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/DownloadJob/SuccessButRequiresRestart/status.groovy b/core/src/main/resources/hudson/model/UpdateCenter/DownloadJob/SuccessButRequiresRestart/status.groovy new file mode 100644 index 0000000000..c331b5c3f9 --- /dev/null +++ b/core/src/main/resources/hudson/model/UpdateCenter/DownloadJob/SuccessButRequiresRestart/status.groovy @@ -0,0 +1,27 @@ +/* + * The MIT License + * + * Copyright (c) 2011, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +img(src:"${imagesURL}/24x24/yellow.png",height:24,width:24) +text(" "); +text(my.message) -- GitLab