hudson.model.Run Maven / Gradle / Ivy
package hudson.model;
import static hudson.Util.combine;
import com.thoughtworks.xstream.XStream;
import hudson.CloseProofOutputStream;
import hudson.ExtensionPoint;
import hudson.Util;
import hudson.XmlFile;
import hudson.FeedAdapter;
import hudson.tasks.LogRotator;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.CharSpool;
import hudson.util.IOException2;
import hudson.util.XStream2;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* A particular execution of {@link Job}.
*
*
* Custom {@link Run} type is always used in conjunction with
* a custom {@link Job} type, so there's no separate registration
* mechanism for custom {@link Run} types.
*
* @author Kohsuke Kawaguchi
*/
public abstract class Run ,RunT extends Run>
extends DirectoryHolder implements ExtensionPoint {
protected transient final JobT project;
/**
* Build number.
*
*
* In earlier versions < 1.24, this number is not unique nor continuous,
* but going forward, it will, and this really replaces the build id.
*/
public /*final*/ int number;
/**
* Previous build. Can be null.
* These two fields are maintained and updated by {@link RunMap}.
*/
protected volatile transient RunT previousBuild;
/**
* Next build. Can be null.
*/
protected volatile transient RunT nextBuild;
/**
* When the build is scheduled.
*/
protected transient final Calendar timestamp;
/**
* The build result.
* This value may change while the state is in {@link State#BUILDING}.
*/
protected volatile Result result;
/**
* Human-readable description. Can be null.
*/
protected volatile String description;
/**
* The current build state.
*/
protected volatile transient State state;
private static enum State {
NOT_STARTED,
BUILDING,
COMPLETED
}
/**
* Number of milli-seconds it took to run this build.
*/
protected long duration;
/**
* Keeps this log entries.
*/
private boolean keepLog;
protected static final SimpleDateFormat ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
/**
* Creates a new {@link Run}.
*/
protected Run(JobT job) throws IOException {
this(job, new GregorianCalendar());
this.number = project.assignBuildNumber();
}
/**
* Constructor for creating a {@link Run} object in
* an arbitrary state.
*/
protected Run(JobT job, Calendar timestamp) {
this.project = job;
this.timestamp = timestamp;
this.state = State.NOT_STARTED;
}
/**
* Loads a run from a log file.
*/
protected Run(JobT project, File buildDir) throws IOException {
this(project, new GregorianCalendar());
try {
this.timestamp.setTime(ID_FORMATTER.parse(buildDir.getName()));
} catch (ParseException e) {
throw new IOException2("Invalid directory name "+buildDir,e);
} catch (NumberFormatException e) {
throw new IOException2("Invalid directory name "+buildDir,e);
}
this.state = State.COMPLETED;
this.result = Result.FAILURE; // defensive measure. value should be overwritten by unmarshal, but just in case the saved data is inconsistent
getDataFile().unmarshal(this); // load the rest of the data
}
/**
* Returns the build result.
*
*
* When a build is {@link #isBuilding() in progress}, this method
* may return null or a temporary intermediate result.
*/
public final Result getResult() {
return result;
}
public void setResult(Result r) {
// state can change only when we are building
assert state==State.BUILDING;
StackTraceElement caller = findCaller(Thread.currentThread().getStackTrace(),"setResult");
// result can only get worse
if(result==null) {
result = r;
LOGGER.info(toString()+" : result is set to "+r+" by "+caller);
} else {
if(r.isWorseThan(result)) {
LOGGER.info(toString()+" : result is set to "+r+" by "+caller);
result = r;
}
}
}
private StackTraceElement findCaller(StackTraceElement[] stackTrace, String callee) {
for(int i=0; i getArtifacts() {
List r = new ArrayList();
addArtifacts(getArtifactsDir(),"",r);
return r;
}
/**
* Returns true if this run has any artifacts.
*
*
* The strange method name is so that we can access it from EL.
*/
public boolean getHasArtifacts() {
return !getArtifacts().isEmpty();
}
private void addArtifacts( File dir, String path, List r ) {
String[] children = dir.list();
if(children==null) return;
for (String child : children) {
if(r.size()>CUTOFF)
return;
File sub = new File(dir, child);
if (sub.isDirectory()) {
addArtifacts(sub, path + child + '/', r);
} else {
r.add(new Artifact(path + child));
}
}
}
private static final int CUTOFF = 17; // 0, 1,... 16, and then "too many"
/**
* A build artifact.
*/
public class Artifact {
/**
* Relative path name from {@link Run#getArtifactsDir()}
*/
private final String relativePath;
private Artifact(String relativePath) {
this.relativePath = relativePath;
}
/**
* Gets the artifact file.
*/
public File getFile() {
return new File(getArtifactsDir(),relativePath);
}
/**
* Returns just the file name portion, without the path.
*/
public String getFileName() {
return getFile().getName();
}
public String toString() {
return relativePath;
}
}
/**
* Returns the log file.
*/
public File getLogFile() {
return new File(getRootDir(),"log");
}
/**
* Deletes this build and its entire log
*
* @throws IOException
* if we fail to delete.
*/
public synchronized void delete() throws IOException {
File rootDir = getRootDir();
File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName());
if(!rootDir.renameTo(tmp))
throw new IOException(rootDir+" is in use");
Util.deleteRecursive(tmp);
removeRunFromParent();
}
@SuppressWarnings("unchecked") // seems this is too clever for Java's type system?
private void removeRunFromParent() {
getParent().removeRun((RunT)this);
}
protected static interface Runner {
Result run( BuildListener listener ) throws Exception;
void post( BuildListener listener );
}
protected final void run(Runner job) {
if(result!=null)
return; // already built.
onStartBuilding();
try {
// to set the state to COMPLETE in the end, even if the thread dies abnormally.
// otherwise the queue state becomes inconsistent
long start = System.currentTimeMillis();
BuildListener listener=null;
try {
try {
final PrintStream log = new PrintStream(new FileOutputStream(getLogFile()));
listener = new BuildListener() {
final PrintWriter pw = new PrintWriter(new CloseProofOutputStream(log),true);
public void started() {}
public PrintStream getLogger() {
return log;
}
public PrintWriter error(String msg) {
pw.println("ERROR: "+msg);
return pw;
}
public PrintWriter fatalError(String msg) {
return error(msg);
}
public void finished(Result result) {
pw.close();
log.close();
}
};
listener.started();
result = job.run(listener);
LOGGER.info(toString()+" main build action completed: "+result);
} catch (ThreadDeath t) {
throw t;
} catch( Throwable e ) {
handleFatalBuildProblem(listener,e);
result = Result.FAILURE;
}
// even if the main buidl fails fatally, try to run post build processing
job.post(listener);
} catch (ThreadDeath t) {
throw t;
} catch( Throwable e ) {
handleFatalBuildProblem(listener,e);
result = Result.FAILURE;
}
long end = System.currentTimeMillis();
duration = end-start;
if(listener!=null)
listener.finished(result);
try {
save();
} catch (IOException e) {
e.printStackTrace();
}
try {
LogRotator lr = getParent().getLogRotator();
if(lr!=null)
lr.perform(getParent());
} catch (IOException e) {
e.printStackTrace();
}
} finally {
onEndBuilding();
}
}
/**
* Handles a fatal build problem (exception) that occured during the build.
*/
private void handleFatalBuildProblem(BuildListener listener, Throwable e) {
if(listener!=null) {
if(e instanceof IOException)
Util.displayIOException((IOException)e,listener);
Writer w = listener.fatalError(e.getMessage());
if(w!=null) {
try {
e.printStackTrace(new PrintWriter(w));
w.close();
} catch (IOException e1) {
// ignore
}
}
}
}
/**
* Called when a job started building.
*/
protected void onStartBuilding() {
state = State.BUILDING;
}
/**
* Called when a job finished building normally or abnormally.
*/
protected void onEndBuilding() {
state = State.COMPLETED;
if(result==null) {
// shouldn't happen, but be defensive until we figure out why
result = Result.FAILURE;
LOGGER.warning(toString()+": No build result is set, so marking as failure. This shouldn't happen");
}
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
getDataFile().write(this);
}
private XmlFile getDataFile() {
return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml"));
}
/**
* Gets the log of the build as a string.
*
* I know, this isn't terribly efficient!
*/
public String getLog() throws IOException {
return Util.loadFile(getLogFile());
}
public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException {
// see Hudson.doNocacheImages. this is a work around for a bug in Firefox
rsp.sendRedirect2(req.getContextPath()+"/nocacheImages/48x48/"+getBuildStatusUrl());
}
public String getBuildStatusUrl() {
return getIconColor()+".gif";
}
public static class Summary {
/**
* Is this build worse or better, compared to the previous build?
*/
public boolean isWorse;
public String message;
public Summary(boolean worse, String message) {
this.isWorse = worse;
this.message = message;
}
}
/**
* Gets an object that computes the single line summary of this build.
*/
public Summary getBuildStatusSummary() {
Run prev = getPreviousBuild();
if(getResult()==Result.SUCCESS) {
if(prev==null || prev.getResult()== Result.SUCCESS)
return new Summary(false,"stable");
else
return new Summary(false,"back to normal");
}
if(getResult()==Result.FAILURE) {
RunT since = getPreviousNotFailedBuild();
if(since==null)
return new Summary(false,"broken for a long time");
if(since==prev)
return new Summary(true,"broken since this build");
return new Summary(false,"broekn since "+since.getDisplayName());
}
if(getResult()==Result.ABORTED)
return new Summary(false,"aborted");
if(getResult()==Result.UNSTABLE) {
if(((Run)this) instanceof Build) {
AbstractTestResultAction trN = ((Build)(Run)this).getTestResultAction();
AbstractTestResultAction trP = prev==null ? null : ((Build) prev).getTestResultAction();
if(trP==null) {
if(trN!=null && trN.getFailCount()>0)
return new Summary(false,combine(trN.getFailCount(),"test faliure"));
else // ???
return new Summary(false,"unstable");
}
if(trP.getFailCount()==0)
return new Summary(true,combine(trP.getFailCount(),"test")+" started to fail");
if(trP.getFailCount() < trN.getFailCount())
return new Summary(true,combine(trN.getFailCount()-trP.getFailCount(),"more test")
+" are failing ("+trN.getFailCount()+" total)");
if(trP.getFailCount() > trN.getFailCount())
return new Summary(false,combine(trP.getFailCount()-trN.getFailCount(),"less test")
+" are failing ("+trN.getFailCount()+" total)");
return new Summary(false,combine(trN.getFailCount(),"test")+" are still failing");
}
}
return new Summary(false,"?");
}
/**
* Serves the artifacts.
*/
public void doArtifact( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
serveFile(req, rsp, getArtifactsDir(), "package.gif", true);
}
/**
* Returns the build number in the body.
*/
public void doBuildNumber( StaplerRequest req, StaplerResponse rsp ) throws IOException {
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("US-ASCII");
rsp.setStatus(HttpServletResponse.SC_OK);
rsp.getWriter().print(number);
}
/**
* Handles incremental log output.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("UTF-8");
rsp.setStatus(HttpServletResponse.SC_OK);
boolean completed = !isBuilding();
File logFile = getLogFile();
if(!logFile.exists()) {
// file doesn't exist yet
rsp.addHeader("X-Text-Size","0");
rsp.addHeader("X-More-Data","true");
return;
}
LargeText text = new LargeText(logFile,completed);
long start = 0;
String s = req.getParameter("start");
if(s!=null)
start = Long.parseLong(s);
CharSpool spool = new CharSpool();
long r = text.writeLogTo(start,spool);
rsp.addHeader("X-Text-Size",String.valueOf(r));
if(!completed)
rsp.addHeader("X-More-Data","true");
spool.writeTo(rsp.getWriter());
}
public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
if(!Hudson.adminCheck(req,rsp))
return;
keepLog = !keepLog;
save();
rsp.forwardToPreviousPage(req);
}
/**
* Deletes the build when the button is pressed.
*/
public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
if(!Hudson.adminCheck(req,rsp))
return;
// We should not simply delete the build if it has been explicitly
// marked to be preserved, or if the build should not be deleted
// due to dependencies!
String why = getWhyKeepLog();
if (why!=null) {
sendError("Unabled to delete "+toString()+": "+why,req,rsp);
return;
}
delete();
rsp.sendRedirect2(req.getContextPath()+'/' + getParent().getUrl());
}
/**
* 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 map that contains environmental variables for this build.
*
* Used by {@link BuildStep}s that invoke external processes.
*/
public Map getEnvVars() {
Map env = new HashMap();
env.put("BUILD_NUMBER",String.valueOf(number));
env.put("BUILD_ID",getId());
env.put("BUILD_TAG","hudson-"+getParent().getName()+"-"+number);
env.put("JOB_NAME",getParent().getName());
Thread t = Thread.currentThread();
if (t instanceof Executor) {
Executor e = (Executor) t;
env.put("EXECUTOR_NUMBER",String.valueOf(e.getNumber()));
}
return env;
}
private static final XStream XSTREAM = new XStream2();
static {
XSTREAM.alias("build",Build.class);
XSTREAM.registerConverter(Result.conv);
}
private static final Logger LOGGER = Logger.getLogger(Run.class.getName());
/**
* Sort by date. Newer ones first.
*/
public static final Comparator ORDER_BY_DATE = new Comparator() {
public int compare(Run lhs, Run rhs) {
return -lhs.getTimestamp().compareTo(rhs.getTimestamp());
}
};
/**
* {@link FeedAdapter} to produce feed from the summary of this build.
*/
public static final FeedAdapter FEED_ADAPTER = new FeedAdapter() {
public String getEntryTitle(Run entry) {
return entry+" ("+entry.getResult()+")";
}
public String getEntryUrl(Run entry) {
return entry.getUrl();
}
// produces a tag URL as per RFC 4151, required by Atom 1.0
public String getEntryID(Run entry) {
return "tag:" + "hudson.dev.java.net,"
+ entry.getTimestamp().get(Calendar.YEAR) + ":"
+ entry.getParent().getName()+':'+entry.getId();
}
public Calendar getEntryTimestamp(Run entry) {
return entry.getTimestamp();
}
};
}