diff --git a/.hudson b/.hudson new file mode 100644 index 0000000000..652bd394f0 --- /dev/null +++ b/.hudson @@ -0,0 +1 @@ +This file is used as a marker for the test harness to discover the root directory of Hudson. diff --git a/core/pom.xml b/core/pom.xml index 5810ae6211..b6ac9345c0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -430,7 +430,7 @@ org.jvnet.hudson commons-jexl - 1.1-hudson-20080725 + 1.1-hudson-20081031 org.jvnet.hudson @@ -475,7 +475,7 @@ junit junit - 3.8 + 4.3.1 javax.servlet diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index e376b4ca89..fe7e26a7c7 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -25,6 +25,7 @@ import hudson.security.Permission; import hudson.security.SecurityRealm; import hudson.slaves.ComputerLauncher; import hudson.slaves.RetentionStrategy; +import hudson.slaves.Cloud; import hudson.tasks.BuildStep; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildWrapper; @@ -33,6 +34,7 @@ import hudson.tasks.Builder; import hudson.tasks.Publisher; import hudson.util.Area; import hudson.util.Iterators; +import hudson.util.DescriptorList; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; import org.apache.commons.jelly.JellyContext; import org.apache.commons.jelly.JellyTagException; @@ -996,6 +998,10 @@ public class Functions { public static List getPageDecorators() { return (List)PageDecorator.ALL; } + + public static DescriptorList getCloudDescriptors() { + return Cloud.ALL; + } private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+"); diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 9d9737d24c..97342ca11f 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -577,6 +577,13 @@ public class Util { } } + /** + * Surrounds by a single-quote. + */ + public static String singleQuote(String s) { + return '\''+s+'\''; + } + /** * Escapes HTML unsafe characters like <, &to the respective character entities. */ diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 43f9806fcf..061a03d80b 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -144,7 +144,7 @@ public abstract class AbstractProject

,R extends A protected AbstractProject(ItemGroup parent, String name) { super(parent,name); - if(!Hudson.getInstance().getSlaves().isEmpty()) { + if(!Hudson.getInstance().getNodes().isEmpty()) { // if a new job is configured with Hudson that already has slave nodes // make it roamable by default canRoam = true; @@ -195,6 +195,21 @@ public abstract class AbstractProject

,R extends A return Hudson.getInstance().getLabel(assignedNode); } + /** + * Sets the assigned label. + */ + public void setAssignedLabel(Label l) throws IOException { + if(l==null) { + canRoam = true; + assignedNode = null; + } else { + canRoam = false; + if(l==Hudson.getInstance().getSelfLabel()) assignedNode = null; + else assignedNode = l.getName(); + } + save(); + } + /** * Get the term used in the UI to represent this kind of {@link AbstractProject}. * Must start with a capital letter. diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index d1a8827239..860b68731d 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -2,24 +2,31 @@ package hudson.model; import hudson.EnvVars; import hudson.Util; -import hudson.slaves.ComputerLauncher; -import hudson.slaves.RetentionStrategy; +import hudson.model.Descriptor.FormException; import hudson.node_monitors.NodeMonitor; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; +import hudson.slaves.ComputerLauncher; +import hudson.slaves.RetentionStrategy; import hudson.tasks.BuildWrapper; import hudson.tasks.Publisher; import hudson.util.DaemonThreadFactory; +import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.RemotingDiagnostics; import hudson.util.RunList; -import hudson.util.ExceptionCatchingThreadFactory; +import hudson.util.Futures; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import javax.servlet.ServletException; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -27,16 +34,10 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.logging.LogRecord; import java.nio.charset.Charset; -import javax.servlet.ServletException; - -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.export.Exported; -import org.kohsuke.stapler.export.ExportedBean; - /** * Represents the running state of a remote computer that holds {@link Executor}s. * @@ -52,7 +53,8 @@ import org.kohsuke.stapler.export.ExportedBean; * * Also, even if you remove a {@link Node}, it takes time for the corresponding * {@link Computer} to be removed, if some builds are already in progress on that - * node. + * node. Or when the node configuration is changed, unaffected {@link Computer} object + * remains intact, while all the {@link Node} objects will go away. * *

* This object also serves UI (since {@link Node} is an interface and can't have @@ -135,20 +137,44 @@ public abstract class Computer extends AbstractModelObject implements AccessCont */ public abstract void doLaunchSlaveAgent( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException; + /** + * @deprecated Use {@link #connect(boolean)} + */ + public final void launch() { + connect(true); + } + /** * Do the same as {@link #doLaunchSlaveAgent(StaplerRequest, StaplerResponse)} * but outside the context of serving a request. * - * If already connected, no-op. + *

+ * If already connected or if this computer doesn't support proactive launching, no-op. + * This method may return immediately + * while the launch operation happens asynchronously. + * + * @see #disconnect() + * + * @param forceReconnect + * If true and a connect activity is already in progress, it will be cancelled and + * the new one will be started. If false, and a connect activity is already in progress, this method + * will do nothing and just return the pending connection operation. + * @return + * A {@link Future} representing pending completion of the task. */ - public abstract void launch(); + public abstract Future connect(boolean forceReconnect); /** * Disconnect this computer. * - * If this is the master, no-op + * If this is the master, no-op. This method may return immediately + * while the launch operation happens asynchronously. + * + * @return + * {@link Future} to track the asynchronous disconnect operation. + * @see #connect(boolean) */ - public void disconnect() { } + public Future disconnect() { return Futures.precomputed(null); } /** * Number of {@link Executor}s that are configured for this computer. @@ -176,7 +202,11 @@ public abstract class Computer extends AbstractModelObject implements AccessCont public Node getNode() { if(nodeName==null) return Hudson.getInstance(); - return Hudson.getInstance().getSlave(nodeName); + return Hudson.getInstance().getNode(nodeName); + } + + public LoadStatistics getLoadStatistics() { + return getNode().getSelfLabel().loadStatistics; } /** @@ -205,6 +235,15 @@ public abstract class Computer extends AbstractModelObject implements AccessCont return temporarilyOffline || getChannel()==null; } + public final boolean isOnline() { + return !isOffline(); + } + + /** + * Is a {@link #connect(boolean)} operation in progress? + */ + public abstract boolean isConnecting(); + /** * Returns true if this computer is supposed to be launched via JNLP. * @deprecated see {@linkplain #isLaunchSupported()} and {@linkplain ComputerLauncher} @@ -300,23 +339,28 @@ public abstract class Computer extends AbstractModelObject implements AccessCont } /** - * Called to notify {@link Computer} that it will be discarded. + * Called by {@link Hudson#updateComputerList()} to notify {@link Computer} that it will be discarded. */ protected void kill() { setNumExecutors(0); } private synchronized void setNumExecutors(int n) { + if(numExecutors==n) return; // no-op + + int diff = n-numExecutors; this.numExecutors = n; - // send signal to all idle executors to potentially kill them off - for( Executor e : executors ) - if(e.isIdle()) - e.interrupt(); - - // if the number is increased, add new ones - while(executors.size() * If this computer is already idle, the return value will point to the @@ -508,6 +563,46 @@ public abstract class Computer extends AbstractModelObject implements AccessCont req.getView(this,"_script.jelly").forward(req,rsp); } + /** + * Accepts the update to the node configuration. + */ + public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { + try { + checkPermission(Hudson.CONFIGURE); // TODO: new permission? + + final Hudson app = Hudson.getInstance(); + + Node result = getNode().getDescriptor().newInstance(req, req.getSubmittedForm()); + + // replace the old Node object by the new one + synchronized (app) { + List nodes = new ArrayList(app.getNodes()); + int i = nodes.indexOf(getNode()); + if(i<0) { + sendError("This slave appears to be removed while you were editing the configuration",req,rsp); + return; + } + + nodes.set(i,result); + app.setNodes(nodes); + } + + // take the user back to the slave top page. + rsp.sendRedirect2("../"+result.getNodeName()+'/'); + } catch (FormException e) { + sendError(e,req,rsp); + } + } + + /** + * Really deletes the slave. + */ + public void doDoDelete(StaplerResponse rsp) throws IOException { + checkPermission(DELETE); + Hudson.getInstance().removeNode(getNode()); + rsp.sendRedirect(".."); + } + /** * Handles incremental log. */ @@ -535,4 +630,10 @@ public abstract class Computer extends AbstractModelObject implements AccessCont public boolean isAcceptingTasks() { return true; } + + // TODO: define this as a separate permission? + public static final Permission CONFIGURE = Hudson.CONFIGURE; + public static final Permission DELETE = Hudson.DELETE; + + } diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java index 0c7b94ede7..084dfdb1ac 100644 --- a/core/src/main/java/hudson/model/ComputerSet.java +++ b/core/src/main/java/hudson/model/ComputerSet.java @@ -1,16 +1,23 @@ package hudson.model; +import hudson.Util; +import hudson.slaves.NodeDescriptor; import hudson.model.Descriptor.FormException; import hudson.node_monitors.NodeMonitor; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import java.io.IOException; +import java.text.ParseException; +import java.util.AbstractList; import java.util.ArrayList; import java.util.List; -import java.io.IOException; -import javax.servlet.ServletException; -import org.kohsuke.stapler.export.ExportedBean; -import org.kohsuke.stapler.export.Exported; /** * Serves as the top of {@link Computer}s in the URL hierarchy. @@ -20,7 +27,7 @@ import org.kohsuke.stapler.export.Exported; * @author Kohsuke Kawaguchi */ @ExportedBean -public final class ComputerSet implements ModelObject { +public final class ComputerSet extends AbstractModelObject { private static final List monitors; @Exported @@ -37,6 +44,66 @@ public final class ComputerSet implements ModelObject { return Hudson.getInstance().getComputers(); } + /** + * Gets all the slave names. + */ + public List get_slaveNames() { + return new AbstractList() { + final List nodes = Hudson.getInstance().getNodes(); + + public String get(int index) { + return nodes.get(index).getNodeName(); + } + + public int size() { + return nodes.size(); + } + }; + } + + /** + * Number of total {@link Executor}s that belong to this label that are functioning. + *

+ * This excludes executors that belong to offline nodes. + */ + @Exported + public int getTotalExecutors() { + int r=0; + for (Computer c : get_all()) { + if(c.isOnline()) + r += c.countExecutors(); + } + return r; + } + + /** + * Number of busy {@link Executor}s that are carrying out some work right now. + */ + @Exported + public int getBusyExecutors() { + int r=0; + for (Computer c : get_all()) { + if(c.isOnline()) + r += c.countBusy(); + } + return r; + } + + /** + * {@code getTotalExecutors()-getBusyExecutors()}, plus executors that are being brought online. + */ + public int getIdleExecutors() { + int r=0; + for (Computer c : get_all()) + if(c.isOnline() || c.isConnecting()) + r += c.countIdle(); + return r; + } + + public String getSearchUrl() { + return "/computers/"; + } + public Computer getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { return Hudson.getInstance().getComputer(token); } @@ -46,7 +113,7 @@ public final class ComputerSet implements ModelObject { for(Computer c : get_all()) { if(c.isLaunchSupported()) - c.launch(); + c.connect(true); } rsp.sendRedirect("."); } @@ -66,6 +133,97 @@ public final class ComputerSet implements ModelObject { rsp.forwardToPreviousPage(req); } + /** + * First check point in creating a new slave. + */ + public synchronized void doCreateItem( StaplerRequest req, StaplerResponse rsp, + @QueryParameter("name") String name, @QueryParameter("mode") String mode, + @QueryParameter("from") String from ) throws IOException, ServletException { + final Hudson app = Hudson.getInstance(); + app.checkPermission(Hudson.CONFIGURE); // TODO: new permission? + + if (checkName(req, rsp, name)) return; + + if(mode!=null && mode.equals("copy")) { + Node src = app.getNode(from); + if(src==null) { + rsp.setStatus(SC_BAD_REQUEST); + if(Util.fixEmpty(from)==null) + sendError(Messages.ComputerSet_SpecifySlaveToCopy(),req,rsp); + else + sendError(Messages.ComputerSet_NoSuchSlave(from),req,rsp); + return; + } + + // copy through XStream + String xml = Hudson.XSTREAM.toXML(src); + Node result = (Node)Hudson.XSTREAM.fromXML(xml); + result.setNodeName(name); + + app.addNode(result); + + // send the browser to the config page + rsp.sendRedirect2(result.getNodeName()+"/configure"); + } else { + // proceed to step 2 + if(mode==null) { + rsp.sendError(SC_BAD_REQUEST); + return; + } + + req.setAttribute("descriptor", NodeDescriptor.ALL.find(mode)); + req.getView(this,"_new.jelly").forward(req,rsp); + } + } + + /** + * Really creates a new slave. + */ + public synchronized void doDoCreateItem( StaplerRequest req, StaplerResponse rsp, + @QueryParameter("name") String name, + @QueryParameter("type") String type ) throws IOException, ServletException { + try { + final Hudson app = Hudson.getInstance(); + app.checkPermission(Hudson.CONFIGURE); // TODO: new permission? + + if (checkName(req, rsp, name)) return; + + Node result = NodeDescriptor.ALL.find(type).newInstance(req, req.getSubmittedForm()); + app.addNode(result); + + // take the user back to the slave list top page + rsp.sendRedirect2("."); + } catch (FormException e) { + sendError(e,req,rsp); + } + } + + /** + * Makes sure that the given name is good as a slave name. + */ + private boolean checkName(StaplerRequest req, StaplerResponse rsp, String name) throws IOException, ServletException { + if(name==null) { + rsp.sendError(HttpServletResponse.SC_BAD_REQUEST,"Query parameter 'name' is required"); + return true; + } + name = name.trim(); + + try { + Hudson.checkGoodName(name); + } catch (ParseException e) { + rsp.setStatus(SC_BAD_REQUEST); + sendError(e,req,rsp); + return true; + } + + if(Hudson.getInstance().getNode(name)!=null) { + rsp.setStatus(SC_BAD_REQUEST); + sendError(Messages.ComputerSet_SlaveAlreadyExists(name),req,rsp); + return true; + } + return false; + } + public Api getApi() { return new Api(this); } diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index 10a4d4a206..43399bf479 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -2,6 +2,7 @@ package hudson.model; import hudson.XmlFile; import hudson.BulkChange; +import static hudson.Util.singleQuote; import hudson.util.CopyOnWriteList; import hudson.scm.CVSSCM; import net.sf.json.JSONArray; @@ -9,6 +10,7 @@ import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.Stapler; import org.springframework.util.StringUtils; +import org.jvnet.tiger_types.Types; import javax.servlet.http.HttpServletRequest; import java.io.File; @@ -18,11 +20,16 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.beans.Introspector; /** * Metadata about a configurable instance. @@ -63,6 +70,12 @@ import java.lang.reflect.Modifier; * However, it is the responsibility of the derived type to properly * invoke {@link #save()} and {@link #load()}. * + *

Reflection Enhancement

+ * {@link Descriptor} defines addition to the standard Java reflection + * and provides reflective information about its corresponding {@link Describable}. + * These are primarily used by tag libraries to + * keep the Jelly scripts concise. + * * @author Kohsuke Kawaguchi * @see Describable */ @@ -84,6 +97,73 @@ public abstract class Descriptor> implements Saveable { private transient final Map checkMethods = new ConcurrentHashMap(); + /** + * Lazily computed list of properties on {@link #clazz}. + */ + private transient volatile Map propertyTypes; + + /** + * Represents a readable property on {@link Describable}. + */ + public static final class PropertyType { + public final Class clazz; + public final Type type; + private volatile Class itemType; + + PropertyType(Class clazz, Type type) { + this.clazz = clazz; + this.type = type; + } + + PropertyType(Field f) { + this(f.getType(),f.getGenericType()); + } + + PropertyType(Method getter) { + this(getter.getReturnType(),getter.getGenericReturnType()); + } + + public Enum[] getEnumConstants() { + return (Enum[])clazz.getEnumConstants(); + } + + /** + * If the property is a collection/array type, what is an item type? + */ + public Class getItemType() { + if(itemType==null) + itemType = computeItemType(); + return itemType; + } + + private Class computeItemType() { + if(clazz.isArray()) { + return clazz.getComponentType(); + } + if(Collection.class.isAssignableFrom(clazz)) { + Type col = Types.getBaseClass(type, Collection.class); + + if (col instanceof ParameterizedType) + return Types.erasure(Types.getTypeArgument(col,0)); + else + return Object.class; + } + return null; + } + + /** + * Returns {@link Descriptor} whose 'clazz' is the same as {@link #getItemType() the item type}. + */ + public Descriptor getItemTypeDescriptor() { + Class itemType = getItemType(); + for( Descriptor d : Descriptor.ALL ) + if(d.clazz==itemType) + return d; + return null; + + } + } + protected Descriptor(Class clazz) { this.clazz = clazz; ALL.add(this); @@ -122,13 +202,31 @@ public abstract class Descriptor> implements Saveable { if(method==NONE) return null; - return '\''+Stapler.getCurrentRequest().getContextPath()+"/descriptor/"+clazz.getName()+"/check"+capitalizedFieldName+"?value='+encode(this.value)"; + return singleQuote(Stapler.getCurrentRequest().getContextPath()+"/descriptor/"+clazz.getName()+"/check"+capitalizedFieldName+"?value=")+"+encode(this.value)"; + } + + /** + * Obtains the property type of the given field of {@link #clazz} + */ + public PropertyType getPropertyType(String field) { + if(propertyTypes ==null) { + Map r = new HashMap(); + for (Field f : clazz.getFields()) + r.put(f.getName(),new PropertyType(f)); + + for (Method m : clazz.getMethods()) + if(m.getName().startsWith("get")) + r.put(Introspector.decapitalize(m.getName().substring(3)),new PropertyType(m)); + + propertyTypes = r; + } + return propertyTypes.get(field); } /** * Gets the class name nicely escaped to be usable as a key in the structured form submission. */ - public String getJsonSafeClassName() { + public final String getJsonSafeClassName() { return clazz.getName().replace('.','-'); } @@ -374,6 +472,10 @@ public abstract class Descriptor> implements Saveable { return null; } + public static Descriptor find(String className) { + return find(ALL.getView(),className); + } + public static final class FormException extends Exception { private final String formField; diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index bfb0abebb7..9b044bbb9b 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -1,7 +1,11 @@ package hudson.model; import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.mapper.Mapper; +import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter; import hudson.BulkChange; +import com.trilead.ssh2.crypto.digest.SHA1; +import hudson.FeedAdapter; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; @@ -48,6 +52,11 @@ import hudson.security.SecurityMode; import hudson.security.SecurityRealm; import hudson.slaves.ComputerListener; import hudson.slaves.RetentionStrategy; +import hudson.slaves.NodeList; +import hudson.slaves.Cloud; +import hudson.slaves.DumbSlave; +import hudson.slaves.NodeDescriptor; +import hudson.slaves.NodeProvisioner; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrappers; @@ -70,7 +79,19 @@ import hudson.util.MultipartFormDataParser; import hudson.util.RemotingDiagnostics; import hudson.util.TextFile; import hudson.util.XStream2; +<<<<<<< .working import hudson.util.HudsonIsRestarting; +======= +import hudson.util.DescribableList; +<<<<<<< .working +>>>>>>> .merge-right.r12882 +======= +import hudson.util.Futures; +<<<<<<< .working +>>>>>>> .merge-right.r13488 +======= +import hudson.util.HudsonIsRestarting; +>>>>>>> .merge-right.r13919 import hudson.widgets.Widget; import net.sf.json.JSONObject; import org.acegisecurity.*; @@ -135,6 +156,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -235,15 +257,34 @@ public final class Hudson extends AbstractModelObject implements ItemGroup> { + public CloudList(Hudson h) { + super(h); + } + + protected void onModified() throws IOException { + super.onModified(); + Hudson.getInstance().trimLabels(); + } + } + /** * Set of installed cluster nodes. - * + *

* We use this field with copy-on-write semantics. * This field has mutable list (to keep the serialization look clean), * but it shall never be modified. Only new completely populated slave * list can be set here. + *

+ * The field name should be really {@code nodes}, but again the backward compatibility + * prevents us from renaming. */ - private volatile List slaves; + private volatile NodeList slaves; /** * Quiet period. @@ -300,6 +341,16 @@ public final class Hudson extends AbstractModelObject implements ItemGroup labelSet; private transient volatile Set

+ * This excludes executors that belong to offline nodes. + */ + @Exported + public int getTotalExecutors() { + int r=0; + for (Node n : getNodes()) { + Computer c = n.toComputer(); + if(c!=null && c.isOnline()) + r += c.countExecutors(); + } + return r; + } + + /** + * Number of busy {@link Executor}s that are carrying out some work right now. + */ + @Exported + public int getBusyExecutors() { + int r=0; + for (Node n : getNodes()) { + Computer c = n.toComputer(); + if(c!=null && c.isOnline()) + r += c.countBusy(); + } + return r; + } + + /** + * Number of idle {@link Executor}s that can start working immediately. + */ + @Exported + public int getIdleExecutors() { + int r=0; + for (Node n : getNodes()) { + Computer c = n.toComputer(); + if(c!=null && (c.isOnline() || c.isConnecting())) + r += c.countIdle(); + } + return r; + } + /** * Returns true if all the nodes of this label is offline. */ + @Exported public boolean isOffline() { for (Node n : getNodes()) { if(n.toComputer() != null && !n.toComputer().isOffline()) @@ -73,6 +169,7 @@ public class Label implements Comparable

Implementation Note

+ *

+ * Instances of this class is not capable of updating the statistics itself + * — instead, it's done by the single {@link #register()} method. + * This is more efficient (as it allows us a single pass to update all stats), + * but it's not clear to me if the loss of autonomy is worth it. + * + * @author Kohsuke Kawaguchi + * @see Label#loadStatistics + * @see Hudson#overallLoad + */ +public abstract class LoadStatistics { + /** + * Number of busy executors and how it changes over time. + */ + public final MultiStageTimeSeries busyExecutors; + + /** + * Number of total executors and how it changes over time. + */ + public final MultiStageTimeSeries totalExecutors; + + /** + * Number of {@link Queue.BuildableItem}s that can run on any node in this node set but blocked. + */ + public final MultiStageTimeSeries queueLength; + + protected LoadStatistics(int initialTotalExecutors, int initialBusyExecutors) { + this.totalExecutors = new MultiStageTimeSeries(initialTotalExecutors,DECAY); + this.busyExecutors = new MultiStageTimeSeries(initialBusyExecutors,DECAY); + this.queueLength = new MultiStageTimeSeries(0,DECAY); + } + + public float getLatestIdleExecutors(TimeScale timeScale) { + return totalExecutors.pick(timeScale).getLatest() - busyExecutors.pick(timeScale).getLatest(); + } + + /** + * Computes the # of idle executors right now and obtains the snapshot value. + */ + public abstract int computeIdleExecutors(); + + /** + * Computes the # of total executors right now and obtains the snapshot value. + */ + public abstract int computeTotalExecutors(); + + /** + * Computes the # of queue length right now and obtains the snapshot value. + */ + public abstract int computeQueueLength(); + + /** + * Creates a trend chart. + */ + public JFreeChart createChart(CategoryDataset ds) { + final JFreeChart chart = ChartFactory.createLineChart(null, // chart title + null, // unused + null, // range axis label + ds, // data + PlotOrientation.VERTICAL, // orientation + true, // include legend + true, // tooltips + false // urls + ); + + chart.setBackgroundPaint(Color.white); + + final CategoryPlot plot = chart.getCategoryPlot(); + plot.setBackgroundPaint(Color.WHITE); + plot.setOutlinePaint(null); + plot.setRangeGridlinesVisible(true); + plot.setRangeGridlinePaint(Color.black); + + final LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer(); + renderer.setBaseStroke(new BasicStroke(3)); + configureRenderer(renderer); + + final CategoryAxis domainAxis = new NoOverlapCategoryAxis(null); + plot.setDomainAxis(domainAxis); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); + domainAxis.setLowerMargin(0.0); + domainAxis.setUpperMargin(0.0); + domainAxis.setCategoryMargin(0.0); + + final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); + rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + + // crop extra space around the graph + plot.setInsets(new RectangleInsets(0, 0, 0, 5.0)); + + return chart; + } + + protected void configureRenderer(LineAndShapeRenderer renderer) { + renderer.setSeriesPaint(0, ColorPalette.BLUE); // total + renderer.setSeriesPaint(1, ColorPalette.RED); // busy + renderer.setSeriesPaint(2, ColorPalette.GREY); // queue + } + + /** + * Creates {@link CategoryDataset} which then becomes the basis + * of the load statistics graph. + */ + public CategoryDataset createDataset(TimeScale timeScale) { + return createDataset(timeScale, + new float[][]{ + busyExecutors.pick(timeScale).getHistory(), + totalExecutors.pick(timeScale).getHistory(), + queueLength.pick(timeScale).getHistory() + }, + new String[]{"Total executors","Busy executors","Queue length"}); + } + + protected final DefaultCategoryDataset createDataset(TimeScale timeScale, float[][] dataPoints, String[] legends) { + assert dataPoints.length==legends.length; + int dataLength = dataPoints[0].length; + for (float[] dataPoint : dataPoints) + assert dataLength ==dataPoint.length; + + DefaultCategoryDataset ds = new DefaultCategoryDataset(); + + DateFormat format = timeScale.createDateFormat(); + + Date dt = new Date(System.currentTimeMillis()-timeScale.tick*dataLength); + for (int i = dataLength-1; i>=0; i--) { + dt = new Date(dt.getTime()+timeScale.tick); + String l = format.format(dt); + for(int j=0; j bis = h.getQueue().getBuildableItems(); + + // update statistics on slaves + for( Label l : h.getLabels() ) { + l.loadStatistics.totalExecutors.update(l.getTotalExecutors()); + l.loadStatistics.busyExecutors .update(l.getBusyExecutors()); + + int q=0; + for (Queue.BuildableItem bi : bis) { + if(bi.task.getAssignedLabel()==l) + q++; + } + l.loadStatistics.queueLength.update(q); + } + + // update statistics of the entire system + ComputerSet cs = h.getComputer(); + h.overallLoad.totalExecutors.update(cs.getTotalExecutors()); + h.overallLoad.busyExecutors .update(cs.getBusyExecutors()); + int q=0; + for (Queue.BuildableItem bi : bis) { + if(bi.task.getAssignedLabel()==null) + q++; + } + h.overallLoad.queueLength.update(q); + h.overallLoad.totalQueueLength.update(bis.size()); + } + }, CLOCK, CLOCK + ); + } + + + /** + * With 0.90 decay ratio for every 10sec, half reduction is about 1 min. + */ + public static final float DECAY = Float.parseFloat(System.getProperty(LoadStatistics.class.getName()+".decay","0.9")); + /** + * Load statistics clock cycle in milliseconds. Specify a small value for quickly debugging this feature and node provisioning through cloud. + */ + public static int CLOCK = Integer.getInteger(LoadStatistics.class.getName()+".clock",10*1000); +} diff --git a/core/src/main/java/hudson/model/MultiStageTimeSeries.java b/core/src/main/java/hudson/model/MultiStageTimeSeries.java new file mode 100644 index 0000000000..29f9833f16 --- /dev/null +++ b/core/src/main/java/hudson/model/MultiStageTimeSeries.java @@ -0,0 +1,98 @@ +package hudson.model; + +import hudson.util.TimeUnit2; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +/** + * Maintains several {@link TimeSeries} with different update frequencies to satisfy three goals; + * (1) retain data over long timespan, (2) save memory, and (3) retain accurate data for the recent past. + * + * All in all, one instance uses about 8KB space. + * + * @author Kohsuke Kawaguchi + */ +public class MultiStageTimeSeries { + /** + * Updated every 10 seconds. Keep data up to 1 hour. + */ + public final TimeSeries sec10; + /** + * Updated every 1 min. Keep data up to 1 day. + */ + public final TimeSeries min; + /** + * Updated every 1 hour. Keep data up to 4 weeks. + */ + public final TimeSeries hour; + + private int counter; + + public MultiStageTimeSeries(float initialValue, float decay) { + this.sec10 = new TimeSeries(initialValue,decay,6*60); + this.min = new TimeSeries(initialValue,decay,60*24); + this.hour = new TimeSeries(initialValue,decay,28*24); + } + + /** + * Call this method every 10 sec and supply a new data point. + */ + public void update(float f) { + counter = (counter+1)%360; // 1hour/10sec = 60mins/10sec=3600secs/10sec = 360 + sec10.update(f); + if(counter%6==0) min.update(f); + if(counter==0) hour.update(f); + } + + /** + * Selects a {@link TimeSeries}. + */ + public TimeSeries pick(TimeScale timeScale) { + switch (timeScale) { + case HOUR: return hour; + case MIN: return min; + case SEC10: return sec10; + default: throw new AssertionError(); + } + } + + /** + * Gets the most up-to-date data point value. + */ + public float getLatest(TimeScale timeScale) { + return pick(timeScale).getLatest(); + } + + /** + * Choose which datapoint to use. + */ + public enum TimeScale { + SEC10(TimeUnit2.SECONDS.toMillis(10)), + MIN(TimeUnit2.MINUTES.toMillis(1)), + HOUR(TimeUnit2.HOURS.toMillis(1)); + + /** + * Number of milliseconds (10 secs, 1 min, and 1 hour) + * that this constant represents. + */ + public final long tick; + + TimeScale(long tick) { + this.tick = tick; + } + + /** + * Creates a new {@link DateFormat} suitable for processing + * this {@link TimeScale}. + */ + public DateFormat createDateFormat() { + switch (this) { + case HOUR: return new SimpleDateFormat("MMM/dd HH"); + case MIN: return new SimpleDateFormat("HH:mm"); + case SEC10: return new SimpleDateFormat("HH:mm:ss"); + default: throw new AssertionError(); + } + } + } +} diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index 5f88ca5e32..b797eef907 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -2,6 +2,9 @@ package hudson.model; import hudson.FilePath; import hudson.Launcher; +import hudson.ExtensionPoint; +import hudson.security.AccessControlled; +import hudson.slaves.NodeDescriptor; import hudson.node_monitors.NodeMonitor; import hudson.util.ClockDifference; import hudson.util.EnumConverter; @@ -11,12 +14,16 @@ import java.io.IOException; import java.util.Set; /** - * Commonality between {@link Slave} and master {@link Hudson}. + * Base type of Hudson slaves (although in practice, you probably extend {@link Slave} to define a new slave type.) + * + *

+ * As a special case, {@link Hudson} extends from here. * * @author Kohsuke Kawaguchi * @see NodeMonitor + * @see NodeDescriptor */ -public interface Node { +public interface Node extends Describable, ExtensionPoint, AccessControlled { /** * Name of this node. * @@ -25,6 +32,18 @@ public interface Node { */ String getNodeName(); + /** + * When the user clones a {@link Node}, Hudson uses this method to change the node name right after + * the cloned {@link Node} object is instantiated. + * + *

+ * This method is never used for any other purpose, and as such for all practical intents and purposes, + * the node name should be treated like immutable. + * + * @deprecated to indicate that this method isn't really meant to be called by random code. + */ + void setNodeName(String name); + /** * Human-readable description of this node. */ @@ -54,10 +73,15 @@ public interface Node { * Gets the corresponding {@link Computer} object. * * @return - * never null. + * this method can return null if there's no {@link Computer} object for this node, + * such as when this node has no executors at all. */ Computer toComputer(); + /** + * Creates a new {@link Computer} object that acts as the UI peer of this {@link Node}. + * Nobody but {@link Hudson#updateComputerList()} should call this method. + */ Computer createComputer(); /** @@ -107,6 +131,8 @@ public interface Node { */ FilePath createPath(String absolutePath); + NodeDescriptor getDescriptor(); + /** * Estimates the clock difference with this slave. * diff --git a/core/src/main/java/hudson/model/OverallLoadStatistics.java b/core/src/main/java/hudson/model/OverallLoadStatistics.java new file mode 100644 index 0000000000..75e3ab3afd --- /dev/null +++ b/core/src/main/java/hudson/model/OverallLoadStatistics.java @@ -0,0 +1,55 @@ +package hudson.model; + +import hudson.model.MultiStageTimeSeries.TimeScale; +import org.jfree.data.category.DefaultCategoryDataset; + +/** + * {@link LoadStatistics} for the entire system (the master and all the slaves combined.) + * + *

+ * {@link #computeQueueLength()} and {@link #queueLength} counts those tasks + * that are unassigned to any node, whereas {@link #totalQueueLength} + * tracks the queue length including tasks that are assigned to a specific node. + * + * @author Kohsuke Kawaguchi + * @see Hudson#overallLoad + */ +public class OverallLoadStatistics extends LoadStatistics { + /** + * Number of total {@link Queue.BuildableItem}s that represents blocked builds. + */ + public final MultiStageTimeSeries totalQueueLength = new MultiStageTimeSeries(0,DECAY); + + /*package*/ OverallLoadStatistics() { + super(0,0); + } + + @Override + public int computeIdleExecutors() { + return Hudson.getInstance().getComputer().getIdleExecutors(); + } + + @Override + public int computeTotalExecutors() { + return Hudson.getInstance().getComputer().getTotalExecutors(); + } + + @Override + public int computeQueueLength() { + return Hudson.getInstance().getQueue().countBuildableItemsFor(null); + } + + /** + * When drawing the overall load statistics, use the total queue length, + * not {@link #queueLength}, which just shows jobs that are to be run on the master. + */ + protected DefaultCategoryDataset createOverallDataset(TimeScale timeScale) { + return createDataset(timeScale, + new float[][]{ + busyExecutors.pick(timeScale).getHistory(), + totalExecutors.pick(timeScale).getHistory(), + totalQueueLength.pick(timeScale).getHistory() + }, + new String[]{"Total executors","Busy executors","Queue length"}); + } +} diff --git a/core/src/main/java/hudson/model/Project.java b/core/src/main/java/hudson/model/Project.java index 38d39b58eb..ec7981d2b8 100644 --- a/core/src/main/java/hudson/model/Project.java +++ b/core/src/main/java/hudson/model/Project.java @@ -150,7 +150,7 @@ public abstract class Project

,B extends Build> return null; } - // +// // // actions // diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 618a1be627..1e565b1557 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -345,6 +345,9 @@ public class Queue extends ResourceController implements Saveable { return r; } + /** + * Gets all the {@link BuildableItem}s that are waiting for an executor in the given {@link Computer}. + */ public synchronized List getBuildableItems(Computer c) { List result = new ArrayList(); for (BuildableItem p : buildables.values()) { @@ -359,6 +362,24 @@ public class Queue extends ResourceController implements Saveable { return result; } + /** + * Gets the snapshot of {@link #buildables}. + */ + public synchronized List getBuildableItems() { + return new ArrayList(buildables.values()); + } + + /** + * How many {@link BuildableItem}s are assigned for the given label? + */ + public synchronized int countBuildableItemsFor(Label l) { + int r = 0; + for (BuildableItem bi : buildables.values()) + if(bi.task.getAssignedLabel()==l) + r++; + return r; + } + /** * Gets the information about the queue item for the given project. * diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 2814b926e4..0368e2f007 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -4,11 +4,14 @@ import hudson.FilePath; import hudson.Launcher; import hudson.Launcher.RemoteLauncher; import hudson.Util; +import hudson.security.ACL; +import hudson.security.Permission; import hudson.slaves.ComputerLauncher; import hudson.slaves.RetentionStrategy; import hudson.slaves.CommandLauncher; import hudson.slaves.JNLPLauncher; import hudson.slaves.SlaveComputer; +import hudson.slaves.DumbSlave; import hudson.model.Descriptor.FormException; import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; @@ -18,6 +21,7 @@ import hudson.util.ClockDifference; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.DataBoundConstructor; +import org.apache.commons.io.IOUtils; import javax.servlet.ServletException; import java.io.File; @@ -26,6 +30,7 @@ import java.io.InputStream; import java.io.Serializable; import java.net.URL; import java.net.URLConnection; +import java.net.MalformedURLException; import java.util.*; /** @@ -35,13 +40,16 @@ import java.util.*; * Ideally this would have been in the hudson.slaves package, * but for compatibility reasons, it can't. * + *

+ * TODO: move out more stuff to {@link DumbSlave}. + * * @author Kohsuke Kawaguchi */ -public final class Slave implements Node, Serializable { +public abstract class Slave implements Node, Serializable { /** * Name of this slave node. */ - protected final String name; + protected String name; /** * Description of this node. @@ -90,9 +98,14 @@ public final class Slave implements Node, Serializable { @DataBoundConstructor public Slave(String name, String description, String remoteFS, String numExecutors, Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException { + this(name,description,remoteFS,Util.tryParseNumber(numExecutors, 1).intValue(),mode,label,launcher,retentionStrategy); + } + + public Slave(String name, String description, String remoteFS, int numExecutors, + Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException { this.name = name; this.description = description; - this.numExecutors = Util.tryParseNumber(numExecutors, 1).intValue(); + this.numExecutors = numExecutors; this.mode = mode; this.remoteFS = remoteFS; this.label = Util.fixNull(label).trim(); @@ -108,8 +121,9 @@ public final class Slave implements Node, Serializable { // so this check is harmful. //if (!localFS.exists()) // throw new FormException("Invalid slave configuration for " + name + ". No such directory exists: " + localFS, null); - if (remoteFS.equals("")) - throw new FormException(Messages.Slave_InvalidConfig_NoRemoteDir(name), null); + +// if (remoteFS.equals("")) +// throw new FormException(Messages.Slave_InvalidConfig_NoRemoteDir(name), null); if (this.numExecutors<=0) throw new FormException(Messages.Slave_InvalidConfig_Executors(name), null); @@ -131,6 +145,10 @@ public final class Slave implements Node, Serializable { return name; } + public void setNodeName(String name) { + this.name = name; + } + public String getNodeDescription() { return description; } @@ -243,6 +261,18 @@ public final class Slave implements Node, Serializable { return new ClockDifference((startTime+endTime)/2 - slaveTime); } + public ACL getACL() { + return Hudson.getInstance().getAuthorizationStrategy().getACL(this); + } + + public final void checkPermission(Permission permission) { + getACL().checkPermission(permission); + } + + public final boolean hasPermission(Permission permission) { + return getACL().hasPermission(permission); + } + public Computer createComputer() { return new SlaveComputer(this); } @@ -285,16 +315,33 @@ public final class Slave implements Node, Serializable { } public void doIndex( StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - URL res = req.getServletContext().getResource("/WEB-INF/" + fileName); + URLConnection con = connect(); + InputStream in = con.getInputStream(); + rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" ); + in.close(); + } + + private URLConnection connect() throws IOException { + URL res = getURL(); + return res.openConnection(); + } + + public URL getURL() throws MalformedURLException { + URL res = Hudson.getInstance().servletContext.getResource("/WEB-INF/" + fileName); if(res==null) { // during the development this path doesn't have the files. res = new URL(new File(".").getAbsoluteFile().toURL(),"target/generated-resources/WEB-INF/"+fileName); } + return res; + } - URLConnection con = res.openConnection(); - InputStream in = con.getInputStream(); - rsp.serveFile(req, in, con.getLastModified(), con.getContentLength(), "*.jar" ); - in.close(); + public byte[] readFully() throws IOException { + InputStream in = connect().getInputStream(); + try { + return IOUtils.toByteArray(in); + } finally { + in.close(); + } } } diff --git a/core/src/main/java/hudson/model/TaskListener.java b/core/src/main/java/hudson/model/TaskListener.java index de7a2bc44a..a01018d2e5 100644 --- a/core/src/main/java/hudson/model/TaskListener.java +++ b/core/src/main/java/hudson/model/TaskListener.java @@ -8,14 +8,21 @@ import java.io.PrintWriter; import java.util.Formatter; /** - * Receives events that happen during some task execution, - * such as a build or SCM change polling. + * Receives events that happen during some lengthy operation + * that has some chance of failures, such as a build, SCM change polling, + * slave launch, and so on. * *

* This interface is implemented by Hudson core and passed to extension points so that - * they can record the progress of the build without really knowing how those information + * they can record the progress of the operation without really knowing how those information * and handled/stored by Hudson. * + *

+ * The information is one way or the other made available to users, and + * so the expectation is that when something goes wrong, enough information + * shall be written to a {@link TaskListener} so that the user can diagnose + * what's going wrong. + * * @author Kohsuke Kawaguchi */ public interface TaskListener { diff --git a/core/src/main/java/hudson/model/TimeSeries.java b/core/src/main/java/hudson/model/TimeSeries.java new file mode 100644 index 0000000000..a3a66aca33 --- /dev/null +++ b/core/src/main/java/hudson/model/TimeSeries.java @@ -0,0 +1,75 @@ +package hudson.model; + +import hudson.CopyOnWrite; + +/** + * Scalar value that changes over the time (such as load average, Q length, # of executors, etc.) + * + *

+ * This class computes + * the exponential moving average from the raw data (to be supplied by {@link #update(float)}). + * + * @author Kohsuke Kawaguchi + */ +public final class TimeSeries { + /** + * Decay ratio. Normally 1-e for some small e. + */ + private final float decay; + + /** + * Historical exponential moving average data. Newer ones first. + */ + @CopyOnWrite + private volatile float[] history; + + /** + * Maximum history size. + */ + private final int historySize; + + public TimeSeries(float initialValue, float decay, int historySize) { + this.history = new float[]{initialValue}; + this.decay = decay; + this.historySize = historySize; + } + + /** + * Pushes a new data point. + * + *

+ * Exponential moving average is calculated, and the {@link #history} is updated. + * This method needs to be called periodically and regularly, and it represents + * the raw data stream. + */ + public void update(float newData) { + float data = history[0]*decay + newData*(1-decay); + + float[] r = new float[Math.min(history.length+1,historySize)]; + System.arraycopy(history,0,r,1,Math.min(history.length,r.length-1)); + r[0] = data; + history = r; + } + + /** + * Gets the history data of the exponential moving average. The returned array should be treated + * as read-only and immutable. + * + * @return + * Always non-null, contains at least one entry. + */ + public float[] getHistory() { + return history; + } + + /** + * Gets the most up-to-date data point value. {@code getHistory[0]}. + */ + public float getLatest() { + return history[0]; + } + + public String toString() { + return Float.toString(history[0]); + } +} diff --git a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java index 66b79e6492..32235da039 100644 --- a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java +++ b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java @@ -26,7 +26,7 @@ public abstract class TopLevelItemDescriptor extends Descriptor { */ public abstract String getDisplayName(); - public final String getNewJobDetailPage() { + public final String newInstanceDetailPage() { return '/'+clazz.getName().replace('.','/').replace('$','/')+"/newJobDetail.jelly"; } diff --git a/core/src/main/java/hudson/security/AuthorizationStrategy.java b/core/src/main/java/hudson/security/AuthorizationStrategy.java index c798ae6000..ec1a5b8e31 100644 --- a/core/src/main/java/hudson/security/AuthorizationStrategy.java +++ b/core/src/main/java/hudson/security/AuthorizationStrategy.java @@ -1,6 +1,7 @@ package hudson.security; import hudson.ExtensionPoint; +import hudson.slaves.Cloud; import hudson.model.AbstractItem; import hudson.model.AbstractProject; import hudson.model.Computer; @@ -9,6 +10,7 @@ import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.User; import hudson.model.View; +import hudson.model.Node; import hudson.util.DescriptorList; import java.io.Serializable; @@ -97,11 +99,28 @@ public abstract class AuthorizationStrategy implements Describable - * The default implementation returns {@link #getRootACL()}. + * The default implementation delegates to {@link #getACL(Node)} * * @since 1.220 */ public ACL getACL(Computer computer) { + return getACL(computer.getNode()); + } + + /** + * Implementation can choose to provide different ACL for different {@link Cloud}s. + * This can be used as a basis for more fine-grained access control. + * + *

+ * The default implementation returns {@link #getRootACL()}. + * + * @since 1.252 + */ + public ACL getACL(Cloud cloud) { + return getRootACL(); + } + + public ACL getACL(Node node) { return getRootACL(); } diff --git a/core/src/main/java/hudson/slaves/Cloud.java b/core/src/main/java/hudson/slaves/Cloud.java new file mode 100644 index 0000000000..73c39a0a83 --- /dev/null +++ b/core/src/main/java/hudson/slaves/Cloud.java @@ -0,0 +1,103 @@ +package hudson.slaves; + +import hudson.ExtensionPoint; +import hudson.slaves.NodeProvisioner.PlannedNode; +import hudson.model.Describable; +import hudson.model.Hudson; +import hudson.model.Node; +import hudson.model.AbstractModelObject; +import hudson.model.Label; +import hudson.security.ACL; +import hudson.security.AccessControlled; +import hudson.security.Permission; +import hudson.util.DescriptorList; + +import java.util.Collection; + +/** + * Creates {@link Node}s to dynamically expand/shrink the slaves attached to Hudson. + * + *

+ * Put another way, this class encapsulates different communication protocols + * needed to start a new slave programmatically. + * + * @author Kohsuke Kawaguchi + * @see NodeProvisioner + */ +public abstract class Cloud extends AbstractModelObject implements ExtensionPoint, Describable, AccessControlled { + + /** + * Uniquely identifies this {@link Cloud} instance among other instances in {@link Hudson#clouds}. + */ + public final String name; + + protected Cloud(String name) { + this.name = name; + } + + public String getDisplayName() { + return name; + } + + public String getSearchUrl() { + return "cloud/"+name; + } + + public ACL getACL() { + return Hudson.getInstance().getAuthorizationStrategy().getACL(this); + } + + public final void checkPermission(Permission permission) { + getACL().checkPermission(permission); + } + + public final boolean hasPermission(Permission permission) { + return getACL().hasPermission(permission); + } + + /** + * Provisions new {@link Node}s from this cloud. + * + *

+ * {@link NodeProvisioner} performs a trend analysis on the load, + * and when it determines that it really needs to bring up + * additional nodes, this method is invoked. + * + *

+ * The implementation of this method asynchronously starts + * node provisioning. + * + * @param label + * The label that indicates what kind of nodes are needed now. + * Newly launched node needs to have this label. + * Only those {@link Label}s that this instance returned true + * from the {@link #canProvision(Label)} method will be passed here. + * @param excessWorkload + * Number of total executors needed to meet the current demand. + * Always >= 1. For example, if this is 3, the implementation + * should launch 3 slaves with 1 executor each, or 1 slave with + * 3 executors, etc. + * + * @return + * {@link PlannedNode}s that represent asynchronous {@link Node} + * launch operations. Can be empty but must not be null. + */ + public abstract Collection provision(Label label, int excessWorkload); + + /** + * Returns true if this cloud is capable of provisioning new nodes for the given label. + */ + public abstract boolean canProvision(Label label); + + /** + * All registered {@link Cloud} implementations. + */ + public static final DescriptorList ALL = new DescriptorList(); + + /** + * Permission constant to control mutation operations on {@link Cloud}. + * + * This includes provisioning a new node, as well as removing it. + */ + public static final Permission PROVISION = Hudson.ADMINISTER; +} diff --git a/core/src/main/java/hudson/slaves/ComputerLauncher.java b/core/src/main/java/hudson/slaves/ComputerLauncher.java index fb588ec8de..318e4ce801 100644 --- a/core/src/main/java/hudson/slaves/ComputerLauncher.java +++ b/core/src/main/java/hudson/slaves/ComputerLauncher.java @@ -9,11 +9,20 @@ import hudson.util.StreamTaskListener; import java.io.InputStream; import java.io.OutputStream; +import java.io.IOException; /** * Extension point to allow control over how {@link Computer}s are "launched", * meaning how they get connected to their slave agent program. * + *

Associated View

+ *
+ *
main.jelly
+ *
+ * This page will be rendered into the top page of the computer (/computer/NAME/) + * Useful for showing launch related commands and status reports. + *
+ * *

* EXPERIMENTAL: SIGNATURE MAY CHANGE IN FUTURE RELEASES * @@ -40,8 +49,13 @@ public abstract class ComputerLauncher implements Describable, * * @param listener * The progress of the launch, as well as any error, should be sent to this listener. + * + * @throws IOException + * if the method throws an {@link IOException} or {@link InterruptedException}, the launch was considered + * a failure and the stack trace is reported into the listener. This handling is just so that the implementation + * of this method doesn't have to dilligently catch those exceptions. */ - public abstract void launch(SlaveComputer computer, StreamTaskListener listener); + public abstract void launch(SlaveComputer computer, StreamTaskListener listener) throws IOException , InterruptedException; /** * Allows the {@link ComputerLauncher} to tidy-up after a disconnect. diff --git a/core/src/main/java/hudson/slaves/ComputerLauncherFilter.java b/core/src/main/java/hudson/slaves/ComputerLauncherFilter.java new file mode 100644 index 0000000000..80c0e9c597 --- /dev/null +++ b/core/src/main/java/hudson/slaves/ComputerLauncherFilter.java @@ -0,0 +1,52 @@ +package hudson.slaves; + +import hudson.util.StreamTaskListener; +import hudson.model.Descriptor; +import hudson.model.Node; + +import java.io.IOException; + +/** + * {@link ComputerLauncher} filter that can be used as a base class for decorators. + * + *

+ * Using this class also protects you from method additions in {@link ComputerLauncher}, + * since these two classes are updated in sync. + * + * @author Kohsuke Kawaguchi + * @see SlaveComputer#grabLauncher(Node) + */ +public abstract class ComputerLauncherFilter extends ComputerLauncher { + protected volatile ComputerLauncher core; + + public ComputerLauncherFilter(ComputerLauncher core) { + this.core = core; + } + + /** + * Returns the delegation target. + */ + public ComputerLauncher getCore() { + return core; + } + + public boolean isLaunchSupported() { + return core.isLaunchSupported(); + } + + public void launch(SlaveComputer computer, StreamTaskListener listener) throws IOException, InterruptedException { + core.launch(computer, listener); + } + + public void afterDisconnect(SlaveComputer computer, StreamTaskListener listener) { + core.afterDisconnect(computer, listener); + } + + public void beforeDisconnect(SlaveComputer computer, StreamTaskListener listener) { + core.beforeDisconnect(computer, listener); + } + + public Descriptor getDescriptor() { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/hudson/slaves/DumbSlave.java b/core/src/main/java/hudson/slaves/DumbSlave.java new file mode 100644 index 0000000000..b6438a6322 --- /dev/null +++ b/core/src/main/java/hudson/slaves/DumbSlave.java @@ -0,0 +1,38 @@ +package hudson.slaves; + +import hudson.model.Slave; +import hudson.model.Descriptor.FormException; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Default {@link Slave} implementation for computers that do not belong to a higher level structure, + * like grid or cloud. + * + * @author Kohsuke Kawaguchi + */ +public final class DumbSlave extends Slave { + @DataBoundConstructor + public DumbSlave(String name, String description, String remoteFS, String numExecutors, Mode mode, String label, ComputerLauncher launcher, RetentionStrategy retentionStrategy) throws FormException { + super(name, description, remoteFS, numExecutors, mode, label, launcher, retentionStrategy); + } + + public DescriptorImpl getDescriptor() { + return DescriptorImpl.INSTANCE; + } + + public static final class DescriptorImpl extends NodeDescriptor { + public static final DescriptorImpl INSTANCE = new DescriptorImpl(); + + private DescriptorImpl() { + super(DumbSlave.class); + } + + public String getDisplayName() { + return "Dumb Slave"; + } + } + + static { + NodeDescriptor.ALL.add(DescriptorImpl.INSTANCE); + } +} diff --git a/core/src/main/java/hudson/slaves/EphemeralNode.java b/core/src/main/java/hudson/slaves/EphemeralNode.java new file mode 100644 index 0000000000..d1706d8d88 --- /dev/null +++ b/core/src/main/java/hudson/slaves/EphemeralNode.java @@ -0,0 +1,18 @@ +package hudson.slaves; + +import hudson.model.Node; + +/** + * {@link Node}s that are created by {@link Cloud} and hence not persisted as configuration by itself. + * + * @author Kohsuke Kawaguchi + */ +public interface EphemeralNode extends Node { + /** + * Gets the {@link Cloud} that created this {@link EphemeralNode}. + * + * @return + * never null. + */ + public Cloud getCloud(); +} diff --git a/core/src/main/java/hudson/slaves/NodeDescriptor.java b/core/src/main/java/hudson/slaves/NodeDescriptor.java new file mode 100644 index 0000000000..85d6a2c071 --- /dev/null +++ b/core/src/main/java/hudson/slaves/NodeDescriptor.java @@ -0,0 +1,42 @@ +package hudson.slaves; + +import hudson.model.Descriptor; +import hudson.model.Slave; +import hudson.model.Node; +import hudson.util.DescriptorList; + +/** + * {@link Descriptor} for {@link Slave}. + * + *

Views

+ *

+ * This object needs to have newInstanceDetail.jelly view, which shows up in + * http://server/hudson/computers/new page as an explanation of this job type. + * + *

Other Implementation Notes

+ * + * @author Kohsuke Kawaguchi + */ +public abstract class NodeDescriptor extends Descriptor { + protected NodeDescriptor(Class clazz) { + super(clazz); + } + + public final String newInstanceDetailPage() { + return '/'+clazz.getName().replace('.','/').replace('$','/')+"/newInstanceDetail.jelly"; + } + + @Override + public String getConfigPage() { + return getViewPage(clazz, "configure-entries.jelly"); + } + + /** + * All the registered instances. + */ + public static final DescriptorList ALL = new DescriptorList(); + + static { + ALL.load(DumbSlave.class); + } +} diff --git a/core/src/main/java/hudson/slaves/NodeList.java b/core/src/main/java/hudson/slaves/NodeList.java new file mode 100644 index 0000000000..7116abd250 --- /dev/null +++ b/core/src/main/java/hudson/slaves/NodeList.java @@ -0,0 +1,65 @@ +package hudson.slaves; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import hudson.model.Node; +import hudson.util.RobustCollectionConverter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * {@link CopyOnWriteArrayList} for {@link Node} that has special serialization semantics + * of not serializing {@link EphemeralNode}s. + * + * @author Kohsuke Kawaguchi + */ +public final class NodeList extends CopyOnWriteArrayList { + public NodeList() { + } + + public NodeList(Collection c) { + super(c); + } + + public NodeList(Node[] toCopyIn) { + super(toCopyIn); + } + + /** + * {@link Converter} implementation for XStream. + * + * Serializaion form is compatible with plain {@link List}. + */ + public static final class ConverterImpl extends RobustCollectionConverter { + public ConverterImpl(XStream xstream) { + super(xstream); + } + + public boolean canConvert(Class type) { + return type==NodeList.class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + for (Node o : (NodeList) source) { + if(o instanceof EphemeralNode) + continue; // skip + writeItem(o, context, writer); + } + } + + protected Object createCollection(Class type) { + return new ArrayList(); + } + + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + return new NodeList((List)super.unmarshal(reader, context)); + } + } +} diff --git a/core/src/main/java/hudson/slaves/NodeProvisioner.java b/core/src/main/java/hudson/slaves/NodeProvisioner.java new file mode 100644 index 0000000000..76d34068c7 --- /dev/null +++ b/core/src/main/java/hudson/slaves/NodeProvisioner.java @@ -0,0 +1,188 @@ +package hudson.slaves; + +import hudson.model.LoadStatistics; +import hudson.model.Node; +import hudson.model.Hudson; +import hudson.model.MultiStageTimeSeries; +import hudson.model.Label; +import static hudson.model.LoadStatistics.DECAY; +import hudson.model.MultiStageTimeSeries.TimeScale; +import hudson.triggers.SafeTimerTask; +import hudson.triggers.Trigger; + +import java.util.concurrent.Future; +import java.util.concurrent.ExecutionException; +import java.util.List; +import java.util.Collection; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.logging.Logger; +import java.util.logging.Level; + +/** + * Uses the {@link LoadStatistics} and determines when we need to allocate + * new {@link Node}s through {@link Cloud}. + * + * @author Kohsuke Kawaguchi + */ +public class NodeProvisioner { + /** + * The node addition activity in progress. + */ + public static final class PlannedNode { + public final String displayName; + public final Future future; + public final int numExecutors; + + public PlannedNode(String displayName, Future future, int numExecutors) { + if(displayName==null || future==null || numExecutors<1) throw new IllegalArgumentException(); + this.displayName = displayName; + this.future = future; + this.numExecutors = numExecutors; + } + } + + /** + * Load for the label. + */ + private final LoadStatistics stat; + + /** + * For which label are we working? + * Null if this {@link NodeProvisioner} is working for the entire Hudson, + * for jobs that are unassigned to any particular node. + */ + private final Label label; + + private List pendingLaunches = new ArrayList(); + + /** + * Exponential moving average of the "planned capacity" over time, which is the number of + * additional executors being brought up. + * + * This is used to filter out high-frequency components from the planned capacity, so that + * the comparison with other low-frequency only variables won't leave spikes. + */ + private final MultiStageTimeSeries plannedCapacitiesEMA = new MultiStageTimeSeries(0,DECAY); + + public NodeProvisioner(Label label, LoadStatistics loadStatistics) { + this.label = label; + this.stat = loadStatistics; + } + + /** + * Periodically invoked to keep track of the load. + * Launches additional nodes if necessary. + */ + private void update() { + Hudson hudson = Hudson.getInstance(); + + // clean up the cancelled launch activity, then count the # of executors that we are about to bring up. + float plannedCapacity = 0; + for (Iterator itr = pendingLaunches.iterator(); itr.hasNext();) { + PlannedNode f = itr.next(); + if(f.future.isDone()) { + LOGGER.info(f.displayName+" provisioning completed. We have now "+hudson.getComputers().length+" computer(s)"); + try { + f.future.get(); + } catch (InterruptedException e) { + throw new AssertionError(e); // since we confirmed that the future is already done + } catch (ExecutionException e) { + LOGGER.log(Level.WARNING, "Provisioned slave failed to launch",e.getCause()); + } + itr.remove(); + } else + plannedCapacity += f.numExecutors; + } + plannedCapacitiesEMA.update(plannedCapacity); + + /* + Here we determine how many additional slaves we need to keep up with the load (if at all), + which involves a simple math. + + Broadly speaking, first we check that all the executors are fully utilized before attempting + to start any new slave (this also helps to ignore the temporary gap between different numbers, + as changes in them are not necessarily synchronized --- for example, there's a time lag between + when a slave launches (thus bringing the planned capacity down) and the time when its executors + pick up builds (thus bringing the queue length down.) + + Once we confirm that, we compare the # of buildable items against the additional slaves + that are being brought online. If we have more jobs than our executors can handle, we'll launch a new slave. + + So this computation involves three stats: + + 1. # of idle executors + 2. # of jobs that are starving for executors + 3. # of additional slaves being provisioned (planned capacities.) + + To ignore a temporary surge/drop, we make conservative estimates on each one of them. That is, + we take the current snapshot value, and we take the current exponential moving average (EMA) value, + and use the max/min. + + This is another measure to be robust against temporary surge/drop in those indicators, and helps + us avoid over-reacting to stats. + + If we only use the snapshot value or EMA value, tests confirmed that the gap creates phantom + excessive loads and Hudson ends up firing excessive capacities. In a static system, over the time + EMA and the snapshot value becomes the same, so this makes sure that in a long run this conservative + estimate won't create a starvation. + */ + + int idleSnapshot = stat.computeIdleExecutors(); + int totalSnapshot = stat.computeTotalExecutors(); + float idle = Math.max(stat.getLatestIdleExecutors(TIME_SCALE), idleSnapshot); + if(idle1-MARGIN) {// and there's more work to do... + LOGGER.info("Excess workload "+excessWorkload+" detected. (planned capacity="+plannedCapacity+",Qlen="+qlen+",idle="+idle+"&"+idleSnapshot+",total="+totalSnapshot+")"); + for( Cloud c : hudson.clouds ) { + if(excessWorkload<0) break; // enough slaves allocated + + // provisioning a new node should be conservative --- for example if exceeWorkload is 1.4, + // we don't want to allocate two nodes but just one. + // OTOH, because of the exponential decay, even when we need one slave, excess workload is always + // something like 0.95, in which case we want to allocate one node. + // so the threshold here is 1-MARGIN, and hence floor(excessWorkload+MARGIN) is needed to handle this. + + Collection additionalCapacities = c.provision(label, (int)Math.round(Math.floor(excessWorkload+MARGIN))); + for (PlannedNode ac : additionalCapacities) { + excessWorkload -= ac.numExecutors; + LOGGER.info("Started provisioning "+ac.displayName+" from "+c.name+" with "+ac.numExecutors+" executors. Remaining excess workload:"+excessWorkload); + } + pendingLaunches.addAll(additionalCapacities); + } + } + } + } + + public static void launch() { + // periodically invoke NodeProvisioners + Trigger.timer.scheduleAtFixedRate(new SafeTimerTask() { + @Override + protected void doRun() { + Hudson h = Hudson.getInstance(); + h.overallNodeProvisioner.update(); + for( Label l : h.getLabels() ) + l.nodeProvisioner.update(); + } + }, + // give some initial warm up time so that statically connected slaves + // can be brought online before we start allocating more. + LoadStatistics.CLOCK*10, + LoadStatistics.CLOCK); + } + + private static final float MARGIN = 0.1f; + private static final Logger LOGGER = Logger.getLogger(NodeProvisioner.class.getName()); + + // TODO: picker should be selectable + private static final TimeScale TIME_SCALE = TimeScale.SEC10; +} diff --git a/core/src/main/java/hudson/slaves/RetentionStrategy.java b/core/src/main/java/hudson/slaves/RetentionStrategy.java index 71d79ae205..0d71ce4b25 100644 --- a/core/src/main/java/hudson/slaves/RetentionStrategy.java +++ b/core/src/main/java/hudson/slaves/RetentionStrategy.java @@ -65,18 +65,12 @@ public abstract class RetentionStrategy implements Describab public Always() { } - /** - * {@inheritDoc} - */ public long check(SlaveComputer c) { if (c.isOffline() && c.isLaunchSupported()) c.tryReconnect(); return 1; } - /** - * {@inheritDoc} - */ public DescriptorImpl getDescriptor() { return DESCRIPTOR; } @@ -91,9 +85,6 @@ public abstract class RetentionStrategy implements Describab super(Always.class); } - /** - * {@inheritDoc} - */ public String getDisplayName() { return Messages.RetentionStrategy_Always_displayName(); } @@ -147,34 +138,28 @@ public abstract class RetentionStrategy implements Describab public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); - /** - * {@inheritDoc} - */ public synchronized long check(SlaveComputer c) { if (c.isOffline()) { final long demandMilliseconds = System.currentTimeMillis() - c.getDemandStartMilliseconds(); if (demandMilliseconds > inDemandDelay * 1000 * 60 /*MINS->MILLIS*/) { // we've been in demand for long enough logger.log(Level.INFO, "Launching computer {0} as it has been in demand for {1}", - new Object[]{c.getNode().getNodeName(), Util.getTimeSpanString(demandMilliseconds)}); + new Object[]{c.getName(), Util.getTimeSpanString(demandMilliseconds)}); if (c.isLaunchSupported()) - c.launch(); + c.connect(true); } } else if (c.isIdle()) { final long idleMilliseconds = System.currentTimeMillis() - c.getIdleStartMilliseconds(); if (idleMilliseconds > idleDelay * 1000 * 60 /*MINS->MILLIS*/) { // we've been idle for long enough logger.log(Level.INFO, "Disconnecting computer {0} as it has been idle for {1}", - new Object[]{c.getNode().getNodeName(), Util.getTimeSpanString(idleMilliseconds)}); + new Object[]{c.getName(), Util.getTimeSpanString(idleMilliseconds)}); c.disconnect(); } } return 1; } - /** - * {@inheritDoc} - */ public Descriptor> getDescriptor() { return DESCRIPTOR; } @@ -187,9 +172,6 @@ public abstract class RetentionStrategy implements Describab super(Demand.class); } - /** - * {@inheritDoc} - */ public String getDisplayName() { return Messages.RetentionStrategy_Demand_displayName(); } diff --git a/core/src/main/java/hudson/slaves/SlaveComputer.java b/core/src/main/java/hudson/slaves/SlaveComputer.java index f3f5220fea..92e4782b05 100644 --- a/core/src/main/java/hudson/slaves/SlaveComputer.java +++ b/core/src/main/java/hudson/slaves/SlaveComputer.java @@ -8,8 +8,10 @@ import hudson.remoting.Callable; import hudson.util.StreamTaskListener; import hudson.util.NullStream; import hudson.util.RingBufferLogHandler; +import hudson.util.Futures; import hudson.FilePath; import hudson.lifecycle.WindowsSlaveInstaller; +import hudson.Util; import hudson.maven.agent.Main; import hudson.maven.agent.PluginManagerInterceptor; @@ -27,6 +29,8 @@ import java.util.List; import java.util.Collections; import java.util.ArrayList; import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.Future; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -39,11 +43,19 @@ import javax.servlet.http.HttpServletResponse; * * @author Kohsuke Kawaguchi */ -public final class SlaveComputer extends Computer { +public class SlaveComputer extends Computer { private volatile Channel channel; private volatile transient boolean acceptingTasks = true; private Charset defaultCharset; private Boolean isUnix; + /** + * Effective {@link ComputerLauncher} that hides the details of + * how we launch a slave agent on this computer. + * + *

+ * This is normally the same as {@link Slave#getLauncher()} but + * can be different. See {@link #grabLauncher(Node)}. + */ private ComputerLauncher launcher; /** @@ -53,6 +65,17 @@ public final class SlaveComputer extends Computer { */ private transient int numRetryAttempt; + /** + * Tracks the status of the last launch operation, which is always asynchronous. + * This can be used to wait for the completion, or cancel the launch activity. + */ + private volatile Future lastConnectActivity = null; + + private Object constructed = new Object(); + + public SlaveComputer(Slave slave) { + super(slave); + } /** * {@inheritDoc} @@ -72,10 +95,6 @@ public final class SlaveComputer extends Computer { this.acceptingTasks = acceptingTasks; } - public SlaveComputer(Slave slave) { - super(slave); - } - /** * True if this computer is a Unix machine (as opposed to Windows machine). * @@ -90,6 +109,14 @@ public final class SlaveComputer extends Computer { return (Slave)super.getNode(); } + @Override + public String getIcon() { + Future l = lastConnectActivity; + if(l!=null && !l.isDone()) + return "computer-flash.gif"; + return super.getIcon(); + } + @Override @Deprecated public boolean isJnlpAgent() { @@ -105,15 +132,30 @@ public final class SlaveComputer extends Computer { return launcher; } - public void launch() { - if(channel!=null) return; + public Future connect(boolean forceReconnect) { + if(channel!=null) return Futures.precomputed(null); + if(!forceReconnect && lastConnectActivity!=null) + return lastConnectActivity; + if(forceReconnect && lastConnectActivity!=null) + logger.fine("Forcing a reconnect"); closeChannel(); - Computer.threadPoolForRemoting.execute(new Runnable() { - public void run() { + return lastConnectActivity = Computer.threadPoolForRemoting.submit(new java.util.concurrent.Callable() { + public Object call() throws Exception { // do this on another thread so that the lengthy launch operation // (which is typical) won't block UI thread. - launcher.launch(SlaveComputer.this, new StreamTaskListener(openLogFile())); + StreamTaskListener listener = new StreamTaskListener(openLogFile()); + try { + launcher.launch(SlaveComputer.this, listener); + return null; + } catch (IOException e) { + Util.displayIOException(e,listener); + e.printStackTrace(listener.error(Messages.ComputerLauncher_unexpectedError())); + throw e; + } catch (InterruptedException e) { + e.printStackTrace(listener.error(Messages.ComputerLauncher_abortedLaunch())); + throw e; + } } }); } @@ -161,6 +203,12 @@ public final class SlaveComputer extends Computer { } } + @Override + public boolean isConnecting() { + Future l = lastConnectActivity; + return isOffline() && l!=null && !l.isDone(); + } + public OutputStream openLogFile() { OutputStream os; try { @@ -174,8 +222,26 @@ public final class SlaveComputer extends Computer { private final Object channelLock = new Object(); + public void setChannel(InputStream in, OutputStream out, TaskListener taskListener, Channel.Listener listener) throws IOException, InterruptedException { + setChannel(in,out,taskListener.getLogger(),listener); + } + /** * Creates a {@link Channel} from the given stream and sets that to this slave. + * + * @param in + * Stream connected to the remote "slave.jar". It's the caller's responsibility to do + * buffering on this stream, if that's necessary. + * @param out + * Stream connected to the remote peer. It's the caller's responsibility to do + * buffering on this stream, if that's necessary. + * @param launchLog + * If non-null, receive the portion of data in is before + * the data goes into the "binary mode". This is useful + * when the established communication channel might include some data that might + * be useful for debugging/trouble-shooting. + * @param listener + * Gets a notification when the channel closes, to perform clean up. */ public void setChannel(InputStream in, OutputStream out, OutputStream launchLog, Channel.Listener listener) throws IOException, InterruptedException { if(this.channel!=null) @@ -204,10 +270,8 @@ public final class SlaveComputer extends Computer { {// send jars that we need for our operations // TODO: maybe I should generalize this kind of "post initialization" processing FilePath dst = new FilePath(channel, remoteFs); - new FilePath(Which.jarFile(Main.class)).copyTo(dst.child("maven-agent.jar")); - log.println("Copied maven-agent.jar"); - new FilePath(Which.jarFile(PluginManagerInterceptor.class)).copyTo(dst.child("maven-interceptor.jar")); - log.println("Copied maven-interceptor.jar"); + copyJar(log, dst, Main.class, "maven-agent"); + copyJar(log, dst, PluginManagerInterceptor.class, "maven-interceptor"); } channel.call(new LogInstaller()); @@ -235,6 +299,26 @@ public final class SlaveComputer extends Computer { Hudson.getInstance().getQueue().scheduleMaintenance(); } + /** + * Copies a jar file from the master to slave. + */ + private void copyJar(PrintWriter log, FilePath dst, Class representative, String seedName) throws IOException, InterruptedException { + // in normal execution environment, the master should be loading 'representative' from this jar, so + // in that way we can find it. + File jar = Which.jarFile(representative); + + if(jar.isDirectory()) { + // but during the development and unit test environment, we may be picking the class up from the classes dir, + // in which case we need to find this in a tricker way. + String dir = Hudson.getInstance().servletContext.getRealPath("/WEB-INF/lib"); + FilePath[] paths = new FilePath(new File(dir)).list(seedName + "-*.jar"); + jar = new File(paths[0].getRemote()); + } + + new FilePath(jar).copyTo(dst.child(seedName +".jar")); + log.println("Copied "+seedName+".jar"); + } + @Override public VirtualChannel getChannel() { return channel; @@ -262,8 +346,8 @@ public final class SlaveComputer extends Computer { } @Override - public void disconnect() { - Computer.threadPoolForRemoting.execute(new Runnable() { + public Future disconnect() { + return Computer.threadPoolForRemoting.submit(new Runnable() { public void run() { // do this on another thread so that any lengthy disconnect operation // (which could be typical) won't block UI thread. @@ -281,7 +365,7 @@ public final class SlaveComputer extends Computer { return; } - launch(); + connect(true); // TODO: would be nice to redirect the user to "launching..." wait page, // then spend a few seconds there and poll for the completion periodically. @@ -293,7 +377,7 @@ public final class SlaveComputer extends Computer { if(numRetryAttempt<6 || (numRetryAttempt%12)==0) { // initially retry several times quickly, and after that, do it infrequently. logger.info("Attempting to reconnect "+nodeName); - launch(); + connect(true); } } @@ -340,10 +424,29 @@ public final class SlaveComputer extends Computer { @Override protected void setNode(Node node) { super.setNode(node); - launcher = ((Slave)node).getLauncher(); + launcher = grabLauncher(node); // maybe the configuration was changed to relaunch the slave, so try to re-launch now. - launch(); + // "constructed==null" test is an ugly hack to avoid launching before the object is fully + // constructed. + if(constructed!=null) + connect(false); + } + + /** + * Grabs a {@link ComputerLauncher} out of {@link Node} to keep it in this {@link Computer}. + * The returned launcher will be set to {@link #launcher} and used to carry out the actual launch operation. + * + *

+ * Subtypes that needs to decorate {@link ComputerLauncher} can do so by overriding this method. + * This is useful for {@link SlaveComputer}s for clouds for example, where one normally needs + * additional pre-launch step (such as waiting for the provisioned node to become available) + * before the user specified launch step (like SSH connection) kicks in. + * + * @see ComputerLauncherFilter + */ + protected ComputerLauncher grabLauncher(Node node) { + return ((Slave)node).getLauncher(); } private static final Logger logger = Logger.getLogger(SlaveComputer.class.getName()); diff --git a/core/src/main/java/hudson/util/ArgumentListBuilder.java b/core/src/main/java/hudson/util/ArgumentListBuilder.java index ccd5e2ee4f..7cb99b8627 100644 --- a/core/src/main/java/hudson/util/ArgumentListBuilder.java +++ b/core/src/main/java/hudson/util/ArgumentListBuilder.java @@ -8,11 +8,17 @@ import java.util.StringTokenizer; import java.util.Arrays; import java.util.Map; import java.util.Properties; +import java.util.BitSet; +import java.util.Properties; import java.util.Map.Entry; import java.io.Serializable; import java.io.StringReader; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.File; +import java.io.StringReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; import org.jvnet.animal_sniffer.IgnoreJRERequirement; @@ -23,11 +29,16 @@ import org.jvnet.animal_sniffer.IgnoreJRERequirement; */ public class ArgumentListBuilder implements Serializable { private final List args = new ArrayList(); + private BitSet mask = new BitSet(); public ArgumentListBuilder add(Object a) { return add(a.toString()); } + public ArgumentListBuilder add(File f) { + return add(f.getAbsolutePath()); + } + public ArgumentListBuilder add(String a) { if(a!=null) args.add(a); @@ -35,6 +46,12 @@ public class ArgumentListBuilder implements Serializable { } public ArgumentListBuilder prepend(String... args) { + // left-shift the mask + BitSet nm = new BitSet(this.args.size()+args.length); + for(int i=0; i toList() { return args; } @@ -141,5 +165,33 @@ public class ArgumentListBuilder implements Serializable { return buf.toString(); } + /** + * Returns true if there are any masked arguments. + * @return true if there are any masked arguments; false otherwise + */ + public boolean hasMaskedArguments() { + return mask.length()>0; + } + + /** + * Returns an array of booleans where the masked arguments are marked as true + * @return an array of booleans. + */ + public boolean[] toMaskArray() { + boolean[] mask = new boolean[args.size()]; + for( int i=0; i * This code works around an issue in {@link DefaultCategoryDataset} where - * order of addition changes the way they are drawn. + * order of addition changes the way they are drawn. + * + * @param + * Names that identify different graphs drawn in the same chart. + * @param + * X-axis. */ public final class DataSetBuilder { diff --git a/core/src/main/java/hudson/util/DescribableList.java b/core/src/main/java/hudson/util/DescribableList.java index a163c7d589..f8be283af4 100644 --- a/core/src/main/java/hudson/util/DescribableList.java +++ b/core/src/main/java/hudson/util/DescribableList.java @@ -71,7 +71,7 @@ public class DescribableList, D extends Descriptor> public void add(T item) throws IOException { data.add(item); - owner.save(); + onModified(); } public T get(D descriptor) { @@ -89,7 +89,7 @@ public class DescribableList, D extends Descriptor> for (T t : data) { if(t.getDescriptor()==descriptor) { data.remove(t); - owner.save(); + onModified(); return; } } @@ -99,6 +99,13 @@ public class DescribableList, D extends Descriptor> return data.iterator(); } + /** + * Called when a list is mutated. + */ + protected void onModified() throws IOException { + owner.save(); + } + @SuppressWarnings("unchecked") public Map toMap() { return (Map)Descriptor.toMap(data); @@ -182,7 +189,7 @@ public class DescribableList, D extends Descriptor> * * Serializaion form is compatible with plain {@link List}. */ - public static final class ConverterImpl extends AbstractCollectionConverter { + public static class ConverterImpl extends AbstractCollectionConverter { CopyOnWriteList.ConverterImpl copyOnWriteListConverter; public ConverterImpl(Mapper mapper) { @@ -191,7 +198,8 @@ public class DescribableList, D extends Descriptor> } public boolean canConvert(Class type) { - return type==DescribableList.class; + // handle subtypes in case the onModified method is overridden. + return DescribableList.class.isAssignableFrom(type); } public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { diff --git a/core/src/main/java/hudson/util/DescriptorList.java b/core/src/main/java/hudson/util/DescriptorList.java index 8e47060bef..7c473d92f2 100644 --- a/core/src/main/java/hudson/util/DescriptorList.java +++ b/core/src/main/java/hudson/util/DescriptorList.java @@ -72,4 +72,11 @@ public final class DescriptorList> extends CopyOnWriteA throw new AssertionError(e); // Can't happen } } + + /** + * Finds the descriptor that has the matching fully-qualified class name. + */ + public Descriptor find(String fqcn) { + return Descriptor.find(this,fqcn); + } } diff --git a/core/src/main/java/hudson/util/FormFieldValidator.java b/core/src/main/java/hudson/util/FormFieldValidator.java index 1af1b7f475..d997421b99 100644 --- a/core/src/main/java/hudson/util/FormFieldValidator.java +++ b/core/src/main/java/hudson/util/FormFieldValidator.java @@ -7,6 +7,8 @@ import hudson.Util; import hudson.model.AbstractProject; import hudson.model.Hudson; import hudson.model.Item; +import hudson.model.Item; +import hudson.model.Job; import hudson.security.Permission; import hudson.security.AccessControlled; @@ -23,6 +25,8 @@ import javax.servlet.ServletException; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.acegisecurity.AccessDeniedException; +import org.kohsuke.stapler.Stapler; +import org.acegisecurity.AccessDeniedException; /** * Base class that provides the framework for doing on-the-fly form field validation. @@ -59,10 +63,28 @@ public abstract class FormFieldValidator { this(request, response, adminOnly?Hudson.getInstance():null, adminOnly?CHECK:null); } + /** + * @deprecated + * Use {@link #FormFieldValidator(Permission)} and remove {@link StaplerRequest} and {@link StaplerResponse} + * from your "doCheck..." method parameter + */ protected FormFieldValidator(StaplerRequest request, StaplerResponse response, Permission permission) { this(request,response,Hudson.getInstance(),permission); } + /** + * @param permission + * Permission needed to perform this validation, or null if no permission is necessary. + */ + protected FormFieldValidator(Permission permission) { + this(Stapler.getCurrentRequest(),Stapler.getCurrentResponse(),permission); + } + + /** + * @deprecated + * Use {@link #FormFieldValidator(AccessControlled,Permission)} and remove {@link StaplerRequest} and {@link StaplerResponse} + * from your "doCheck..." method parameter + */ protected FormFieldValidator(StaplerRequest request, StaplerResponse response, AccessControlled subject, Permission permission) { this.request = request; this.response = response; @@ -70,6 +92,10 @@ public abstract class FormFieldValidator { this.permission = permission; } + protected FormFieldValidator(AccessControlled subject, Permission permission) { + this(Stapler.getCurrentRequest(),Stapler.getCurrentResponse(),subject,permission); + } + /** * Runs the validation code. */ @@ -120,7 +146,11 @@ public abstract class FormFieldValidator { public void warning(String message) throws IOException, ServletException { warningWithMarkup(message==null?null:Util.escape(message)); } - + + public void ok(String message) throws IOException, ServletException { + okWithMarkup(message==null?null:Util.escape(message)); + } + /** * Sends out a string error message that indicates an error, * by formatting it with {@link String#format(String, Object[])} @@ -133,6 +163,10 @@ public abstract class FormFieldValidator { warning(String.format(format,args)); } + public void ok(String format, Object... args) throws IOException, ServletException { + ok(String.format(format,args)); + } + /** * Sends out an HTML fragment that indicates an error. * @@ -152,6 +186,10 @@ public abstract class FormFieldValidator { _errorWithMarkup(message,"warning"); } + public void okWithMarkup(String message) throws IOException, ServletException { + _errorWithMarkup(message,"ok"); + } + private void _errorWithMarkup(String message, String cssClass) throws IOException, ServletException { if(message==null) { ok(); @@ -501,4 +539,48 @@ public abstract class FormFieldValidator { ok(); } } + + /** + * Verifies that the 'value' parameter is correct base64 encoded text. + * + * @since 1.257 + */ + public static class Base64 extends FormFieldValidator { + private final boolean allowWhitespace; + private final boolean allowEmpty; + private final String errorMessage; + + public Base64(StaplerRequest request, StaplerResponse response, boolean allowWhitespace, boolean allowEmpty, String errorMessage) { + super(request, response, false); + this.allowWhitespace = allowWhitespace; + this.allowEmpty = allowEmpty; + this.errorMessage = errorMessage; + } + + protected void check() throws IOException, ServletException { + try { + String v = request.getParameter("value"); + if(!allowWhitespace) { + if(v.indexOf(' ')>=0 || v.indexOf('\n')>=0) { + fail(); + return; + } + } + v=v.trim(); + if(!allowEmpty && v.length()==0) { + fail(); + return; + } + + com.trilead.ssh2.crypto.Base64.decode(v.toCharArray()); + ok(); + } catch (IOException e) { + fail(); + } + } + + protected void fail() throws IOException, ServletException { + error(errorMessage); + } + } } diff --git a/core/src/main/java/hudson/util/Futures.java b/core/src/main/java/hudson/util/Futures.java new file mode 100644 index 0000000000..406e49f88a --- /dev/null +++ b/core/src/main/java/hudson/util/Futures.java @@ -0,0 +1,39 @@ +package hudson.util; + +import hudson.remoting.Future; + +import java.util.concurrent.TimeUnit; + +/** + * Various {@link Future} implementations. + * + * @author Kohsuke Kawaguchi + */ +public class Futures { + /** + * Creates a {@link Future} instance that already has its value pre-computed. + */ + public static Future precomputed(final T value) { + return new Future() { + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + public boolean isCancelled() { + return false; + } + + public boolean isDone() { + return true; + } + + public T get() { + return value; + } + + public T get(long timeout, TimeUnit unit) { + return value; + } + }; + } +} diff --git a/core/src/main/java/hudson/util/NoOverlapCategoryAxis.java b/core/src/main/java/hudson/util/NoOverlapCategoryAxis.java new file mode 100644 index 0000000000..413bcab4bc --- /dev/null +++ b/core/src/main/java/hudson/util/NoOverlapCategoryAxis.java @@ -0,0 +1,144 @@ +package hudson.util; + +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.AxisState; +import org.jfree.chart.axis.CategoryTick; +import org.jfree.chart.axis.CategoryLabelPosition; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.entity.EntityCollection; +import org.jfree.chart.entity.CategoryLabelEntity; +import org.jfree.ui.RectangleEdge; +import org.jfree.ui.RectangleAnchor; +import org.jfree.text.TextBlock; + +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Point2D; +import java.util.*; + +/** + * This class implements X-axis label skipping algorithm to + * avoid drawing overlapping labels. + * + * @author Kohsuke Kawaguchi + */ +public class NoOverlapCategoryAxis extends CategoryAxis { + public NoOverlapCategoryAxis(String label) { + super(label); + } + + @Override + protected AxisState drawCategoryLabels(Graphics2D g2, + Rectangle2D plotArea, + Rectangle2D dataArea, + RectangleEdge edge, + AxisState state, + PlotRenderingInfo plotState) { + + if (state == null) { + throw new IllegalArgumentException("Null 'state' argument."); + } + + if (isTickLabelsVisible()) { + java.util.List ticks = refreshTicks(g2, state, plotArea, edge); + state.setTicks(ticks); + + // remember the last drawn label so that we can avoid drawing overlapping labels. + Rectangle2D r = null; + + int categoryIndex = 0; + Iterator iterator = ticks.iterator(); + while (iterator.hasNext()) { + + CategoryTick tick = (CategoryTick) iterator.next(); + g2.setFont(getTickLabelFont(tick.getCategory())); + g2.setPaint(getTickLabelPaint(tick.getCategory())); + + CategoryLabelPosition position + = this.getCategoryLabelPositions().getLabelPosition(edge); + double x0 = 0.0; + double x1 = 0.0; + double y0 = 0.0; + double y1 = 0.0; + if (edge == RectangleEdge.TOP) { + x0 = getCategoryStart(categoryIndex, ticks.size(), + dataArea, edge); + x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, + edge); + y1 = state.getCursor() - this.getCategoryLabelPositionOffset(); + y0 = y1 - state.getMax(); + } + else if (edge == RectangleEdge.BOTTOM) { + x0 = getCategoryStart(categoryIndex, ticks.size(), + dataArea, edge); + x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, + edge); + y0 = state.getCursor() + this.getCategoryLabelPositionOffset(); + y1 = y0 + state.getMax(); + } + else if (edge == RectangleEdge.LEFT) { + y0 = getCategoryStart(categoryIndex, ticks.size(), + dataArea, edge); + y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, + edge); + x1 = state.getCursor() - this.getCategoryLabelPositionOffset(); + x0 = x1 - state.getMax(); + } + else if (edge == RectangleEdge.RIGHT) { + y0 = getCategoryStart(categoryIndex, ticks.size(), + dataArea, edge); + y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, + edge); + x0 = state.getCursor() + this.getCategoryLabelPositionOffset(); + x1 = x0 - state.getMax(); + } + Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), + (y1 - y0)); + if(r==null || !r.intersects(area)) { + Point2D anchorPoint = RectangleAnchor.coordinates(area, + position.getCategoryAnchor()); + TextBlock block = tick.getLabel(); + block.draw(g2, (float) anchorPoint.getX(), + (float) anchorPoint.getY(), position.getLabelAnchor(), + (float) anchorPoint.getX(), (float) anchorPoint.getY(), + position.getAngle()); + Shape bounds = block.calculateBounds(g2, + (float) anchorPoint.getX(), (float) anchorPoint.getY(), + position.getLabelAnchor(), (float) anchorPoint.getX(), + (float) anchorPoint.getY(), position.getAngle()); + if (plotState != null && plotState.getOwner() != null) { + EntityCollection entities + = plotState.getOwner().getEntityCollection(); + if (entities != null) { + String tooltip = getCategoryLabelToolTip( + tick.getCategory()); + entities.add(new CategoryLabelEntity(tick.getCategory(), + bounds, tooltip, null)); + } + } + r = bounds.getBounds2D(); + } + + categoryIndex++; + } + + if (edge.equals(RectangleEdge.TOP)) { + double h = state.getMax(); + state.cursorUp(h); + } + else if (edge.equals(RectangleEdge.BOTTOM)) { + double h = state.getMax(); + state.cursorDown(h); + } + else if (edge == RectangleEdge.LEFT) { + double w = state.getMax(); + state.cursorLeft(w); + } + else if (edge == RectangleEdge.RIGHT) { + double w = state.getMax(); + state.cursorRight(w); + } + } + return state; + } +} diff --git a/core/src/main/java/hudson/util/Secret.java b/core/src/main/java/hudson/util/Secret.java new file mode 100644 index 0000000000..a0653c77fe --- /dev/null +++ b/core/src/main/java/hudson/util/Secret.java @@ -0,0 +1,146 @@ +package hudson.util; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; +import com.trilead.ssh2.crypto.Base64; +import hudson.model.Hudson; + +import javax.crypto.SecretKey; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; + +/** + * Glorified {@link String} that uses encryption in the persisted form, to avoid accidental exposure of a secret. + * + *

+ * Note that since the cryptography relies on {@link Hudson#getSecretKey()}, this is not meant as a protection + * against code running in the same VM, nor against an attacker who has local file system access. + * + * @author Kohsuke Kawaguchi + */ +public final class Secret { + /** + * Unencrypted secret text. + */ + private final String value; + + private Secret(String value) { + this.value = value; + } + + /** + * Obtains the secret text. + */ + public String toString() { + return value; + } + + public boolean equals(Object that) { + return that instanceof Secret && value.equals(((Secret)that).value); + } + + public int hashCode() { + return value.hashCode(); + } + + /** + * Turns {@link Hudson#getSecretKey()} into an AES key. + */ + private static SecretKey getKey() throws UnsupportedEncodingException, GeneralSecurityException { + String secret = SECRET; + if(secret==null) secret = Hudson.getInstance().getSecretKey(); + + // turn secretKey into 256 bit hash + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.reset(); + digest.update(secret.getBytes("UTF-8")); + + // Due to the stupid US export restriction JDK only ships 128bit version. + return new SecretKeySpec(digest.digest(),0,128/8, "AES"); + } + + /** + * Encrypts {@link #value} and returns it in an encoded printable form. + */ + public String getEncryptedValue() { + try { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, getKey()); + // add the magic suffix which works like a check sum. + return new String(Base64.encode(cipher.doFinal((value+MAGIC).getBytes("UTF-8")))); + } catch (GeneralSecurityException e) { + throw new Error(e); // impossible + } catch (UnsupportedEncodingException e) { + throw new Error(e); // impossible + } + } + + /** + * Reverse operation of {@link #getEncryptedValue()}. Returns null + * if the given cipher text was invalid. + */ + public static Secret decrypt(String data) { + if(data==null) return null; + try { + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.DECRYPT_MODE, getKey()); + String plainText = new String(cipher.doFinal(Base64.decode(data.toCharArray())), "UTF-8"); + if(plainText.endsWith(MAGIC)) + return new Secret(plainText.substring(0,plainText.length()-MAGIC.length())); + return null; + } catch (GeneralSecurityException e) { + return null; + } catch (UnsupportedEncodingException e) { + throw new Error(e); // impossible + } catch (IOException e) { + return null; + } + } + + /** + * Attempts to treat the given string first as a cipher text, and if it doesn't work, + * treat the given string as the unencrypted secret value. + * + *

+ * Useful for recovering a value from a form field. + * + * @return never null + */ + public static Secret fromString(String data) { + Secret s = decrypt(data); + if(s==null) s=new Secret(data); + return s; + } + + public static final class ConverterImpl implements Converter { + public ConverterImpl() { + } + + public boolean canConvert(Class type) { + return type==Secret.class; + } + + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + Secret src = (Secret) source; + writer.setValue(src.getEncryptedValue()); + } + + public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) { + return Secret.decrypt(reader.getValue()); + } + } + + private static final String MAGIC = "::::MAGIC::::"; + + /** + * For testing only. Override the secret key so that we can test this class without {@link Hudson}. + */ + /*package*/ static String SECRET = null; +} diff --git a/core/src/main/java/hudson/util/ShiftedCategoryAxis.java b/core/src/main/java/hudson/util/ShiftedCategoryAxis.java index c4f0165c5e..7e3ac4576b 100644 --- a/core/src/main/java/hudson/util/ShiftedCategoryAxis.java +++ b/core/src/main/java/hudson/util/ShiftedCategoryAxis.java @@ -21,12 +21,8 @@ import java.util.Iterator; /** * {@link CategoryAxis} shifted to left to eliminate redundant space * between area and the Y-axis. - * - *

- * This class also implements X-axis label skipping algorithm to - * avoid drawing overlapping labels. */ -public final class ShiftedCategoryAxis extends CategoryAxis { +public final class ShiftedCategoryAxis extends NoOverlapCategoryAxis { public ShiftedCategoryAxis(String label) { super(label); } @@ -51,118 +47,4 @@ public final class ShiftedCategoryAxis extends CategoryAxis { - calculateCategorySize(categoryCount, area, edge) / 2; } - @Override - protected AxisState drawCategoryLabels(Graphics2D g2, - Rectangle2D plotArea, - Rectangle2D dataArea, - RectangleEdge edge, - AxisState state, - PlotRenderingInfo plotState) { - - if (state == null) { - throw new IllegalArgumentException("Null 'state' argument."); - } - - if (isTickLabelsVisible()) { - List ticks = refreshTicks(g2, state, plotArea, edge); - state.setTicks(ticks); - - // remember the last drawn label so that we can avoid drawing overlapping labels. - Rectangle2D r = null; - - int categoryIndex = 0; - Iterator iterator = ticks.iterator(); - while (iterator.hasNext()) { - - CategoryTick tick = (CategoryTick) iterator.next(); - g2.setFont(getTickLabelFont(tick.getCategory())); - g2.setPaint(getTickLabelPaint(tick.getCategory())); - - CategoryLabelPosition position - = this.getCategoryLabelPositions().getLabelPosition(edge); - double x0 = 0.0; - double x1 = 0.0; - double y0 = 0.0; - double y1 = 0.0; - if (edge == RectangleEdge.TOP) { - x0 = getCategoryStart(categoryIndex, ticks.size(), - dataArea, edge); - x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, - edge); - y1 = state.getCursor() - this.getCategoryLabelPositionOffset(); - y0 = y1 - state.getMax(); - } - else if (edge == RectangleEdge.BOTTOM) { - x0 = getCategoryStart(categoryIndex, ticks.size(), - dataArea, edge); - x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, - edge); - y0 = state.getCursor() + this.getCategoryLabelPositionOffset(); - y1 = y0 + state.getMax(); - } - else if (edge == RectangleEdge.LEFT) { - y0 = getCategoryStart(categoryIndex, ticks.size(), - dataArea, edge); - y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, - edge); - x1 = state.getCursor() - this.getCategoryLabelPositionOffset(); - x0 = x1 - state.getMax(); - } - else if (edge == RectangleEdge.RIGHT) { - y0 = getCategoryStart(categoryIndex, ticks.size(), - dataArea, edge); - y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, - edge); - x0 = state.getCursor() + this.getCategoryLabelPositionOffset(); - x1 = x0 - state.getMax(); - } - Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), - (y1 - y0)); - if(r==null || !r.intersects(area)) { - Point2D anchorPoint = RectangleAnchor.coordinates(area, - position.getCategoryAnchor()); - TextBlock block = tick.getLabel(); - block.draw(g2, (float) anchorPoint.getX(), - (float) anchorPoint.getY(), position.getLabelAnchor(), - (float) anchorPoint.getX(), (float) anchorPoint.getY(), - position.getAngle()); - Shape bounds = block.calculateBounds(g2, - (float) anchorPoint.getX(), (float) anchorPoint.getY(), - position.getLabelAnchor(), (float) anchorPoint.getX(), - (float) anchorPoint.getY(), position.getAngle()); - if (plotState != null && plotState.getOwner() != null) { - EntityCollection entities - = plotState.getOwner().getEntityCollection(); - if (entities != null) { - String tooltip = getCategoryLabelToolTip( - tick.getCategory()); - entities.add(new CategoryLabelEntity(tick.getCategory(), - bounds, tooltip, null)); - } - } - r = bounds.getBounds2D(); - } - - categoryIndex++; - } - - if (edge.equals(RectangleEdge.TOP)) { - double h = state.getMax(); - state.cursorUp(h); - } - else if (edge.equals(RectangleEdge.BOTTOM)) { - double h = state.getMax(); - state.cursorDown(h); - } - else if (edge == RectangleEdge.LEFT) { - double w = state.getMax(); - state.cursorLeft(w); - } - else if (edge == RectangleEdge.RIGHT) { - double w = state.getMax(); - state.cursorRight(w); - } - } - return state; - } } diff --git a/core/src/main/java/hudson/util/XStream2.java b/core/src/main/java/hudson/util/XStream2.java index 0e9d3bc703..c87b7fdaf8 100644 --- a/core/src/main/java/hudson/util/XStream2.java +++ b/core/src/main/java/hudson/util/XStream2.java @@ -50,6 +50,7 @@ public class XStream2 extends XStream { private void init() { registerConverter(new RobustCollectionConverter(getMapper(),getReflectionProvider()),10); registerConverter(new CopyOnWriteMap.Tree.ConverterImpl(getMapper()),10); // needs to override MapConverter + registerConverter(new DescribableList.ConverterImpl(getMapper()),10); // explicitly added to handle subtypes // this should come after all the XStream's default simpler converters, // but before reflection-based one kicks in. diff --git a/core/src/main/resources/hudson/model/Computer/configure.jelly b/core/src/main/resources/hudson/model/Computer/configure.jelly new file mode 100644 index 0000000000..3fd4a15979 --- /dev/null +++ b/core/src/main/resources/hudson/model/Computer/configure.jelly @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Computer/delete.jelly b/core/src/main/resources/hudson/model/Computer/delete.jelly new file mode 100644 index 0000000000..18abc44d3f --- /dev/null +++ b/core/src/main/resources/hudson/model/Computer/delete.jelly @@ -0,0 +1,11 @@ + + + + +

+ ${%Are you sure about deleting the slave?} + + + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Computer/index.jelly b/core/src/main/resources/hudson/model/Computer/index.jelly index 2edf15baf7..37515703a2 100644 --- a/core/src/main/resources/hudson/model/Computer/index.jelly +++ b/core/src/main/resources/hudson/model/Computer/index.jelly @@ -18,7 +18,10 @@

- ${it.caption} (${it.node.nodeDescription}) + ${it.caption} + + (${it.node.nodeDescription}) +

diff --git a/core/src/main/resources/hudson/model/Computer/load-statistics.jelly b/core/src/main/resources/hudson/model/Computer/load-statistics.jelly new file mode 100644 index 0000000000..0ab8b39c3b --- /dev/null +++ b/core/src/main/resources/hudson/model/Computer/load-statistics.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Computer/sidepanel.jelly b/core/src/main/resources/hudson/model/Computer/sidepanel.jelly index de7d081c42..fe2d72865d 100644 --- a/core/src/main/resources/hudson/model/Computer/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/Computer/sidepanel.jelly @@ -6,10 +6,13 @@ - - - - + + + + + + + diff --git a/core/src/main/resources/hudson/model/ComputerSet/_new.jelly b/core/src/main/resources/hudson/model/ComputerSet/_new.jelly new file mode 100644 index 0000000000..00c790cd54 --- /dev/null +++ b/core/src/main/resources/hudson/model/ComputerSet/_new.jelly @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/ComputerSet/index.jelly b/core/src/main/resources/hudson/model/ComputerSet/index.jelly index d6df6e9ee6..a504fd1b3d 100644 --- a/core/src/main/resources/hudson/model/ComputerSet/index.jelly +++ b/core/src/main/resources/hudson/model/ComputerSet/index.jelly @@ -37,6 +37,11 @@ + + + + +
diff --git a/core/src/main/resources/hudson/model/ComputerSet/new.jelly b/core/src/main/resources/hudson/model/ComputerSet/new.jelly new file mode 100644 index 0000000000..9350520c2f --- /dev/null +++ b/core/src/main/resources/hudson/model/ComputerSet/new.jelly @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/ComputerSet/sidepanel.jelly b/core/src/main/resources/hudson/model/ComputerSet/sidepanel.jelly index ed2766e30e..e15cd0135f 100644 --- a/core/src/main/resources/hudson/model/ComputerSet/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/ComputerSet/sidepanel.jelly @@ -6,6 +6,8 @@ + + diff --git a/core/src/main/resources/hudson/model/Hudson/configure.jelly b/core/src/main/resources/hudson/model/Hudson/configure.jelly index 90c7db426b..bcaa08efb9 100644 --- a/core/src/main/resources/hudson/model/Hudson/configure.jelly +++ b/core/src/main/resources/hudson/model/Hudson/configure.jelly @@ -12,6 +12,13 @@ + + + + + + @@ -119,6 +126,17 @@ + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors.jelly b/core/src/main/resources/hudson/model/Hudson/configureExecutors.jelly deleted file mode 100644 index 94740c9c2c..0000000000 --- a/core/src/main/resources/hudson/model/Hudson/configureExecutors.jelly +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - ${%Master} - - - ${%This Hudson server} - - - - - - ${it.rootDir} - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
- -
- -
-
-
-
- - - - -
-
-
-
diff --git a/core/src/main/resources/hudson/model/Hudson/load-statistics.jelly b/core/src/main/resources/hudson/model/Hudson/load-statistics.jelly new file mode 100644 index 0000000000..edd1291a4e --- /dev/null +++ b/core/src/main/resources/hudson/model/Hudson/load-statistics.jelly @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Hudson/manage.jelly b/core/src/main/resources/hudson/model/Hudson/manage.jelly index bc59b85f23..ccad601649 100644 --- a/core/src/main/resources/hudson/model/Hudson/manage.jelly +++ b/core/src/main/resources/hudson/model/Hudson/manage.jelly @@ -47,9 +47,6 @@ ${%Configure global settings and paths.} - - ${%Configure resources available for executing jobs.} - ${%Discard all the loaded data in memory and reload everything from file system.} ${%Useful when you modified config files directly on disk.} @@ -66,10 +63,13 @@ ${%SystemLogText} + + ${%LoadStatisticsText} + ${%Executes arbitrary script for administration/trouble-shooting/diagnostics.} - + ${%Check the health of slaves and controls them.} diff --git a/core/src/main/resources/hudson/model/Hudson/manage.properties b/core/src/main/resources/hudson/model/Hudson/manage.properties index f924935828..d1e157dc7b 100644 --- a/core/src/main/resources/hudson/model/Hudson/manage.properties +++ b/core/src/main/resources/hudson/model/Hudson/manage.properties @@ -1,3 +1,4 @@ SystemLogText=\ System log captures output from java.util.logging output related to Hudson. -NewVersionAvailable=New version of Hudson ({0}) is available for download. \ No newline at end of file +NewVersionAvailable=New version of Hudson ({0}) is available for download. +LoadStatisticsText=Check your resource utilization and see if you need more computers for your builds. \ No newline at end of file diff --git a/core/src/main/resources/hudson/model/Label/load-statistics.jelly b/core/src/main/resources/hudson/model/Label/load-statistics.jelly new file mode 100644 index 0000000000..0ab8b39c3b --- /dev/null +++ b/core/src/main/resources/hudson/model/Label/load-statistics.jelly @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Label/sidepanel.jelly b/core/src/main/resources/hudson/model/Label/sidepanel.jelly index 5dd261741c..dbf8fdcdb6 100644 --- a/core/src/main/resources/hudson/model/Label/sidepanel.jelly +++ b/core/src/main/resources/hudson/model/Label/sidepanel.jelly @@ -6,6 +6,8 @@ + + - \ No newline at end of file + diff --git a/core/src/main/resources/hudson/model/LoadStatistics/main.jelly b/core/src/main/resources/hudson/model/LoadStatistics/main.jelly new file mode 100644 index 0000000000..0d74ff7c81 --- /dev/null +++ b/core/src/main/resources/hudson/model/LoadStatistics/main.jelly @@ -0,0 +1,20 @@ + + +

+ + ${%title(it.displayName)} +

+
+ ${%Timespan}: + ${%Short} + + ${%Medium} + + ${%Long} +
+ + +
+ ${%blurb} +
+
diff --git a/core/src/main/resources/hudson/model/LoadStatistics/main.properties b/core/src/main/resources/hudson/model/LoadStatistics/main.properties new file mode 100644 index 0000000000..b39197e677 --- /dev/null +++ b/core/src/main/resources/hudson/model/LoadStatistics/main.properties @@ -0,0 +1,30 @@ +title=Load statistics: {0} +blurb=\ + Load statistics keep track of three key metrics of resource utilization: \ +
\ +
Total number of executors
\ +
\ + For a computer, this is the number of executors that the computer has. \ + For a label, this is the sum of all executors across all computers in this label. \ + For the entire Hudson, this is the sum of all executors across all computers in this Hudson installation. \ + Other than configuration changes, this value can also change when slaves go offline. \ +
\ +
Number of busy executors
\ +
\ + This line tracks the number of executors (among the executors counted above) \ + that are carrying out builds. The ratio of this to the total number of executors \ + gives you the resource utilization. If all your executors are busy for \ + a prolonged period of time, consider adding more computers to your Hudson cluster.\ +
\ +
Queue length
\ +
\ + This is the number of jobs that are in the build queue, waiting for an \ + available executor (of this computer, of this label, or in this Hudson, respectively.) \ + This doesn't include jobs that are in the quiet period, nor does it include \ + jobs that are in the queue because earlier builds are still in progress. \ + If this line ever goes above 0, that means your Hudson will run more builds by \ + adding more computers.\ +
\ +
\ + The graph is exponential moving average of periodically collected data values. \ + 3 timespans are updated every 10 seconds, 1 minute, and 1 hour respectively. \ 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 c822859eb7..cee298de05 100644 --- a/core/src/main/resources/hudson/model/Messages.properties +++ b/core/src/main/resources/hudson/model/Messages.properties @@ -33,6 +33,9 @@ BallColor.Unstable=Unstable Computer.Caption=Slave {0} +ComputerSet.NoSuchSlave=No such slave: {0} +ComputerSet.SlaveAlreadyExists=Slave called ''{0}'' already exists +ComputerSet.SpecifySlaveToCopy=Specify which slave to copy Executor.NotAvailable=N/A ExternalJob.DisplayName=Monitor an external job diff --git a/core/src/main/resources/hudson/model/View/newJob.jelly b/core/src/main/resources/hudson/model/View/newJob.jelly index 7d030b96d2..5c0424b52e 100644 --- a/core/src/main/resources/hudson/model/View/newJob.jelly +++ b/core/src/main/resources/hudson/model/View/newJob.jelly @@ -6,57 +6,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - ${%Copy from} - - - - - - - - + + diff --git a/core/src/main/resources/hudson/slaves/ComputerLauncherFilter/main.jelly b/core/src/main/resources/hudson/slaves/ComputerLauncherFilter/main.jelly new file mode 100644 index 0000000000..bbea6d701c --- /dev/null +++ b/core/src/main/resources/hudson/slaves/ComputerLauncherFilter/main.jelly @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries.jelly b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries.jelly new file mode 100644 index 0000000000..0417cdb02e --- /dev/null +++ b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries.jelly @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_de.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_de.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_de.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_de.properties diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_fr.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_fr.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_fr.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_fr.properties diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_ja.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_ja.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_ja.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_ja.properties diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_pt_BR.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_pt_BR.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_pt_BR.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_pt_BR.properties diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_ru.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_ru.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_ru.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_ru.properties diff --git a/core/src/main/resources/hudson/model/Hudson/configureExecutors_tr.properties b/core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_tr.properties similarity index 100% rename from core/src/main/resources/hudson/model/Hudson/configureExecutors_tr.properties rename to core/src/main/resources/hudson/slaves/DumbSlave/configure-entries_tr.properties diff --git a/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.jelly b/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.jelly new file mode 100644 index 0000000000..c6dfbf53ed --- /dev/null +++ b/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.jelly @@ -0,0 +1,3 @@ +
+ ${%detail} +
\ No newline at end of file diff --git a/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.properties b/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.properties new file mode 100644 index 0000000000..ae1751217f --- /dev/null +++ b/core/src/main/resources/hudson/slaves/DumbSlave/newInstanceDetail.properties @@ -0,0 +1,5 @@ +detail=\ + Adds a plain, dumb slave to Hudson. This is called "dumb" because Hudson doesn''t provide \ + higher level of integration with these slaves, such as dynamic provisioning. \ + Select this type if no other slave types apply — for example such as when you are adding \ + a physical computer, virtual machines managed outside Hudson, etc. \ No newline at end of file diff --git a/core/src/main/resources/hudson/slaves/RetentionStrategy/Demand/config.jelly b/core/src/main/resources/hudson/slaves/RetentionStrategy/Demand/config.jelly index 8c1401bf29..90b6148fcd 100644 --- a/core/src/main/resources/hudson/slaves/RetentionStrategy/Demand/config.jelly +++ b/core/src/main/resources/hudson/slaves/RetentionStrategy/Demand/config.jelly @@ -3,11 +3,11 @@ + checkUrl="'${rootURL}/fieldCheck?errorText='+escape('${%In demand delay is mandatory.}')+'&value='+escape(this.value)"/> + checkUrl="'${rootURL}/fieldCheck?errorText='+escape('${%Idle delay is mandatory.}')+'&value='+escape(this.value)"/> \ No newline at end of file diff --git a/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel.jelly b/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel.jelly deleted file mode 100644 index a1ed55f987..0000000000 --- a/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel.jelly +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel2.jelly b/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel2.jelly new file mode 100644 index 0000000000..6086367f49 --- /dev/null +++ b/core/src/main/resources/hudson/slaves/SlaveComputer/sidepanel2.jelly @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/enum.jelly b/core/src/main/resources/lib/form/enum.jelly new file mode 100644 index 0000000000..df2c1c4192 --- /dev/null +++ b/core/src/main/resources/lib/form/enum.jelly @@ -0,0 +1,17 @@ + + + + diff --git a/core/src/main/resources/lib/form/password.jelly b/core/src/main/resources/lib/form/password.jelly index 8bb176aea6..e4dd36d8fc 100644 --- a/core/src/main/resources/lib/form/password.jelly +++ b/core/src/main/resources/lib/form/password.jelly @@ -2,7 +2,12 @@ Glorified --> - + \ No newline at end of file diff --git a/core/src/main/resources/lib/form/repeatable.jelly b/core/src/main/resources/lib/form/repeatable.jelly index 663917cf79..b8dfba910b 100644 --- a/core/src/main/resources/lib/form/repeatable.jelly +++ b/core/src/main/resources/lib/form/repeatable.jelly @@ -64,7 +64,23 @@ - + + + + + + + + + + + + + + +