diff --git a/core/pom.xml b/core/pom.xml index 70adff7e8f..5f6c0a9acf 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -85,6 +85,21 @@ THE SOFTWARE. true + + + + org.sonatype.sisu + sisu-guice + 3.0.3 + + org.jruby.ext.posix jna-posix diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index 213df5f0c0..5fd1dd313b 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2004-2010, Sun Microsystems, Inc., InfraDNA, Inc. + * Copyright (c) 2004-2010, Sun Microsystems, Inc., InfraDNA, Inc., 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 @@ -23,8 +23,20 @@ */ package hudson; +import com.google.inject.AbstractModule; +import com.google.inject.Binding; +import com.google.inject.CreationException; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.name.Names; import com.google.common.collect.ImmutableList; import hudson.init.InitMilestone; +import hudson.model.Descriptor; import hudson.model.Hudson; import jenkins.model.Jenkins; import net.java.sezpoz.Index; @@ -32,7 +44,11 @@ import net.java.sezpoz.IndexItem; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import java.lang.annotation.Annotation; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; import java.util.logging.Logger; import java.util.logging.Level; import java.util.List; @@ -127,13 +143,216 @@ public abstract class ExtensionFinder implements ExtensionPoint { public void scout(Class extensionType, Hudson hudson) { } + @Extension + public static final class GuiceFinder extends AbstractGuiceFinder { + public GuiceFinder() { + super(Extension.class); + } + + @Override + protected boolean isOptional(Extension annotation) { + return annotation.optional(); + } + + @Override + protected double getOrdinal(Extension annotation) { + return annotation.ordinal(); + } + } + /** - * The default implementation that looks for the {@link Extension} marker. + * Discovers components via sezpoz but instantiates them by using Guice. + */ + public static abstract class AbstractGuiceFinder extends ExtensionFinder { + private Injector container; + + private final Map annotations = new HashMap(); + + public AbstractGuiceFinder(final Class annotationType) { + List modules = new ArrayList(); + modules.add(new SezpozModule(annotationType,Jenkins.getInstance().getPluginManager().uberClassLoader)); + + for (ExtensionComponent ec : new Sezpoz().find(Module.class, Hudson.getInstance())) { + modules.add(ec.getInstance()); + } + + try { + container = Guice.createInjector(modules); + } catch (CreationException e) { + LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e); + // failing to load all bindings are disastrous, so recover by creating minimum that works + // by just including the core + container = Guice.createInjector(new SezpozModule(annotationType,Jenkins.class.getClassLoader())); + } + } + + protected abstract double getOrdinal(T annotation); + + /** + * Hook to enable subtypes to control which ones to pick up and which ones to ignore. + */ + protected boolean isActive(AnnotatedElement e) { + return true; + } + + protected abstract boolean isOptional(T annotation); + + private Object instantiate(IndexItem item) { + try { + return item.instance(); + } catch (LinkageError e) { + // sometimes the instantiation fails in an indirect classloading failure, + // which results in a LinkageError + LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, + "Failed to load "+item.className(), e); + } catch (InstantiationException e) { + LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, + "Failed to load "+item.className(), e); + } + return null; + } + + public Collection> find(Class type, Hudson hudson) { + List> result = new ArrayList>(); + + for (Entry, Binding> e : container.getBindings().entrySet()) { + if (type.isAssignableFrom(e.getKey().getTypeLiteral().getRawType())) { + T a = annotations.get(e.getKey()); + Object o = e.getValue().getProvider().get(); + if (o!=null) + result.add(new ExtensionComponent(type.cast(o),a!=null?getOrdinal(a):0)); + } + } + + return result; + } + + /** + * TODO: need to learn more about concurrent access to {@link Injector} and how it interacts + * with classloading. + */ + @Override + public void scout(Class extensionType, Hudson hudson) { + } + + /** + * {@link Scope} that allows a failure to create a component, + * and change the value to null. + * + *

+ * This is necessary as a failure to load one plugin shouldn't fail the startup of the entire Jenkins. + * Instead, we should just drop the failing plugins. + */ + public static final Scope FAULT_TOLERANT_SCOPE = new Scope() { + public Provider scope(Key key, Provider unscoped) { + final Provider base = Scopes.SINGLETON.scope(key,unscoped); + return new Provider() { + public T get() { + try { + return base.get(); + } catch (Exception e) { + LOGGER.log(Level.WARNING,"Failed to instantiate",e); + return null; + } + } + }; + } + }; + + private static final Logger LOGGER = Logger.getLogger(GuiceFinder.class.getName()); + + private class SezpozModule extends AbstractModule { + private final Class annotationType; + private final ClassLoader cl; + + public SezpozModule(Class annotationType, ClassLoader cl) { + this.annotationType = annotationType; + this.cl = cl; + } + + /** + * Guice performs various reflection operations on the class to figure out the dependency graph, + * and that process can cause additional classloading problems, which will fail the injector creation, + * which in turn has disastrous effect on the startup. + * + *

+ * Ultimately I'd like to isolate problems to plugins and selectively disable them, allowing + * Jenkins to start with plugins that work, but I haven't figured out how. + * + * So this is an attempt to detect subset of problems eagerly, by invoking various reflection + * operations and try to find non-existent classes early. + */ + private void resolve(Class c) { + try { + c.getGenericSuperclass(); + c.getGenericInterfaces(); + ClassLoader ecl = c.getClassLoader(); + Method m = ClassLoader.class.getDeclaredMethod("resolveClass", Class.class); + m.setAccessible(true); + m.invoke(ecl, c); + } catch (Exception x) { + throw (LinkageError)new LinkageError("Failed to resolve "+c).initCause(x); + } + } + + @SuppressWarnings({"unchecked", "ChainOfInstanceofChecks"}) + @Override + protected void configure() { + int id=0; + + for (final IndexItem item : Index.load(annotationType, Object.class, cl)) { + id++; + try { + AnnotatedElement e = item.element(); + if (!isActive(e)) continue; + T a = item.annotation(); + + if (e instanceof Class) { + Key key = Key.get((Class)e); + resolve((Class)e); + annotations.put(key,a); + bind(key).in(FAULT_TOLERANT_SCOPE); + } else { + Class extType; + if (e instanceof Field) { + extType = ((Field)e).getType(); + } else + if (e instanceof Method) { + extType = ((Method)e).getReturnType(); + } else + throw new AssertionError(); + + resolve(extType); + + // use arbitrary id to make unique key, because Guice wants that. + Key key = Key.get(extType, Names.named(String.valueOf(id))); + annotations.put(key,a); + bind(key).toProvider(new Provider() { + public Object get() { + return instantiate(item); + } + }).in(FAULT_TOLERANT_SCOPE); + } + } catch (LinkageError e) { + // sometimes the instantiation fails in an indirect classloading failure, + // which results in a LinkageError + LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, + "Failed to load "+item.className(), e); + } catch (InstantiationException e) { + LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, + "Failed to load "+item.className(), e); + } + } + } + } + } + + /** + * The bootstrap implementation that looks for the {@link Extension} marker. * *

* Uses Sezpoz as the underlying mechanism. */ - @Extension public static final class Sezpoz extends ExtensionFinder { private volatile List> indices; @@ -230,6 +449,6 @@ public abstract class ExtensionFinder implements ExtensionPoint { return item.annotation().optional() ? Level.FINE : Level.WARNING; } } - + private static final Logger LOGGER = Logger.getLogger(ExtensionFinder.class.getName()); } diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 54ed063e8a..a8582d4a38 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -502,7 +502,9 @@ public final class FilePath implements Serializable { f.setLastModified(e.getTime()); try { FilePath target = new FilePath(f); - target.chmod(e.getUnixMode()); + int mode = e.getUnixMode(); + if (mode!=0) // Ant returns 0 if the archive doesn't record the access mode + target.chmod(mode); } catch (InterruptedException ex) { LOGGER.log(Level.WARNING, "unable to set permissions", ex); } diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 3079b5056f..ef05f794d6 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -27,6 +27,8 @@ import static hudson.init.InitMilestone.PLUGINS_PREPARED; import static hudson.init.InitMilestone.PLUGINS_STARTED; import static hudson.init.InitMilestone.PLUGINS_LISTED; +import com.google.inject.Guice; +import com.google.inject.Injector; import hudson.PluginWrapper.Dependency; import hudson.init.InitStrategy; import hudson.init.InitializerFinder; @@ -277,6 +279,8 @@ public abstract class PluginManager extends AbstractModelObject { Jenkins.getInstance().lookup.set(PluginInstanceStore.class,new PluginInstanceStore()); TaskGraphBuilder g = new TaskGraphBuilder(); + Jenkins.getInstance().lookup.set(Injector.class,Guice.createInjector()); + // schedule execution of loading plugins for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) { g.followedBy().notFatal().attains(PLUGINS_PREPARED).add("Loading plugin " + p.getShortName(), new Executable() { diff --git a/core/src/main/java/hudson/util/Iterators.java b/core/src/main/java/hudson/util/Iterators.java index b93c564528..f01b2635ca 100644 --- a/core/src/main/java/hudson/util/Iterators.java +++ b/core/src/main/java/hudson/util/Iterators.java @@ -23,6 +23,8 @@ */ package hudson.util; +import com.google.common.base.Predicates; + import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -276,12 +278,7 @@ public class Iterators { * Wraps another iterator and throws away nulls. */ public static Iterator removeNull(final Iterator itr) { - return new FilterIterator(itr) { - @Override - protected boolean filter(T t) { - return t!=null; - } - }; + return com.google.common.collect.Iterators.filter(itr, Predicates.notNull()); } /** @@ -302,8 +299,31 @@ public class Iterators { }; } + /** + * Filters another iterator by eliminating duplicates. + */ + public static Iterator removeDups(Iterator iterator) { + return new FilterIterator(iterator) { + final Set found = new HashSet(); + @Override + protected boolean filter(T t) { + return found.add(t); + } + }; + } + + /** + * Filters another iterator by eliminating duplicates. + */ + public static Iterable removeDups(final Iterable base) { + return new Iterable() { + public Iterator iterator() { + return removeDups(base.iterator()); + } + }; + } + public static Iterator sequence(Iterator... iterators) { return com.google.common.collect.Iterators.concat(iterators); } - } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index c21880860f..d36781dda0 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -25,6 +25,7 @@ */ package jenkins.model; +import com.google.inject.Injector; import hudson.model.Messages; import hudson.model.Node; import hudson.model.AbstractCIBase; @@ -1939,6 +1940,16 @@ public class Jenkins extends AbstractCIBase implements ModifiableItemGroup animals; + + @Inject + public Set cute; + + /* + If Cat has @Singleton, multiple bindings result in the same instance. + How can I achieve that without changing annotation? + */ + + public static void main(String[] args) { +// Injector i = Guice.createInjector(new AbstractModule() { +// @Override +// protected void configure() { +// bind(Cat.class).in(Singleton.class); +// +// Multibinder ab = Multibinder.newSetBinder(binder(), Animal.class); +// ab.addBinding().to(Dog.class); +// ab.addBinding().to(Cat.class); +// +// Multibinder cb = Multibinder.newSetBinder(binder(), Cute.class); +// cb.addBinding().to(Cat.class); +// cb.addBinding().to(Banana.class); +// } +// }); +// i.getInstance(Driver.class).run(); +// +// // Dog isn't singleton scoped, so you get a different instance, but Cat is singleton, +// // so you get the same value +// System.out.println(i.getInstance(Key.get(new TypeLiteral>(){}))); + } + + public void run() { + System.out.println(animals); + System.out.println(cute); + } +} diff --git a/test/src/main/java/org/jvnet/hudson/test/TestExtensionLoader.java b/test/src/main/java/org/jvnet/hudson/test/TestExtensionLoader.java index e9f050b618..b292a4da58 100644 --- a/test/src/main/java/org/jvnet/hudson/test/TestExtensionLoader.java +++ b/test/src/main/java/org/jvnet/hudson/test/TestExtensionLoader.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2004-2010, Sun Microsystems, Inc. + * Copyright (c) 2004-2010, Sun Microsystems, Inc., 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 @@ -24,20 +24,11 @@ package org.jvnet.hudson.test; import hudson.Extension; -import hudson.ExtensionComponent; -import hudson.ExtensionFinder; -import hudson.model.Hudson; -import net.java.sezpoz.Index; -import net.java.sezpoz.IndexItem; +import hudson.ExtensionFinder.AbstractGuiceFinder; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; /** * Loads {@link TestExtension}s. @@ -45,53 +36,37 @@ import java.util.logging.Logger; * @author Kohsuke Kawaguchi */ @Extension -public class TestExtensionLoader extends ExtensionFinder { +public class TestExtensionLoader extends AbstractGuiceFinder { + public TestExtensionLoader() { + super(TestExtension.class); + } + @Override - public Collection> find(Class type, Hudson hudson) { + protected boolean isOptional(TestExtension annotation) { + return false; + } + + @Override + protected double getOrdinal(TestExtension annotation) { + return 0; + } + + @Override + protected boolean isActive(AnnotatedElement e) { TestEnvironment env = TestEnvironment.get(); - List> result = new ArrayList>(); - - if (env==null) return result; // not in a test - - ClassLoader cl = hudson.getPluginManager().uberClassLoader; - for (IndexItem item : Index.load(TestExtension.class, Object.class, cl)) { - try { - AnnotatedElement e = item.element(); - Class extType; - if (e instanceof Class) { - extType = (Class) e; - if (!isActive(env, extType)) continue; - } else - if (e instanceof Field) { - Field f = (Field) e; - if (!f.getDeclaringClass().isInstance(env.testCase)) - continue; // not executing the enclosing test - extType = f.getType(); - } else - if (e instanceof Method) { - Method m = (Method) e; - if (!m.getDeclaringClass().isInstance(env.testCase)) - continue; // not executing the enclosing test - extType = m.getReturnType(); - } else - throw new AssertionError(); - - String testName = item.annotation().value(); - if (testName.length()>0 && !env.testCase.getName().equals(testName)) - continue; // doesn't apply to this test - - if(type.isAssignableFrom(extType)) { - Object instance = item.instance(); - if(instance!=null) - result.add(new ExtensionComponent(type.cast(instance))); - } - } catch (InstantiationException e) { - LOGGER.log(Level.WARNING, "Failed to load "+item.className(),e); - } + if (e instanceof Class) { + return isActive(env, (Class)e); } - - return result; + if (e instanceof Field) { + Field f = (Field) e; + return f.getDeclaringClass().isInstance(env.testCase); + } + if (e instanceof Method) { + Method m = (Method) e; + return m.getDeclaringClass().isInstance(env.testCase); + } + return false; } private boolean isActive(TestEnvironment env, Class extType) { @@ -102,6 +77,4 @@ public class TestExtensionLoader extends ExtensionFinder { return true; // enclosed return false; } - - private static final Logger LOGGER = Logger.getLogger(TestExtensionLoader.class.getName()); } diff --git a/test/src/test/java/hudson/ExtensionFinderTest.java b/test/src/test/java/hudson/ExtensionFinderTest.java new file mode 100644 index 0000000000..7f6c998340 --- /dev/null +++ b/test/src/test/java/hudson/ExtensionFinderTest.java @@ -0,0 +1,120 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2010, 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; + +import com.google.inject.AbstractModule; +import hudson.model.PageDecorator; +import org.jvnet.hudson.test.HudsonTestCase; +import org.jvnet.hudson.test.TestEnvironment; +import org.jvnet.hudson.test.TestExtension; + +import javax.inject.Inject; +import javax.inject.Qualifier; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author Kohsuke Kawaguchi + */ +public class ExtensionFinderTest extends HudsonTestCase { + /** + * It's OK for some extensions to fail to load. The system needs to tolerate that. + */ + public void testFailingInstance() { + FailingExtension i = PageDecorator.all().get(FailingExtension.class); + assertNull("Instantiation should have failed",i); + assertTrue("Instantiation should have been attempted", FailingExtension.error); + } + + @TestExtension("testFailingInstance") + public static class FailingExtension extends PageDecorator { + public FailingExtension() { + super(FailingExtension.class); + error = true; + throw new LinkageError(); // this component fails to load + } + public static boolean error; + } + + + + + + /** + * Extensions are Guice components, so it should support injection. + */ + public void testInjection() { + InjectingExtension i = PageDecorator.all().get(InjectingExtension.class); + assertNotNull(i.foo); + assertEquals("lion king",i.value); + } + + @TestExtension("testInjection") + public static class InjectingExtension extends PageDecorator { + @Inject + Foo foo; + + @Inject @LionKing + String value; + + + public InjectingExtension() { + super(InjectingExtension.class); + } + public static class Foo {} + } + + + @Retention(RetentionPolicy.RUNTIME) @Qualifier + public static @interface LionKing {} + + @Extension + public static class ModuleImpl extends AbstractModule { + protected void configure() { + if (TestEnvironment.get().testCase instanceof ExtensionFinderTest) { + bind(String.class).annotatedWith(LionKing.class).toInstance("lion king"); + } + } + } + + + /** + * Tests the error recovery behaviour. + * + * One failure in binding definition shouldn't prevent Jenkins from booting. + */ + public void testErrorRecovery() { + BrokenExtension i = PageDecorator.all().get(BrokenExtension.class); + assertNull(i); + } + + @TestExtension("testErrorRecovery") + public static class BrokenExtension extends PageDecorator { + @Inject Comparable c; + + public BrokenExtension() { + super(InjectingExtension.class); + } + } +}