/*******************************************************************************
*
* Copyright (c) 2004-2013 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Daniel Dyer, Red Hat, Inc., Tom Huybrechts,
* Romain Seguy, Yahoo! Inc., Darek Ostolski, Roy Varghese
*
*
*******************************************************************************/
package hudson.model;
import hudson.console.ConsoleLogFilter;
import hudson.Functions;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.EnvVars;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.FilePath;
import hudson.Util;
import hudson.XmlFile;
import hudson.cli.declarative.CLIMethod;
import hudson.console.AnnotatedLargeText;
import hudson.console.ConsoleNote;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixRun;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.RunListener;
import hudson.model.listeners.SaveableListener;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.tasks.LogRotator;
import hudson.tasks.Mailer;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildStep;
import hudson.tasks.test.AbstractTestResultAction;
import hudson.util.FlushProofOutputStream;
import hudson.util.IOException2;
import hudson.util.LogTaskListener;
import hudson.util.XStream2;
import hudson.util.ProcessTree;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
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.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.lang3.time.FastDateFormat;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
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 com.thoughtworks.xstream.XStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import static java.util.logging.Level.FINE;
/**
* A particular execution of {@link Job}.
*
* <p> 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<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, RunT>>
extends Actionable implements ExtensionPoint, Comparable<RunT>, AccessControlled, PersistenceRoot, DescriptorByNameOwner {
protected transient final JobT project;
/**
* Build number.
*
* <p> 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;
/**
* Pointer to the next younger build in progress. This data structure is
* lazily updated, so it may point to the build that's already completed.
* This pointer is set to 'this' if the computation determines that
* everything earlier than this build is already completed.
*/
/* does not compile on JDK 7: private*/ volatile transient RunT previousBuildInProgress;
/**
* When the build is scheduled.
*/
protected transient 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;
/**
* Human-readable name of this build. Can be null. If non-null, this text is
* displayed instead of "#NNN", which is the default.
*
* @since 1.390
*/
private volatile String displayName;
/**
* The current build state.
*/
private volatile transient State state;
/**
* Stores any exception thrown when loading from 'dataFile'(build.xml)
*/
private transient Exception dataFileLoadException;
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;
/**
* Charset in which the log file is written. For compatibility reason, this
* field may be null. For persistence, this field is string and not
* {@link Charset}.
*
* @see #getCharset()
* @since 1.257
*/
protected String charset;
/**
* Keeps this log entries.
*/
private boolean keepLog;
/**
* If the build is in progress, remember {@link Runner} that's running it.
* This field is not persisted.
*/
private volatile transient Runner runner;
protected static final Hudson.HudsonDateFormat ID_FORMATTER = new Hudson.HudsonDateFormat("yyyy-MM-dd_HH-mm-ss");
/**
* State when a Run is being created from cached values.
*/
private static final ThreadLocal<Boolean> IS_LOADING_CACHED_VALUES =
new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return false;
}
};
/**
* 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;
setState(State.NOT_STARTED);
}
/**
* Loads a run from a log file.
*/
protected Run(JobT project, File buildDir) throws IOException {
this(project, parseTimestampFromBuildDir(buildDir));
this.previousBuildInProgress = _this(); // loaded builds are always completed
setState(State.COMPLETED);
setResult(Result.FAILURE); // defensive measure. value should be overwritten by unmarshal, but just in case the saved data is inconsistent
// Throwing an exception at this point means the exception
// is thrown when the object is lazily retrieved. For compatibility,
// we cannot have this, a 'dummy' object needs to be returned, which
// will get whatever information has been previously cached injected
// back into it.
try {
getDataFile().unmarshal(this); // load the rest of the data
}
catch (Exception ex) {
LOGGER.log(Level.SEVERE, "Unable to load Run object: " + ex.getMessage());
dataFileLoadException = ex;
RunMap.LazyRunValue.Key key = RunMap.LazyRunValue.getCurrentKey();
if ( key != null ) {
loadCachedValues(key);
}
}
}
/**
* Called after the build is loaded and the object is added to the build
* list.
*/
protected void onLoad() {
for (Action a : getActions()) {
if (a instanceof RunAction) {
((RunAction) a).onLoad();
}
}
}
public boolean hasLoadFailure() {
return dataFileLoadException != null;
}
public Exception getLoadFailure() {
return dataFileLoadException;
}
@Override
public void addAction(Action a) {
super.addAction(a);
if (a instanceof RunAction) {
((RunAction) a).onAttached(this);
}
}
/*package*/ static long parseTimestampFromBuildDir(File buildDir) throws IOException {
try {
return ID_FORMATTER.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);
}
}
/**
* Obtains 'this' in a more type safe signature.
*/
@SuppressWarnings({"unchecked" })
private RunT _this() {
return (RunT) this;
}
/**
* Ordering based on build numbers.
*/
public int compareTo(RunT that) {
return this.number - that.number;
}
/**
* Returns the build result.
*
* <p> When a build is {@link #isBuilding() in progress}, this method
* returns an intermediate result.
*/
@Exported
public Result getResult() {
return result;
}
public void setResult(Result r) {
// state can change only when we are building
// roy: can also change during testing
// assert state == State.BUILDING;
// result can only get worse
if (result == null) {
result = r;
LOGGER.log(FINE, toString() + " : result is set to " + r);
} else {
if (r.isWorseThan(result)) {
LOGGER.log(FINE, toString() + " : result is set to " + r);
result = r;
}
}
}
final void setState(State state) {
this.state = state;
}
final State getState() {
return state;
}
final void loadCachedValues(RunMap.LazyRunValue.Key key) {
this.number = key.referenced.buildNumber;
this.description = String.format("%s\n%s",
this.dataFileLoadException.getMessage(),
key.referenced.description == null? "":
key.referenced.description);
this.displayName = String.format("%s [In Error]",key.referenced.displayName);
this.duration = key.referenced.duration;
this.result = key.referenced.result;
this.state = key.referenced.state;
this.timestamp = key.referenced.timeInMillis;
// Try to load other builds only if not already processing another
// cached build
if (! IS_LOADING_CACHED_VALUES.get()) {
try {
IS_LOADING_CACHED_VALUES.set(true);
this.nextBuild = (RunT)key.referenced.getNextBuild();
this.previousBuild = (RunT)key.referenced.getPreviousBuild();
}
finally {
IS_LOADING_CACHED_VALUES.remove();
}
} else {
IS_LOADING_CACHED_VALUES.remove();
}
}
/**
* Gets the subset of {@link #getActions()} that consists of
* {@link BuildBadgeAction}s.
*/
public List<BuildBadgeAction> getBadgeActions() {
return getBadgeActions(this);
}
static List<BuildBadgeAction> getBadgeActions(Run run) {
List<BuildBadgeAction> r = null;
for (Action a : run.getActions()) {
if (a instanceof BuildBadgeAction) {
if (r == null) {
r = new ArrayList<BuildBadgeAction>();
}
r.add((BuildBadgeAction) a);
}
}
if (run.isKeepLog()) {
if (r == null) {
r = new ArrayList<BuildBadgeAction>();
}
r.add(run.new KeepLogBuildBadge());
}
if (r == null) {
return Collections.emptyList();
} else {
return r;
}
}
/**
* Returns true if the build is not completed yet. This includes "not
* started yet" state.
*/
@Exported
public boolean isBuilding() {
return state.compareTo(State.POST_PRODUCTION) < 0;
}
/**
* Returns true if the log file is still being updated.
*/
public boolean isLogUpdated() {
return state.compareTo(State.COMPLETED) < 0;
}
/**
* Gets the {@link Executor} building this job, if it's being built.
* Otherwise null.
*/
final public Executor getExecutor() {
return getExecutor(this);
}
static Executor getExecutor(Run run) {
for (Computer c : Hudson.getInstance().getComputers()) {
for (Executor e : c.getExecutors()) {
if (e.getCurrentExecutable() == run) {
return e;
}
}
}
return null;
}
/**
* Gets the charset in which the log file is written.
*
* @return never null.
* @since 1.257
*/
public final Charset getCharset() {
if (charset == null) {
return Charset.defaultCharset();
}
return Charset.forName(charset);
}
/**
* Returns the {@link Cause}s that tirggered a build.
*
* <p> If a build sits in the queue for a long time, multiple build requests
* made during this period are all rolled up into one build, hence this
* method may return a list.
*
* @return can be empty but never null. read-only.
* @since 1.321
*/
public List<Cause> getCauses() {
CauseAction a = getAction(CauseAction.class);
if (a == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(a.getCauses());
}
/**
* Returns a {@link Cause} of a particular type.
*
* @since 1.362
*/
public <T extends Cause> T getCause(Class<T> type) {
for (Cause c : getCauses()) {
if (type.isInstance(c)) {
return type.cast(c);
}
}
return null;
}
/**
* Returns true if this log file should be kept and not deleted.
*
* This is used as a signal to the {@link LogRotator}.
*/
@Exported
public final boolean isKeepLog() {
return getWhyKeepLog() != null;
}
/**
* If {@link #isKeepLog()} returns true, returns a human readable one-line
* string that explains why it's being kept.
*/
public String getWhyKeepLog() {
if (keepLog) {
return Messages.Run_MarkedExplicitly();
}
return null; // not marked at all
}
/**
* The project this build is for.
*/
public JobT getParent() {
return project;
}
/**
* When the build is scheduled.
*/
@Exported
public Calendar getTimestamp() {
GregorianCalendar c = new GregorianCalendar();
c.setTimeInMillis(timestamp);
return c;
}
/**
* Same as {@link #getTimestamp()} but in a different type.
*/
public final Date getTime() {
return new Date(timestamp);
}
/**
* Same as {@link #getTimestamp()} but in a different type, that is since
* the time of the epoc.
*/
public final long getTimeInMillis() {
return timestamp;
}
@Exported
public String getDescription() {
return description;
}
/**
* Returns the length-limited description.
*
* @return The length-limited description.
*/
final public String getTruncatedDescription() {
return getTruncatedDescription(description);
}
static String getTruncatedDescription(String description) {
final int maxDescrLength = 100;
if (description == null || description.length() < maxDescrLength) {
return description;
}
final String ending = "...";
final int sz = description.length(), maxTruncLength = maxDescrLength - ending.length();
boolean inTag = false;
int displayChars = 0;
int lastTruncatablePoint = -1;
for (int i = 0; i < sz; i++) {
char ch = description.charAt(i);
if (ch == '<') {
inTag = true;
} else if (ch == '>') {
inTag = false;
if (displayChars <= maxTruncLength) {
lastTruncatablePoint = i + 1;
}
}
if (!inTag) {
displayChars++;
if (displayChars <= maxTruncLength && ch == ' ') {
lastTruncatablePoint = i;
}
}
}
String truncDesc = description;
// Could not find a preferred truncable index, force a trunc at maxTruncLength
if (lastTruncatablePoint == -1) {
lastTruncatablePoint = maxTruncLength;
}
if (displayChars >= maxDescrLength) {
truncDesc = truncDesc.substring(0, lastTruncatablePoint) + ending;
}
return truncDesc;
}
/**
* Gets the string that says how long since this build has started.
*
* @return string like "3 minutes" "1 day" etc.
*/
public String getTimestampString() {
return getTimestampString(timestamp);
}
final static String getTimestampString(long timestamp) {
long duration = new GregorianCalendar().getTimeInMillis() - timestamp;
return Util.getPastTimeString(duration);
}
/**
* Returns the timestamp formatted in xs:dateTime.
*/
public String getTimestampString2() {
return getTimestampString2(timestamp);
}
final static String getTimestampString2(long timestamp) {
return Util.XS_DATETIME_FORMATTER.format(new Date(timestamp));
}
/**
* Gets the string that says how long the build took to run.
*/
public String getDurationString() {
if (isBuilding()) {
return Messages.Run_InProgressDuration(
Util.getTimeSpanString(System.currentTimeMillis() - timestamp));
}
return Util.getTimeSpanString(duration);
}
/**
* Gets the millisecond it took to build.
*/
@Exported
public long getDuration() {
return duration;
}
/**
* Gets the icon color for display.
*/
public BallColor getIconColor() {
if (!isBuilding()) {
// already built
return getResult().color;
}
// a new build is in progress
BallColor baseColor;
if (previousBuild == null) {
baseColor = BallColor.GREY;
} else {
baseColor = previousBuild.getIconColor();
}
return baseColor.anime();
}
/**
* Returns true if the build is still queued and hasn't started yet.
*/
public boolean hasntStartedYet() {
return state == State.NOT_STARTED;
}
@Override
public String toString() {
return getFullName();
}
public String getFullName() {
return project.getFullName() + " #" + number;
}
@Exported
public String getFullDisplayName() {
return project.getFullDisplayName() + ' ' + getDisplayName();
}
public String getDisplayName() {
return displayName != null ? displayName : "#" + number;
}
public boolean hasCustomDisplayName() {
return displayName != null;
}
/**
* @param value Set to null to revert back to the default "#NNN".
*/
public void setDisplayName(String value) throws IOException {
checkPermission(UPDATE);
this.displayName = value;
save();
}
@Exported(visibility = 2)
public int getNumber() {
return number;
}
public RunT getPreviousBuild() {
return previousBuild;
}
/**
* Gets the most recent {@linkplain #isBuilding() completed} build excluding
* 'this' Run itself.
*/
public RunT getPreviousCompletedBuild() {
RunT r = getPreviousBuild();
while (r != null && r.isBuilding()) {
r = r.getPreviousBuild();
}
return r;
}
/**
* Obtains the next younger build in progress. It uses a skip-pointer so
* that we can compute this without O(n) computation time. This method also
* fixes up the skip list as we go, in a way that's concurrency safe.
*
* <p> We basically follow the existing skip list, and wherever we find a
* non-optimal pointer, we remember them in 'fixUp' and update them later.
*/
public RunT getPreviousBuildInProgress() {
if (previousBuildInProgress == this) {
return null; // the most common case
}
List<RunT> fixUp = new ArrayList<RunT>();
RunT r = _this(); // 'r' is the source of the pointer (so that we can add it to fix up if we find that the target of the pointer is inefficient.)
RunT answer;
while (true) {
RunT n = r.previousBuildInProgress;
if (n == null) { // no field computed yet.
n = r.getPreviousBuild();
fixUp.add(r);
}
if (r == n || n == null) {
// this indicates that we know there's no build in progress beyond this point
answer = null;
break;
}
if (n.isBuilding()) {
// we now know 'n' is the target we wanted
answer = n;
break;
}
fixUp.add(r); // r contains the stale 'previousBuildInProgress' back pointer
r = n;
}
// fix up so that the next look up will run faster
for (RunT f : fixUp) {
f.previousBuildInProgress = answer == null ? f : answer;
}
return answer;
}
/**
* Returns the last build that was actually built - i.e., skipping any with
* Result.NOT_BUILT
*/
public RunT getPreviousBuiltBuild() {
RunT r = previousBuild;
// in certain situations (aborted m2 builds) r.getResult() can still be null, although it should theoretically never happen
while (r != null && (r.getResult() == null || r.getResult() == Result.NOT_BUILT)) {
r = r.previousBuild;
}
return r;
}
/**
* Returns the last build that didn't fail before this build.
*/
public RunT getPreviousNotFailedBuild() {
RunT r = previousBuild;
while (r != null && r.getResult() == Result.FAILURE) {
r = r.previousBuild;
}
return r;
}
/**
* Returns the last failed build before this build.
*/
public RunT getPreviousFailedBuild() {
RunT r = previousBuild;
while (r != null && r.getResult() != Result.FAILURE) {
r = r.previousBuild;
}
return r;
}
/**
* Returns the last successful build before this build.
*
* @since 1.383
*/
public RunT getPreviousSuccessfulBuild() {
RunT r = previousBuild;
while (r != null && r.getResult() != Result.SUCCESS) {
r = r.previousBuild;
}
return r;
}
/**
* Returns the last 'numberOfBuilds' builds with a build result >=
* 'threshold'.
*
* @param numberOfBuilds the desired number of builds
* @param threshold the build result threshold
* @return a list with the builds (youngest build first). May be smaller
* than 'numberOfBuilds' or even empty if not enough builds satisfying the
* threshold have been found. Never null.
* @since 1.383
*/
public List<RunT> getPreviousBuildsOverThreshold(int numberOfBuilds, Result threshold) {
List<RunT> builds = new ArrayList<RunT>(numberOfBuilds);
RunT r = getPreviousBuild();
while (r != null && builds.size() < numberOfBuilds) {
if (!r.isBuilding()
&& (r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) {
builds.add(r);
}
r = r.getPreviousBuild();
}
return builds;
}
public RunT getNextBuild() {
return nextBuild;
}
/**
* Returns the URL of this {@link Run}, relative to the context root of
* Hudson.
*
* @return String like "job/foo/32/" with trailing slash but no leading
* slash.
*/
// I really messed this up. I'm hoping to fix this some time
// it shouldn't have trailing '/', and instead it should have leading '/'
public String getUrl() {
return project.getUrl() + getNumber() + '/';
}
/**
* Obtains the absolute URL to this build.
*
* @deprecated This method shall <b>NEVER</b> 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.
*/
@Exported
public String getId() {
return ID_FORMATTER.format(new Date(timestamp));
}
/**
* Get the date formatter used to convert the directory name in to a
* timestamp This is nasty exposure of private data, but needed all the time
* the directory containing the build is used as it's timestamp.
* @since 3.3.0 creates new DateFormat on each call to eliminate ThreadLocal
*/
public static DateFormat getIDFormatter() {
return new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
}
public Descriptor getDescriptorByName(String className) {
return Hudson.getInstance().getDescriptorByName(className);
}
/**
* 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 artifacts (relative to {@link #getArtifactsDir()}.
*/
@Exported
public List<Artifact> getArtifacts() {
return getArtifactsUpTo(Integer.MAX_VALUE);
}
/**
* Gets the first N artifacts.
*/
public List<Artifact> getArtifactsUpTo(int n) {
ArtifactList r = new ArtifactList();
addArtifacts(getArtifactsDir(), "", "", r, null, n);
r.computeDisplayName();
return r;
}
/**
* Returns true if this run has any artifacts.
*
* <p> The strange method name is so that we can access it from EL.
*/
public boolean getHasArtifacts() {
return !getArtifactsUpTo(1).isEmpty();
}
private int addArtifacts(File dir, String path, String pathHref, ArtifactList r, Artifact parent, int upTo) {
String[] children = dir.list();
if (children == null) {
return 0;
}
Arrays.sort(children, String.CASE_INSENSITIVE_ORDER);
int n = 0;
for (String child : children) {
String childPath = path + child;
String childHref = pathHref + Util.rawEncode(child);
File sub = new File(dir, child);
boolean collapsed = (children.length == 1 && parent != null);
Artifact a;
if (collapsed) {
// Collapse single items into parent node where possible:
a = new Artifact(parent.getFileName() + '/' + child, childPath,
sub.isDirectory() ? null : childHref, parent.getTreeNodeId());
r.tree.put(a, r.tree.remove(parent));
} else {
// Use null href for a directory:
a = new Artifact(child, childPath,
sub.isDirectory() ? null : childHref, "n" + ++r.idSeq);
r.tree.put(a, parent != null ? parent.getTreeNodeId() : null);
}
if (sub.isDirectory()) {
n += addArtifacts(sub, childPath + '/', childHref + '/', r, a, upTo - n);
if (n >= upTo) {
break;
}
} else {
// Don't store collapsed path in ArrayList (for correct data in external API)
r.add(collapsed ? new Artifact(child, a.relativePath, a.href, a.treeNodeId) : a);
if (++n >= upTo) {
break;
}
}
}
return n;
}
/**
* Maximum number of artifacts to list before using switching to the tree
* view.
*/
public static final int LIST_CUTOFF = Integer.parseInt(System.getProperty("hudson.model.Run.ArtifactList.listCutoff", "16"));
/**
* Maximum number of artifacts to show in tree view before just showing a
* link.
*/
public static final int TREE_CUTOFF = Integer.parseInt(System.getProperty("hudson.model.Run.ArtifactList.treeCutoff", "40"));
// ..and then "too many"
public final class ArtifactList extends ArrayList<Artifact> {
/**
* Map of Artifact to treeNodeId of parent node in tree view. Contains
* Artifact objects for directories and files (the ArrayList contains
* only files).
*/
private LinkedHashMap<Artifact, String> tree = new LinkedHashMap<Artifact, String>();
private int idSeq = 0;
public Map<Artifact, String> getTree() {
return tree;
}
public void computeDisplayName() {
if (size() > LIST_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 < tokens.length; i++) {
tokens[i] = get(i).relativePath.split("[\\\\/]+");
maxDepth = Math.max(maxDepth, tokens[i].length);
len[i] = 1;
}
boolean collision;
int depth = 0;
do {
collision = false;
Map<String, Integer/*index*/> names = new HashMap<String, Integer>();
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++ < maxDepth);
for (int i = 0; i < tokens.length; i++) {
get(i).displayPath = combineLast(tokens[i], len[i]);
}
// OUTER:
// for( int n=1; n<maxLen; n++ ) {
// // if we just display the last n token, would it be suffice for disambiguation?
// Set<String> names = new HashSet<String>();
// 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) {
StringBuilder buf = new StringBuilder();
for (int i = Math.max(0, token.length - n); i < token.length; i++) {
if (buf.length() > 0) {
buf.append('/');
}
buf.append(token[i]);
}
return buf.toString();
}
}
/**
* A build artifact.
*/
@ExportedBean
public class Artifact {
/**
* Relative path name from {@link Run#getArtifactsDir()}
*/
@Exported(visibility = 3)
public final String relativePath;
/**
* Truncated form of {@link #relativePath} just enough to disambiguate
* {@link Artifact}s.
*/
/*package*/ String displayPath;
/**
* The filename of the artifact. (though when directories with single
* items are collapsed for tree view, name may include multiple path
* components, like "dist/pkg/mypkg")
*/
private String name;
/**
* Properly encoded relativePath for use in URLs. This field is null for
* directories.
*/
private String href;
/**
* Id of this node for use in tree view.
*/
private String treeNodeId;
/*package for test*/ Artifact(String name, String relativePath, String href, String treeNodeId) {
this.name = name;
this.relativePath = relativePath;
this.href = href;
this.treeNodeId = treeNodeId;
}
/**
* Gets the artifact file.
*/
public File getFile() {
return new File(getArtifactsDir(), relativePath);
}
/**
* Returns just the file name portion, without the path.
*/
@Exported(visibility = 3)
public String getFileName() {
return name;
}
@Exported(visibility = 3)
public String getDisplayPath() {
return displayPath;
}
public String getHref() {
return href;
}
public String getTreeNodeId() {
return treeNodeId;
}
@Override
public String toString() {
return relativePath;
}
}
/**
* Returns the log file.
*/
public File getLogFile() {
return new File(getRootDir(), "log");
}
/**
* Returns an input stream that reads from the log file. It will use a
* gzip-compressed log file (log.gz) if that exists.
*
* @throws IOException
* @return an input stream from the log file, or null if none exists
* @since 1.349
*/
public InputStream getLogInputStream() throws IOException {
File logFile = getLogFile();
if (logFile.exists()) {
return new FileInputStream(logFile);
}
File compressedLogFile = new File(logFile.getParentFile(), logFile.getName() + ".gz");
if (compressedLogFile.exists()) {
return new GZIPInputStream(new FileInputStream(compressedLogFile));
}
return new NullInputStream(0);
}
public Reader getLogReader() throws IOException {
if (charset == null) {
return new InputStreamReader(getLogInputStream());
} else {
return new InputStreamReader(getLogInputStream(), charset);
}
}
/**
* Used from <tt>console.jelly</tt> to write annotated log to the given
* output.
*
* @since 1.349
*/
public void writeLogTo(long offset, XMLOutput out) throws IOException {
try {
getLogText().writeHtmlTo(offset, out.asWriter());
} catch (IOException e) {
// try to fall back to the old getLogInputStream()
// mainly to support .gz compressed files
// In this case, console annotation handling will be turned off.
InputStream input = getLogInputStream();
try {
IOUtils.copy(input, out.asWriter());
} finally {
IOUtils.closeQuietly(input);
}
}
}
/**
* Used to URL-bind {@link AnnotatedLargeText}.
*/
public AnnotatedLargeText getLogText() {
return new AnnotatedLargeText(getLogFile(), getCharset(), !isLogUpdated(), this);
}
@Override
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() {
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's artifacts.
*
* @throws IOException if we fail to delete.
*
* @since 1.350
*/
public synchronized void deleteArtifacts() throws IOException {
File artifactsDir = getArtifactsDir();
Util.deleteContentsRecursive(artifactsDir);
}
/**
* Deletes this build and its entire log
*
* @throws IOException if we fail to delete.
*/
public synchronized void delete() throws IOException {
RunListener.fireDeleted(this);
// if we have a symlink, delete it, too
File link = new File(project.getBuildDir(), String.valueOf(getNumber()));
link.delete();
File rootDir = getRootDir();
File tmp = new File(rootDir.getParentFile(), '.' + rootDir.getName());
boolean renamingSucceeded = rootDir.renameTo(tmp);
Util.deleteRecursive(tmp);
// some user reported that they see some left-over .xyz files in the workspace,
// so just to make sure we've really deleted it, schedule the deletion on VM exit, too.
if (tmp.exists()) {
tmp.deleteOnExit();
}
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);
}
/**
* @see CheckPoint#report()
*/
/*package*/ static void reportCheckpoint(CheckPoint id) {
RunnerStack.INSTANCE.peek().checkpoints.report(id);
}
/**
* @see CheckPoint#block()
*/
/*package*/ static void waitForCheckpoint(CheckPoint id) throws InterruptedException {
while (true) {
Run b = RunnerStack.INSTANCE.peek().getBuild().getPreviousBuildInProgress();
if (b == null) {
return; // no pending earlier build
}
Run.Runner runner = b.runner;
if (runner == null) {
// polled at the wrong moment. try again.
Thread.sleep(0);
continue;
}
if (runner.checkpoints.waitForCheckPoint(id)) {
return; // confirmed that the previous build reached the check point
}
// the previous build finished without ever reaching the check point. try again.
}
}
protected abstract class Runner {
/**
* Keeps track of the check points attained by a build, and abstracts
* away the synchronization needed to maintain this data structure.
*/
private final class CheckpointSet {
/**
* Stages of the builds that this runner has completed. This is used
* for concurrent {@link Runner}s to coordinate and serialize their
* executions where necessary.
*/
private final Set<CheckPoint> checkpoints = new HashSet<CheckPoint>();
private boolean allDone;
protected synchronized void report(CheckPoint identifier) {
checkpoints.add(identifier);
notifyAll();
}
protected synchronized boolean waitForCheckPoint(CheckPoint identifier) throws InterruptedException {
final Thread t = Thread.currentThread();
final String oldName = t.getName();
t.setName(oldName + " : waiting for " + identifier + " on " + getFullDisplayName());
try {
while (!allDone && !checkpoints.contains(identifier)) {
wait();
}
return checkpoints.contains(identifier);
} finally {
t.setName(oldName);
}
}
/**
* Notifies that the build is fully completed and all the checkpoint
* locks be released.
*/
private synchronized void allDone() {
allDone = true;
notifyAll();
}
}
private final CheckpointSet checkpoints = new CheckpointSet();
/**
* Performs the main build and returns the status code.
*
* @throws Exception exception will be recorded and the build will be
* considered a failure.
*/
public abstract Result run(BuildListener listener) throws Exception, RunnerAbortedException;
/**
* Performs the post-build action. <p> This method is called after
* {@linkplain #run(BuildListener) the main portion of the build is completed.}
* 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()}.
*/
public abstract void post(BuildListener listener) throws Exception;
/**
* Performs final clean up action. <p> 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. <p> Among other things, this is often a necessary
* pre-condition before invoking other builds that depend on this build.
*/
public abstract void cleanUp(BuildListener listener) throws Exception;
protected final RunT getBuild() {
return _this();
}
}
/**
* 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 (getResult() != null) {
return; // already built.
}
StreamBuildListener listener = null;
runner = job;
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();
OutputStream logger = null;
try {
try {
Charset charset = Computer.currentComputer().getDefaultCharset();
this.charset = charset.name();
// don't do buffering so that what's written to the listener
// gets reflected to the file immediately, which can then be
// served to the browser immediately
logger = new FileOutputStream(getLogFile());
RunT build = job.getBuild();
// Global log filters
for (ConsoleLogFilter filter : ConsoleLogFilter.all()) {
logger = filter.decorateLogger((AbstractBuild) build, logger);
}
// Project specific log filterss
if (project instanceof BuildableItemWithBuildWrappers && build instanceof AbstractBuild) {
BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project;
for (BuildWrapper bw : biwbw.getBuildWrappersList()) {
logger = bw.decorateLogger((AbstractBuild) build, logger);
}
}
listener = new StreamBuildListener(logger, charset);
listener.started(getCauses());
RunListener.fireStarted(this, listener);
// create a symlink from build number to ID.
Util.createSymlink(getParent().getBuildDir(), getId(), String.valueOf(getNumber()), listener);
setResult(job.run(listener));
LOGGER.log(Level.INFO, "Build " + this + " main build action completed: " + getResult());
CheckPoint.MAIN_COMPLETED.report();
} catch (ThreadDeath t) {
throw t;
} catch (AbortException e) { // orderly abortion.
setResult(Result.FAILURE);
listener.error(e.getMessage());
LOGGER.log(Level.INFO, "Build " + this + " aborted", e);
} catch (RunnerAbortedException e) { // orderly abortion.
setResult(Result.FAILURE);
LOGGER.log(Level.INFO, "Build " + this + " aborted", e);
} catch (InterruptedException e) {
// aborted
setResult(Result.ABORTED);
listener.getLogger().println(Messages.Run_BuildAborted());
LOGGER.log(Level.INFO, toString() + " aborted", e);
} catch (Throwable e) {
handleFatalBuildProblem(listener, e);
setResult(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);
setResult(Result.FAILURE);
} finally {
long end = System.currentTimeMillis();
duration = Math.max(end - start, 0); // @see HUDSON-5844
// 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.
setState(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(getResult());
}
if (listener != null) {
listener.closeQuietly();
}
if (logger != null) {
IOUtils.closeQuietly(logger);
}
try {
save();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to save build record", e);
}
}
try {
getParent().logRotate();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log", e);
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log", e);
}
} 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() {
setState(State.BUILDING);
if (runner != null) {
RunnerStack.INSTANCE.push(runner);
}
}
/**
* Called when a job finished building normally or abnormally.
*/
protected void onEndBuilding() {
// signal that we've finished building.
if (runner != null) {
// MavenBuilds may be created without their corresponding runners.
setState(State.COMPLETED);
runner.checkpoints.allDone();
runner = null;
RunnerStack.INSTANCE.pop();
} else {
setState(State.COMPLETED);
}
if (getResult() == null) {
setResult(Result.FAILURE);
LOGGER.warning(toString() + ": No build result is set, so marking as failure. This shouldn't happen.");
}
RunListener.fireFinalized(this);
}
/**
* Save the settings to a file.
*/
public synchronized void save() throws IOException {
// Don't save if load didn't succeed.
if (hasLoadFailure()) {
return;
}
if (BulkChange.contains(this)) {
return;
}
getDataFile().write(this);
SaveableListener.fireOnChange(this, getDataFile());
}
private XmlFile getDataFile() {
return new XmlFile(XSTREAM, new File(getRootDir(), "build.xml"));
}
/**
* Gets the log of the build as a string.
*
* @deprecated since 2007-11-11. 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(), getCharset());
}
/**
* 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<String> getLog(int maxLines) throws IOException {
int lineCount = 0;
List<String> logLines = new LinkedList<String>();
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(getLogFile()), getCharset()));
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 ConsoleNote.removeNotes(logLines);
}
public void doBuildStatus(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.sendRedirect2(req.getContextPath() + "/images/48x48/" + getBuildStatusUrl());
}
public String getBuildStatusUrl() {
return getIconColor().getImage();
}
public static class Summary {
/**
* Is this build worse or better, compared to the previous build?
*/
//TODO: review and check whether we can do it private
public boolean isWorse;
public String message;
public Summary(boolean worse, String message) {
this.isWorse = worse;
this.message = message;
}
public boolean isWorse() {
return isWorse;
}
public String getMessage() {
return 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, Messages.Run_Summary_Stable());
} else {
return new Summary(false, Messages.Run_Summary_BackToNormal());
}
}
if (getResult() == Result.FAILURE) {
RunT since = getPreviousNotFailedBuild();
if (since == null) {
return new Summary(false, Messages.Run_Summary_BrokenForALongTime());
}
if (since == prev) {
return new Summary(true, Messages.Run_Summary_BrokenSinceThisBuild());
}
RunT failedBuild = since.getNextBuild();
return new Summary(false, Messages.Run_Summary_BrokenSince(failedBuild.getDisplayName()));
}
if (getResult() == Result.ABORTED) {
return new Summary(false, Messages.Run_Summary_Aborted());
}
if (getResult() == Result.UNSTABLE) {
if (((Run) this) instanceof AbstractBuild) {
AbstractTestResultAction trN = ((AbstractBuild) (Run) this).getTestResultAction();
AbstractTestResultAction trP = prev == null ? null : ((AbstractBuild) prev).getTestResultAction();
if (trP == null) {
if (trN != null && trN.getFailCount() > 0) {
return new Summary(false, Messages.Run_Summary_TestFailures(trN.getFailCount()));
} else // ???
{
return new Summary(false, Messages.Run_Summary_Unstable());
}
}
if (trP.getFailCount() == 0) {
return new Summary(true, Messages.Run_Summary_TestsStartedToFail(trN.getFailCount()));
}
if (trP.getFailCount() < trN.getFailCount()) {
return new Summary(true, Messages.Run_Summary_MoreTestsFailing(trN.getFailCount() - trP.getFailCount(), trN.getFailCount()));
}
if (trP.getFailCount() > trN.getFailCount()) {
return new Summary(false, Messages.Run_Summary_LessTestsFailing(trP.getFailCount() - trN.getFailCount(), trN.getFailCount()));
}
return new Summary(false, Messages.Run_Summary_TestsStillFailing(trN.getFailCount()));
}
}
return new Summary(false, Messages.Run_Summary_Unknown());
}
/**
* Serves the artifacts.
*/
public DirectoryBrowserSupport doArtifact() {
if (Functions.isArtifactsPermissionEnabled()) {
checkPermission(ARTIFACTS);
}
return new DirectoryBrowserSupport(this, new FilePath(getArtifactsDir()), project.getDisplayName() + ' ' + getDisplayName(), "package.png", true);
}
/**
* Returns the build number in the body.
*/
public void doBuildNumber(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 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(getTime()));
}
/**
* Sends out the raw console output.
*/
public void doConsoleText(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain;charset=UTF-8");
// Prevent jelly from flushing stream so Content-Length header can be added afterwards
FlushProofOutputStream out = null;
try {
out = new FlushProofOutputStream(rsp.getCompressedOutputStream(req));
getLogText().writeLogTo(0, out);
} finally {
IOUtils.closeQuietly(out);
}
}
/**
* Handles incremental log output.
*
* @deprecated as of 1.352 Use
* {@code getLogText().doProgressiveText(req,rsp)}
*/
public void doProgressiveLog(StaplerRequest req, StaplerResponse rsp) throws IOException {
getLogText().doProgressText(req, rsp);
}
public void doToggleLogKeep(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
keepLog(!keepLog);
rsp.forwardToPreviousPage(req);
}
/**
* Marks this build to keep the log.
*/
@CLIMethod(name = "keep-build")
public final void keepLog() throws IOException {
keepLog(true);
}
public void keepLog(boolean newValue) throws IOException {
checkPermission(UPDATE);
keepLog = newValue;
save();
}
/**
* Deletes the build when the button is pressed.
*/
public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
requirePOST();
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 {
setDescription(req.getParameter("description"));
rsp.sendRedirect("."); // go to the top page
}
/**
* @deprecated as of 1.292 Use {@link #getEnvironment()} instead.
*/
public Map<String, String> getEnvVars() {
try {
return getEnvironment();
} catch (IOException e) {
return new EnvVars();
} catch (InterruptedException e) {
return new EnvVars();
}
}
/**
* @deprecated as of 1.305 use {@link #getEnvironment(TaskListener)}
*/
public EnvVars getEnvironment() throws IOException, InterruptedException {
return getEnvironment(new LogTaskListener(LOGGER, Level.INFO));
}
/**
* Returns the map that contains environmental variables to be used for
* launching processes for this build.
*
* <p> {@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.
*
* <p> Unlike earlier {@link #getEnvVars()}, this map contains the whole
* environment, not just the overrides, so one can introspect values to
* change its behavior.
*
* @since 1.305
*/
public EnvVars getEnvironment(TaskListener log) throws IOException, InterruptedException {
EnvVars env = getCharacteristicEnvVars();
Computer c = Computer.currentComputer();
if (c != null) {
env = c.getEnvironment().overrideAll(env);
}
String rootUrl = Hudson.getInstance().getRootUrl();
if (rootUrl != null) {
env.put("HUDSON_URL", rootUrl);
env.put("BUILD_URL", rootUrl + getUrl());
env.put("JOB_URL", rootUrl + getParent().getUrl());
}
if (!env.containsKey("HUDSON_HOME")) {
env.put("HUDSON_HOME", Hudson.getInstance().getRootDir().getPath());
}
//Update HUDSON_USER property if it is not set. (see HUDSON-4463 discussion)
if (!env.containsKey(EnvVars.HUDSON_USER_ENV_KEY)) {
String value = EnvVars.getHudsonUserEnvValue();
if (null != value) {
env.put(EnvVars.HUDSON_USER_ENV_KEY, value);
}
}
Thread t = Thread.currentThread();
if (t instanceof Executor) {
Executor e = (Executor) t;
env.put("EXECUTOR_NUMBER", String.valueOf(e.getNumber()));
env.put("NODE_NAME", e.getOwner().getName());
Node n = e.getOwner().getNode();
if (n != null) {
env.put("NODE_LABELS", Util.join(n.getAssignedLabels(), " "));
}
}
for (EnvironmentContributor ec : EnvironmentContributor.all()) {
ec.buildEnvironmentFor(this, env, log);
}
return env;
}
/**
* Builds up the environment variable map that's sufficient to identify a
* process as ours. This is used to kill run-away processes via
* {@link ProcessTree#killAll(Map)}.
*/
public final EnvVars getCharacteristicEnvVars() {
EnvVars env = new EnvVars();
env.put("HUDSON_SERVER_COOKIE", Util.getDigestOf("ServerID:" + Hudson.getInstance().getSecretKey()));
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());
return env;
}
public String getExternalizableId() {
return project.getName() + "#" + getNumber();
}
public static Run<?, ?> fromExternalizableId(String id) {
int hash = id.lastIndexOf('#');
if (hash <= 0) {
throw new IllegalArgumentException("Invalid id");
}
String jobName = id.substring(0, hash);
int number = Integer.parseInt(id.substring(hash + 1));
Job<?, ?> job = (Job<?, ?>) Hudson.getInstance().getItem(jobName);
return job.getBuildByNumber(number);
}
/**
* Returns the estimated duration for this run if it is currently running.
* Default to {@link Job#getEstimatedDuration()}, may be overridden in
* subclasses if duration may depend on run specific parameters (like
* incremental Maven builds).
*
* @return the estimated duration in milliseconds
* @since 1.383
*/
public long getEstimatedDuration() {
return project.getEstimatedDuration();
}
public HttpResponse doConfigSubmit(StaplerRequest req) throws IOException, ServletException, FormException {
checkPermission(UPDATE);
BulkChange bc = new BulkChange(this);
try {
JSONObject json = req.getSubmittedForm();
submit(json);
bc.commit();
} finally {
bc.abort();
}
return HttpResponses.redirectToDot();
}
protected void submit(JSONObject json) throws IOException {
setDisplayName(Util.fixEmptyAndTrim(json.getString("displayName")));
setDescription(json.getString("description"));
}
public 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<Run> ORDER_BY_DATE = new Comparator<Run>() {
public int compare(Run lhs, Run rhs) {
long lt = lhs.getTimeInMillis();
long rt = rhs.getTimeInMillis();
if (lt > rt) {
return -1;
}
if (lt < rt) {
return 1;
}
return 0;
}
};
/**
* {@link FeedAdapter} to produce feed from the summary of this build.
*/
public static final FeedAdapter<Run> FEED_ADAPTER = new DefaultFeedAdapter();
/**
* {@link FeedAdapter} to produce feeds to show one build per project.
*/
public static final FeedAdapter<Run> FEED_ADAPTER_LATEST = new DefaultFeedAdapter() {
/**
* The entry unique ID needs to be tied to a project, so that new builds
* will replace the old result.
*/
@Override
public String getEntryID(Run e) {
// can't use a meaningful year field unless we remember when the job was created.
return "tag:hudson.java.net,2008:" + e.getParent().getAbsoluteUrl();
}
};
/**
* {@link BuildBadgeAction} that shows the logs are being kept.
*/
public final class KeepLogBuildBadge implements BuildBadgeAction {
public String getIconFileName() {
return null;
}
public String getDisplayName() {
return null;
}
public String getUrlName() {
return null;
}
public String getWhyKeepLog() {
return Run.this.getWhyKeepLog();
}
}
public static final PermissionGroup PERMISSIONS = new PermissionGroup(Run.class, Messages._Run_Permissions_Title());
public static final Permission DELETE = new Permission(PERMISSIONS, "Delete", Messages._Run_DeletePermission_Description(), Permission.DELETE);
public static final Permission UPDATE = new Permission(PERMISSIONS, "Update", Messages._Run_UpdatePermission_Description(), Permission.UPDATE);
/**
* See {@link hudson.Functions#isArtifactsPermissionEnabled}
*/
public static final Permission ARTIFACTS = new Permission(PERMISSIONS, "Artifacts", Messages._Run_ArtifactsPermission_Description(), null,
Functions.isArtifactsPermissionEnabled());
private static class DefaultFeedAdapter implements FeedAdapter<Run> {
private static final String DESCRIPTION_SUFIX = "description:";
public String getEntryTitle(Run entry) {
return entry + " (" + entry.getBuildStatusSummary().message + ")";
}
public String getEntryUrl(Run entry) {
return entry.getUrl();
}
public String getEntryID(Run entry) {
return "tag:" + "hudson.java.net,"
+ entry.getTimestamp().get(Calendar.YEAR) + ":"
+ entry.getParent().getName() + ':' + entry.getId();
}
public String getEntryDescription(Run entry) {
return (entry.getDescription() != null ? DESCRIPTION_SUFIX + entry.getDescription() : null);
}
public Calendar getEntryTimestamp(Run entry) {
return entry.getTimestamp();
}
public String getEntryAuthor(Run entry) {
return Mailer.descriptor().getAdminAddress();
}
}
@Override
public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
Object result = super.getDynamic(token, req, rsp);
if (result == null) // Next/Previous Build links on an action page (like /job/Abc/123/testReport)
// will also point to same action (/job/Abc/124/testReport), but other builds
// may not have the action.. tell browsers to redirect up to the build page.
{
result = new RedirectUp();
}
return result;
}
public static class RedirectUp {
public void doDynamic(StaplerResponse rsp) throws IOException {
// Compromise to handle both browsers (auto-redirect) and programmatic access
// (want accurate 404 response).. send 404 with javscript to redirect browsers.
rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
rsp.setContentType("text/html;charset=UTF-8");
PrintWriter out = rsp.getWriter();
out.println("<html><head>"
+ "<meta http-equiv='refresh' content='1;url=..'/>"
+ "<script>window.location.replace('..');</script>"
+ "</head>"
+ "<body style='background-color:white; color:white;'>"
+ "Not found</body></html>");
out.flush();
}
}
}