Skip to content

Commit

Permalink
Notify systemd(1) about start-up completion and other service statu…
Browse files Browse the repository at this point in the history
…s changes (jenkinsci#6228)

(cherry picked from commit 405e977)
  • Loading branch information
basil authored and MarkEWaite committed Mar 4, 2022
1 parent 8af9422 commit affedf2
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 13 deletions.
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/WebAppMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public void run() {
Files.deleteIfExists(BootFailure.getBootFailureFile(_home).toPath());

// at this point we are open for business and serving requests normally
LOGGER.info("Jenkins is fully up and running");
Jenkins.get().getLifecycle().onReady();
success = true;
} catch (Error e) {
new HudsonFailedToLoad(e).publish(context, _home);
Expand Down
54 changes: 54 additions & 0 deletions core/src/main/java/hudson/lifecycle/Lifecycle.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

package hudson.lifecycle;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.Util;
Expand Down Expand Up @@ -107,6 +109,9 @@ public void verifyRestartable() throws RestartNotSupportedException {
} else if (System.getenv("SMF_FMRI") != null && System.getenv("SMF_RESTARTER") != null) {
// when we are run by Solaris SMF, these environment variables are set.
instance = new SolarisSMFLifecycle();
} else if (System.getenv("NOTIFY_SOCKET") != null) {
// When we are running under systemd with Type=notify, this environment variable is set.
instance = new SystemdLifecycle();
} else {
// if run on Unix, we can do restart
try {
Expand Down Expand Up @@ -232,5 +237,54 @@ public boolean canRestart() {
}
}

/**
* Called when Jenkins startup is finished or when Jenkins has finished reloading its
* configuration.
*/
public void onReady() {
LOGGER.log(Level.INFO, "Jenkins is fully up and running");
}

/**
* Called when Jenkins is reloading its configuration.
*
* <p>Callers must also send an {@link #onReady()} notification when Jenkins has finished
* reloading its configuration.
*/
public void onReload(@NonNull String user, @CheckForNull String remoteAddr) {
if (remoteAddr != null) {
LOGGER.log(
Level.INFO,
"Reloading Jenkins as requested by {0} from {1}",
new Object[] {user, remoteAddr});
} else {
LOGGER.log(Level.INFO, "Reloading Jenkins as requested by {0}", user);
}
}

/**
* Called when Jenkins is beginning its shutdown.
*/
public void onStop(@NonNull String user, @CheckForNull String remoteAddr) {
if (remoteAddr != null) {
LOGGER.log(
Level.INFO,
"Stopping Jenkins as requested by {0} from {1}",
new Object[] {user, remoteAddr});
} else {
LOGGER.log(Level.INFO, "Stopping Jenkins as requested by {0}", user);
}
}

/**
* Called when Jenkins service state has changed.
*
* @param status The status string. This is free-form and can be used for various purposes:
* general state feedback, completion percentages, human-readable error message, etc.
*/
public void onStatusUpdate(String status) {
LOGGER.log(Level.INFO, status);
}

private static final Logger LOGGER = Logger.getLogger(Lifecycle.class.getName());
}
62 changes: 62 additions & 0 deletions core/src/main/java/hudson/lifecycle/SystemdLifecycle.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package hudson.lifecycle;

import com.sun.jna.LastErrorException;
import com.sun.jna.Library;
import com.sun.jna.Native;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* {@link Lifecycle} that delegates its responsibility to {@code systemd(1)}.
*
* @author Basil Crow
*/
@Restricted(NoExternalUse.class)
@Extension(optional = true)
public class SystemdLifecycle extends ExitLifecycle {

private static final Logger LOGGER = Logger.getLogger(SystemdLifecycle.class.getName());

interface Systemd extends Library {
Systemd INSTANCE = Native.load("systemd", Systemd.class);

int sd_notify(int unset_environment, String state) throws LastErrorException;
}

@Override
public void onReady() {
super.onReady();
notify("READY=1");
}

@Override
public void onReload(@NonNull String user, @CheckForNull String remoteAddr) {
super.onReload(user, remoteAddr);
notify("RELOADING=1");
}

@Override
public void onStop(@NonNull String user, @CheckForNull String remoteAddr) {
super.onStop(user, remoteAddr);
notify("STOPPING=1");
}

@Override
public void onStatusUpdate(String status) {
super.onStatusUpdate(status);
notify(String.format("STATUS=%s", status));
}

private static synchronized void notify(String message) {
try {
Systemd.INSTANCE.sd_notify(0, message);
} catch (LastErrorException e) {
LOGGER.log(Level.WARNING, null, e);
}
}
}
26 changes: 14 additions & 12 deletions core/src/main/java/jenkins/model/Jenkins.java
Original file line number Diff line number Diff line change
Expand Up @@ -3564,7 +3564,7 @@ public void cleanUp() {
cleanUpStarted = true;
}
try {
LOGGER.log(Level.INFO, "Stopping Jenkins");
getLifecycle().onStatusUpdate("Stopping Jenkins");

final List<Throwable> errors = new ArrayList<>();

Expand Down Expand Up @@ -3596,7 +3596,7 @@ public void cleanUp() {

_cleanUpReleaseAllLoggers(errors);

LOGGER.log(Level.INFO, "Jenkins stopped");
getLifecycle().onStatusUpdate("Jenkins stopped");

if (!errors.isEmpty()) {
StringBuilder message = new StringBuilder("Unexpected issues encountered during cleanUp: ");
Expand Down Expand Up @@ -4306,7 +4306,7 @@ public Slave.JnlpJar doJnlpJars(StaplerRequest req) {
@RequirePOST
public synchronized HttpResponse doReload() throws IOException {
checkPermission(MANAGE);
LOGGER.log(Level.WARNING, "Reloading Jenkins as requested by {0}", getAuthentication2().getName());
getLifecycle().onReload(getAuthentication2().getName(), null);

// engage "loading ..." UI and then run the actual task in a separate thread
WebApp.get(servletContext).setApp(new HudsonIsLoading());
Expand All @@ -4316,6 +4316,7 @@ public synchronized HttpResponse doReload() throws IOException {
public void run() {
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
reload();
getLifecycle().onReady();
} catch (Exception e) {
LOGGER.log(SEVERE, "Failed to reload Jenkins config", e);
new JenkinsReloadFailed(e).publish(servletContext, root);
Expand Down Expand Up @@ -4503,8 +4504,9 @@ public void restart() throws RestartNotSupportedException {
public void run() {
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
// give some time for the browser to load the "reloading" page
lifecycle.onStatusUpdate("Restart in 5 seconds");
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
LOGGER.info(String.format("Restarting VM as requested by %s", exitUser));
lifecycle.onStop(exitUser, null);
Listeners.notify(RestartListener.class, true, RestartListener::onRestart);
lifecycle.restart();
} catch (InterruptedException | InterruptedIOException e) {
Expand Down Expand Up @@ -4539,13 +4541,13 @@ public void run() {
if (isQuietingDown()) {
servletContext.setAttribute("app", new HudsonIsRestarting());
// give some time for the browser to load the "reloading" page
LOGGER.info("Restart in 10 seconds");
lifecycle.onStatusUpdate("Restart in 10 seconds");
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
LOGGER.info(String.format("Restarting VM as requested by %s", exitUser));
lifecycle.onStop(exitUser, null);
Listeners.notify(RestartListener.class, true, RestartListener::onRestart);
lifecycle.restart();
} else {
LOGGER.info("Safe-restart mode cancelled");
lifecycle.onStatusUpdate("Safe-restart mode cancelled");
}
} catch (Throwable e) {
LOGGER.log(Level.WARNING, "Failed to restart Jenkins", e);
Expand Down Expand Up @@ -4585,6 +4587,8 @@ protected RestartCause() {
@RequirePOST
public void doExit(StaplerRequest req, StaplerResponse rsp) throws IOException {
checkPermission(ADMINISTER);
final String exitUser = getAuthentication2().getName();
final String exitAddr = req != null ? req.getRemoteAddr() : null;
if (rsp != null) {
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.setContentType("text/plain");
Expand All @@ -4598,8 +4602,7 @@ public void doExit(StaplerRequest req, StaplerResponse rsp) throws IOException {
@SuppressFBWarnings(value = "DM_EXIT", justification = "Exit is really intended.")
public void run() {
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
LOGGER.info(String.format("Shutting down VM as requested by %s from %s",
getAuthentication2().getName(), req != null ? req.getRemoteAddr() : "???"));
getLifecycle().onStop(exitUser, exitAddr);

cleanUp();
System.exit(0);
Expand All @@ -4620,14 +4623,13 @@ public HttpResponse doSafeExit(StaplerRequest req) throws IOException {
checkPermission(ADMINISTER);
quietDownInfo = new QuietDownInfo();
final String exitUser = getAuthentication2().getName();
final String exitAddr = req != null ? req.getRemoteAddr() : "unknown";
final String exitAddr = req != null ? req.getRemoteAddr() : null;
new Thread("safe-exit thread") {
@Override
@SuppressFBWarnings(value = "DM_EXIT", justification = "Exit is really intended.")
public void run() {
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
LOGGER.info(String.format("Shutting down VM as requested by %s from %s",
exitUser, exitAddr));
getLifecycle().onStop(exitUser, exitAddr);
// Wait 'til we have no active executors.
doQuietDown(true, 0, null);
// Make sure isQuietingDown is still true.
Expand Down

0 comments on commit affedf2

Please sign in to comment.