Skip to content

Commit

Permalink
[JENKINS-72111] Allow Lifecycle to load implementations from plugins (
Browse files Browse the repository at this point in the history
#8589)

Allow `Lifecycle` to load implementations from plugins
  • Loading branch information
jglick committed Oct 13, 2023
1 parent 8132436 commit 298e34b
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 9 deletions.
13 changes: 10 additions & 3 deletions core/src/main/java/hudson/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,10 @@ protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle) {

// obtain topologically sorted list and overwrite the list
for (PluginWrapper p : cgd.getSorted()) {
if (p.isActive())
if (p.isActive()) {
activePlugins.add(p);
((UberClassLoader) uberClassLoader).clearCacheMisses();
}
}
} catch (CycleDetectedException e) { // TODO this should be impossible, since we override reactOnCycle to not throw the exception
stop(); // disable all plugins since classloading from them can lead to StackOverflow
Expand Down Expand Up @@ -932,9 +934,10 @@ public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<Plu
// so existing plugins can't be depending on this newly deployed one.

plugins.add(p);
if (p.isActive())
if (p.isActive()) {
activePlugins.add(p);
((UberClassLoader) uberClassLoader).loaded.clear();
((UberClassLoader) uberClassLoader).clearCacheMisses();
}

// TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener?
CustomClassFilter.Contributed.load();
Expand Down Expand Up @@ -2385,6 +2388,10 @@ protected Enumeration<URL> findResources(String name) throws IOException {
return Collections.enumeration(resources);
}

void clearCacheMisses() {
loaded.values().removeIf(Optional::isEmpty);
}

@Override
public String toString() {
// only for debugging purpose
Expand Down
33 changes: 27 additions & 6 deletions core/src/main/java/hudson/lifecycle/Lifecycle.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.Util;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
Expand All @@ -40,6 +42,8 @@
import jenkins.model.Jenkins;
import jenkins.util.SystemProperties;
import org.apache.commons.io.FileUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Provides the capability for starting/stopping/restarting/uninstalling Hudson.
Expand All @@ -48,7 +52,8 @@
* The steps to perform these operations depend on how Hudson is launched,
* so the concrete instance of this method (which is VM-wide singleton) is discovered
* by looking up a FQCN from the system property "hudson.lifecycle".
*
* (This may be set to a class defined in a plugin,
* in which case the singleton switches during startup.)
* @author Kohsuke Kawaguchi
* @since 1.254
*/
Expand All @@ -57,9 +62,8 @@ public abstract class Lifecycle implements ExtensionPoint {

/**
* Gets the singleton instance.
*
* @return never null
*/
@NonNull
public static synchronized Lifecycle get() {
if (INSTANCE == null) {
Lifecycle instance;
Expand All @@ -81,9 +85,8 @@ public static synchronized Lifecycle get() {
x.initCause(e);
throw x;
} catch (ClassNotFoundException e) {
NoClassDefFoundError x = new NoClassDefFoundError(e.getMessage());
x.initCause(e);
throw x;
LOGGER.log(Level.FINE, e, () -> "Failed to load " + p + " so will try again later");
instance = new PlaceholderLifecycle();
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
Expand Down Expand Up @@ -307,5 +310,23 @@ public void onStatusUpdate(String status) {
LOGGER.log(Level.INFO, status);
}

@Restricted(NoExternalUse.class)
public static final class PlaceholderLifecycle extends ExitLifecycle {

@Initializer(after = InitMilestone.PLUGINS_STARTED, before = InitMilestone.EXTENSIONS_AUGMENTED)
public static synchronized void replacePlaceholder() {
if (get() instanceof PlaceholderLifecycle) {
String p = SystemProperties.getString("hudson.lifecycle");
try {
INSTANCE = (Lifecycle) Jenkins.get().getPluginManager().uberClassLoader.loadClass(p).getConstructor().newInstance();
LOGGER.fine(() -> "Updated to " + INSTANCE);
} catch (Exception | LinkageError x) {
LOGGER.log(Level.WARNING, x, () -> "Failed to load " + p + "; using fallback exit lifecycle");
}
}
}

}

private static final Logger LOGGER = Logger.getLogger(Lifecycle.class.getName());
}
62 changes: 62 additions & 0 deletions test/src/test/java/hudson/lifecycle/LifecycleTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.lifecycle;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import java.lang.reflect.Field;
import java.util.logging.Level;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.RealJenkinsRule;

public final class LifecycleTest {

@Rule
public RealJenkinsRule rr = new RealJenkinsRule()
.addPlugins("plugins/custom-lifecycle.hpi")
.javaOptions("-Dhudson.lifecycle=test.custom_lifecycle.CustomLifecycle")
.withLogger(Lifecycle.class, Level.FINE);

@Test
public void definedInPlugin() throws Throwable {
rr.then(LifecycleTest::_definedInPlugin);
}

private static void _definedInPlugin(JenkinsRule r) throws Throwable {
Class<? extends Lifecycle> type = Jenkins.get().getPluginManager().uberClassLoader
.loadClass("test.custom_lifecycle.CustomLifecycle").asSubclass(Lifecycle.class);
Lifecycle l = Lifecycle.get();
assertThat(l.getClass(), is(type));
Field count = type.getField("count");
assertThat(count.get(l), is(0));
Lifecycle.get().restart();
assertThat(count.get(l), is(1));
}

}
Binary file not shown.

0 comments on commit 298e34b

Please sign in to comment.