diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index 8279fb8ca1d04698d2ef646ccc35139eebc7fb51..18cbb3e0fa2737a574980f8a1331454f804d4acd 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -377,6 +377,45 @@ public class ClassicPluginStrategy implements PluginStrategy { plugin.getPlugin().start(); } + /** + * Called when a plugin is deployed, and there is a plugin optionally depending on that plugin. + * The class loader of the existing depending plugin should be updated + * to load classes from the newly deployed plugin. + * + * @param depender the plugin to update its class loader + * @param dependee + * @see hudson.PluginStrategy#updateDependency(hudson.PluginWrapper, hudson.PluginWrapper) + */ + @Override + public void updateDependency(PluginWrapper depender, PluginWrapper dependee) { + DependencyClassLoader classLoader = findAncestorDependencyClassLoader(depender.classLoader); + if (classLoader != null) { + classLoader.updateTransientDependencies(); + LOGGER.log(Level.INFO, "Updated dependency of {0}", depender.getShortName()); + } + } + + private DependencyClassLoader findAncestorDependencyClassLoader(ClassLoader classLoader) + { + for (; classLoader != null; classLoader = classLoader.getParent()) { + if (classLoader instanceof DependencyClassLoader) { + return (DependencyClassLoader)classLoader; + } + + if (classLoader instanceof AntClassLoader) { + // AntClassLoaders hold parents not only as AntClassLoader#getParent() + // but also as AntClassLoader#getConfiguredParent() + DependencyClassLoader ret = findAncestorDependencyClassLoader( + ((AntClassLoader)classLoader).getConfiguredParent() + ); + if (ret != null) { + return ret; + } + } + } + return null; + } + private static File resolve(File base, String relative) { File rel = new File(relative); if(rel.isAbsolute()) @@ -523,6 +562,11 @@ public class ClassicPluginStrategy implements PluginStrategy { this.dependencies = dependencies; } + private void updateTransientDependencies() { + // This will be recalculated at the next time. + transientDependencies = null; + } + private List getTransitiveDependencies() { if (transientDependencies==null) { CyclicGraphDetector cgd = new CyclicGraphDetector() { diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index bd3d3dd093295fda28fc5a10f853646d45dc9acb..d77f12087db5cf8dac957c8311d11acedffe5c84 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -453,6 +453,23 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } catch (ReactorException e) { throw new IOException("Failed to initialize "+ sn +" plugin",e); } + + // recalculate dependencies of plugins optionally depending the newly deployed one. + for (PluginWrapper depender: plugins) { + if (depender.equals(p)) { + // skip itself. + continue; + } + for (Dependency d: depender.getOptionalDependencies()) { + if (d.shortName.equals(p.getShortName())) { + // this plugin depends on the newly loaded one! + // recalculate dependencies! + getPluginStrategy().updateDependency(depender, p); + break; + } + } + } + LOGGER.info("Plugin " + sn + " dynamically installed"); } diff --git a/core/src/main/java/hudson/PluginStrategy.java b/core/src/main/java/hudson/PluginStrategy.java index 6024c3571f4cb382b5cebeedc2e3e4acce028095..d61e23732a4cbad35a4242e52bd364eeeaf96f98 100644 --- a/core/src/main/java/hudson/PluginStrategy.java +++ b/core/src/main/java/hudson/PluginStrategy.java @@ -77,4 +77,12 @@ public interface PluginStrategy extends ExtensionPoint { * @since 1.400 */ List> findComponents(Class type, Hudson hudson); + + /** + * Called when a plugin that is depended by another plugin is newly deployed. + * + * @param depender plugin depending on dependee. + * @param dependee newly loaded plugin. + */ + void updateDependency(PluginWrapper depender, PluginWrapper dependee); } diff --git a/test/src/test/java/hudson/PluginManagerTest.java b/test/src/test/java/hudson/PluginManagerTest.java index 6add26b1078b772547b8ac096f0377d88c667ef1..b068598c94fb2d99cc47fd1b29c58a76d661661e 100644 --- a/test/src/test/java/hudson/PluginManagerTest.java +++ b/test/src/test/java/hudson/PluginManagerTest.java @@ -41,6 +41,7 @@ import org.jvnet.hudson.test.recipes.WithPlugin; import org.jvnet.hudson.test.recipes.WithPluginManager; import java.io.File; +import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; @@ -222,4 +223,129 @@ public class PluginManagerTest extends HudsonTestCase { // TODO required plugin installed but inactive } + // plugin "depender" optionally depends on plugin "dependee". + // they are written like this: + // org.jenkinsci.plugins.dependencytest.dependee: + // public class Dependee { + // public static String getValue() { + // return "dependee"; + // } + // } + // + // public abstract class DependeeExtensionPoint implements ExtensionPoint { + // } + // + // org.jenkinsci.plugins.dependencytest.depender: + // public class Depender { + // public static String getValue() { + // if (Jenkins.getInstance().getPlugin("dependee") != null) { + // return Dependee.getValue(); + // } + // return "depender"; + // } + // } + // + // @Extension(optional=true) + // public class DependerExtension extends DependeeExtensionPoint { + // } + + + /** + * call org.jenkinsci.plugins.dependencytest.depender.Depender.getValue(). + * + * @return + * @throws Exception + */ + private String callDependerValue() throws Exception { + Class c = jenkins.getPluginManager().uberClassLoader.loadClass("org.jenkinsci.plugins.dependencytest.depender.Depender"); + Method m = c.getMethod("getValue"); + return (String)m.invoke(null); + } + + /** + * Load "dependee" and then load "depender". + * Asserts that "depender" can access to "dependee". + * + * @throws Exception + */ + public void testInstallDependingPluginWithoutRestart() throws Exception { + // Load dependee. + { + String target = "dependee.hpi"; + URL src = getClass().getClassLoader().getResource(String.format("plugins/%s", target)); + File dest = new File(jenkins.getRootDir(), String.format("plugins/%s", target)); + FileUtils.copyURLToFile(src, dest); + jenkins.pluginManager.dynamicLoad(dest); + } + + // before load depender, of course failed to call Depender.getValue() + try { + callDependerValue(); + fail(); + } catch (ClassNotFoundException _) { + } + + // No extensions exist. + assertTrue(jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty()); + + // Load depender. + { + String target = "depender.hpi"; + URL src = getClass().getClassLoader().getResource(String.format("plugins/%s", target)); + File dest = new File(jenkins.getRootDir(), String.format("plugins/%s", target)); + FileUtils.copyURLToFile(src, dest); + jenkins.pluginManager.dynamicLoad(dest); + } + + // depender successfully accesses to dependee. + assertEquals("dependee", callDependerValue()); + + // Extension in depender is loaded. + assertFalse(jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty()); + } + + /** + * Load "depender" and then load "dependee". + * Asserts that "depender" can access to "dependee". + * + * @throws Exception + */ + public void testInstallDependedPluginWithoutRestart() throws Exception { + // Load depender. + { + String target = "depender.hpi"; + URL src = getClass().getClassLoader().getResource(String.format("plugins/%s", target)); + File dest = new File(jenkins.getRootDir(), String.format("plugins/%s", target)); + FileUtils.copyURLToFile(src, dest); + jenkins.pluginManager.dynamicLoad(dest); + } + + // before load dependee, depender does not access to dependee. + assertEquals("depender", callDependerValue()); + + // before load dependee, of course failed to list extensions for dependee. + try { + jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint"); + fail(); + } catch( ClassNotFoundException _ ){ + } + + // Load dependee. + { + String target = "dependee.hpi"; + URL src = getClass().getClassLoader().getResource(String.format("plugins/%s", target)); + File dest = new File(jenkins.getRootDir(), String.format("plugins/%s", target)); + FileUtils.copyURLToFile(src, dest); + jenkins.pluginManager.dynamicLoad(dest); + } + + // (MUST) Not throws an exception + // (SHOULD) depender successfully accesses to dependee. + assertEquals("dependee", callDependerValue()); + + // No extensions exist. + // extensions in depender is not loaded. + assertTrue(jenkins.getExtensionList("org.jenkinsci.plugins.dependencytest.dependee.DependeeExtensionPoint").isEmpty()); + } + } diff --git a/test/src/test/resources/plugins/dependee.hpi b/test/src/test/resources/plugins/dependee.hpi new file mode 100644 index 0000000000000000000000000000000000000000..0459ae63727d9427b4fe44137ef88d90413c3338 Binary files /dev/null and b/test/src/test/resources/plugins/dependee.hpi differ diff --git a/test/src/test/resources/plugins/depender.hpi b/test/src/test/resources/plugins/depender.hpi new file mode 100644 index 0000000000000000000000000000000000000000..bebd4b6943a835bdf14eeaae5cb0768862779a37 Binary files /dev/null and b/test/src/test/resources/plugins/depender.hpi differ