diff --git a/core/src/main/java/hudson/cli/QuietDownCommand.java b/core/src/main/java/hudson/cli/QuietDownCommand.java index 740ee10864..4541be077d 100644 --- a/core/src/main/java/hudson/cli/QuietDownCommand.java +++ b/core/src/main/java/hudson/cli/QuietDownCommand.java @@ -47,6 +47,9 @@ public class QuietDownCommand extends CLICommand { @Option(name="-timeout",usage="If non-zero, only block up to the specified number of milliseconds") public int timeout = 0; + @Option(name="-reason",usage="Reason for quiet down that will be visible to users") + public String reason = null; + @Override public String getShortDescription() { return Messages.QuietDownCommand_ShortDescription(); @@ -54,7 +57,7 @@ public class QuietDownCommand extends CLICommand { @Override protected int run() throws Exception { - Jenkins.get().doQuietDown(block, timeout); + Jenkins.get().doQuietDown(block, timeout, reason); return 0; } } diff --git a/core/src/main/java/jenkins/management/ShutdownLink.java b/core/src/main/java/jenkins/management/ShutdownLink.java index 36f5f4c713..ae9041547d 100644 --- a/core/src/main/java/jenkins/management/ShutdownLink.java +++ b/core/src/main/java/jenkins/management/ShutdownLink.java @@ -28,38 +28,61 @@ import hudson.Extension; import hudson.model.ManagementLink; import hudson.security.Permission; import jenkins.model.Jenkins; +import net.sf.json.JSONObject; import org.jenkinsci.Symbol; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.verb.POST; + +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; import edu.umd.cs.findbugs.annotations.NonNull; -/** - * @author Nicolas De Loof - */ -@Extension(ordinal = Integer.MIN_VALUE) @Symbol("shutDown") +@Extension(ordinal = Integer.MIN_VALUE) +@Symbol("prepareQuietDown") public class ShutdownLink extends ManagementLink { + private static final Logger LOGGER = Logger.getLogger(ShutdownLink.class.getName()); + @Override public String getIconFileName() { return "system-log-out.png"; } public String getDisplayName() { - return Jenkins.get().isQuietingDown() ? Messages.ShutdownLink_DisplayName_cancel() : Messages.ShutdownLink_DisplayName_prepare(); + return Jenkins.get().isQuietingDown() ? Messages.ShutdownLink_DisplayName_update() : Messages.ShutdownLink_DisplayName_prepare(); } @Override public String getDescription() { - return Jenkins.get().isQuietingDown() ? "" : Messages.ShutdownLink_Description(); + return Jenkins.get().isQuietingDown() ? Messages.ShutdownLink_ShuttingDownInProgressDescription() : Messages.ShutdownLink_Description(); } @Override public String getUrlName() { - return Jenkins.get().isQuietingDown() ? "cancelQuietDown" : "quietDown"; + return "prepareShutdown"; } - @Override - public boolean getRequiresPOST() { - return true; + @POST + public synchronized void doPrepare(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { + Jenkins.get().checkPermission(Jenkins.MANAGE); + + JSONObject submittedForm = req.getSubmittedForm(); + String inputReason = submittedForm.getString("shutdownReason"); + String shutdownReason = inputReason.isEmpty() ? null : inputReason; + LOGGER.log(Level.FINE, "Shutdown requested by user {0}", Jenkins.getAuthentication().getName()); + Jenkins.get().doQuietDown(false, 0, shutdownReason).generateResponse(req, rsp, null); + } + + @POST + public synchronized void doCancel(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + Jenkins.get().checkPermission(Jenkins.MANAGE); + + LOGGER.log(Level.FINE, "Shutdown cancel requested by user {0}", Jenkins.getAuthentication().getName()); + Jenkins.get().doCancelQuietDown().generateResponse(req, rsp, null); } @NonNull diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 2e473e77f6..1309844eb4 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -453,7 +453,8 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ private static Jenkins theInstance; - private transient volatile boolean isQuietingDown; + @CheckForNull + private transient volatile QuietDownInfo quietDownInfo; private transient volatile boolean terminating; @GuardedBy("Jenkins.class") private transient boolean cleanUpStarted; @@ -2799,7 +2800,20 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Exported public boolean isQuietingDown() { - return isQuietingDown; + return quietDownInfo != null; + } + + /** + * Returns quiet down reason if it was indicated. + * @return + * Reason if it was indicated. null otherwise + * @since TODO + */ + @Exported + @CheckForNull + public String getQuietDownReason() { + final QuietDownInfo info = quietDownInfo; + return info != null ? info.reason : null; } /** @@ -3877,7 +3891,24 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public synchronized HttpRedirect doQuietDown() { try { - return doQuietDown(false,0); + return doQuietDown(false, 0, null); + } catch (IOException | InterruptedException e) { + throw new AssertionError(); // impossible + } + } + + /** + * Quiet down Jenkins - preparation for a restart + * Presented for compatibility. + * + * @param block Block until the system really quiets down and no builds are running + * @param timeout If non-zero, only block up to the specified number of milliseconds + * @deprecated since TODO; use {@link #doQuietDown(boolean, int, String)} instead. + */ + @Deprecated + public synchronized HttpRedirect doQuietDown(boolean block, int timeout) { + try { + return doQuietDown(block, timeout, null); } catch (IOException | InterruptedException e) { throw new AssertionError(); // impossible } @@ -3888,17 +3919,21 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * * @param block Block until the system really quiets down and no builds are running * @param timeout If non-zero, only block up to the specified number of milliseconds + * @param reason Quiet reason that will be visible to user + * @since TODO */ @RequirePOST - public HttpRedirect doQuietDown(@QueryParameter boolean block, @QueryParameter int timeout) throws InterruptedException, IOException { + public HttpRedirect doQuietDown(@QueryParameter boolean block, + @QueryParameter int timeout, + @QueryParameter @CheckForNull String reason) throws InterruptedException, IOException { synchronized (this) { checkPermission(MANAGE); - isQuietingDown = true; + quietDownInfo = new QuietDownInfo(reason); } if (block) { long waitUntil = timeout; if (timeout > 0) waitUntil += System.currentTimeMillis(); - while (isQuietingDown + while (isQuietingDown() && (timeout <= 0 || System.currentTimeMillis() < waitUntil) && !RestartListener.isAllReady()) { Thread.sleep(TimeUnit.SECONDS.toMillis(1)); @@ -3913,7 +3948,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public synchronized HttpRedirect doCancelQuietDown() { checkPermission(MANAGE); - isQuietingDown = false; + quietDownInfo = null; getQueue().scheduleMaintenance(); return new HttpRedirect("."); } @@ -4335,7 +4370,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve public void safeRestart() throws RestartNotSupportedException { final Lifecycle lifecycle = restartableLifecycle(); // Quiet down so that we won't launch new builds. - isQuietingDown = true; + quietDownInfo = new QuietDownInfo(); new Thread("safe-restart thread") { final String exitUser = getAuthentication2().getName(); @@ -4344,10 +4379,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) { // Wait 'til we have no active executors. - doQuietDown(true, 0); + doQuietDown(true, 0, null); // Make sure isQuietingDown is still true. - if (isQuietingDown) { + if (isQuietingDown()) { servletContext.setAttribute("app",new HudsonIsRestarting()); // give some time for the browser to load the "reloading" page LOGGER.info("Restart in 10 seconds"); @@ -4432,7 +4467,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve @RequirePOST public HttpResponse doSafeExit(StaplerRequest req) throws IOException { checkPermission(ADMINISTER); - isQuietingDown = true; + quietDownInfo = new QuietDownInfo(); final String exitUser = getAuthentication2().getName(); final String exitAddr = req!=null ? req.getRemoteAddr() : "unknown"; new Thread("safe-exit thread") { @@ -4443,9 +4478,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve LOGGER.info(String.format("Shutting down VM as requested by %s from %s", exitUser, exitAddr)); // Wait 'til we have no active executors. - doQuietDown(true, 0); + doQuietDown(true, 0, null); // Make sure isQuietingDown is still true. - if (isQuietingDown) { + if (isQuietingDown()) { cleanUp(); System.exit(0); } @@ -5425,4 +5460,17 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } } + private static final class QuietDownInfo { + + @CheckForNull + final String reason; + + QuietDownInfo() { + this(null); + } + + QuietDownInfo(final String reason) { + this.reason = reason; + } + } } diff --git a/core/src/main/resources/jenkins/management/Messages.properties b/core/src/main/resources/jenkins/management/Messages.properties index 8e5e595a83..55fca7d7a1 100644 --- a/core/src/main/resources/jenkins/management/Messages.properties +++ b/core/src/main/resources/jenkins/management/Messages.properties @@ -55,6 +55,10 @@ NodesLink.Description=Add, remove, control and monitor the various nodes that Je ShutdownLink.DisplayName_prepare=Prepare for Shutdown ShutdownLink.DisplayName_cancel=Cancel Shutdown +ShutdownLink.DisplayName_update=Update shutdown preparation ShutdownLink.Description=Stops executing new builds, so that the system can be eventually shut down safely. +ShutdownLink.ShuttingDownInProgressDescription=Jenkins is currently shutting down. New builds are not executing. +ShutdownLink.ShutDownReason_title=Reason +ShutdownLink.ShutDownReason_update=Update reason AdministrativeMonitorsDecorator.DisplayName=Administrative Monitors Notifier diff --git a/core/src/main/resources/jenkins/management/ShutdownLink/index.groovy b/core/src/main/resources/jenkins/management/ShutdownLink/index.groovy new file mode 100644 index 0000000000..252dbae1f4 --- /dev/null +++ b/core/src/main/resources/jenkins/management/ShutdownLink/index.groovy @@ -0,0 +1,47 @@ +package jenkins.management.ShutdownNewLink + +import jenkins.management.Messages + +def f = namespace(lib.FormTagLib) +def l = namespace(lib.LayoutTagLib) +def st = namespace("jelly:stapler") + +l.layout(norefresh: true, permission: app.MANAGE, title: my.displayName) { + l.side_panel { + l.tasks { + l.task(icon: "icon-up icon-md", href: rootURL + '/', title: _("Back to Dashboard")) + l.task(icon: "icon-gear2 icon-md", href: "${rootURL}/manage", title: _("Manage Jenkins")) + } + } + l.main_panel { + h1 { + l.icon(class: 'icon-system-log-out icon-xlg') + text(Messages.ShutdownLink_DisplayName_prepare()) + } + + p { + text(my.description) + } + + f.form(method: "post", name: "prepareShutdown", action: "prepare") { + f.entry(title: Messages.ShutdownLink_ShutDownReason_title()) { + f.textbox(name: "parameter.shutdownReason", value: app.quietDownReason ?: null) + } + + + f.bottomButtonBar { + f.submit(value: _(app.isQuietingDown() + ? Messages.ShutdownLink_ShutDownReason_update() + : Messages.ShutdownLink_DisplayName_prepare())) + } + } + + if (app.isQuietingDown()) { + f.form(method: "post", name: "cancelShutdown", action: "cancel") { + f.bottomButtonBar { + f.submit(value: _(Messages.ShutdownLink_DisplayName_cancel())) + } + } + } + } +} diff --git a/core/src/main/resources/jenkins/model/Jenkins/_api.jelly b/core/src/main/resources/jenkins/model/Jenkins/_api.jelly index 39af30fc90..4899690c53 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/_api.jelly +++ b/core/src/main/resources/jenkins/model/Jenkins/_api.jelly @@ -39,8 +39,9 @@ THE SOFTWARE.
- Jenkins will enter into the "quiet down" mode by sending a request to this URL.
- You can cancel this mode by sending a request to this URL. On environments
+ Jenkins will enter into the "quiet down" mode by sending a POST request with optional reason query parameter to this URL.
+ You can also send another request to this URL to update the reason.
+ You can cancel this mode by sending a POST request to this URL. On environments
where Jenkins can restart itself (such as when Jenkins is installed as a Windows service), POSTing to
this URL will start the restart sequence, or
this URL to restart once no jobs are running.
diff --git a/core/src/main/resources/lib/layout/main-panel.jelly b/core/src/main/resources/lib/layout/main-panel.jelly
index 92e2e12d28..cd9713aef3 100644
--- a/core/src/main/resources/lib/layout/main-panel.jelly
+++ b/core/src/main/resources/lib/layout/main-panel.jelly
@@ -30,7 +30,11 @@ THE SOFTWARE.
${%Shut down reason}: ${app.getQuietDownReason()}
+