
hudson.model.Run Maven / Gradle / Ivy
package hudson.model;
import com.thoughtworks.xstream.XStream;
import hudson.CloseProofOutputStream;
import hudson.EnvVars;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.FilePath;
import hudson.Util;
import static hudson.Util.combine;
import hudson.XmlFile;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixRun;
import hudson.model.listeners.RunListener;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildWrapper;
import hudson.tasks.LogRotator;
import hudson.tasks.Mailer;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.IOException2;
import hudson.util.XStream2;
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 org.kohsuke.stapler.framework.io.LargeText;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
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
* @see RunListener
*/
@ExportedBean
public abstract class Run ,RunT extends Run>
extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot {
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 long 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 {
/**
* Build is created/queued but we haven't started building it.
*/
NOT_STARTED,
/**
* Build is in progress.
*/
BUILDING,
/**
* Build is completed now, and the status is determined,
* but log files are still being updated.
*/
POST_PRODUCTION,
/**
* Build is completed now, and log file is closed.
*/
COMPLETED
}
/**
* Number of milli-seconds it took to run this build.
*/
protected long duration;
/**
* Keeps this log entries.
*/
private boolean keepLog;
protected static final ThreadLocal ID_FORMATTER =
new ThreadLocal() {
@Override
protected SimpleDateFormat initialValue() {
return 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(job,timestamp.getTimeInMillis());
}
protected Run(JobT job, long 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, parseTimestampFromBuildDir(buildDir));
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
}
/*package*/ static long parseTimestampFromBuildDir(File buildDir) throws IOException {
try {
return ID_FORMATTER.get().parse(buildDir.getName()).getTime();
} catch (ParseException e) {
throw new IOException2("Invalid directory name "+buildDir,e);
} catch (NumberFormatException e) {
throw new IOException2("Invalid directory name "+buildDir,e);
}
}
/**
* Ordering based on build numbers.
*/
public int compareTo(RunT that) {
return this.number - that.number;
}
/**
* Returns the build result.
*
*
* When a build is {@link #isBuilding() in progress}, this method
* may return null or a temporary intermediate result.
*/
@Exported
public 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.fine(toString()+" : result is set to "+r+" by "+caller);
} else {
if(r.isWorseThan(result)) {
LOGGER.fine(toString()+" : result is set to "+r+" by "+caller);
result = r;
}
}
}
/**
* Gets the subset of {@link #getActions()} that consists of {@link BuildBadgeAction}s.
*/
public List getBadgeActions() {
List r = null;
for (Action a : getActions()) {
if(a instanceof BuildBadgeAction) {
if(r==null)
r = new ArrayList();
r.add((BuildBadgeAction)a);
}
}
if(isKeepLog()) {
if(r==null)
r = new ArrayList();
r.add(new KeepLogBuildBadge());
}
if(r==null) return Collections.emptyList();
else return r;
}
private StackTraceElement findCaller(StackTraceElement[] stackTrace, String callee) {
for(int i=0; iNEVER be used during HTML page rendering, as it won't work with
* network set up like Apache reverse proxy.
* This method is only intended for the remote API clients who cannot resolve relative references
* (even this won't work for the same reason, which should be fixed.)
*/
@Exported(visibility=2,name="url")
public final String getAbsoluteUrl() {
return project.getAbsoluteUrl()+getNumber()+'/';
}
public final String getSearchUrl() {
return getNumber()+"/";
}
/**
* Unique ID of this build.
*/
public String getId() {
return ID_FORMATTER.get().format(new Date(timestamp));
}
/**
* Root directory of this {@link Run} on the master.
*
* Files related to this {@link Run} should be stored below this directory.
*/
public File getRootDir() {
File f = new File(project.getBuildDir(),getId());
f.mkdirs();
return f;
}
/**
* Gets the directory where the artifacts are archived.
*/
public File getArtifactsDir() {
return new File(getRootDir(),"archive");
}
/**
* Gets the first {@value #CUTOFF} artifacts (relative to {@link #getArtifactsDir()}.
*/
public List getArtifacts() {
ArtifactList r = new ArtifactList();
addArtifacts(getArtifactsDir(),"",r);
r.computeDisplayName();
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"
public final class ArtifactList extends ArrayList {
public void computeDisplayName() {
if(size()>CUTOFF) return; // we are not going to display file names, so no point in computing this
int maxDepth = 0;
int[] len = new int[size()];
String[][] tokens = new String[size()][];
for( int i=0; i names = new HashMap();
for (int i = 0; i < tokens.length; i++) {
String[] token = tokens[i];
String displayName = combineLast(token,len[i]);
Integer j = names.put(displayName, i);
if(j!=null) {
collision = true;
if(j>=0)
len[j]++;
len[i]++;
names.put(displayName,-1); // occupy this name but don't let len[i] incremented with additional collisions
}
}
} while(collision && depth++ names = new HashSet();
// for (String[] token : tokens) {
// if(!names.add(combineLast(token,n)))
// continue OUTER; // collision. Increase n and try again
// }
//
// // this n successfully diambiguates
// for (int i = 0; i < tokens.length; i++) {
// String[] token = tokens[i];
// get(i).displayPath = combineLast(token,n);
// }
// return;
// }
// // it's impossible to get here, as that means
// // we have the same artifacts archived twice, but be defensive
// for (Artifact a : this)
// a.displayPath = a.relativePath;
}
/**
* Combines last N token into the "a/b/c" form.
*/
private String combineLast(String[] token, int n) {
StringBuffer buf = new StringBuffer();
for( int i=Math.max(0,token.length-n); i0) buf.append('/');
buf.append(token[i]);
}
return buf.toString();
}
}
/**
* A build artifact.
*/
public class Artifact {
/**
* Relative path name from {@link Run#getArtifactsDir()}
*/
public final String relativePath;
/**
* Truncated form of {@link #relativePath} just enough
* to disambiguate {@link Artifact}s.
*/
/*package*/ String displayPath;
/*package for test*/ 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 getDisplayPath() {
return displayPath;
}
public String toString() {
return relativePath;
}
}
/**
* Returns the log file.
*/
public File getLogFile() {
return new File(getRootDir(),"log");
}
protected SearchIndexBuilder makeSearchIndex() {
SearchIndexBuilder builder = super.makeSearchIndex()
.add("console")
.add("changes");
for (Action a : getActions()) {
if(a.getIconFileName()!=null)
builder.add(a.getUrlName());
}
return builder;
}
public Api getApi(final StaplerRequest req) {
return new Api(this);
}
public void checkPermission(Permission p) {
getACL().checkPermission(p);
}
public boolean hasPermission(Permission p) {
return getACL().hasPermission(p);
}
public ACL getACL() {
// for now, don't maintain ACL per run, and do it at project level
return getParent().getACL();
}
/**
* 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());
boolean renamingSucceeded = rootDir.renameTo(tmp);
Util.deleteRecursive(tmp);
if(!renamingSucceeded)
throw new IOException(rootDir+" is in use");
removeRunFromParent();
}
@SuppressWarnings("unchecked") // seems this is too clever for Java's type system?
private void removeRunFromParent() {
getParent().removeRun((RunT)this);
}
protected static interface Runner {
/**
* Performs the main build and returns the status code.
*
* @throws Exception
* exception will be recorded and the build will be considered a failure.
*/
Result run( BuildListener listener ) throws Exception, RunnerAbortedException;
/**
* Performs the post-build action.
*
* This method is called after the status of the build is determined.
* This is a good opportunity to do notifications based on the result
* of the build. When this method is called, the build is not really
* finalized yet, and the build is still considered in progress --- for example,
* even if the build is successful, this build still won't be picked up
* by {@link Job#getLastSuccessfulBuild()}.
*/
void post( BuildListener listener ) throws Exception;
/**
* Performs final clean up action.
*
* This method is called after {@link #post(BuildListener)},
* after the build result is fully finalized. This is the point
* where the build is already considered completed.
*
* Among other things, this is often a necessary pre-condition
* before invoking other builds that depend on this build.
*/
void cleanUp(BuildListener listener) throws Exception;
}
/**
* Used in {@link Runner#run} to indicates that a fatal error in a build
* is reported to {@link BuildListener} and the build should be simply aborted
* without further recording a stack trace.
*/
public static final class RunnerAbortedException extends RuntimeException {}
protected final void run(Runner job) {
if(result!=null)
return; // already built.
BuildListener listener=null;
PrintStream log = null;
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();
try {
try {
log = new PrintStream(new FileOutputStream(getLogFile()));
listener = new StreamBuildListener(new CloseProofOutputStream(log));
listener.started();
RunListener.fireStarted(this,listener);
setResult(job.run(listener));
LOGGER.info(toString()+" main build action completed: "+result);
} catch (ThreadDeath t) {
throw t;
} catch( AbortException e ) {
result = Result.FAILURE;
} catch( RunnerAbortedException e ) {
result = Result.FAILURE;
} catch( InterruptedException e) {
// aborted
result = Result.ABORTED;
listener.getLogger().println(Messages.Run_BuildAborted());
LOGGER.log(Level.INFO,toString()+" aborted",e);
} catch( Throwable e ) {
handleFatalBuildProblem(listener,e);
result = Result.FAILURE;
}
// even if the main build 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;
} finally {
long end = System.currentTimeMillis();
duration = end-start;
// advance the state.
// the significance of doing this is that Hudson
// will now see this build as completed.
// things like triggering other builds requires this as pre-condition.
// see issue #980.
state = State.POST_PRODUCTION;
try {
job.cleanUp(listener);
} catch (Exception e) {
handleFatalBuildProblem(listener,e);
// too late to update the result now
}
RunListener.fireCompleted(this,listener);
if(listener!=null)
listener.finished(result);
if(log!=null)
log.close();
try {
save();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
getParent().logRotate();
} catch (IOException e) {
e.printStackTrace();
}
} finally {
onEndBuilding();
}
}
/**
* Handles a fatal build problem (exception) that occurred 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 {
if(BulkChange.contains(this)) return;
getDataFile().write(this);
}
private XmlFile getDataFile() {
return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml"));
}
/**
* Gets the log of the build as a string.
*
* @deprecated Use {@link #getLog(int)} instead as it avoids loading
* the whole log into memory unnecessarily.
*/
@Deprecated
public String getLog() throws IOException {
return Util.loadFile(getLogFile());
}
/**
* Gets the log of the build as a list of strings (one per log line).
* The number of lines returned is constrained by the maxLines parameter.
*
* @param maxLines The maximum number of log lines to return. If the log
* is bigger than this, only the most recent lines are returned.
* @return A list of log lines. Will have no more than maxLines elements.
* @throws IOException If there is a problem reading the log file.
*/
public List getLog(int maxLines) throws IOException {
int lineCount = 0;
List logLines = new LinkedList();
BufferedReader reader = new BufferedReader(new FileReader(getLogFile()));
try {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
logLines.add(line);
++lineCount;
// If we have too many lines, remove the oldest line. This way we
// never have to hold the full contents of a huge log file in memory.
// Adding to and removing from the ends of a linked list are cheap
// operations.
if (lineCount > maxLines)
logLines.remove(0);
}
} finally {
reader.close();
}
// If the log has been truncated, include that information.
// Use set (replaces the first element) rather than add so that
// the list doesn't grow beyond the specified maximum number of lines.
if (lineCount > maxLines)
logLines.set(0, "[...truncated " + (lineCount - (maxLines - 1)) + " lines...]");
return logLines;
}
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().getImage();
}
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,"broken 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 failure"));
else // ???
return new Summary(false,"unstable");
}
if(trP.getFailCount()==0)
return new Summary(true,combine(trN.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, InterruptedException {
new DirectoryBrowserSupport(this,project.getDisplayName()+' '+getDisplayName())
.serveFile(req, rsp, new FilePath(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);
}
/**
* Returns the build time stamp in the body.
*/
public void doBuildTimestamp( StaplerRequest req, StaplerResponse rsp, @QueryParameter("format") String format) throws IOException {
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("US-ASCII");
rsp.setStatus(HttpServletResponse.SC_OK);
DateFormat df = format==null ?
DateFormat.getDateTimeInstance(DateFormat.SHORT,DateFormat.SHORT, Locale.ENGLISH) :
new SimpleDateFormat(format,req.getLocale());
rsp.getWriter().print(df.format(getTimestamp().getTime()));
}
/**
* Handles incremental log output.
*/
public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException {
new LargeText(getLogFile(),!isLogUpdated()).doProgressText(req,rsp);
}
public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
checkPermission(UPDATE);
keepLog(!keepLog);
rsp.forwardToPreviousPage(req);
}
/**
* Marks this build to keep the log.
*/
public final void keepLog() throws IOException {
keepLog(true);
}
public void keepLog(boolean newValue) throws IOException {
keepLog = newValue;
save();
}
/**
* Deletes the build when the button is pressed.
*/
public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
checkPermission(DELETE);
// 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(Messages.Run_UnableToDelete(toString(),why),req,rsp);
return;
}
delete();
rsp.sendRedirect2(req.getContextPath()+'/' + getParent().getUrl());
}
public void setDescription(String description) throws IOException {
checkPermission(UPDATE);
this.description = description;
save();
}
/**
* Accepts the new description.
*/
public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
req.setCharacterEncoding("UTF-8");
setDescription(req.getParameter("description"));
rsp.sendRedirect("."); // go to the top page
}
/**
* Returns the map that contains environmental variable overrides for this build.
*
*
* {@link BuildStep}s that invoke external processes should use this.
* This allows {@link BuildWrapper}s and other project configurations (such as JDK selection)
* to take effect.
*
*
* On Windows systems, environment variables are case-preserving but
* comparison/query is case insensitive (IOW, you can set 'Path' to something
* and you get the same value by doing '%PATH%'.) So to implement this semantics
* the map returned from here is a {@link TreeMap} with a special comparator.
*
*/
public Map getEnvVars() {
EnvVars env = new EnvVars();
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().getFullName());
String rootUrl = Hudson.getInstance().getRootUrl();
if(rootUrl!=null)
env.put("HUDSON_URL", rootUrl);
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",FreeStyleBuild.class);
XSTREAM.alias("matrix-build",MatrixBuild.class);
XSTREAM.alias("matrix-run",MatrixRun.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) {
long lt = lhs.getTimestamp().getTimeInMillis();
long rt = rhs.getTimestamp().getTimeInMillis();
if(lt>rt) return -1;
if(lt