diff --git a/core/src/main/java/hudson/util/DoubleLaunchChecker.java b/core/src/main/java/hudson/util/DoubleLaunchChecker.java index d541452ca1..6fac015c02 100644 --- a/core/src/main/java/hudson/util/DoubleLaunchChecker.java +++ b/core/src/main/java/hudson/util/DoubleLaunchChecker.java @@ -24,27 +24,22 @@ package hudson.util; -import static hudson.init.InitMilestone.JOB_CONFIG_ADAPTED; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.Main; import hudson.Util; -import hudson.init.Initializer; -import hudson.triggers.SafeTimerTask; +import hudson.model.AdministrativeMonitor; +import hudson.model.AperiodicWork; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; +import java.time.Duration; import java.util.Random; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.ServletException; import jenkins.model.Jenkins; -import jenkins.util.Timer; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.interceptor.RequirePOST; /** * Makes sure that no other Hudson uses our {@code JENKINS_HOME} directory, @@ -63,21 +58,15 @@ import org.kohsuke.stapler.interceptor.RequirePOST; * @author Kohsuke Kawaguchi * @since 1.178 */ -@SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "The random is just used for load distribution.") -public class DoubleLaunchChecker { +@Extension +public class DoubleLaunchChecker extends AdministrativeMonitor { /** * The timestamp of the owner file when we updated it for the last time. * 0 to indicate that there was no update before. */ private long lastWriteTime = 0L; - /** - * Once the error is reported, the user can choose to ignore and proceed anyway, - * in which case the flag is set to true. - */ - private boolean ignore = false; - - private final Random random = new Random(); + private boolean activated; public final File home; @@ -91,20 +80,29 @@ public class DoubleLaunchChecker { home = Jenkins.get().getRootDir(); } + @Override + public String getDisplayName() { + return Messages.DoubleLaunchChecker_duplicate_jenkins_checker(); + } + + @Override + public boolean isActivated() { + return activated; + } + protected void execute() { + LOGGER.fine("running detector"); File timestampFile = new File(home, ".owner"); long t = timestampFile.lastModified(); - if (t != 0 && lastWriteTime != 0 && t != lastWriteTime && !ignore) { + if (t != 0 && lastWriteTime != 0 && t != lastWriteTime && isEnabled()) { try { collidingId = Files.readString(Util.fileToPath(timestampFile), Charset.defaultCharset()); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to read collision file", e); } // we noticed that someone else have updated this file. - // switch GUI to display this error. - // TODO seems drastic; could this just be switched to an AdministrativeMonitor? - Jenkins.get().servletContext.setAttribute("app", this); + activated = true; LOGGER.severe("Collision detected. timestamp=" + t + ", expected=" + lastWriteTime); // we need to continue updating this file, so that the other Hudson would notice the problem, too. } @@ -113,11 +111,10 @@ public class DoubleLaunchChecker { Files.writeString(Util.fileToPath(timestampFile), getId(), Charset.defaultCharset()); lastWriteTime = timestampFile.lastModified(); } catch (IOException e) { + LOGGER.log(Level.FINE, null, e); // if failed to write, err on the safe side and assume things are OK. lastWriteTime = 0; } - - schedule(); } /** @@ -131,44 +128,29 @@ public class DoubleLaunchChecker { return collidingId; } - /** - * Schedules the next execution. - */ - public void schedule() { - // randomize the scheduling so that multiple Hudson instances will write at the file at different time - long MINUTE = 1000 * 60; // TODO use TimeUnit.MINUTE.toMillis + @SuppressFBWarnings(value = "PREDICTABLE_RANDOM", justification = "The random is just used for load distribution.") + @Extension + public static final class Schedule extends AperiodicWork { - Timer.get() - .schedule(new SafeTimerTask() { - @Override - protected void doRun() { - execute(); - } - }, (random.nextInt(30) + 60) * MINUTE, TimeUnit.MILLISECONDS); - } + private final Random random = new Random(); - @Initializer(after = JOB_CONFIG_ADAPTED) - public static void init() { - // TODO AperiodicWork would be more idiomatic - new DoubleLaunchChecker().schedule(); - } + @Override + public AperiodicWork getNewInstance() { + // Awkward to use DoubleLaunchChecker itself as the AperiodicWork since it is stateful, and we may not return this. + return new Schedule(); + } - /** - * Serve all URLs with the index view. - */ - public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - rsp.setStatus(SC_INTERNAL_SERVER_ERROR); - req.getView(this, "index.jelly").forward(req, rsp); - } + @Override + public long getRecurrencePeriod() { + // randomize the scheduling so that multiple Jenkins instances will write at the file at different time + return (Main.isUnitTest ? Duration.ofSeconds(random.nextInt(10) + 20) : Duration.ofMinutes(random.nextInt(30) + 60)).toMillis(); + } + + @Override + protected void doAperiodicRun() { + ExtensionList.lookupSingleton(DoubleLaunchChecker.class).execute(); + } - /** - * Ignore the problem and go back to using Hudson. - */ - @RequirePOST - public void doIgnore(StaplerRequest req, StaplerResponse rsp) throws IOException { - ignore = true; - Jenkins.get().servletContext.setAttribute("app", Jenkins.get()); - rsp.sendRedirect2(req.getContextPath() + '/'); } private static final Logger LOGGER = Logger.getLogger(DoubleLaunchChecker.class.getName()); diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.jelly b/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.jelly new file mode 100644 index 0000000000..bb405e9319 --- /dev/null +++ b/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.jelly @@ -0,0 +1,4 @@ + + + ${%blurb} + diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.properties new file mode 100644 index 0000000000..ee54268d61 --- /dev/null +++ b/core/src/main/resources/hudson/util/DoubleLaunchChecker/description.properties @@ -0,0 +1 @@ +blurb = Detects if more than one Jenkins process is attempting to run in the same $JENKINS_HOME directory simultaneously. diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index.jelly b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message.jelly similarity index 85% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index.jelly rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message.jelly index cfbbda0abd..9f7354230d 100644 --- a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index.jelly +++ b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message.jelly @@ -24,13 +24,7 @@ THE SOFTWARE. - - - - - - -

${%Error}

+

${%message(it.home)}

@@ -45,10 +39,9 @@ THE SOFTWARE.
-
+
- - +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_bg.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_bg.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_bg.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_bg.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_da.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_da.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_da.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_da.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_de.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_de.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_de.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_de.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_es.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_es.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_es.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_es.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_fr.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_fr.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_fr.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_fr.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_it.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_it.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_it.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_it.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_ja.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_ja.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_ja.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_ja.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_nl.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_nl.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_nl.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_nl.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_pt_BR.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_pt_BR.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_pt_BR.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_pt_BR.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_ru.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_ru.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_ru.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_ru.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_sr.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_sr.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_sr.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_sr.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_tr.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_tr.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_tr.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_tr.properties diff --git a/core/src/main/resources/hudson/util/DoubleLaunchChecker/index_zh_TW.properties b/core/src/main/resources/hudson/util/DoubleLaunchChecker/message_zh_TW.properties similarity index 100% rename from core/src/main/resources/hudson/util/DoubleLaunchChecker/index_zh_TW.properties rename to core/src/main/resources/hudson/util/DoubleLaunchChecker/message_zh_TW.properties diff --git a/core/src/main/resources/hudson/util/Messages.properties b/core/src/main/resources/hudson/util/Messages.properties index b4c8f3325a..a44fcfd5b3 100644 --- a/core/src/main/resources/hudson/util/Messages.properties +++ b/core/src/main/resources/hudson/util/Messages.properties @@ -24,6 +24,7 @@ ClockDifference.InSync=In sync ClockDifference.Ahead={0} ahead ClockDifference.Behind={0} behind ClockDifference.Failed=Failed to check +DoubleLaunchChecker.duplicate_jenkins_checker=Duplicate Jenkins checker FormFieldValidator.did_not_manage_to_validate_may_be_too_sl=Did not manage to validate {0} (may be too slow) FormValidation.ValidateRequired=Required FormValidation.Error.Details=(show details) diff --git a/test/src/test/java/hudson/util/DoubleLaunchCheckerTest.java b/test/src/test/java/hudson/util/DoubleLaunchCheckerTest.java new file mode 100644 index 0000000000..cb6772a4d6 --- /dev/null +++ b/test/src/test/java/hudson/util/DoubleLaunchCheckerTest.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * + * Copyright 2023 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 hudson.util; + +import static org.awaitility.Awaitility.await; + +import hudson.ExtensionList; +import java.time.Duration; +import java.util.logging.Level; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.PrefixedOutputStream; +import org.jvnet.hudson.test.RealJenkinsRule; + +public final class DoubleLaunchCheckerTest { + + @Rule + public RealJenkinsRule mainController = new RealJenkinsRule(). + withName("main"). + withColor(PrefixedOutputStream.Color.BLUE). + withLogger(DoubleLaunchChecker.class, Level.FINE); + + @Rule + public RealJenkinsRule duplicateController = new RealJenkinsRule(mainController). + withName("dupe"). + withColor(PrefixedOutputStream.Color.RED). + withLogger(DoubleLaunchChecker.class, Level.FINE); + + @Test + public void activated() throws Throwable { + mainController.startJenkins(); + duplicateController.startJenkins(); + mainController.runRemotely(DoubleLaunchCheckerTest::waitForWarning); + } + + private static void waitForWarning(JenkinsRule r) throws Throwable { + await().atMost(Duration.ofMinutes(3)).until(ExtensionList.lookupSingleton(DoubleLaunchChecker.class)::isActivated); + } + +}