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 extends T> 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 dynamicLabels = null;
+ /**
+ * Load statistics of the entire system.
+ */
+ public transient final OverallLoadStatistics overallLoad = new OverallLoadStatistics();
+
+ /**
+ * {@link NodeProvisioner} that reacts to {@link OverallLoadStatistics}.
+ */
+ public transient final NodeProvisioner overallNodeProvisioner = new NodeProvisioner(null,overallLoad);
+
public transient final ServletContext servletContext;
/**
@@ -319,9 +370,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroupconfig.xml to avoid
+ * accidental exposure.
*/
- private final String secretKey;
+ private transient final String secretKey;
private transient final UpdateCenter updateCenter = new UpdateCenter();
@@ -387,7 +439,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup();
+ if(slaves==null) slaves = new NodeList();
// work around to have MavenModule register itself until we either move it to a plugin
// or make it a part of the core.
@@ -423,7 +475,16 @@ public final class Hudson extends AbstractModelObject implements ItemGroup>>>>>> .merge-right.r13488
+=======
+ UsageStatistics.register();
+>>>>>>> .merge-right.r13919
}
public TcpSlaveAgentListener getTcpSlaveAgentListener() {
@@ -454,6 +515,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroup>>>>>> .merge-right.r13919
/**
* Does this {@link View} has any associated user information recorded?
*/
@@ -495,7 +567,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup byName = new HashMap();
for (Computer c : computers.values()) {
if(c.getNode()==null)
@@ -701,7 +775,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup used = new HashSet();
updateComputer(this, byName, used);
- for (Slave s : getSlaves())
+ for (Node s : getNodes())
updateComputer(s, byName, used);
// find out what computers are removed, and kill off all executors.
@@ -721,8 +795,10 @@ public final class Hudson extends AbstractModelObject implements ItemGroup0)
+ if(n.getNumExecutors()>0) {
computers.put(n,c=n.createComputer());
+ c.connect(true);
+ }
}
used.add(c);
}
@@ -929,7 +1005,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup getLabels() {
Set r = new TreeSet();
for (Label l : labels.values()) {
- if(!l.getNodes().isEmpty())
+ if(!l.isEmpty())
r.add(l);
}
return r;
@@ -968,35 +1044,126 @@ public final class Hudson extends AbstractModelObject implements ItemGroup getSlaves() {
+ return (List)Collections.unmodifiableList(slaves);
+ }
+
+ /**
+ * Returns all {@link Node}s in the system, excluding {@link Hudson} instance itself which
+ * represents the master.
+ */
+ public List getNodes() {
return Collections.unmodifiableList(slaves);
}
/**
* Updates the slave list.
+ *
+ * @deprecated
+ * Use {@link #setNodes(List)}. Since 1.252.
*/
public void setSlaves(List slaves) throws IOException {
- this.slaves = new ArrayList(slaves);
- updateComputerList();
+ setNodes(slaves);
+ }
- // label trim off
+ /**
+ * Adds one more {@link Node} to Hudson.
+ */
+ public synchronized void addNode(Node n) throws IOException {
+ ArrayList nl = new ArrayList(this.slaves);
+ nl.add(n);
+ setNodes(nl);
+ }
+
+ /**
+ * Removes a {@link Node} from Hudson.
+ */
+ public synchronized void removeNode(Node n) throws IOException {
+ n.toComputer().disconnect();
+
+ ArrayList nl = new ArrayList(this.slaves);
+ nl.remove(n);
+ setNodes(nl);
+ }
+
+ public void setNodes(List extends Node> nodes) throws IOException {
+ // make sure that all names are unique
+ Set names = new HashSet();
+ for (Node n : nodes)
+ if(!names.add(n.getNodeName()))
+ throw new IllegalArgumentException(n.getNodeName()+" is defined more than once");
+ this.slaves = new NodeList(nodes);
+ updateComputerList();
+ trimLabels();
+ save();
+ }
+
+ /**
+ * Resets all labels and remove invalid ones.
+ */
+ private void trimLabels() {
for (Iterator itr = labels.values().iterator(); itr.hasNext();) {
Label l = itr.next();
l.reset();
- if(l.getNodes().isEmpty())
+ if(l.isEmpty())
itr.remove();
}
+ }
- save();
+ public NodeDescriptor getDescriptor() {
+ return DescriptorImpl.INSTANCE;
+ }
+
+ public static final class DescriptorImpl extends NodeDescriptor {
+ public static final DescriptorImpl INSTANCE = new DescriptorImpl();
+ private DescriptorImpl() {
+ super(Hudson.class);
+ }
+
+ public String getDisplayName() {
+ throw new UnsupportedOperationException();
+ }
+
+ // to route /descriptor/FQCN/xxx to getDescriptor(FQCN).xxx
+ public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
+ return Hudson.getInstance().getDescriptor(token);
+ }
}
/**
@@ -1513,7 +1680,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup> pending = new HashSet>();
terminating = true;
for( Computer c : computers.values() ) {
c.interrupt();
c.kill();
- c.disconnect();
+ pending.add(c.disconnect());
}
ExternalJob.reloadThread.interrupt();
Trigger.timer.cancel();
+ // TODO: how to wait for the completion of the last job?
Trigger.timer = null;
if(tcpSlaveAgentListener!=null)
tcpSlaveAgentListener.shutdown();
@@ -1589,7 +1758,18 @@ public final class Hudson extends AbstractModelObject implements ItemGroup f : pending)
+ try {
+ f.get(10, TimeUnit.SECONDS); // if clean up operation didn't complete in time, we fail the test
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break; // someone wants us to die now. quick!
+ } catch (ExecutionException e) {
+ LOGGER.log(Level.WARNING, "Failed to shut down properly",e);
+ } catch (TimeoutException e) {
+ LOGGER.log(Level.WARNING, "Failed to shut down properly",e);
+ }
+
LogFactory.releaseAll();
theInstance = null;
@@ -1615,6 +1795,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup>>>>>> .merge-right.r12882
* Accepts the new description.
*/
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
@@ -1831,7 +2025,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup>>>>>> .merge-right.r13919
// ability to run arbitrary script is dangerous
checkPermission(ADMINISTER);
@@ -2593,6 +2802,11 @@ public final class Hudson extends AbstractModelObject implements ItemGroup connect(boolean forceReconnect) {
+ return Futures.precomputed(null);
}
/**
@@ -2779,7 +2998,7 @@ public final class Hudson extends AbstractModelObject implements ItemGroup, ModelObject {
private final String name;
private volatile Set nodes;
+ private volatile Set clouds;
+
+ public final LoadStatistics loadStatistics;
+ public final NodeProvisioner nodeProvisioner;
public Label(String name) {
this.name = name;
+ // passing these causes an infinite loop - getTotalExecutors(),getBusyExecutors());
+ this.loadStatistics = new LoadStatistics(0,0) {
+ @Override
+ public int computeIdleExecutors() {
+ return Label.this.getIdleExecutors();
+ }
+
+ @Override
+ public int computeTotalExecutors() {
+ return Label.this.getTotalExecutors();
+ }
+
+ @Override
+ public int computeQueueLength() {
+ return Hudson.getInstance().getQueue().countBuildableItemsFor(Label.this);
+ }
+ };
+ this.nodeProvisioner = new NodeProvisioner(this, loadStatistics);
}
+ @Exported
public String getName() {
return name;
}
@@ -38,30 +72,92 @@ public class Label implements Comparable, ModelObject {
public boolean isSelfLabel() {
Set nodes = getNodes();
return nodes.size() == 1 && nodes.iterator().next().getSelfLabel() == this;
-
}
/**
* Gets all {@link Node}s that belong to this label.
*/
+ @Exported
public Set getNodes() {
if(nodes==null) {
Set r = new HashSet();
Hudson h = Hudson.getInstance();
if(h.getAssignedLabels().contains(this))
r.add(h);
- for (Slave s : h.getSlaves()) {
- if(s.getAssignedLabels().contains(this))
- r.add(s);
+ for (Node n : h.getNodes()) {
+ if(n.getAssignedLabels().contains(this))
+ r.add(n);
}
nodes = Collections.unmodifiableSet(r);
}
return nodes;
}
+ /**
+ * Gets all {@link Cloud}s that can launch for this label.
+ */
+ @Exported
+ public Set getClouds() {
+ if(clouds==null) {
+ Set r = new HashSet();
+ Hudson h = Hudson.getInstance();
+ for (Cloud c : h.clouds) {
+ if(c.canProvision(this))
+ r.add(c);
+ }
+ clouds = Collections.unmodifiableSet(r);
+ }
+ return clouds;
+ }
+
+ /**
+ * 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 (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, ModelObject {
/**
* Returns a human readable text that explains this label.
*/
+ @Exported
public String getDescription() {
Set nodes = getNodes();
if(nodes.isEmpty())
@@ -98,6 +195,7 @@ public class Label implements Comparable, ModelObject {
/**
* Returns projects that are tied on this node.
*/
+ @Exported
public List getTiedJobs() {
List r = new ArrayList();
for( AbstractProject p : Util.filter(Hudson.getInstance().getItems(),AbstractProject.class) ) {
@@ -111,14 +209,25 @@ public class Label implements Comparable, ModelObject {
return getNodes().contains(node);
}
+ /**
+ * If there's no such label defined in {@link Node} or {@link Cloud}.
+ * This is usually used as a signal that this label is invalid.
+ */
public boolean isEmpty() {
- return getNodes().isEmpty();
+ return getNodes().isEmpty() && getClouds().isEmpty();
}
/*package*/ void reset() {
nodes = null;
+ clouds = null;
}
+ /**
+ * Expose this object to the remote API.
+ */
+ public Api getApi() {
+ return new Api(this);
+ }
public boolean equals(Object that) {
if (this == that) return true;
@@ -132,8 +241,30 @@ public class Label implements Comparable, ModelObject {
return name.hashCode();
}
-
public int compareTo(Label that) {
return this.name.compareTo(that.name);
}
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static final class ConverterImpl implements Converter {
+ public ConverterImpl() {
+ }
+
+ public boolean canConvert(Class type) {
+ return type==Label.class;
+ }
+
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ Label src = (Label) source;
+ writer.setValue(src.getName());
+ }
+
+ public Object unmarshal(HierarchicalStreamReader reader, final UnmarshallingContext context) {
+ return Hudson.getInstance().getLabel(reader.getValue());
+ }
+ }
}
diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java
index 86f83cee68..93a702cb72 100644
--- a/core/src/main/java/hudson/model/ListView.java
+++ b/core/src/main/java/hudson/model/ListView.java
@@ -50,6 +50,15 @@ public class ListView extends View {
return this;
}
+ /**
+ * Returns the transient {@link Action}s associated with the top page.
+ *
+ * @see Hudson#getActions()
+ */
+ public List getActions() {
+ return Hudson.getInstance().getActions();
+ }
+
/**
* Returns a read-only view of all {@link Job}s in this view.
*
diff --git a/core/src/main/java/hudson/model/LoadStatistics.java b/core/src/main/java/hudson/model/LoadStatistics.java
new file mode 100644
index 0000000000..bc0cdf0bad
--- /dev/null
+++ b/core/src/main/java/hudson/model/LoadStatistics.java
@@ -0,0 +1,224 @@
+package hudson.model;
+
+import hudson.model.MultiStageTimeSeries.TimeScale;
+import hudson.triggers.SafeTimerTask;
+import hudson.triggers.Trigger;
+import hudson.util.ChartUtil;
+import hudson.util.ColorPalette;
+import hudson.util.NoOverlapCategoryAxis;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.CategoryLabelPositions;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.category.LineAndShapeRenderer;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.ui.RectangleInsets;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+import java.awt.*;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Utilization statistics for a node or a set of nodes.
+ *
+ * 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 extends Node> 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 extends Node> 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 @@
+
+
+
+
+
+
+
+
\ 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 @@
-
-
-
-
-
-
-
- ${m.description}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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)}
+
+
+
+
+
+ ${%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 @@
-
-
-
-
-
-
-
-
-
-
-
-
- ${d.displayName}
-
-
-
-
-
-
-
-
-
- ${%Copy existing job}
-
-
- ${%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 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/lib/form/slave-mode.jelly b/core/src/main/resources/lib/form/slave-mode.jelly
new file mode 100644
index 0000000000..1ba5f66d6c
--- /dev/null
+++ b/core/src/main/resources/lib/form/slave-mode.jelly
@@ -0,0 +1,16 @@
+
+
+
+
+
+ ${m.description}
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/lib/form/validateButton.jelly b/core/src/main/resources/lib/form/validateButton.jelly
new file mode 100644
index 0000000000..baf8b824f7
--- /dev/null
+++ b/core/src/main/resources/lib/form/validateButton.jelly
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
${progress}
+
+
+
+
\ No newline at end of file
diff --git a/core/src/main/resources/lib/hudson/executors.jelly b/core/src/main/resources/lib/hudson/executors.jelly
index 4d77646f56..bc98d6a9ca 100644
--- a/core/src/main/resources/lib/hudson/executors.jelly
+++ b/core/src/main/resources/lib/hudson/executors.jelly
@@ -2,7 +2,7 @@
displays the status of executors.
-->
-
+
diff --git a/core/src/main/resources/lib/hudson/newFromList/form.jelly b/core/src/main/resources/lib/hudson/newFromList/form.jelly
new file mode 100644
index 0000000000..89cedb64be
--- /dev/null
+++ b/core/src/main/resources/lib/hudson/newFromList/form.jelly
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ${d.displayName}
+
+
+
+
+
+
+
+
+
+ ${copyTitle}
+
+
+ ${%Copy from}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/lib/hudson/newFromList/taglib b/core/src/main/resources/lib/hudson/newFromList/taglib
new file mode 100644
index 0000000000..af05b5996f
--- /dev/null
+++ b/core/src/main/resources/lib/hudson/newFromList/taglib
@@ -0,0 +1,2 @@
+These tags provide a higher level primitive for building a form page for creating a new item from a list of descriptors.
+Used in "create new job" page, for an example.
\ No newline at end of file
diff --git a/core/src/test/java/hudson/model/LoadStatisticsTest.java b/core/src/test/java/hudson/model/LoadStatisticsTest.java
new file mode 100644
index 0000000000..8cb65fb7df
--- /dev/null
+++ b/core/src/test/java/hudson/model/LoadStatisticsTest.java
@@ -0,0 +1,47 @@
+package hudson.model;
+
+import hudson.model.MultiStageTimeSeries.TimeScale;
+import junit.framework.TestCase;
+import org.jfree.chart.JFreeChart;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class LoadStatisticsTest extends TestCase {
+ public void testGraph() throws IOException {
+ LoadStatistics ls = new LoadStatistics(0, 0) {
+ public int computeIdleExecutors() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int computeTotalExecutors() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int computeQueueLength() {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ for(int i=0;i<50;i++) {
+ ls.totalExecutors.update(4);
+ ls.busyExecutors.update(3);
+ ls.queueLength.update(3);
+ }
+
+ for(int i=0;i<50;i++) {
+ ls.totalExecutors.update(0);
+ ls.busyExecutors.update(0);
+ ls.queueLength.update(1);
+ }
+
+ JFreeChart chart = ls.createChart(ls.createDataset(TimeScale.SEC10));
+ BufferedImage image = chart.createBufferedImage(400,200);
+ ImageIO.write(image, "PNG", new FileOutputStream("chart.png"));
+ }
+}
diff --git a/core/src/test/java/hudson/model/TimeSeriesTest.java b/core/src/test/java/hudson/model/TimeSeriesTest.java
new file mode 100644
index 0000000000..eff20d98b7
--- /dev/null
+++ b/core/src/test/java/hudson/model/TimeSeriesTest.java
@@ -0,0 +1,24 @@
+package hudson.model;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class TimeSeriesTest extends TestCase {
+ public void test1() {
+ TimeSeries ts = new TimeSeries(0,1-0.1f,100);
+ float last;
+ assertEquals(0f,last=ts.getLatest());
+ for( int i=0; i<100; i++ ) {
+ assertEquals(ts.getHistory().length,i+1);
+ ts.update(1);
+ assertTrue(last<=ts.getLatest());
+ assertTrue(ts.getLatest()<=1);
+ last = ts.getLatest();
+ }
+
+ for( int i=0; i<100; i++ )
+ ts.update(1);
+ }
+}
diff --git a/core/src/test/java/hudson/slaves/NodeListTest.java b/core/src/test/java/hudson/slaves/NodeListTest.java
new file mode 100644
index 0000000000..837f70eb6c
--- /dev/null
+++ b/core/src/test/java/hudson/slaves/NodeListTest.java
@@ -0,0 +1,132 @@
+package hudson.slaves;
+
+import junit.framework.TestCase;
+import hudson.model.Hudson;
+import hudson.model.Node;
+import hudson.model.TaskListener;
+import hudson.model.Computer;
+import hudson.model.Label;
+import hudson.model.TopLevelItem;
+import hudson.XmlFile;
+import hudson.Launcher;
+import hudson.FilePath;
+import hudson.security.ACL;
+import hudson.security.Permission;
+import hudson.util.ClockDifference;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Set;
+
+import org.apache.commons.io.FileUtils;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class NodeListTest extends TestCase {
+ static class DummyNode implements Node {
+ public String getNodeName() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setNodeName(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getNodeDescription() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Launcher createLauncher(TaskListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getNumExecutors() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Mode getMode() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Computer toComputer() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Computer createComputer() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Set getAssignedLabels() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Set getDynamicLabels() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Label getSelfLabel() {
+ throw new UnsupportedOperationException();
+ }
+
+ public FilePath getWorkspaceFor(TopLevelItem item) {
+ throw new UnsupportedOperationException();
+ }
+
+ public FilePath getRootPath() {
+ throw new UnsupportedOperationException();
+ }
+
+ public FilePath createPath(String absolutePath) {
+ throw new UnsupportedOperationException();
+ }
+
+ public ClockDifference getClockDifference() throws IOException, InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ public NodeDescriptor getDescriptor() {
+ throw new UnsupportedOperationException();
+ }
+
+ public ACL getACL() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void checkPermission(Permission permission) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean hasPermission(Permission permission) {
+ throw new UnsupportedOperationException();
+ }
+ }
+ static class EphemeralNode extends DummyNode implements hudson.slaves.EphemeralNode {
+ public Cloud getCloud() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ public void testSerialization() throws Exception {
+ NodeList nl = new NodeList();
+ nl.add(new DummyNode());
+ nl.add(new EphemeralNode());
+
+ File tmp = File.createTempFile("test","test");
+ try {
+ XmlFile x = new XmlFile(Hudson.XSTREAM, tmp);
+ x.write(nl);
+
+ String xml = FileUtils.readFileToString(tmp);
+ System.out.println(xml);
+ assertEquals(4,xml.split("\n").length);
+
+ NodeList back = (NodeList)x.read();
+
+ assertEquals(1,back.size());
+ assertEquals(DummyNode.class,back.get(0).getClass());
+ } finally {
+ tmp.delete();
+ }
+ }
+}
diff --git a/core/src/test/java/hudson/util/ArgumentListBuilderTest.java b/core/src/test/java/hudson/util/ArgumentListBuilderTest.java
new file mode 100644
index 0000000000..8f16397fcc
--- /dev/null
+++ b/core/src/test/java/hudson/util/ArgumentListBuilderTest.java
@@ -0,0 +1,84 @@
+package hudson.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ArgumentListBuilderTest extends Assert {
+
+ public static void assertArrayEquals(String msg, boolean[] expected, boolean[] actual) {
+ assertArrayEquals(msg,box(expected),box(actual));
+ }
+
+ private static Boolean[] box(boolean[] a) {
+ if(a==null) return null;
+ Boolean[] r = new Boolean[a.length];
+ for (int i = 0; i < a.length; i++)
+ r[i] = a[i];
+ return r;
+ }
+
+
+ @Test
+ public void assertEmptyMask() {
+ ArgumentListBuilder builder = new ArgumentListBuilder();
+ builder.add("arg");
+ builder.add("other", "arguments");
+
+ assertFalse("There shouldnt be any masked arguments", builder.hasMaskedArguments());
+ boolean[] array = builder.toMaskArray();
+ assertNotNull("The mask array should not be null", array);
+ assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,false}, array);
+ }
+
+ @Test
+ public void assertLastArgumentIsMasked() {
+ ArgumentListBuilder builder = new ArgumentListBuilder();
+ builder.add("arg");
+ builder.addMasked("ismasked");
+
+ assertTrue("There should be masked arguments", builder.hasMaskedArguments());
+ boolean[] array = builder.toMaskArray();
+ assertNotNull("The mask array should not be null", array);
+ assertArrayEquals("The mask array was incorrect", new boolean[]{false,true}, array);
+ }
+
+ @Test
+ public void assertSeveralMaskedArguments() {
+ ArgumentListBuilder builder = new ArgumentListBuilder();
+ builder.add("arg");
+ builder.addMasked("ismasked");
+ builder.add("non masked arg");
+ builder.addMasked("ismasked2");
+
+ assertTrue("There should be masked arguments", builder.hasMaskedArguments());
+ boolean[] array = builder.toMaskArray();
+ assertNotNull("The mask array should not be null", array);
+ assertArrayEquals("The mask array was incorrect", new boolean[]{false,true, false, true}, array);
+ }
+
+ @Test
+ public void assertPrependAfterAddingMasked() {
+ ArgumentListBuilder builder = new ArgumentListBuilder();
+ builder.addMasked("ismasked");
+ builder.add("arg");
+ builder.prepend("first", "second");
+
+ assertTrue("There should be masked arguments", builder.hasMaskedArguments());
+ boolean[] array = builder.toMaskArray();
+ assertNotNull("The mask array should not be null", array);
+ assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,true,false}, array);
+ }
+
+ @Test
+ public void assertPrependBeforeAddingMasked() {
+ ArgumentListBuilder builder = new ArgumentListBuilder();
+ builder.prepend("first", "second");
+ builder.addMasked("ismasked");
+ builder.add("arg");
+
+ assertTrue("There should be masked arguments", builder.hasMaskedArguments());
+ boolean[] array = builder.toMaskArray();
+ assertNotNull("The mask array should not be null", array);
+ assertArrayEquals("The mask array was incorrect", new boolean[]{false,false,true,false}, array);
+ }
+}
diff --git a/core/src/test/java/hudson/util/SecretTest.java b/core/src/test/java/hudson/util/SecretTest.java
new file mode 100644
index 0000000000..684da5bd52
--- /dev/null
+++ b/core/src/test/java/hudson/util/SecretTest.java
@@ -0,0 +1,51 @@
+package hudson.util;
+
+import junit.framework.TestCase;
+
+import java.security.SecureRandom;
+
+import hudson.Util;
+import hudson.model.Hudson;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class SecretTest extends TestCase {
+ protected void setUp() throws Exception {
+ SecureRandom sr = new SecureRandom();
+ byte[] random = new byte[32];
+ sr.nextBytes(random);
+ Secret.SECRET = Util.toHexString(random);
+
+ }
+
+ protected void tearDown() throws Exception {
+ Secret.SECRET = null;
+ }
+
+ public void testEncrypt() {
+ Secret secret = Secret.fromString("abc");
+ assertEquals("abc",secret.toString());
+
+ // make sure we got some encryption going
+ System.out.println(secret.getEncryptedValue());
+ assertTrue(!"abc".equals(secret.getEncryptedValue()));
+
+ // can we round trip?
+ assertEquals(secret,Secret.fromString(secret.getEncryptedValue()));
+ }
+
+ public void testDecrypt() {
+ assertEquals("abc",Secret.fromString("abc").toString());
+ }
+
+ public void testSerialization() {
+ Secret s = Secret.fromString("Mr.Hudson");
+ String xml = Hudson.XSTREAM.toXML(s);
+ System.out.println(xml);
+ assertTrue(!xml.contains(s.toString()));
+ assertTrue(xml.contains(s.getEncryptedValue()));
+ Object o = Hudson.XSTREAM.fromXML(xml);
+ assertEquals(o,s);
+ }
+}
diff --git a/maven-interceptor/pom.xml b/maven-interceptor/pom.xml
index c1dede8385..83442eac19 100644
--- a/maven-interceptor/pom.xml
+++ b/maven-interceptor/pom.xml
@@ -18,4 +18,4 @@
${maven.version}
-
\ No newline at end of file
+
diff --git a/test/pom.xml b/test/pom.xml
index 6b12906ce0..1f7646f6a7 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -77,6 +77,13 @@
htmlunit
2.2-hudson-9
+
+
+ org.jvnet.hudson
+ htmlunit-core-js
+ 2.2-hudson-2
+
+
org.jvnet.hudson
diff --git a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
index 40f0754bc5..b3efd0d0de 100644
--- a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
+++ b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
@@ -13,10 +13,15 @@ import com.gargoylesoftware.htmlunit.javascript.host.Stylesheet;
import com.gargoylesoftware.htmlunit.javascript.host.XMLHttpRequest;
import hudson.matrix.MatrixProject;
import hudson.model.*;
+import hudson.model.Node.Mode;
+import hudson.model.JDK;
+import hudson.model.Label;
import hudson.tasks.Mailer;
import hudson.tasks.Maven;
import hudson.tasks.BuildStep;
import hudson.tasks.Maven.MavenInstallation;
+import hudson.tasks.Maven;
+import hudson.tasks.Maven.MavenInstallation;
import hudson.Launcher.LocalLauncher;
import hudson.util.StreamTaskListener;
import hudson.util.ProcessTreeKiller;
@@ -26,6 +31,13 @@ import hudson.FilePath;
import hudson.Functions;
import hudson.WebAppMain;
import hudson.CloseProofOutputStream;
+import hudson.slaves.DumbSlave;
+import hudson.slaves.CommandLauncher;
+import hudson.slaves.RetentionStrategy;
+import hudson.maven.MavenModuleSet;
+import hudson.FilePath;
+import hudson.Functions;
+import hudson.WebAppMain;
import junit.framework.TestCase;
import org.jvnet.hudson.test.HudsonHomeLoader.CopyExisting;
import org.jvnet.hudson.test.recipes.Recipe;
@@ -73,7 +85,7 @@ import java.util.logging.LogRecord;
* @author Kohsuke Kawaguchi
*/
public abstract class HudsonTestCase extends TestCase {
- protected Hudson hudson;
+ public Hudson hudson;
protected final TestEnvironment env = new TestEnvironment();
protected HudsonHomeLoader homeLoader = HudsonHomeLoader.NEW;
@@ -179,6 +191,7 @@ public abstract class HudsonTestCase extends TestCase {
}
protected void runTest() throws Throwable {
+ System.out.println("=== Starting "+getName());
new JavaScriptEngine(null); // ensure that ContextFactory is initialized
Context cx= ContextFactory.getGlobal().enterContext();
try {
@@ -322,8 +335,28 @@ public abstract class HudsonTestCase extends TestCase {
/**
* Allocates a new temporary directory for the duration of this test.
*/
- protected File createTmpDir() throws IOException {
- return TestEnvironment.get().temporaryDirectoryAllocator.allocate();
+ public File createTmpDir() throws IOException {
+ return env.temporaryDirectoryAllocator.allocate();
+ }
+
+ public DumbSlave createSlave() throws Exception {
+ return createSlave(null);
+ }
+
+ /**
+ * Creates and launches a new slave on the local host.
+ */
+ public DumbSlave createSlave(Label l) throws Exception {
+ CommandLauncher launcher = new CommandLauncher(
+ System.getProperty("java.home") + "/bin/java -jar " + hudson.getJnlpJars("slave.jar").getURL().getPath());
+
+ // this synchronization block is so that we don't end up adding the same slave name more than once.
+ synchronized (hudson) {
+ DumbSlave slave = new DumbSlave("slave" + hudson.getNodes().size(), "dummy",
+ createTmpDir().getPath(), "1", Mode.NORMAL, l==null?"":l.getName(), launcher, RetentionStrategy.NOOP);
+ hudson.addNode(slave);
+ return slave;
+ }
}
/**
diff --git a/test/src/main/java/org/jvnet/hudson/test/SleepBuilder.java b/test/src/main/java/org/jvnet/hudson/test/SleepBuilder.java
new file mode 100644
index 0000000000..0b36e562cf
--- /dev/null
+++ b/test/src/main/java/org/jvnet/hudson/test/SleepBuilder.java
@@ -0,0 +1,55 @@
+package org.jvnet.hudson.test;
+
+import hudson.Launcher;
+import hudson.model.AbstractBuild;
+import hudson.model.BuildListener;
+import hudson.model.Descriptor;
+import hudson.tasks.BuildStep;
+import hudson.tasks.Builder;
+import net.sf.json.JSONObject;
+import org.kohsuke.stapler.StaplerRequest;
+
+import java.io.IOException;
+
+/**
+ * {@link Builder} that just sleeps for the specified amount of milli-seconds.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class SleepBuilder extends Builder {
+ public final long time;
+
+ public SleepBuilder(long time) {
+ this.time = time;
+ }
+
+ public boolean perform(AbstractBuild, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
+ listener.getLogger().println("Sleeping "+time+"ms");
+ Thread.sleep(time);
+ return true;
+ }
+
+ public Descriptor getDescriptor() {
+ return DESCRIPTOR;
+ }
+
+ public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
+
+ public static final class DescriptorImpl extends Descriptor {
+ private DescriptorImpl() {
+ super(SleepBuilder.class);
+ }
+
+ public Builder newInstance(StaplerRequest req, JSONObject data) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDisplayName() {
+ return "Sleep";
+ }
+ }
+
+ static {
+ BuildStep.BUILDERS.add(DESCRIPTOR);
+ }
+}
diff --git a/test/src/main/java/org/jvnet/hudson/test/WarExploder.java b/test/src/main/java/org/jvnet/hudson/test/WarExploder.java
index e4157b63e7..fc12fc0c3a 100644
--- a/test/src/main/java/org/jvnet/hudson/test/WarExploder.java
+++ b/test/src/main/java/org/jvnet/hudson/test/WarExploder.java
@@ -43,8 +43,7 @@ final class WarExploder {
File d = new File(".").getAbsoluteFile();
for( ; d!=null; d=d.getParentFile()) {
if(!d.getName().equals("main")) continue;
- File p = d.getParentFile();
- if(p!=null && p.getName().equals("hudson"))
+ if(new File(d,".hudson").exists())
break;
}
if(d!=null) {
@@ -62,15 +61,15 @@ final class WarExploder {
throw new AssertionError("hudson.war is not in the classpath.");
File war = Which.jarFile(Class.forName("executable.Executable"));
- File explodeDir = new File("./target/hudson-for-test");
+ File explodeDir = new File("./target/hudson-for-test").getAbsoluteFile();
File timestamp = new File(explodeDir,".timestamp");
if(!timestamp.exists() || (timestamp.lastModified()!=war.lastModified())) {
System.out.println("Exploding hudson.war at "+war);
new FilePath(explodeDir).deleteRecursive();
new FilePath(war).unzip(new FilePath(explodeDir));
- // Recreating the File Object seems to be necessary to fix HUDSON-2605
- timestamp = timestamp.getCanonicalFile();
+ if(!explodeDir.exists()) // this is supposed to be impossible, but I'm investigating HUDSON-2605
+ throw new IOException("Failed to explode "+war);
new FileOutputStream(timestamp).close();
timestamp.setLastModified(war.lastModified());
} else {
diff --git a/test/src/main/resources/org/jvnet/hudson/test/SleepBuilder/config.jelly b/test/src/main/resources/org/jvnet/hudson/test/SleepBuilder/config.jelly
new file mode 100644
index 0000000000..bba0a3e788
--- /dev/null
+++ b/test/src/main/resources/org/jvnet/hudson/test/SleepBuilder/config.jelly
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java
index a7b0325a2c..a6144261ef 100644
--- a/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java
+++ b/test/src/test/java/hudson/bugs/JnlpAccessWithSecuredHudsonTest.java
@@ -7,6 +7,7 @@ import hudson.model.Node.Mode;
import hudson.model.Slave;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.RetentionStrategy;
+import hudson.slaves.DumbSlave;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.DOMReader;
@@ -30,7 +31,7 @@ public class JnlpAccessWithSecuredHudsonTest extends HudsonTestCase {
* Creates a new slave that needs to be launched via JNLP.
*/
protected Slave createNewJnlpSlave(String name) throws Exception {
- return new Slave(name,"",System.getProperty("java.io.tmpdir")+'/'+name,"2", Mode.NORMAL, "", new JNLPLauncher(), RetentionStrategy.INSTANCE);
+ return new DumbSlave(name,"",System.getProperty("java.io.tmpdir")+'/'+name,"2", Mode.NORMAL, "", new JNLPLauncher(), RetentionStrategy.INSTANCE);
}
@PresetData(DataSet.NO_ANONYMOUS_READACCESS)
diff --git a/test/src/test/java/hudson/slaves/DummyCloudImpl.java b/test/src/test/java/hudson/slaves/DummyCloudImpl.java
new file mode 100644
index 0000000000..3b7564cca8
--- /dev/null
+++ b/test/src/test/java/hudson/slaves/DummyCloudImpl.java
@@ -0,0 +1,99 @@
+package hudson.slaves;
+
+import hudson.model.Computer;
+import hudson.model.Descriptor;
+import hudson.model.Node;
+import hudson.model.Label;
+import hudson.slaves.NodeProvisioner.PlannedNode;
+import org.jvnet.hudson.test.HudsonTestCase;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+/**
+ * {@link Cloud} implementation useful for testing.
+ *
+ *
+ * This implementation launches "java -jar slave.jar" on the localhost when provisioning a new slave.
+ *
+ * @author Kohsuke Kawaguchi
+*/
+public class DummyCloudImpl extends Cloud {
+ private final transient HudsonTestCase caller;
+
+ /**
+ * Configurable delay between the {@link Cloud#provision(Label,int)} and the actual launch of a slave,
+ * to emulate a real cloud that takes some time for provisioning a new system.
+ *
+ *
+ * Number of milliseconds.
+ */
+ private final int delay;
+
+ // stats counter to perform assertions later
+ public int numProvisioned;
+
+ /**
+ * Only reacts to provisioning for this label.
+ */
+ public Label label;
+
+ public DummyCloudImpl(HudsonTestCase caller, int delay) {
+ super("test");
+ this.caller = caller;
+ this.delay = delay;
+ }
+
+ public Collection provision(Label label, int excessWorkload) {
+ List r = new ArrayList();
+ if(label!=this.label) return r; // provisioning impossible
+
+ while(excessWorkload>0) {
+ System.out.println("Provisioning");
+ numProvisioned++;
+ Future f = Computer.threadPoolForRemoting.submit(new Launcher(delay));
+ r.add(new PlannedNode(name+" #"+numProvisioned,f,1));
+ excessWorkload-=1;
+ }
+ return r;
+ }
+
+ public boolean canProvision(Label label) {
+ return label==this.label;
+ }
+
+ private final class Launcher implements Callable {
+ private final long time;
+ /**
+ * This is so that we can find out the status of Callable from the debugger.
+ */
+ private volatile Computer computer;
+
+ private Launcher(long time) {
+ this.time = time;
+ }
+
+ public Node call() throws Exception {
+ // simulate the delay in provisioning a new slave,
+ // since it's normally some async operation.
+ Thread.sleep(time);
+
+ System.out.println("launching slave");
+ DumbSlave slave = caller.createSlave(label);
+ computer = slave.toComputer();
+ computer.connect(false).get();
+ synchronized (DummyCloudImpl.this) {
+ System.out.println(computer.getName()+" launch"+(computer.isOnline()?"ed successfully":" failed"));
+ System.out.println(computer.getLog());
+ }
+ return slave;
+ }
+ }
+
+ public Descriptor getDescriptor() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
index 9743683d10..d66ca670d8 100644
--- a/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
+++ b/test/src/test/java/hudson/slaves/JNLPLauncherTest.java
@@ -99,7 +99,7 @@ public class JNLPLauncherTest extends HudsonTestCase {
private Computer addTestSlave() throws Exception {
List slaves = new ArrayList(hudson.getSlaves());
File dir = Util.createTempDir();
- slaves.add(new Slave("test","dummy",dir.getAbsolutePath(),"1", Mode.NORMAL, "",
+ slaves.add(new DumbSlave("test","dummy",dir.getAbsolutePath(),"1", Mode.NORMAL, "",
new JNLPLauncher(), RetentionStrategy.INSTANCE));
hudson.setSlaves(slaves);
Computer c = hudson.getComputer("test");
diff --git a/test/src/test/java/hudson/slaves/NodeProvisionerTest.java b/test/src/test/java/hudson/slaves/NodeProvisionerTest.java
new file mode 100644
index 0000000000..5de95b089c
--- /dev/null
+++ b/test/src/test/java/hudson/slaves/NodeProvisionerTest.java
@@ -0,0 +1,181 @@
+package hudson.slaves;
+
+import hudson.BulkChange;
+import hudson.model.FreeStyleBuild;
+import hudson.model.FreeStyleProject;
+import hudson.model.LoadStatistics;
+import hudson.model.Node;
+import hudson.model.Result;
+import hudson.model.Label;
+import org.jvnet.hudson.test.HudsonTestCase;
+import org.jvnet.hudson.test.SleepBuilder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public class NodeProvisionerTest extends HudsonTestCase {
+ private int original;
+
+ @Override
+ protected void setUp() throws Exception {
+ original = LoadStatistics.CLOCK;
+ LoadStatistics.CLOCK = 10; // run x1000 the regular speed to speed up the test
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ LoadStatistics.CLOCK = original;
+ }
+
+ /**
+ * Scenario: schedule a build and see if one slave is provisioned.
+ */
+ public void testAutoProvision() throws Exception {
+ BulkChange bc = new BulkChange(hudson);
+ try {
+ DummyCloudImpl cloud = initHudson(10);
+
+
+ FreeStyleProject p = createJob(10);
+
+ Future f = p.scheduleBuild2(0);
+ f.get(30, TimeUnit.SECONDS); // if it's taking too long, abort.
+
+ // since there's only one job, we expect there to be just one slave
+ assertEquals(1,cloud.numProvisioned);
+ } finally {
+ bc.abort();
+ }
+ }
+
+ /**
+ * Scenario: we got a lot of jobs all of the sudden, and we need to fire up a few nodes.
+ */
+ public void testLoadSpike() throws Exception {
+ BulkChange bc = new BulkChange(hudson);
+ try {
+ DummyCloudImpl cloud = initHudson(0);
+
+ verifySuccessfulCompletion(buildAll(create5SlowJobs()));
+
+ // the time it takes to complete a job is eternally long compared to the time it takes to launch
+ // a new slave, so in this scenario we end up allocating 5 slaves for 5 jobs.
+ assertEquals(5,cloud.numProvisioned);
+ } finally {
+ bc.abort();
+ }
+ }
+
+ /**
+ * Scenario: make sure we take advantage of statically configured slaves.
+ */
+ public void testBaselineSlaveUsage() throws Exception {
+ BulkChange bc = new BulkChange(hudson);
+ try {
+ DummyCloudImpl cloud = initHudson(0);
+ // add slaves statically upfront
+ createSlave().toComputer().connect(false).get();
+ createSlave().toComputer().connect(false).get();
+
+ verifySuccessfulCompletion(buildAll(create5SlowJobs()));
+
+ // we should have used two static slaves, thus only 3 slaves should have been provisioned
+ assertEquals(3,cloud.numProvisioned);
+ } finally {
+ bc.abort();
+ }
+ }
+
+ /**
+ * Scenario: loads on one label shouldn't translate to load on another label.
+ */
+ public void testLabels() throws Exception {
+ BulkChange bc = new BulkChange(hudson);
+ try {
+ DummyCloudImpl cloud = initHudson(0);
+ Label blue = hudson.getLabel("blue");
+ Label red = hudson.getLabel("red");
+ cloud.label = red;
+
+ // red jobs
+ List redJobs = create5SlowJobs();
+ for (FreeStyleProject p : redJobs)
+ p.setAssignedLabel(red);
+
+ // blue jobs
+ List blueJobs = create5SlowJobs();
+ for (FreeStyleProject p : blueJobs)
+ p.setAssignedLabel(blue);
+
+ // build all
+ List> blueBuilds = buildAll(blueJobs);
+ verifySuccessfulCompletion(buildAll(redJobs));
+
+ // cloud should only give us 5 nodes for 5 red jobs
+ assertEquals(5,cloud.numProvisioned);
+
+ // and all blue jobs should be still stuck in the queue
+ for (Future bb : blueBuilds)
+ assertFalse(bb.isDone());
+ } finally {
+ bc.abort();
+ }
+ }
+
+
+ private FreeStyleProject createJob(int delay) throws IOException {
+ FreeStyleProject p = createFreeStyleProject();
+ p.setAssignedLabel(null); // let it roam free, or else it ties itself to the master since we have no slaves
+ p.getBuildersList().add(new SleepBuilder(delay));
+ return p;
+ }
+
+ private DummyCloudImpl initHudson(int delay) throws IOException {
+ // start a dummy service
+ DummyCloudImpl cloud = new DummyCloudImpl(this, delay);
+ hudson.clouds.add(cloud);
+
+ // no build on the master, to make sure we get everything from the cloud
+ hudson.setNumExecutors(0);
+ hudson.setNodes(Collections.emptyList());
+ return cloud;
+ }
+
+ private List create5SlowJobs() throws IOException {
+ List jobs = new ArrayList();
+ for( int i=0; i<5; i++)
+ //set a large delay, to simulate the situation where we need to provision more slaves
+ // to keep up with the load
+ jobs.add(createJob(3000));
+ return jobs;
+ }
+
+ /**
+ * Builds all the given projects at once.
+ */
+ private List> buildAll(List jobs) {
+ System.out.println("Scheduling builds for "+jobs.size()+" jobs");
+ List> builds = new ArrayList>();
+ for (FreeStyleProject job : jobs)
+ builds.add(job.scheduleBuild2(0));
+ return builds;
+ }
+
+ private void verifySuccessfulCompletion(List> builds) throws InterruptedException, ExecutionException {
+ System.out.println("Waiting for a completion");
+ for (Future f : builds) {
+ FreeStyleBuild b = f.get();// if it's taking too long, abort.
+ assertEquals(Result.SUCCESS,b.getResult());
+ }
+ }
+}
diff --git a/war/images/makeBalls.sh b/war/images/makeBalls.sh
index 4cc410daf0..d0a0cbe1d8 100755
--- a/war/images/makeBalls.sh
+++ b/war/images/makeBalls.sh
@@ -1,19 +1,11 @@
#!/bin/sh -ex
# build flashing balls
-t=/tmp/makeBalls$$
-
for sz in 16x16 24x24 32x32 48x48
do
for color in grey blue yellow red green
do
cp $sz/$color.gif ../resources/images/$sz/$color.gif
- convert $sz/$color.gif -fill white -colorize 20% $t.80.gif
- convert $sz/$color.gif -fill white -colorize 40% $t.60.gif
- convert $sz/$color.gif -fill white -colorize 60% $t.40.gif
- convert $sz/$color.gif -fill white -colorize 80% $t.20.gif
- convert -delay 10 $sz/$color.gif $t.80.gif $t.60.gif $t.40.gif $t.20.gif $sz/nothing.gif $t.20.gif $t.40.gif $t.60.gif $t.80.gif -loop 0 ../resources/images/$sz/${color}_anime.gif
+ ./makeFlash.sh $sz/$color.gif ../resources/images/$sz/${color}_anime.gif
done
done
-
-rm $t.*.gif
diff --git a/war/images/makeFlash.sh b/war/images/makeFlash.sh
new file mode 100755
index 0000000000..ac4b9d4c56
--- /dev/null
+++ b/war/images/makeFlash.sh
@@ -0,0 +1,15 @@
+#!/bin/sh -ex
+# build a flashing animation image from a still picture
+# Usage
+t=/tmp/flash$$
+
+src=$1
+dst=$2
+convert $src -fill white -colorize 20% $t.80.gif
+convert $src -fill white -colorize 40% $t.60.gif
+convert $src -fill white -colorize 60% $t.40.gif
+convert $src -fill white -colorize 80% $t.20.gif
+convert $src -fill white -colorize 100% $t.0.gif
+convert -delay 10 $src $t.80.gif $t.60.gif $t.40.gif $t.20.gif $t.0.gif $t.20.gif $t.40.gif $t.60.gif $t.80.gif -loop 0 $dst
+
+rm $t.*.gif
diff --git a/war/images/utilities-system-monitor.svg b/war/images/monitor.svg
similarity index 100%
rename from war/images/utilities-system-monitor.svg
rename to war/images/monitor.svg
diff --git a/war/images/new-computer.svg b/war/images/new-computer.svg
new file mode 100644
index 0000000000..da5e064be7
--- /dev/null
+++ b/war/images/new-computer.svg
@@ -0,0 +1,784 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+ Computer
+ 2005-03-08
+
+
+ Jakub Steiner
+
+
+
+
+ workstation
+ computer
+ node
+ client
+
+
+
+ http://jimmac.musichall.cz/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/war/resources/css/style.css b/war/resources/css/style.css
index c898003110..28e6a0c36b 100644
--- a/war/resources/css/style.css
+++ b/war/resources/css/style.css
@@ -18,6 +18,10 @@ td {
vertical-align: top;
}
+dt {
+ font-weight: bold;
+}
+
.center {
text-align: center;
}
diff --git a/war/resources/images/16x16/computer-flash.gif b/war/resources/images/16x16/computer-flash.gif
new file mode 100644
index 0000000000..2f8265f437
Binary files /dev/null and b/war/resources/images/16x16/computer-flash.gif differ
diff --git a/war/resources/images/24x24/computer-flash.gif b/war/resources/images/24x24/computer-flash.gif
new file mode 100644
index 0000000000..dc12816d54
Binary files /dev/null and b/war/resources/images/24x24/computer-flash.gif differ
diff --git a/war/resources/images/24x24/monitor.gif b/war/resources/images/24x24/monitor.gif
new file mode 100644
index 0000000000..5917dcb20e
Binary files /dev/null and b/war/resources/images/24x24/monitor.gif differ
diff --git a/war/resources/images/24x24/new-computer.gif b/war/resources/images/24x24/new-computer.gif
new file mode 100644
index 0000000000..7d486983d5
Binary files /dev/null and b/war/resources/images/24x24/new-computer.gif differ
diff --git a/war/resources/images/32x32/computer-flash.gif b/war/resources/images/32x32/computer-flash.gif
new file mode 100644
index 0000000000..b1f68ba371
Binary files /dev/null and b/war/resources/images/32x32/computer-flash.gif differ
diff --git a/war/resources/images/48x48/computer-flash.gif b/war/resources/images/48x48/computer-flash.gif
new file mode 100644
index 0000000000..574139c73b
Binary files /dev/null and b/war/resources/images/48x48/computer-flash.gif differ
diff --git a/war/resources/images/48x48/monitor.gif b/war/resources/images/48x48/monitor.gif
new file mode 100644
index 0000000000..e59e41e756
Binary files /dev/null and b/war/resources/images/48x48/monitor.gif differ
diff --git a/war/resources/scripts/hudson-behavior.js b/war/resources/scripts/hudson-behavior.js
index a37b2a3e0e..0d50717fda 100644
--- a/war/resources/scripts/hudson-behavior.js
+++ b/war/resources/scripts/hudson-behavior.js
@@ -76,6 +76,35 @@ function findFollowingTR(input, className) {
return tr;
}
+/**
+ * Traverses a form in the reverse document order starting from the given element (but excluding it),
+ * until the given filter matches, or run out of an element.
+ */
+function findPrevious(src,filter) {
+ function prev(e) {
+ var p = e.previousSibling;
+ if(p==null) return e.parentNode;
+ while(p.lastChild!=null)
+ p = p.lastChild;
+ return p;
+ }
+
+ while(src!=null) {
+ src = prev(src);
+ if(filter(src))
+ return src;
+ }
+ return null;
+}
+/**
+ * Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
+ */
+function findPreviousFormItem(src,name) {
+ var name2 = "_."+name; // handles notation silently
+ return findPrevious(src,function(e){ return e.tagName=="INPUT" && (e.name==name || e.name==name2); });
+}
+
+
// shared tooltip object
var tooltip;
@@ -122,10 +151,13 @@ function registerValidator(e) {
* onclick handler
*/
function makeButton(e,onclick) {
+ var h = e.onclick;
var clsName = e.className;
var btn = new YAHOO.widget.Button(e,{});
if(onclick!=null)
btn.addListener("click",onclick);
+ if(h!=null)
+ btn.addListener("click",h);
Element.addClassName(btn.get("element"),clsName);
return btn;
}
@@ -1403,3 +1435,29 @@ function applySafeRedirector(url) {
});
}, 5000);
}
+
+// logic behind
+function validateButton(checkUrl,paramList,button) {
+ button = button._button;
+
+ var parameters = {};
+
+ paramList.split(',').each(function(name) {
+ var p = findPreviousFormItem(button,name);
+ if(p!=null)
+ parameters[name] = p.value;
+ });
+
+ var spinner = Element.up(button,"DIV").nextSibling;
+ var target = spinner.nextSibling;
+ spinner.style.display="block";
+
+ new Ajax.Request(checkUrl, {
+ method: "post",
+ parameters: parameters,
+ onComplete: function(rsp) {
+ spinner.style.display="none";
+ target.innerHTML = rsp.responseText;
+ }
+ });
+}