/******************************************************************************* * * 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(); } } }