hudson.model.Job Maven / Gradle / Ivy
package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.XmlFile;
import hudson.tasks.BuildTrigger;
import hudson.tasks.LogRotator;
import hudson.util.ChartUtil;
import hudson.util.DataSetBuilder;
import hudson.util.IOException2;
import hudson.util.RunList;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.TextFile;
import hudson.util.XStream2;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
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.AreaRendererEndType;
import org.jfree.chart.renderer.category.AreaRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.Collections;
/**
* A job is an runnable entity under the monitoring of Hudson.
*
*
* Every time it "runs", it will be recorded as a {@link Run} object.
*
*
* To register a custom {@link Job} class from a plugin, add it to
* {@link Jobs#JOBS}. Also see {@link Job#XSTREAM}.
*
* @author Kohsuke Kawaguchi
*/
public abstract class Job, RunT extends Run>
extends DirectoryHolder implements Describable>, ExtensionPoint {
/**
* Project name.
*/
protected /*final*/ transient String name;
/**
* Project description. Can be HTML.
*/
protected String description;
/**
* Root directory for this job.
*/
protected transient File root;
/**
* Next bulid number.
* Kept in a separate file because this is the only information
* that gets updated often. This allows the rest of the configuration
* to be in the VCS.
*
* In 1.28 and earlier, this field was stored in the project configuration file,
* so even though this is marked as transient, don't move it around.
*/
protected transient int nextBuildNumber = 1;
private transient Hudson parent;
private LogRotator logRotator;
private boolean keepDependencies;
protected Job(Hudson parent,String name) {
this.parent = parent;
doSetName(name);
getBuildDir().mkdirs();
}
/**
* Called when a {@link Job} is loaded from disk.
*/
protected void onLoad(Hudson root, String name) throws IOException {
this.parent = root;
doSetName(name);
TextFile f = getNextBuildNumberFile();
if(f.exists()) {
// starting 1.28, we store nextBuildNumber in a separate file.
// but old Hudson didn't do it, so if the file doesn't exist,
// assume that nextBuildNumber was read from config.xml
try {
this.nextBuildNumber = Integer.parseInt(f.readTrim());
} catch (NumberFormatException e) {
throw new IOException2(f+" doesn't contain a number",e);
}
} else {
// this must be the old Hudson. create this file now.
saveNextBuildNumber();
save(); // and delete it from the config.xml
}
}
/**
* Just update {@link #name} and {@link #root}, since they are linked.
*/
private void doSetName(String name) {
this.name = name;
this.root = new File(new File(parent.root,"jobs"),name);
}
public File getRootDir() {
return root;
}
private TextFile getNextBuildNumberFile() {
return new TextFile(new File(this.root,"nextBuildNumber"));
}
private void saveNextBuildNumber() throws IOException {
getNextBuildNumberFile().write(String.valueOf(nextBuildNumber)+'\n');
}
public final Hudson getParent() {
return parent;
}
public boolean isInQueue() {
return false;
}
/**
* If true, it will keep all the build logs of dependency components.
*/
public boolean isKeepDependencies() {
return keepDependencies;
}
/**
* Allocates a new buildCommand number.
*/
public synchronized int assignBuildNumber() throws IOException {
int r = nextBuildNumber++;
saveNextBuildNumber();
return r;
}
public int getNextBuildNumber() {
return nextBuildNumber;
}
/**
* Gets the project description HTML.
*/
public String getDescription() {
return description;
}
/**
* Sets the project description HTML.
*/
public void setDescription(String description) {
this.description = description;
}
/**
* Returns the log rotator for this job, or null if none.
*/
public LogRotator getLogRotator() {
return logRotator;
}
public void setLogRotator(LogRotator logRotator) {
this.logRotator = logRotator;
}
public String getName() {
return name;
}
public String getDisplayName() {
return getName();
}
/**
* Renames a job.
*/
public void renameTo(String newName) throws IOException {
// always synchronize from bigger objects first
synchronized(parent) {
synchronized(this) {
// sanity check
if(newName==null)
throw new IllegalArgumentException("New name is not given");
if(parent.getJob(newName)!=null)
throw new IllegalArgumentException("Job "+newName+" already exists");
// noop?
if(this.name.equals(newName))
return;
String oldName = this.name;
File oldRoot = this.root;
doSetName(newName);
File newRoot = this.root;
{// rename data files
boolean interrupted=false;
boolean renamed = false;
// try to rename the job directory.
// this may fail on Windows due to some other processes accessing a file.
// so retry few times before we fall back to copy.
for( int retry=0; retry<5; retry++ ) {
if(oldRoot.renameTo(newRoot)) {
renamed = true;
break; // succeeded
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// process the interruption later
interrupted = true;
}
}
if(interrupted)
Thread.currentThread().interrupt();
if(!renamed) {
// failed to rename. it must be that some lengthy process is going on
// to prevent a rename operation. So do a copy. Ideally we'd like to
// later delete the old copy, but we can't reliably do so, as before the VM
// shuts down there might be a new job created under the old name.
Copy cp = new Copy();
cp.setProject(new org.apache.tools.ant.Project());
cp.setTodir(newRoot);
FileSet src = new FileSet();
src.setDir(getRootDir());
cp.addFileset(src);
cp.setOverwrite(true);
cp.setPreserveLastModified(true);
cp.setFailOnError(false); // keep going even if there's an error
cp.execute();
// try to delete as much as possible
try {
Util.deleteRecursive(oldRoot);
} catch (IOException e) {
// but ignore the error, since we expect that
e.printStackTrace();
}
}
}
parent.onRenamed(this,oldName,newName);
// update BuildTrigger of other projects that point to this object.
// can't we generalize this?
for( Project p : parent.getProjects() ) {
BuildTrigger t = (BuildTrigger) p.getPublishers().get(BuildTrigger.DESCRIPTOR);
if(t!=null) {
if(t.onJobRenamed(oldName,newName))
p.save();
}
}
}
}
}
/**
* Returns true if we should display "build now" icon
*/
public abstract boolean isBuildable();
/**
* Gets all the builds.
*
* @return
* never null. The first entry is the latest buildCommand.
*/
public List getBuilds() {
return new ArrayList(_getRuns().values());
}
/**
* Gets all the builds in a map.
*/
public SortedMap getBuildsAsMap() {
return Collections.unmodifiableSortedMap(_getRuns());
}
/**
* @deprecated
* This is only used to support backward compatibility with
* old URLs.
*/
@Deprecated
public RunT getBuild(String id) {
for (RunT r : _getRuns().values()) {
if(r.getId().equals(id))
return r;
}
return null;
}
/**
* @param n
* The build number.
* @see Run#getNumber()
*/
public RunT getBuildByNumber(int n) {
return _getRuns().get(n);
}
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
try {
// try to interpret the token as build number
return _getRuns().get(Integer.valueOf(token));
} catch (NumberFormatException e) {
return super.getDynamic(token,req,rsp);
}
}
/**
* The file we save our configuration.
*/
protected static XmlFile getConfigFile(File dir) {
return new XmlFile(XSTREAM,new File(dir,"config.xml"));
}
File getConfigFile() {
return new File(root,"config.xml");
}
/**
* Directory for storing {@link Run} records.
*
* Some {@link Job}s may not have backing data store for {@link Run}s,
* but those {@link Job}s that use file system for storing data
* should use this directory for consistency.
*
* @see RunMap
*/
protected File getBuildDir() {
return new File(root,"builds");
}
/**
* Returns the URL of this project.
*/
public String getUrl() {
return "job/"+name+'/';
}
/**
* Gets all the runs.
*
* The resulting map must be immutable (by employing copy-on-write semantics.)
*/
protected abstract SortedMap _getRuns();
/**
* Called from {@link Run} to remove it from this job.
*
* The files are deleted already. So all the callee needs to do
* is to remove a reference from this {@link Job}.
*/
protected abstract void removeRun(RunT run);
/**
* Returns the last build.
*/
public RunT getLastBuild() {
SortedMap runs = _getRuns();
if(runs.isEmpty()) return null;
return runs.get(runs.firstKey());
}
/**
* Returns the oldest build in the record.
*/
public RunT getFirstBuild() {
SortedMap runs = _getRuns();
if(runs.isEmpty()) return null;
return runs.get(runs.lastKey());
}
/**
* Returns the last successful build, if any. Otherwise null.
*/
public RunT getLastSuccessfulBuild() {
RunT r = getLastBuild();
// temporary hack till we figure out what's causing this bug
while(r!=null && (r.isBuilding() || r.getResult()==null || r.getResult().isWorseThan(Result.UNSTABLE)))
r=r.getPreviousBuild();
return r;
}
/**
* Returns the last stable build, if any. Otherwise null.
*/
public RunT getLastStableBuild() {
RunT r = getLastBuild();
while(r!=null && (r.isBuilding() || r.getResult().isWorseThan(Result.SUCCESS)))
r=r.getPreviousBuild();
return r;
}
/**
* Returns the last failed build, if any. Otherwise null.
*/
public RunT getLastFailedBuild() {
RunT r = getLastBuild();
while(r!=null && (r.isBuilding() || r.getResult()!=Result.FAILURE))
r=r.getPreviousBuild();
return r;
}
/**
* Used as the color of the status ball for the project.
*/
public BallColor getIconColor() {
RunT lastBuild = getLastBuild();
while(lastBuild!=null && lastBuild.hasntStartedYet())
lastBuild = lastBuild.getPreviousBuild();
if(lastBuild!=null)
return lastBuild.getIconColor();
else
return BallColor.GREY;
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
getConfigFile(root).write(this);
}
/**
* Loads a project from a config file.
*/
static Job load(Hudson root, File dir) throws IOException {
Job job = (Job)getConfigFile(dir).read();
job.onLoad(root,dir.getName());
return job;
}
//
//
// actions
//
//
/**
* Accepts submission from the configuration page.
*/
public synchronized void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
if(!Hudson.adminCheck(req,rsp))
return;
req.setCharacterEncoding("UTF-8");
description = req.getParameter("description");
if(req.getParameter("logrotate")!=null)
logRotator = LogRotator.DESCRIPTOR.newInstance(req);
else
logRotator = null;
keepDependencies = req.getParameter("keepDependencies")!=null;
save();
String newName = req.getParameter("name");
if(newName!=null && !newName.equals(name)) {
rsp.sendRedirect("rename?newName="+newName);
} else {
rsp.sendRedirect(".");
}
}
/**
* Accepts the new description.
*/
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
if(!Hudson.adminCheck(req,rsp))
return;
req.setCharacterEncoding("UTF-8");
description = req.getParameter("description");
save();
rsp.sendRedirect("."); // go to the top page
}
/**
* Returns the image that shows the current buildCommand status.
*/
public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException {
rsp.sendRedirect2(req.getContextPath()+"/nocacheImages/48x48/"+getBuildStatusUrl());
}
public String getBuildStatusUrl() {
return getIconColor()+".gif";
}
/**
* Returns the graph that shows how long each build took.
*/
public void doBuildTimeGraph( StaplerRequest req, StaplerResponse rsp ) throws IOException {
class Label implements Comparable