diff --git a/core/pom.xml b/core/pom.xml index c5c0943ca0..4bd209eab0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -362,6 +362,11 @@ THE SOFTWARE. org.jvnet.hudson annotation-indexer + 1.2 + + + org.jvnet.hudson + task-reactor 1.1 diff --git a/core/src/main/java/hudson/BulkChange.java b/core/src/main/java/hudson/BulkChange.java index 9c9047a9de..9625452c95 100644 --- a/core/src/main/java/hudson/BulkChange.java +++ b/core/src/main/java/hudson/BulkChange.java @@ -157,8 +157,18 @@ public class BulkChange { */ public static boolean contains(Saveable s) { for(BulkChange b=current(); b!=null; b=b.parent) - if(b.saveable== s) + if(b.saveable==s || b.saveable==ALL) return true; return false; } + + /** + * Magic {@link Saveable} instance that can make {@link BulkChange} veto + * all the save operations by making the {@link #contains(Saveable)} method return + * true for everything. + */ + public static final Saveable ALL = new Saveable() { + public void save() { + } + }; } diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index c0c93e2c04..ad6d956dd5 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -72,16 +72,16 @@ public class ClassicPluginStrategy implements PluginStrategy { } public PluginWrapper createPluginWrapper(File archive) throws IOException { - LOGGER.info("Loading plugin: " + archive); + LOGGER.info("Preparing plugin: " + archive); final Manifest manifest; URL baseResourceURL; - boolean isLinked = archive.getName().endsWith(".hpl"); File expandDir = null; // if .hpi, this is the directory where war is expanded + boolean isLinked = archive.getName().endsWith(".hpl"); if (isLinked) { // resolve the .hpl file to the location of the manifest file String firstLine = new BufferedReader(new FileReader(archive)) @@ -102,21 +102,25 @@ public class ClassicPluginStrategy implements PluginStrategy { in.close(); } } else { - expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive)); - explode(archive, expandDir); + if (archive.isDirectory()) {// already expanded + expandDir = archive; + } else { + expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive)); + explode(archive, expandDir); + } - File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF"); - if (!manifestFile.exists()) { - throw new IOException( - "Plugin installation failed. No manifest at " - + manifestFile); - } - FileInputStream fin = new FileInputStream(manifestFile); - try { - manifest = new Manifest(fin); - } finally { - fin.close(); - } + File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF"); + if (!manifestFile.exists()) { + throw new IOException( + "Plugin installation failed. No manifest at " + + manifestFile); + } + FileInputStream fin = new FileInputStream(manifestFile); + try { + manifest = new Manifest(fin); + } finally { + fin.close(); + } } final Attributes atts = manifest.getMainAttributes(); @@ -327,8 +331,6 @@ public class ClassicPluginStrategy implements PluginStrategy { if(explodeTime.exists() && explodeTime.lastModified()==archive.lastModified()) return; // no need to expand - LOGGER.info("Extracting "+archive); - // delete the contents so that old files won't interfere with new files Util.deleteContentsRecursive(destDir); diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index ad4d620218..5ea668fb70 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -23,17 +23,25 @@ */ package hudson; +import static hudson.init.InitMilestone.PLUGINS_PREPARED; +import static hudson.init.InitMilestone.PLUGINS_STARTED; +import static hudson.init.InitMilestone.PLUGINS_LISTED; +import hudson.init.InitStrategy; import hudson.model.AbstractModelObject; import hudson.model.Failure; import hudson.model.Hudson; -import hudson.model.UpdateSite; import hudson.model.UpdateCenter; +import hudson.model.UpdateSite; import hudson.util.Service; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.LogFactory; +import org.jvnet.hudson.reactor.Executable; +import org.jvnet.hudson.reactor.Reactor; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.hudson.reactor.TaskGraphBuilder; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; @@ -44,17 +52,17 @@ import org.kohsuke.stapler.WebApp; import javax.servlet.ServletContext; import javax.servlet.ServletException; import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.Map; +import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -102,7 +110,7 @@ public final class PluginManager extends AbstractModelObject { /** * Strategy for creating and initializing plugins */ - private PluginStrategy strategy; + private final PluginStrategy strategy; public PluginManager(ServletContext context) { this.context = context; @@ -112,58 +120,8 @@ public final class PluginManager extends AbstractModelObject { rootDir = new File(Hudson.getInstance().getRootDir(),"plugins"); if(!rootDir.exists()) rootDir.mkdirs(); - - Collection bundledPlugins = loadBundledPlugins(); - - File[] archives = rootDir.listFiles(new FilenameFilter() { - public boolean accept(File dir, String name) { - return name.endsWith(".hpi") // plugin jar file - || name.endsWith(".hpl"); // linked plugin. for debugging. - } - }); - - if(archives==null) { - LOGGER.severe("Hudson is unable to create "+rootDir+"\nPerhaps its security privilege is insufficient"); - return; - } strategy = createPluginStrategy(); - - // load plugins from a system property, for use in the "mvn hudson-dev:run" - List archivesList = new ArrayList(Arrays.asList(archives)); - String hplProperty = System.getProperty("hudson.bundled.plugins"); - if (hplProperty != null) { - for (String hplLocation: hplProperty.split(",")) { - File hpl = new File(hplLocation.trim()); - if (hpl.exists()) - archivesList.add(hpl); - else - LOGGER.warning("bundled plugin " + hplLocation + " does not exist"); - } - } - - for( File arc : archivesList ) { - try { - PluginWrapper p = strategy.createPluginWrapper(arc); - p.isBundled = bundledPlugins.contains(arc.getName()); - plugins.add(p); - if(p.isActive()) - activePlugins.add(p); - } catch (IOException e) { - failedPlugins.add(new FailedPlugin(arc.getName(),e)); - LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + arc, e); - } - } - - for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) - try { - strategy.load(p); - } catch (IOException e) { - failedPlugins.add(new FailedPlugin(p.getShortName(),e)); - LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + p.getShortName(), e); - activePlugins.remove(p); - plugins.remove(p); - } } /** @@ -171,18 +129,114 @@ public final class PluginManager extends AbstractModelObject { * This is a separate method so that code executed from here will see a valid value in * {@link Hudson#pluginManager}. */ - public void initialize() { - for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) { - strategy.initializeComponents(p); - try { - p.getPlugin().postInitialize(); - } catch (Exception e) { - failedPlugins.add(new FailedPlugin(p.getShortName(), e)); - LOGGER.log(Level.SEVERE, "Failed to post-initialize a plug-in " + p.getShortName(), e); - activePlugins.remove(p); - plugins.remove(p); + public TaskBuilder initTasks(final InitStrategy initStrategy) { + return new TaskGraphBuilder() { + List archives; + Collection bundledPlugins; + + { + Handle loadBundledPlugins = add("Loading bundled plugins", new Executable() { + public void run(Reactor session) throws Exception { + bundledPlugins = loadBundledPlugins(); + } + }); + + Handle listUpPlugins = requires(loadBundledPlugins).add("Listing up plugins", new Executable() { + public void run(Reactor session) throws Exception { + archives = initStrategy.listPluginArchives(PluginManager.this); + } + }); + + requires(listUpPlugins).attains(PLUGINS_LISTED).add("Preparing plugins",new Executable() { + public void run(Reactor session) throws Exception { + TaskGraphBuilder g = new TaskGraphBuilder(); + + final Map inspectedShortNames = new HashMap(); + + for( final File arc : archives ) { + g.followedBy().notFatal().attains(PLUGINS_LISTED).add("Inspecting plugin " + arc, new Executable() { + public void run(Reactor session) throws Exception { + try { + PluginWrapper p = strategy.createPluginWrapper(arc); + if (isDuplicate(p)) return; + + p.isBundled = bundledPlugins.contains(arc.getName()); + plugins.add(p); + if(p.isActive()) + activePlugins.add(p); + } catch (IOException e) { + failedPlugins.add(new FailedPlugin(arc.getName(),e)); + throw e; + } + } + + /** + * Inspects duplication. this happens when you run hpi:run on a bundled plugin, + * as well as putting numbered hpi files, like "cobertura-1.0.hpi" and "cobertura-1.1.hpi" + */ + private boolean isDuplicate(PluginWrapper p) { + String shortName = p.getShortName(); + if (inspectedShortNames.containsKey(shortName)) { + LOGGER.info("Ignoring "+arc+" because "+inspectedShortNames.get(shortName)+" is already loaded"); + return true; + } + + inspectedShortNames.put(shortName,arc); + return false; + } + }); + } + + g.requires(PLUGINS_LISTED).attains(PLUGINS_PREPARED).add("Loading plugins",new Executable() { + /** + * Once the plugins are listed, schedule their initialization. + */ + public void run(Reactor session) throws Exception { + TaskGraphBuilder g = new TaskGraphBuilder(); + + // 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() { + public void run(Reactor session) throws Exception { + try { + strategy.load(p); + } catch (IOException e) { + failedPlugins.add(new FailedPlugin(p.getShortName(), e)); + activePlugins.remove(p); + plugins.remove(p); + throw e; + } + } + }); + } + + // schedule execution of initializing plugins + for (final PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) { + g.followedBy().notFatal().attains(PLUGINS_STARTED).add("Initializing plugin " + p.getShortName(), new Executable() { + public void run(Reactor session) throws Exception { + try { + p.getPlugin().postInitialize(); + } catch (Exception e) { + failedPlugins.add(new FailedPlugin(p.getShortName(), e)); + activePlugins.remove(p); + plugins.remove(p); + throw e; + } + } + }); + } + + // register them all + session.addAll(g.discoverTasks(session)); + } + }); + + // register them all + session.addAll(g.discoverTasks(session)); + } + }); } - } + }; } /** diff --git a/core/src/main/java/hudson/PluginStrategy.java b/core/src/main/java/hudson/PluginStrategy.java index 8336474567..0fe0e7233b 100644 --- a/core/src/main/java/hudson/PluginStrategy.java +++ b/core/src/main/java/hudson/PluginStrategy.java @@ -41,8 +41,7 @@ public interface PluginStrategy extends ExtensionPoint { /** * Creates a plugin wrapper, which provides a management interface for the plugin * @param archive - * @return - * @throws IOException + * Either a directory that points to a pre-exploded plugin, or an hpi file, or an hpl file. */ public abstract PluginWrapper createPluginWrapper(File archive) throws IOException; diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index 4f610a0910..0f15f063a2 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -295,8 +295,7 @@ public final class PluginWrapper { try { plugin.stop(); } catch(Throwable t) { - System.err.println("Failed to shut down "+shortName); - System.err.println(t); + LOGGER.log(WARNING, "Failed to shut down "+shortName, t); } // Work around a bug in commons-logging. // See http://www.szegedi.org/articles/memleak.html diff --git a/core/src/main/java/hudson/init/InitMilestone.java b/core/src/main/java/hudson/init/InitMilestone.java new file mode 100644 index 0000000000..8bf7a8f03c --- /dev/null +++ b/core/src/main/java/hudson/init/InitMilestone.java @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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.init; + +import org.jvnet.hudson.reactor.Executable; +import org.jvnet.hudson.reactor.Milestone; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.hudson.reactor.TaskGraphBuilder; + +/** + * Various key milestone in the initialization process of Hudson. + * + *

+ * Plugins can use these milestones to execute their initialization at the right moment + * (in addition to defining their own milestones by implementing {@link Milestone}. + * + *

+ * These milestones are achieve in this order. + * + * @author Kohsuke Kawaguchi + */ +public enum InitMilestone implements Milestone { + /** + * The very first milestone that gets achieved without doing anything. + * + * This is used in {@link Initializer#after()} since annotations cannot have null as the default value. + */ + STARTED, + + /** + * By this milestone, all plugins metadata are inspected and their dependencies figured out. + */ + PLUGINS_LISTED, + + /** + * By this milestone, all plugin metadata are loaded and its classloader set up. + */ + PLUGINS_PREPARED, + /** + * By this milestone, all plugins start executing. + * + *

+ * This is a separate milestone from {@link #PLUGINS_PREPARED} since the execution + * of a plugin often involves finding extension point implementations, which in turn + * require all the classes from all the plugins to be loadable. + */ + PLUGINS_STARTED, + /** + * By this milestone, all jobs and their build records are loaded from disk. + */ + JOB_LOADED, + /** + * The very last milestone + * + * This is used in {@link Initializer#before()} since annotations cannot have null as the default value. + */ + COMPLETED; + + + /** + * Creates a set of dummy tasks to enforce ordering among {@link InitMilestone}s. + */ + public static TaskBuilder ordering() { + TaskGraphBuilder b = new TaskGraphBuilder(); + InitMilestone[] v = values(); + for (int i=0; i + * Because the act of initializing plugins is a part of the Hudson initialization, + * this extension point cannot be implemented in a plugin. You need to place your jar + * inside {@code WEB-INF/lib} instead. + * + *

+ * To register, put {@link MetaInfServices} on your implementation. + * + * @author Kohsuke Kawaguchi + * @see Hudson#buildReactorListener() + */ +public interface InitReactorListener extends ReactorListener { +} diff --git a/core/src/main/java/hudson/init/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java new file mode 100644 index 0000000000..8a3ea4f3ae --- /dev/null +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -0,0 +1,101 @@ +package hudson.init; + +import org.kohsuke.MetaInfServices; +import org.jvnet.hudson.reactor.Task; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.logging.Logger; + +import hudson.PluginManager; +import hudson.util.ServiceLoader; + +/** + * Strategy pattern of the various key decision making during the Hudson initialization. + * + *

+ * Because the act of initializing plugins is a part of the Hudson initialization, + * this extension point cannot be implemented in a plugin. You need to place your jar + * inside {@code WEB-INF/lib} instead. + * + *

+ * To register, put {@link MetaInfServices} on your implementation. + * + * @author Kohsuke Kawaguchi + */ +public class InitStrategy { + /** + * Returns the list of *.hpi and *.hpl to expand and load. + * + *

+ * Normally we look at {@code $HUDSON_HOME/plugins/*.hpi} and *.hpl. + * + * @return + * never null but can be empty. The list can contain different versions of the same plugin, + * and when that happens, Hudson will ignore all but the first one in the list. + */ + public List listPluginArchives(PluginManager pm) throws IOException { + File[] archives = pm.rootDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".hpi") // plugin jar file + || name.endsWith(".hpl"); // linked plugin. for debugging. + } + }); + if (archives == null) + throw new IOException("Hudson is unable to create " + pm.rootDir + "\nPerhaps its security privilege is insufficient"); + + List r = new ArrayList(); + getBundledPluginsFromProperty(r); + r.addAll(Arrays.asList(archives)); + + return r; + } + + /** + * Lists up additional bundled plugins from the system property. + * + * For use in the "mvn hudson-dev:run". + * TODO: maven-hpi-plugin should inject its own InitStrategy instead of having this in the core. + */ + protected void getBundledPluginsFromProperty(List r) { + String hplProperty = System.getProperty("hudson.bundled.plugins"); + if (hplProperty != null) { + for (String hplLocation : hplProperty.split(",")) { + File hpl = new File(hplLocation.trim()); + if (hpl.exists()) + r.add(hpl); + else + LOGGER.warning("bundled plugin " + hplLocation + " does not exist"); + } + } + } + + /** + * Selectively skip some of the initialization tasks. + * + * @return + * true to skip the execution. + */ + public boolean skipInitTask(Task task) { + return false; + } + + + /** + * Obtains the instance to be used. + */ + public static InitStrategy get(ClassLoader cl) throws IOException { + List r = ServiceLoader.load(cl, InitStrategy.class); + if (r.isEmpty()) return new InitStrategy(); // default + + InitStrategy s = r.get(0); + LOGGER.fine("Using "+s+" as InitStrategy"); + return s; + } + + private static final Logger LOGGER = Logger.getLogger(InitStrategy.class.getName()); +} diff --git a/core/src/main/java/hudson/init/Initializer.java b/core/src/main/java/hudson/init/Initializer.java new file mode 100644 index 0000000000..2f74a10e40 --- /dev/null +++ b/core/src/main/java/hudson/init/Initializer.java @@ -0,0 +1,92 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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.init; + +import org.jvnet.hudson.annotation_indexer.Indexed; +import org.jvnet.hudson.reactor.Task; + +import java.lang.annotation.Documented; +import static java.lang.annotation.ElementType.METHOD; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Target; + +import static hudson.init.InitMilestone.STARTED; +import static hudson.init.InitMilestone.COMPLETED; + +/** + * Placed on static methods to indicate that this method is to be run during the Hudson start up to perform + * some sort of initialization tasks. + * + * @author Kohsuke Kawaguchi + */ +@Indexed +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Initializer { + /** + * Indicates that the specified milestone is necessary before executing this initializer. + * + *

+ * This has the identical purpose as {@link #requires()}, but it's separated to allow better type-safety + * when using {@link InitMilestone} as a requirement (since enum member definitions need to be constant.) + */ + InitMilestone after() default STARTED; + + /** + * Indicates that this initializer is a necessary step before achieving the specified milestone. + * + *

+ * This has the identical purpose as {@link #attains()}. See {@link #after()} for why there are two things + * to achieve the same goal. + */ + InitMilestone before() default COMPLETED; + + /** + * Indicates the milestones necessary before executing this initializer. + */ + String[] requires() default {}; + + /** + * Indicates the milestones that this initializer contributes to. + * + * A milestone is considered attained if all the initializers that attains the given milestone + * completes. So it works as a kind of join. + */ + String[] attains() default {}; + + /** + * Key in Messages.properties that represents what this task is about. Used for rendering the progress. + * Defaults to "${short class name}.${method Name}". + */ + String displayName() default ""; + + /** + * Should the failure in this task prevent Hudson from starting up? + * + * @see Task#failureIsFatal() + */ + boolean fatal() default true; +} diff --git a/core/src/main/java/hudson/init/InitializerFinder.java b/core/src/main/java/hudson/init/InitializerFinder.java new file mode 100644 index 0000000000..c181d4a664 --- /dev/null +++ b/core/src/main/java/hudson/init/InitializerFinder.java @@ -0,0 +1,183 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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.init; + +import org.jvnet.hudson.annotation_indexer.Index; +import org.jvnet.hudson.reactor.Milestone; +import org.jvnet.hudson.reactor.Task; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.hudson.reactor.MilestoneImpl; +import org.jvnet.hudson.reactor.Reactor; +import org.jvnet.localizer.ResourceBundleHolder; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import hudson.model.Hudson; + +/** + * Discovers initialization tasks from {@link Initializer}. + * + * @author Kohsuke Kawaguchi + */ +public class InitializerFinder extends TaskBuilder { + private final ClassLoader cl; + + public InitializerFinder(ClassLoader cl) { + this.cl = cl; + } + + public InitializerFinder() { + this(Thread.currentThread().getContextClassLoader()); + } + + public Collection discoverTasks(Reactor session) throws IOException { + List result = new ArrayList(); + for (Method e : Index.list(Initializer.class,cl,Method.class)) { + if (!Modifier.isStatic(e.getModifiers())) + throw new IOException(e+" is not a static method"); + + Initializer i = e.getAnnotation(Initializer.class); + if (i==null) continue; // stale index + + result.add(new TaskImpl(i, e)); + } + return result; + } + + /** + * Obtains the display name of the given initialization task + */ + protected String getDisplayNameOf(Method e, Initializer i) { + try { + Class c = e.getDeclaringClass(); + ResourceBundleHolder rb = ResourceBundleHolder.get(c.getClassLoader().loadClass(c.getPackage().getName() + ".Messages")); + + String key = i.displayName(); + if (key.length()==0) return c.getSimpleName()+"."+e.getName(); + return rb.format(key); + } catch (ClassNotFoundException x) { + throw (Error)new NoClassDefFoundError(x.getMessage()+" for "+e.toString()).initCause(x); + } + } + + /** + * Invokes the given initialization method. + */ + protected void invoke(Method e) { + try { + Class[] pt = e.getParameterTypes(); + Object[] args = new Object[pt.length]; + for (int i=0; i type) { + if (type== Hudson.class) + return Hudson.getInstance(); + throw new IllegalArgumentException("Unable to inject "+type); + } + + /** + * Task implementation. + */ + public class TaskImpl implements Task { + final Collection requires; + final Collection attains; + private final Initializer i; + private final Method e; + + private TaskImpl(Initializer i, Method e) { + this.i = i; + this.e = e; + requires = toMilestones(i.requires(), i.after()); + attains = toMilestones(i.attains(), i.before()); + } + + /** + * {@link Initializer} annotaion on the {@linkplain #getMethod() method} + */ + public Initializer getAnnotation() { + return i; + } + + /** + * Static method that runs the initialization, that this task wraps. + */ + public Method getMethod() { + return e; + } + + public Collection requires() { + return requires; + } + + public Collection attains() { + return attains; + } + + public String getDisplayName() { + return getDisplayNameOf(e, i); + } + + public boolean failureIsFatal() { + return i.fatal(); + } + + public void run(Reactor session) { + invoke(e); + } + + public String toString() { + return e.toString(); + } + + private Collection toMilestones(String[] tokens, InitMilestone m) { + List r = new ArrayList(); + for (String s : tokens) { + try { + r.add(InitMilestone.valueOf(s)); + } catch (IllegalArgumentException x) { + r.add(new MilestoneImpl(s)); + } + } + r.add(m); + return r; + } + } +} diff --git a/core/src/main/java/hudson/init/impl/GroovyInitScript.java b/core/src/main/java/hudson/init/impl/GroovyInitScript.java new file mode 100644 index 0000000000..aa4a1860c7 --- /dev/null +++ b/core/src/main/java/hudson/init/impl/GroovyInitScript.java @@ -0,0 +1,54 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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.init.impl; + +import groovy.lang.GroovyShell; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import hudson.model.Hudson; +import static hudson.init.InitMilestone.JOB_LOADED; +import hudson.init.Initializer; + +/** + * Run the initialization script, if it exists. + * + * @author Kohsuke Kawaguchi + */ +public class GroovyInitScript { + @Initializer(after=JOB_LOADED) + public static void init(Hudson h) throws IOException { + + File initScript = new File(h.getRootDir(),"init.groovy"); + if(initScript.exists()) { + LOGGER.info("Executing "+initScript); + GroovyShell shell = new GroovyShell(h.getPluginManager().uberClassLoader); + shell.evaluate(initScript); + } + } + + private static final Logger LOGGER = Logger.getLogger(GroovyInitScript.class.getName()); +} diff --git a/core/src/main/java/hudson/init/impl/InitialUserContent.java b/core/src/main/java/hudson/init/impl/InitialUserContent.java new file mode 100644 index 0000000000..c00e89ca0b --- /dev/null +++ b/core/src/main/java/hudson/init/impl/InitialUserContent.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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.init.impl; + +import static hudson.init.InitMilestone.JOB_LOADED; +import hudson.init.Initializer; +import hudson.model.Hudson; +import hudson.model.Messages; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; + +/** + * Prepares userContent folder and put a readme if it doesn't exist. + * @author Kohsuke Kawaguchi + */ +public class InitialUserContent { + @Initializer(after=JOB_LOADED) + public static void init(Hudson h) throws IOException { + File userContentDir = new File(h.getRootDir(), "userContent"); + if(!userContentDir.exists()) { + userContentDir.mkdirs(); + FileUtils.writeStringToFile(new File(userContentDir,"readme.txt"), Messages.Hudson_USER_CONTENT_README()); + } + } +} diff --git a/core/src/main/java/hudson/init/package-info.java b/core/src/main/java/hudson/init/package-info.java new file mode 100644 index 0000000000..b19a2a705e --- /dev/null +++ b/core/src/main/java/hudson/init/package-info.java @@ -0,0 +1,47 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, 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. + */ + +/** + * Logic for Hudson startup. + * + *

+ * Hudson's start up is based on the same idea as the modern Unix init mechanism like initng/upstart/SMF. + * It first builds a set of {@link Task}s that are units of the initialization work, and have them declare + * dependencies among themselves. For example, jobs are only loaded after all the plugins are initialized, + * and restoring the build queue requires all the jobs to be loaded. + * + *

+ * Such micro-scopic dependencies are organized into a bigger directed acyclic graph, which is then executed + * via {@link Session}. During execution of the reactor, additional tasks can be discovred and added to + * the DAG. We use this additional indirection to: + * + *

    + *
  1. Perform initialization in parallel where possible. + *
  2. Provide progress report on where we are in the initialization. + *
  3. Collect status of the initialization and their failures. + *
+ */ +package hudson.init; + +import org.jvnet.hudson.reactor.Task; \ No newline at end of file diff --git a/core/src/main/java/hudson/logging/LogRecorderManager.java b/core/src/main/java/hudson/logging/LogRecorderManager.java index b864b51362..dc734a474f 100644 --- a/core/src/main/java/hudson/logging/LogRecorderManager.java +++ b/core/src/main/java/hudson/logging/LogRecorderManager.java @@ -25,6 +25,7 @@ package hudson.logging; import hudson.FeedAdapter; import hudson.Functions; +import hudson.init.Initializer; import hudson.model.AbstractModelObject; import hudson.model.Hudson; import hudson.model.RSS; @@ -177,4 +178,9 @@ public class LogRecorderManager extends AbstractModelObject { } },req,rsp); } + + @Initializer + public static void init(Hudson h) throws IOException { + h.getLog().load(); + } } diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index 6fac89f0fd..fb67243ef6 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -24,8 +24,12 @@ package hudson.model; import com.thoughtworks.xstream.XStream; -import groovy.lang.GroovyShell; import hudson.BulkChange; +import hudson.DescriptorExtensionList; +import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionListView; +import hudson.ExtensionPoint; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; @@ -36,35 +40,34 @@ import hudson.PluginWrapper; import hudson.ProxyConfiguration; import hudson.StructuredForm; import hudson.TcpSlaveAgentListener; +import hudson.UDPBroadcastThread; import hudson.Util; import static hudson.Util.fixEmpty; import static hudson.Util.fixNull; import hudson.WebAppMain; import hudson.XmlFile; -import hudson.UDPBroadcastThread; -import hudson.ExtensionList; -import hudson.ExtensionPoint; -import hudson.DescriptorExtensionList; -import hudson.ExtensionListView; -import hudson.Extension; -import hudson.tools.ToolInstallation; -import hudson.tools.ToolDescriptor; import hudson.cli.CliEntryPoint; import hudson.cli.CliManagerImpl; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; -import hudson.logging.LogRecorderManager; +import static hudson.init.InitMilestone.JOB_LOADED; +import static hudson.init.InitMilestone.PLUGINS_STARTED; +import hudson.init.InitializerFinder; +import hudson.init.InitMilestone; +import hudson.init.InitReactorListener; +import hudson.init.InitStrategy; import hudson.lifecycle.Lifecycle; +import hudson.logging.LogRecorderManager; import hudson.lifecycle.RestartNotSupportedException; import hudson.model.Descriptor.FormException; import hudson.model.listeners.ItemListener; import hudson.model.listeners.JobListener; import hudson.model.listeners.JobListener.JobListenerAdapter; -import hudson.model.listeners.SaveableListener; import hudson.model.listeners.SCMListener; +import hudson.model.listeners.SaveableListener; +import hudson.remoting.Channel; import hudson.remoting.LocalChannel; import hudson.remoting.VirtualChannel; -import hudson.remoting.Channel; import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; @@ -82,70 +85,87 @@ import hudson.security.PermissionGroup; import hudson.security.SecurityMode; import hudson.security.SecurityRealm; import hudson.security.csrf.CrumbIssuer; -import hudson.slaves.ComputerListener; -import hudson.slaves.NodeProperty; -import hudson.slaves.NodePropertyDescriptor; -import hudson.slaves.RetentionStrategy; -import hudson.slaves.NodeList; import hudson.slaves.Cloud; +import hudson.slaves.ComputerListener; import hudson.slaves.DumbSlave; import hudson.slaves.NodeDescriptor; +import hudson.slaves.NodeList; +import hudson.slaves.NodeProperty; +import hudson.slaves.NodePropertyDescriptor; import hudson.slaves.NodeProvisioner; import hudson.slaves.OfflineCause; +import hudson.slaves.RetentionStrategy; import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.Mailer; import hudson.tasks.Publisher; +import hudson.tools.ToolDescriptor; +import hudson.tools.ToolInstallation; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; +import hudson.util.AdministrativeError; import hudson.util.CaseInsensitiveComparator; import hudson.util.ClockDifference; import hudson.util.CopyOnWriteList; import hudson.util.CopyOnWriteMap; import hudson.util.DaemonThreadFactory; +import hudson.util.DescribableList; +import hudson.util.FormValidation; +import hudson.util.Futures; import hudson.util.HudsonIsLoading; +import hudson.util.HudsonIsRestarting; +import hudson.util.Iterators; +import hudson.util.Memoizer; import hudson.util.MultipartFormDataParser; import hudson.util.RemotingDiagnostics; -import hudson.util.TextFile; -import hudson.util.XStream2; -import hudson.util.HudsonIsRestarting; -import hudson.util.DescribableList; -import hudson.util.Futures; -import hudson.util.Memoizer; -import hudson.util.Iterators; -import hudson.util.FormValidation; -import hudson.util.VersionNumber; import hudson.util.StreamTaskListener; -import hudson.util.AdministrativeError; +import hudson.util.TextFile; +import hudson.util.VersionNumber; +import hudson.util.XStream2; +import hudson.util.ServiceLoader; import hudson.widgets.Widget; import net.sf.json.JSONObject; -import org.acegisecurity.*; +import org.acegisecurity.AccessDeniedException; +import org.acegisecurity.AcegiSecurityException; +import org.acegisecurity.Authentication; +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.acegisecurity.ui.AbstractProcessingFilter; -import org.apache.commons.logging.LogFactory; -import org.apache.commons.jelly.Script; import org.apache.commons.jelly.JellyException; -import org.apache.commons.io.FileUtils; +import org.apache.commons.jelly.Script; +import org.apache.commons.logging.LogFactory; +import org.jvnet.hudson.reactor.Executable; +import org.jvnet.hudson.reactor.ReactorException; +import org.jvnet.hudson.reactor.Task; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.hudson.reactor.TaskGraphBuilder; +import org.jvnet.hudson.reactor.Milestone; +import org.jvnet.hudson.reactor.Reactor; +import org.jvnet.hudson.reactor.ReactorListener; +import org.jvnet.hudson.reactor.TaskGraphBuilder.Handle; import org.kohsuke.stapler.Ancestor; +import org.kohsuke.stapler.HttpRedirect; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.MetaClass; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerFallback; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.StaplerFallback; import org.kohsuke.stapler.WebApp; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.HttpRedirect; -import org.kohsuke.stapler.HttpResponses; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff; -import org.kohsuke.stapler.jelly.JellyRequestDispatcher; -import org.kohsuke.stapler.framework.adjunct.AdjunctManager; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.stapler.framework.adjunct.AdjunctManager; +import org.kohsuke.stapler.jelly.JellyClassLoaderTearOff; +import org.kohsuke.stapler.jelly.JellyRequestDispatcher; import org.xml.sax.InputSource; +import javax.crypto.SecretKey; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.Cookie; @@ -157,14 +177,15 @@ import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.io.PrintWriter; import java.io.InputStream; -import java.net.URL; +import java.io.PrintWriter; import java.net.BindException; +import java.net.URL; +import java.nio.charset.Charset; import java.security.SecureRandom; +import java.text.Collator; import java.text.NumberFormat; import java.text.ParseException; -import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -176,32 +197,29 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; import java.util.Set; import java.util.Stack; import java.util.StringTokenizer; import java.util.Timer; import java.util.TreeSet; -import java.util.Properties; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeoutException; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; +import static java.util.logging.Level.SEVERE; import java.util.logging.LogRecord; import java.util.logging.Logger; -import static java.util.logging.Level.SEVERE; import java.util.regex.Pattern; -import java.nio.charset.Charset; -import javax.servlet.RequestDispatcher; -import javax.crypto.SecretKey; /** * Root object of the system. @@ -509,7 +527,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl */ private transient final LogRecorderManager log = new LogRecorderManager(); - public Hudson(File root, ServletContext context) throws IOException, InterruptedException { + public Hudson(File root, ServletContext context) throws IOException, InterruptedException, ReactorException { // As hudson is starting, grant this process full controll SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); try { @@ -520,8 +538,9 @@ public final class Hudson extends Node implements ItemGroup, Stapl throw new IllegalStateException("second instance"); theInstance = this; - log.load(); - + // doing this early allows InitStrategy to set environment upfront + final InitStrategy is = InitStrategy.get(Thread.currentThread().getContextClassLoader()); + Trigger.timer = new Timer("Hudson cron thread"); queue = new Queue(CONSISTENT_HASH?LoadBalancer.CONSISTENT_HASH:LoadBalancer.DEFAULT); @@ -551,30 +570,18 @@ public final class Hudson extends Node implements ItemGroup, Stapl } catch (IOException e) { LOGGER.log(SEVERE, "Failed to load proxy configuration", e); } - - // load plugins. - pluginManager = new PluginManager(context); - pluginManager.initialize(); - // if we are loading old data that doesn't have this field - if(slaves==null) slaves = new NodeList(); + pluginManager = new PluginManager(context); adjuncts = new AdjunctManager(servletContext, pluginManager.uberClassLoader,"adjuncts/"+VERSION_HASH); - load(); - - // try { - // // fill up the cache - // load(); - // - // Controller c = new Controller(); - // c.startCPUProfiling(ProfilingModes.CPU_TRACING,""); // "java.*"); - // load(); - // c.stopCPUProfiling(); - // c.captureSnapshot(ProfilingModes.SNAPSHOT_WITHOUT_HEAP); - // } catch (Exception e) { - // throw new Error(e); - // } + // initialization consists of ... + executeReactor( is, + new InitializerFinder(), // misc. stuff + pluginManager.initTasks(is), // loading and preparing plugins + loadTasks(), // load jobs + InitMilestone.ordering() // forced ordering among key milestones + ); if(slaveAgentPort!=-1) { try { @@ -599,54 +606,89 @@ public final class Hudson extends Node implements ItemGroup, Stapl cl.onOnline(c,new StreamTaskListener(System.out)); } - getQueue().load(); - for (ItemListener l : ItemListener.all()) l.onLoaded(); - - // run the initialization script, if it exists. - File initScript = new File(getRootDir(),"init.groovy"); - if(initScript.exists()) { - LOGGER.info("Executing "+initScript); - GroovyShell shell = new GroovyShell(pluginManager.uberClassLoader); - try { - shell.evaluate(initScript); - } catch (Throwable t) { - t.printStackTrace(); - } - } - - File userContentDir = new File(getRootDir(), "userContent"); - if(!userContentDir.exists()) { - userContentDir.mkdirs(); - FileUtils.writeStringToFile(new File(userContentDir,"readme.txt"),Messages.Hudson_USER_CONTENT_README()); - } - - updateCenter.load(); // this has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first. - - Trigger.init(); -// pending SEZPOZ-8 -// // invoke post initialization methods -// for ( IndexItem i : Index.load(PostInit.class, Void.class, pluginManager.uberClassLoader)) { -// try { -// Method m = (Method)i.element(); -// if (Modifier.isStatic(m.getModifiers())) -// m.invoke(null); -// else -// LOGGER.severe(m+" is annotated with @PostInit but it's not a static method"); -// } catch (InstantiationException e) { -// LOGGER.log(SEVERE,"Failed to invoke @PostInit: "+i,e); -// } catch (IllegalAccessException e) { -// LOGGER.log(SEVERE,"Failed to invoke @PostInit: "+i,e); -// } catch (InvocationTargetException e) { -// LOGGER.log(SEVERE,"Failed to invoke @PostInit: "+i,e); -// } -// } } finally { SecurityContextHolder.clearContext(); } } + /** + * Executes a reactor. + * + * @param is + * If non-null, this can be consulted for ignoring some tasks. Only used during the initialization of Hudson. + */ + private void executeReactor(final InitStrategy is, TaskBuilder... builders) throws IOException, InterruptedException, ReactorException { + Reactor reactor = new Reactor(builders) { + /** + * Sets the thread name to the task for better diagnostics. + */ + protected void runTask(Task task) throws Exception { + if (is!=null && is.skipInitTask(task)) return; + + SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); // full access in the initialization thread + String taskName = task.getDisplayName(); + + Thread t = Thread.currentThread(); + String name = t.getName(); + if (taskName !=null) + t.setName(taskName); + try { + long start = System.currentTimeMillis(); + super.runTask(task); + if(LOG_STARTUP_PERFORMANCE) + LOGGER.info(String.format("Took %dms for %s by %s", + System.currentTimeMillis()-start, taskName, name)); + } finally { + t.setName(name); + SecurityContextHolder.clearContext(); + } + } + }; + + ExecutorService es; + if (PARALLEL_LOAD) + es = new ThreadPoolExecutor( + TWICE_CPU_NUM, TWICE_CPU_NUM, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue(), new DaemonThreadFactory()); + else + es = Executors.newSingleThreadExecutor(new DaemonThreadFactory()); + try { + reactor.execute(es,buildReactorListener()); + } finally { + es.shutdownNow(); // upon a successful return the executor queue should be empty. Upon an exception, we want to cancel all pending tasks + } + } + + /** + * Aggregates all the listeners into one and returns it. + * + *

+ * At this point plugins are not loaded yet, so we fall back to the META-INF/services look up to discover implementations. + * As such there's no way for plugins to participate into this process. + */ + private ReactorListener buildReactorListener() throws IOException { + List r = (List) ServiceLoader.load(Thread.currentThread().getContextClassLoader(), InitReactorListener.class); + r.add(new ReactorListener() { + public void onTaskStarted(Task t) { + LOGGER.fine("Started "+t.getDisplayName()); + } + + public void onTaskCompleted(Task t) { + LOGGER.fine("Completed "+t.getDisplayName()); + } + + public void onTaskFailed(Task t, Throwable err, boolean fatal) { + LOGGER.log(Level.SEVERE, "Failed "+t.getDisplayName(),err); + } + + public void onAttained(Milestone milestone) { + LOGGER.fine("Attained "+milestone.toString()); + } + }); + return new ReactorListener.Aggregator(r); + } + public TcpSlaveAgentListener getTcpSlaveAgentListener() { return tcpSlaveAgentListener; } @@ -1036,7 +1078,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl } else { if(n.getNumExecutors()>0) { computers.put(n,c=n.createComputer()); - if (!n.holdOffLaunchUntilSave) { + if (!n.holdOffLaunchUntilSave && AUTOMATIC_SLAVE_LAUNCH) { RetentionStrategy retentionStrategy = c.getRetentionStrategy(); if (retentionStrategy != null) { // if there is a retention strategy, it is responsible for deciding to start the computer @@ -1998,18 +2040,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl return new MasterComputer(); } - private synchronized void load() throws IOException { - long startTime = System.currentTimeMillis(); - XmlFile cfg = getConfigFile(); - if(cfg.exists()) { - // reset some data that may not exit in the disk file - // so that we can take a proper compensation action later. - primaryView = null; - views.clear(); - cfg.unmarshal(this); - } - clouds.setOwner(this); - + private synchronized TaskBuilder loadTasks() throws IOException { File projectsDir = new File(root,"jobs"); if(!projectsDir.isDirectory() && !projectsDir.mkdirs()) { if(projectsDir.exists()) @@ -2021,110 +2052,95 @@ public final class Hudson extends Node implements ItemGroup, Stapl return child.isDirectory() && Items.getConfigFile(child).exists(); } }); - items.clear(); - if(PARALLEL_LOAD) { - // load jobs in parallel for better performance - LOGGER.info("Loading in "+TWICE_CPU_NUM+" parallel threads"); - List> loaders = new ArrayList>(); - for (final File subdir : subdirs) { - loaders.add(threadPoolForLoad.submit(new Callable() { - public TopLevelItem call() throws Exception { - Thread t = Thread.currentThread(); - String name = t.getName(); - t.setName("Loading "+subdir); - try { - long start = System.currentTimeMillis(); - TopLevelItem item = (TopLevelItem) Items.load(Hudson.this, subdir); - if(LOG_STARTUP_PERFORMANCE) - LOGGER.info("Loaded "+item.getName()+" in "+(System.currentTimeMillis()-start)+"ms by "+name); - return item; - } finally { - t.setName(name); - } - } - })); - } - for (Future loader : loaders) { - try { - TopLevelItem item = loader.get(); - items.put(item.getName(), item); - } catch (ExecutionException e) { - LOGGER.log(Level.WARNING, "Failed to load a project",e.getCause()); - } catch (InterruptedException e) { - e.printStackTrace(); // this is probably not the right thing to do + TaskGraphBuilder g = new TaskGraphBuilder(); + Handle loadHudson = g.requires(PLUGINS_STARTED).attains(JOB_LOADED).add("Loading global config", new Executable() { + public void run(Reactor session) throws Exception { + XmlFile cfg = getConfigFile(); + if (cfg.exists()) { + // reset some data that may not exit in the disk file + // so that we can take a proper compensation action later. + primaryView = null; + views.clear(); + + // load from disk + cfg.unmarshal(Hudson.this); } + + // if we are loading old data that doesn't have this field + if (slaves == null) slaves = new NodeList(); + + clouds.setOwner(Hudson.this); + items.clear(); } - } else { - for (File subdir : subdirs) { - try { - long start = System.currentTimeMillis(); - TopLevelItem item = (TopLevelItem)Items.load(this,subdir); - if(LOG_STARTUP_PERFORMANCE) - LOGGER.info("Loaded "+item.getName()+" in "+(System.currentTimeMillis()-start)+"ms"); + }); + + for (final File subdir : subdirs) { + g.requires(loadHudson).attains(JOB_LOADED).notFatal().add("Loading job "+subdir.getName(),new Executable() { + public void run(Reactor session) throws Exception { + TopLevelItem item = (TopLevelItem) Items.load(Hudson.this, subdir); items.put(item.getName(), item); - } catch (Error e) { - LOGGER.log(Level.WARNING, "Failed to load "+subdir,e); - } catch (RuntimeException e) { - LOGGER.log(Level.WARNING, "Failed to load "+subdir,e); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to load "+subdir,e); } + }); + } + + g.requires(JOB_LOADED).add("Finalizing set up",new Executable() { + public void run(Reactor session) throws Exception { + rebuildDependencyGraph(); + + {// recompute label objects - populates the labels mapping. + for (Node slave : slaves) + // Note that not all labels are visible until the slaves have connected. + slave.getAssignedLabels(); + getAssignedLabels(); + } + + // initialize views by inserting the default view if necessary + // this is both for clean Hudson and for backward compatibility. + if(views.size()==0 || primaryView==null) { + View v = new AllView(Messages.Hudson_ViewName()); + v.owner = Hudson.this; + views.add(0,v); + primaryView = v.getViewName(); + } + + // read in old data that doesn't have the security field set + if(authorizationStrategy==null) { + if(useSecurity==null || !useSecurity) + authorizationStrategy = AuthorizationStrategy.UNSECURED; + else + authorizationStrategy = new LegacyAuthorizationStrategy(); + } + if(securityRealm==null) { + if(useSecurity==null || !useSecurity) + setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); + else + setSecurityRealm(new LegacySecurityRealm()); + } else { + // force the set to proxy + setSecurityRealm(securityRealm); + } + + if(useSecurity!=null && !useSecurity) { + // forced reset to the unsecure mode. + // this works as an escape hatch for people who locked themselves out. + authorizationStrategy = AuthorizationStrategy.UNSECURED; + setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); + } + + // Initialize the filter with the crumb issuer + setCrumbIssuer(crumbIssuer); + + // auto register root actions + for (Action a : getExtensionList(RootAction.class)) + if (!actions.contains(a)) actions.add(a); + + if(KILL_AFTER_LOAD) + System.exit(0); } - } - rebuildDependencyGraph(); + }); - {// recompute label objects - populates the labels mapping. - for (Node slave : slaves) - // Note that all labels are not visible until the slaves have - // connected. - slave.getAssignedLabels(); - getAssignedLabels(); - } - - // initialize views by inserting the default view if necessary - // this is both for clean Hudson and for backward compatibility. - if(views.size()==0 || primaryView==null) { - View v = new AllView(Messages.Hudson_ViewName()); - v.owner = this; - views.add(0,v); - primaryView = v.getViewName(); - } - - // read in old data that doesn't have the security field set - if(authorizationStrategy==null) { - if(useSecurity==null || !useSecurity) - authorizationStrategy = AuthorizationStrategy.UNSECURED; - else - authorizationStrategy = new LegacyAuthorizationStrategy(); - } - if(securityRealm==null) { - if(useSecurity==null || !useSecurity) - setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); - else - setSecurityRealm(new LegacySecurityRealm()); - } else { - // force the set to proxy - setSecurityRealm(securityRealm); - } - - if(useSecurity!=null && !useSecurity) { - // forced reset to the unsecure mode. - // this works as an escape hatch for people who locked themselves out. - authorizationStrategy = AuthorizationStrategy.UNSECURED; - setSecurityRealm(SecurityRealm.NO_AUTHENTICATION); - } - - // Initialize the filter with the crumb issuer - setCrumbIssuer(crumbIssuer); - - // auto register root actions - for (Action a : getExtensionList(RootAction.class)) - if (!actions.contains(a)) actions.add(a); - - LOGGER.info(String.format("Took %s ms to load",System.currentTimeMillis()-startTime)); - if(KILL_AFTER_LOAD) - System.exit(0); + return g; } /** @@ -2396,14 +2412,14 @@ public final class Hudson extends Node implements ItemGroup, Stapl } @CLIMethod(name="quiet-down") - public synchronized HttpRedirect doQuietDown() throws IOException, ServletException { + public synchronized HttpRedirect doQuietDown() { checkPermission(ADMINISTER); isQuietingDown = true; return new HttpRedirect("."); } @CLIMethod(name="cancel-quiet-down") - public synchronized HttpRedirect doCancelQuietDown() throws IOException, ServletException { + public synchronized HttpRedirect doCancelQuietDown() { checkPermission(ADMINISTER); isQuietingDown = false; getQueue().scheduleMaintenance(); @@ -2703,11 +2719,13 @@ public final class Hudson extends Node implements ItemGroup, Stapl @Override public void run() { try { - load(); - User.reload(); - servletContext.setAttribute("app",Hudson.this); + reload(); } catch (IOException e) { LOGGER.log(SEVERE,"Failed to reload Hudson config",e); + } catch (ReactorException e) { + LOGGER.log(SEVERE,"Failed to reload Hudson config",e); + } catch (InterruptedException e) { + LOGGER.log(SEVERE,"Failed to reload Hudson config",e); } } }.start(); @@ -2715,6 +2733,15 @@ public final class Hudson extends Node implements ItemGroup, Stapl return HttpResponses.redirectViaContextPath("/"); } + /** + * Reloads the configuration synchronously. + */ + public void reload() throws IOException, InterruptedException, ReactorException { + executeReactor(null,loadTasks()); + User.reload(); + servletContext.setAttribute("app", this); + } + /** * Do a finger-print check. */ @@ -3546,6 +3573,11 @@ public final class Hudson extends Node implements ItemGroup, Stapl */ public static boolean CONCURRENT_BUILD = true; + /** + * Automatically try to launch a slave when Hudson is initialized or a new slave is created. + */ + public static boolean AUTOMATIC_SLAVE_LAUNCH = true; + private static final Logger LOGGER = Logger.getLogger(Hudson.class.getName()); private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+"); diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 1c4effcfa7..7b492b71a9 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -29,6 +29,8 @@ import hudson.ExtensionPoint; import hudson.Util; import hudson.XmlFile; import hudson.matrix.MatrixConfiguration; +import hudson.init.Initializer; +import static hudson.init.InitMilestone.JOB_LOADED; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.remoting.AsyncFutureImpl; @@ -46,6 +48,7 @@ import hudson.util.OneShotEvent; import hudson.util.TimeUnit2; import hudson.util.XStream2; import hudson.util.ConsistentHash; +import hudson.util.DoubleLaunchChecker; import hudson.util.ConsistentHash.Hash; import java.io.BufferedReader; @@ -1573,4 +1576,12 @@ public class Queue extends ResourceController implements Saveable { public static Queue getInstance() { return Hudson.getInstance().getQueue(); } + + /** + * Restores the queue content during the start up. + */ + @Initializer(after=JOB_LOADED) + public static void init(Hudson h) { + h.getQueue().load(); + } } diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index 95eb0774d5..cc30cd247e 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -32,6 +32,11 @@ import hudson.PluginWrapper; import hudson.ProxyConfiguration; import hudson.Util; import hudson.XmlFile; +import hudson.triggers.SafeTimerTask; +import hudson.init.Initializer; +import hudson.init.InitMilestone; +import static hudson.init.InitMilestone.JOB_LOADED; +import static hudson.init.InitMilestone.PLUGINS_STARTED; import hudson.lifecycle.Lifecycle; import hudson.model.UpdateSite.Data; import hudson.model.UpdateSite.Plugin; @@ -40,6 +45,7 @@ import hudson.util.DaemonThreadFactory; import hudson.util.IOException2; import hudson.util.PersistedList; import hudson.util.XStream2; +import hudson.util.DoubleLaunchChecker; import org.acegisecurity.Authentication; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.CountingInputStream; @@ -839,6 +845,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { } } + /** + * Initializes the update center. + * + * This has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first. + */ + @Initializer(after=PLUGINS_STARTED) + public static void init(Hudson h) throws IOException { + h.getUpdateCenter().load(); + } + /** * Sequence number generator. */ @@ -849,6 +865,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable { public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never"); public static final XStream2 XSTREAM = new XStream2(); + static { XSTREAM.alias("site",UpdateSite.class); XSTREAM.alias("sites",PersistedList.class); diff --git a/core/src/main/java/hudson/triggers/Trigger.java b/core/src/main/java/hudson/triggers/Trigger.java index 912c95e24e..b85968b0a1 100644 --- a/core/src/main/java/hudson/triggers/Trigger.java +++ b/core/src/main/java/hudson/triggers/Trigger.java @@ -29,6 +29,9 @@ import hudson.DependencyRunner.ProjectRunnable; import hudson.ExtensionPoint; import hudson.DescriptorExtensionList; import hudson.Extension; +import hudson.init.Initializer; +import hudson.init.InitMilestone; +import static hudson.init.InitMilestone.JOB_LOADED; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Build; @@ -250,6 +253,7 @@ public abstract class Trigger implements Describable> */ public static Timer timer; + @Initializer(after=JOB_LOADED) public static void init() { new DoubleLaunchChecker().schedule(); diff --git a/core/src/main/java/hudson/util/ServiceLoader.java b/core/src/main/java/hudson/util/ServiceLoader.java new file mode 100644 index 0000000000..05d4d1a4c0 --- /dev/null +++ b/core/src/main/java/hudson/util/ServiceLoader.java @@ -0,0 +1,50 @@ +package hudson.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import static java.util.logging.Level.WARNING; +import java.util.logging.Logger; + +/** + * Poorman's clone of JDK6 ServiceLoader. + * + * @author Kohsuke Kawaguchi + */ +public class ServiceLoader { + public static List load(ClassLoader classLoader, Class type) throws IOException { + List result = new ArrayList(); + + final Enumeration e = classLoader.getResources("META-INF/services/"+type.getName()); + while (e.hasMoreElements()) { + URL url = e.nextElement(); + BufferedReader configFile = new BufferedReader(new InputStreamReader(url.openStream(),"UTF-8")); + String line; + while ((line = configFile.readLine()) != null) { + line = line.trim(); + if (line.startsWith("#") || line.length()==0) continue; + + try { + Class t = classLoader.loadClass(line); + if (!type.isAssignableFrom(t)) continue; // invalid type + + result.add(type.cast(t.newInstance())); + } catch (ClassNotFoundException x) { + LOGGER.log(WARNING,"Failed to load "+line,x); + } catch (InstantiationException x) { + LOGGER.log(WARNING,"Failed to load "+line,x); + } catch (IllegalAccessException x) { + LOGGER.log(WARNING,"Failed to load "+line,x); + } + } + } + + return result; + } + + private static final Logger LOGGER = Logger.getLogger(ServiceLoader.class.getName()); +} diff --git a/core/src/main/resources/hudson/init/impl/Messages.properties b/core/src/main/resources/hudson/init/impl/Messages.properties new file mode 100644 index 0000000000..90001daca9 --- /dev/null +++ b/core/src/main/resources/hudson/init/impl/Messages.properties @@ -0,0 +1,2 @@ +GroovyInitScript.init=Executing user-defined init script +InitialUserContent.init=Preparing initial user content \ No newline at end of file diff --git a/core/src/main/resources/hudson/logging/Messages.properties b/core/src/main/resources/hudson/logging/Messages.properties new file mode 100644 index 0000000000..c1a182260b --- /dev/null +++ b/core/src/main/resources/hudson/logging/Messages.properties @@ -0,0 +1 @@ +LogRecorderManager.init=Initialing log recorders \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Messages.properties b/core/src/main/resources/hudson/model/Messages.properties index 2b79ca51a8..efc14e84b4 100644 --- a/core/src/main/resources/hudson/model/Messages.properties +++ b/core/src/main/resources/hudson/model/Messages.properties @@ -147,6 +147,7 @@ Queue.NodeOffline={0} is offline Queue.Unknown=??? Queue.WaitingForNextAvailableExecutor=Waiting for next available executor Queue.WaitingForNextAvailableExecutorOn=Waiting for next available executor on {0} +Queue.init=Restoring the build queue Run.BuildAborted=Build was aborted Run.MarkedExplicitly=explicitly marked to keep the record @@ -198,6 +199,7 @@ UpdateCenter.Status.UnknownHostException=\ UpdateCenter.Status.ConnectionFailed=\ Failed to connect to {0}. \ Perhaps you need to configure HTTP proxy? +UpdateCenter.init=Initialing update center Permalink.LastBuild=Last build Permalink.LastStableBuild=Last stable build diff --git a/core/src/main/resources/hudson/triggers/Messages.properties b/core/src/main/resources/hudson/triggers/Messages.properties index f7b6b72e2f..e7baa839b9 100644 --- a/core/src/main/resources/hudson/triggers/Messages.properties +++ b/core/src/main/resources/hudson/triggers/Messages.properties @@ -24,4 +24,5 @@ SCMTrigger.DisplayName=Poll SCM SCMTrigger.getDisplayName={0} Polling Log SCMTrigger.SCMTriggerCause.ShortDescription=Started by an SCM change TimerTrigger.DisplayName=Build periodically -TimerTrigger.TimerTriggerCause.ShortDescription=Started by timer \ No newline at end of file +TimerTrigger.TimerTriggerCause.ShortDescription=Started by timer +Trigger.init=Initializing timer for triggers \ No newline at end of file diff --git a/test/pom.xml b/test/pom.xml index ebca423534..a2a939b767 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -58,7 +58,7 @@ THE SOFTWARE. true -1 - -XX:MaxPermSize=128m -Dfile.encoding=UTF-8 + -XX:MaxPermSize=192m -Dfile.encoding=UTF-8 diff --git a/test/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java b/test/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java index 70b476e8f2..422a7f687a 100644 --- a/test/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java +++ b/test/src/test/java/hudson/tasks/junit/JUnitResultArchiverTest.java @@ -94,12 +94,8 @@ public class JUnitResultArchiverTest extends HudsonTestCase { assertTestResults(build); } - private void reloadHudson() throws NoSuchMethodException, - IllegalAccessException, InvocationTargetException { - Method m = Hudson.class.getDeclaredMethod("load"); - m.setAccessible(true); - m.invoke(hudson); - + private void reloadHudson() throws Exception { + hudson.reload(); project = (FreeStyleProject) hudson.getItem("junit"); }