/*
* The MIT License
*
* Copyright (c) 2004-2012, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Daniel Dyer, Red Hat, Inc., Tom Huybrechts, Romain Seguy, Yahoo! Inc.,
* Darek Ostolski, CloudBees, Inc.
* Copyright (c) 2012, Martin Schroeder, Intel Mobile Communications GmbH
* Copyright (c) 2019 Intel Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import com.jcraft.jzlib.GZIPInputStream;
import com.thoughtworks.xstream.XStream;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.EnvVars;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.Functions;
import hudson.console.AnnotatedLargeText;
import hudson.console.ConsoleLogFilter;
import hudson.console.ConsoleNote;
import hudson.console.ModelHyperlinkNote;
import hudson.console.PlainTextConsoleOutputStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.StandardOpenOption;
import jenkins.util.SystemProperties;
import hudson.Util;
import hudson.XmlFile;
import hudson.cli.declarative.CLIMethod;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.RunListener;
import hudson.model.listeners.SaveableListener;
import hudson.model.queue.SubTask;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Fingerprinter.FingerprintAction;
import hudson.util.FormApply;
import hudson.util.LogTaskListener;
import hudson.util.ProcessTree;
import hudson.util.XStream2;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.lang.UnsupportedOperationException;
import java.lang.SecurityException;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.StandardCopyOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import static java.util.logging.Level.*;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import jenkins.model.ArtifactManager;
import jenkins.model.ArtifactManagerConfiguration;
import jenkins.model.ArtifactManagerFactory;
import jenkins.model.BuildDiscarder;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import jenkins.model.RunAction2;
import jenkins.model.StandardArtifactManager;
import jenkins.model.lazy.BuildReference;
import jenkins.model.lazy.LazyBuildMixIn;
import jenkins.security.MasterToSlaveCallable;
import jenkins.util.VirtualFile;
import jenkins.util.io.OnMaster;
import net.sf.json.JSONObject;
import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.Authentication;
import org.apache.commons.io.IOUtils;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.lang.ArrayUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.POST;
/**
* A particular execution of {@link Job}.
*
*
* Custom {@link Run} type is always used in conjunction with
* a custom {@link Job} type, so there's no separate registration
* mechanism for custom {@link Run} types.
*
* @author Kohsuke Kawaguchi
* @see RunListener
*/
@ExportedBean
public abstract class Run ,RunT extends Run>
extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster, StaplerProxy {
/**
* The original {@link Queue.Item#getId()} has not yet been mapped onto the {@link Run} instance.
* @since 1.601
*/
public static final long QUEUE_ID_UNKNOWN = -1;
/**
* Target size limit for truncated {@link #description}s in the Build History Widget.
* This is applied to the raw, unformatted description. Especially complex formatting
* like hyperlinks can result in much less text being shown than this might imply.
* Negative values will disable truncation, {@code 0} will enforce empty strings.
* @since 2.223
*/
private static /* non-final for Groovy */ int TRUNCATED_DESCRIPTION_LIMIT = SystemProperties.getInteger("historyWidget.descriptionLimit", 100);
protected transient final @Nonnull JobT project;
/**
* Build number.
*
*
* In earlier versions < 1.24, this number is not unique nor continuous,
* but going forward, it will, and this really replaces the build id.
*/
public transient /*final*/ int number;
/**
* The original Queue task ID from where this Run instance originated.
*/
private long queueId = Run.QUEUE_ID_UNKNOWN;
/**
* Previous build. Can be null.
* TODO JENKINS-22052 this is not actually implemented any more
*
* External code should use {@link #getPreviousBuild()}
*/
@Restricted(NoExternalUse.class)
protected volatile transient RunT previousBuild;
/**
* Next build. Can be null.
*
* External code should use {@link #getNextBuild()}
*/
@Restricted(NoExternalUse.class)
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;
/** ID as used for historical build records; otherwise null. */
private @CheckForNull String id;
/**
* When the build is scheduled.
*/
protected /*final*/ long timestamp;
/**
* When the build has started running.
*
* For historical reasons, 0 means no value is recorded.
*
* @see #getStartTimeInMillis()
*/
private long startTime;
/**
* The build result.
* This value may change while the state is in {@link Run.State#BUILDING}.
*/
protected volatile Result result;
/**
* Human-readable description which is used on the main build page.
* It can also be quite long, and it may use markup in a format defined by a {@link hudson.markup.MarkupFormatter}.
* {@link #getTruncatedDescription()} may be used to retrieve a size-limited description,
* but it implies some limitations.
*/
@CheckForNull
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;
private 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.
*
* The significance of this state is that Jenkins
* will now see this build as completed. Things like
* "triggering other builds" requires this as pre-condition.
* See JENKINS-980.
*/
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 build.
*/
private boolean keepLog;
/**
* If the build is in progress, remember {@link RunExecution} that's running it.
* This field is not persisted.
*/
private volatile transient RunExecution runner;
/**
* Artifact manager associated with this build, if any.
* @since 1.532
*/
private @CheckForNull ArtifactManager artifactManager;
/**
* Creates a new {@link Run}.
* @param job Owner job
*/
protected Run(@Nonnull JobT job) throws IOException {
this(job, System.currentTimeMillis());
this.number = project.assignBuildNumber();
LOGGER.log(FINER, "new {0} @{1}", new Object[] {this, hashCode()});
}
/**
* Constructor for creating a {@link Run} object in
* an arbitrary state.
* {@link #number} must be set manually.
*
May be used in a {@link SubTask#createExecutable} (instead of calling {@link LazyBuildMixIn#newBuild}).
* For example, {@code MatrixConfiguration.newBuild} does this
* so that the {@link #timestamp} as well as {@link #number} are shared with the parent build.
*/
protected Run(@Nonnull JobT job, @Nonnull Calendar timestamp) {
this(job,timestamp.getTimeInMillis());
}
/** @see #Run(Job, Calendar) */
protected Run(@Nonnull JobT job, long timestamp) {
this.project = job;
this.timestamp = timestamp;
this.state = State.NOT_STARTED;
}
/**
* Loads a run from a log file.
*/
protected Run(@Nonnull JobT project, @Nonnull File buildDir) throws IOException {
this.project = project;
this.previousBuildInProgress = _this(); // loaded builds are always completed
number = Integer.parseInt(buildDir.getName());
reload();
}
/**
* Reloads the build record from disk.
*
* @since 1.410
*/
public void reload() throws IOException {
this.state = State.COMPLETED;
// TODO ABORTED would perhaps make more sense than FAILURE:
this.result = Result.FAILURE; // defensive measure. value should be overwritten by unmarshal, but just in case the saved data is inconsistent
getDataFile().unmarshal(this); // load the rest of the data
if (state == State.COMPLETED) {
LOGGER.log(FINER, "reload {0} @{1}", new Object[] {this, hashCode()});
} else {
LOGGER.log(WARNING, "reload {0} @{1} with anomalous state {2}", new Object[] {this, hashCode(), state});
}
// not calling onLoad upon reload. partly because we don't want to call that from Run constructor,
// and partly because some existing use of onLoad isn't assuming that it can be invoked multiple times.
}
/**
* Called after the build is loaded and the object is added to the build list.
*/
@SuppressWarnings("deprecation")
protected void onLoad() {
for (Action a : getAllActions()) {
if (a instanceof RunAction2) {
try {
((RunAction2) a).onLoad(this);
} catch (RuntimeException x) {
LOGGER.log(WARNING, "failed to load " + a + " from " + getDataFile(), x);
removeAction(a); // if possible; might be in an inconsistent state
}
} else if (a instanceof RunAction) {
((RunAction) a).onLoad();
}
}
if (artifactManager != null) {
artifactManager.onLoad(this);
}
}
/**
* Return all transient actions associated with this build.
*
* @return the list can be empty but never null. read only.
* @deprecated Use {@link #getAllActions} instead.
*/
@Deprecated
public List getTransientActions() {
List actions = new ArrayList<>();
for (TransientBuildActionFactory factory: TransientBuildActionFactory.all()) {
for (Action created : factory.createFor(this)) {
if (created == null) {
LOGGER.log(WARNING, "null action added by {0}", factory);
continue;
}
actions.add(created);
}
}
return Collections.unmodifiableList(actions);
}
/**
* {@inheritDoc}
* A {@link RunAction2} is handled specially.
*/
@SuppressWarnings("deprecation")
@Override
public void addAction(@Nonnull Action a) {
super.addAction(a);
if (a instanceof RunAction2) {
((RunAction2) a).onAttached(this);
} else if (a instanceof RunAction) {
((RunAction) a).onAttached(this);
}
}
/**
* Obtains 'this' in a more type safe signature.
*/
@SuppressWarnings({"unchecked"})
protected @Nonnull RunT _this() {
return (RunT)this;
}
/**
* Ordering based on build numbers.
* If numbers are equal order based on names of parent projects.
*/
public int compareTo(@Nonnull RunT that) {
final int res = this.number - that.number;
if (res == 0)
return this.getParent().getFullName().compareTo(that.getParent().getFullName());
return res;
}
/**
* Get the {@link Queue.Item#getId()} of the original queue item from where this Run instance
* originated.
* @return The queue item ID.
* @since 1.601
*/
@Exported
public long getQueueId() {
return queueId;
}
/**
* Set the queue item ID.
*
* Mapped from the {@link Queue.Item#getId()}.
* @param queueId The queue item ID.
*/
@Restricted(NoExternalUse.class)
public void setQueueId(long queueId) {
this.queueId = queueId;
}
/**
* Returns the build result.
*
*
* When a build is {@link #isBuilding() in progress}, this method
* returns an intermediate result.
* @return The status of the build, if it has completed or some build step has set a status; may be null if the build is ongoing.
*/
@Exported
public @CheckForNull Result getResult() {
return result;
}
/**
* Sets the {@link #getResult} of this build.
* Has no effect when the result is already set and worse than the proposed result.
* May only be called after the build has started and before it has moved into post-production
* (normally meaning both {@link #isInProgress} and {@link #isBuilding} are true).
* @param r the proposed new result
* @throws IllegalStateException if the build has not yet started, is in post-production, or is complete
*/
public void setResult(@Nonnull Result r) {
if (state != State.BUILDING) {
throw new IllegalStateException("cannot change build result while in " + state);
}
// result can only get worse
if (result==null || r.isWorseThan(result)) {
result = r;
LOGGER.log(FINE, this + " in " + getRootDir() + ": result is set to " + r, LOGGER.isLoggable(Level.FINER) ? new Exception() : null);
}
}
/**
* Gets the subset of {@link #getActions()} that consists of {@link BuildBadgeAction}s.
*/
public @Nonnull List getBadgeActions() {
List r = getActions(BuildBadgeAction.class);
if(isKeepLog()) {
r = new ArrayList<>(r);
r.add(new KeepLogBuildBadge());
}
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;
}
/**
* Determine whether the run is being build right now.
* @return true if after started and before completed.
* @since 1.538
*/
protected boolean isInProgress() {
return state.equals(State.BUILDING) || state.equals(State.POST_PRODUCTION);
}
/**
* 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.
*
* This method looks for {@link Executor} who's {@linkplain Executor#getCurrentExecutable() assigned to this build},
* and because of that this might not be necessarily in sync with the return value of {@link #isBuilding()} —
* an executor holds on to {@link Run} some more time even after the build is finished (for example to
* perform {@linkplain Run.State#POST_PRODUCTION post-production processing}.)
* @see Executor#of
*/
@Exported
public @CheckForNull Executor getExecutor() {
return this instanceof Queue.Executable ? Executor.of((Queue.Executable) this) : null;
}
/**
* Gets the one off {@link Executor} building this job, if it's being built.
* Otherwise null.
* @since 1.433
*/
public @CheckForNull Executor getOneOffExecutor() {
for( Computer c : Jenkins.get().getComputers() ) {
for (Executor e : c.getOneOffExecutors()) {
if(e.getCurrentExecutable()==this)
return e;
}
}
return null;
}
/**
* Gets the charset in which the log file is written.
* @return never null.
* @since 1.257
*/
public final @Nonnull Charset getCharset() {
if(charset==null) return Charset.defaultCharset();
return Charset.forName(charset);
}
/**
* Returns the {@link Cause}s that triggered a build.
*
*
* 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 @Nonnull List 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 @CheckForNull T getCause(Class type) {
for (Cause c : getCauses())
if (type.isInstance(c))
return type.cast(c);
return null;
}
/**
* Returns true if this build should be kept and not deleted.
* (Despite the name, this refers to the entire build, not merely the log file.)
* This is used as a signal to the {@link BuildDiscarder}.
*/
@Exported
public final boolean isKeepLog() {
return getWhyKeepLog()!=null;
}
/**
* If {@link #isKeepLog()} returns true, returns a short, human-readable
* sentence that explains why it's being kept.
*/
public @CheckForNull String getWhyKeepLog() {
if(keepLog)
return Messages.Run_MarkedExplicitly();
return null; // not marked at all
}
/**
* The project this build is for.
*/
public @Nonnull JobT getParent() {
return project;
}
/**
* When the build is scheduled.
*
* @see #getStartTimeInMillis()
*/
@Exported
public @Nonnull Calendar getTimestamp() {
GregorianCalendar c = new GregorianCalendar();
c.setTimeInMillis(timestamp);
return c;
}
/**
* Same as {@link #getTimestamp()} but in a different type.
*/
public final @Nonnull Date getTime() {
return new Date(timestamp);
}
/**
* Same as {@link #getTimestamp()} but in a different type, that is since the time of the epoch.
*/
public final long getTimeInMillis() {
return timestamp;
}
/**
* When the build has started running in an executor.
*
* For example, if a build is scheduled 1pm, and stayed in the queue for 1 hour (say, no idle agents),
* then this method returns 2pm, which is the time the job moved from the queue to the building state.
*
* @see #getTimestamp()
*/
public final long getStartTimeInMillis() {
if (startTime==0) return timestamp; // fallback: approximate by the queuing time
return startTime;
}
@Exported
@CheckForNull
public String getDescription() {
return description;
}
/**
* Returns the length-limited description.
* The method tries to take HTML tags within the description into account, but it is a best-effort attempt.
* Also, the method will likely not work properly if a non-HTML {@link hudson.markup.MarkupFormatter} is used.
* @return The length-limited description.
* @deprecated truncated description based on the {@link #TRUNCATED_DESCRIPTION_LIMIT} setting.
*/
@Deprecated
public @CheckForNull String getTruncatedDescription() {
if (TRUNCATED_DESCRIPTION_LIMIT < 0) { // disabled
return description;
}
if (TRUNCATED_DESCRIPTION_LIMIT == 0) { // Someone wants to suppress descriptions, why not?
return "";
}
final int maxDescrLength = TRUNCATED_DESCRIPTION_LIMIT;
final String localDescription = description;
if (localDescription == null || localDescription.length() < maxDescrLength) {
return localDescription;
}
final String ending = "...";
final int sz = localDescription.length(), maxTruncLength = maxDescrLength - ending.length();
boolean inTag = false;
int displayChars = 0;
int lastTruncatablePoint = -1;
for (int i=0; i') {
inTag = false;
if (displayChars <= maxTruncLength) {
lastTruncatablePoint = i + 1;
}
}
if (!inTag) {
displayChars++;
if (displayChars <= maxTruncLength && ch == ' ') {
lastTruncatablePoint = i;
}
}
}
String truncDesc = localDescription;
// Could not find a preferred truncatable 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 @Nonnull String getTimestampString() {
long duration = new GregorianCalendar().getTimeInMillis()-timestamp;
return Util.getTimeSpanString(duration);
}
/**
* Returns the timestamp formatted in xs:dateTime.
*/
public @Nonnull String getTimestampString2() {
return Util.XS_DATETIME_FORMATTER.format(new Date(timestamp));
}
/**
* Gets the string that says how long the build took to run.
*/
public @Nonnull String getDurationString() {
if (hasntStartedYet()) {
return Messages.Run_NotStartedYet();
} else if (isBuilding()) {
return Messages.Run_InProgressDuration(
Util.getTimeSpanString(System.currentTimeMillis()-startTime));
}
return Util.getTimeSpanString(duration);
}
/**
* Gets the millisecond it took to build.
*/
@Exported
public long getDuration() {
return duration;
}
/**
* Gets the icon color for display.
*/
public @Nonnull BallColor getIconColor() {
if(!isBuilding()) {
// already built
return getResult().color;
}
// a new build is in progress
BallColor baseColor;
RunT pb = getPreviousBuild();
if(pb==null)
baseColor = BallColor.NOTBUILT;
else
baseColor = pb.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() {
if (project == null) {
return "";
}
return project.getFullName() + " #" + number;
}
@Exported
public String getFullDisplayName() {
return project.getFullDisplayName()+' '+getDisplayName();
}
@Exported
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;
}
/**
* Called by {@link RunMap} to obtain a reference to this run.
* @return Reference to the build. Never null
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#createReference
* @since 1.556
*/
protected @Nonnull BuildReference createReference() {
return new BuildReference<>(getId(), _this());
}
/**
* Called by {@link RunMap} to drop bi-directional links in preparation for
* deleting a build.
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#dropLinks
* @since 1.556
*/
protected void dropLinks() {
if(nextBuild!=null)
nextBuild.previousBuild = previousBuild;
if(previousBuild!=null)
previousBuild.nextBuild = nextBuild;
}
/**
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#getPreviousBuild
*/
public @CheckForNull RunT getPreviousBuild() {
return previousBuild;
}
/**
* Gets the most recent {@linkplain #isBuilding() completed} build excluding 'this' Run itself.
*/
public final @CheckForNull 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.
*
*
* 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 final @CheckForNull RunT getPreviousBuildInProgress() {
if(previousBuildInProgress==this) return null; // the most common case
List fixUp = new ArrayList<>();
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 @CheckForNull RunT getPreviousBuiltBuild() {
RunT r=getPreviousBuild();
// 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.getPreviousBuild();
return r;
}
/**
* Returns the last build that didn't fail before this build.
*/
public @CheckForNull RunT getPreviousNotFailedBuild() {
RunT r=getPreviousBuild();
while( r!=null && r.getResult()==Result.FAILURE )
r=r.getPreviousBuild();
return r;
}
/**
* Returns the last failed build before this build.
*/
public @CheckForNull RunT getPreviousFailedBuild() {
RunT r=getPreviousBuild();
while( r!=null && r.getResult()!=Result.FAILURE )
r=r.getPreviousBuild();
return r;
}
/**
* Returns the last successful build before this build.
* @since 1.383
*/
public @CheckForNull RunT getPreviousSuccessfulBuild() {
RunT r=getPreviousBuild();
while( r!=null && r.getResult()!=Result.SUCCESS )
r=r.getPreviousBuild();
return r;
}
/**
* Returns the last {@code numberOfBuilds} builds with a build result ≥ {@code 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 @Nonnull List getPreviousBuildsOverThreshold(int numberOfBuilds, @Nonnull Result threshold) {
RunT r = getPreviousBuild();
if (r != null) {
return r.getBuildsOverThreshold(numberOfBuilds, threshold);
}
return new ArrayList<>(numberOfBuilds);
}
/**
* Returns the last {@code numberOfBuilds} builds with a build result ≥ {@code 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 2.202
*/
protected @Nonnull List getBuildsOverThreshold(int numberOfBuilds, @Nonnull Result threshold) {
List builds = new ArrayList<>(numberOfBuilds);
RunT r = _this();
while (r != null && builds.size() < numberOfBuilds) {
if (!r.isBuilding() &&
(r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) {
builds.add(r);
}
r = r.getPreviousBuild();
}
return builds;
}
/**
* @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#getNextBuild
*/
public @CheckForNull 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 @Nonnull String getUrl() {
// RUN may be accessed using permalinks, as "/lastSuccessful" or other, so try to retrieve this base URL
// looking for "this" in the current request ancestors
// @see also {@link AbstractItem#getUrl}
StaplerRequest req = Stapler.getCurrentRequest();
if (req != null) {
String seed = Functions.getNearestAncestorUrl(req,this);
if(seed!=null) {
// trim off the context path portion and leading '/', but add trailing '/'
return seed.substring(req.getContextPath().length()+1)+'/';
}
}
return project.getUrl()+getNumber()+'/';
}
/**
* Obtains the absolute URL to this build.
*
* @deprecated
* This method shall NEVER be used during HTML page rendering, as it's too easy for
* misconfiguration to break this value, with network set up like Apache reverse proxy.
* This method is only intended for the remote API clients who cannot resolve relative references.
*/
@Exported(visibility=2,name="url")
@Deprecated
public final @Nonnull String getAbsoluteUrl() {
return project.getAbsoluteUrl()+getNumber()+'/';
}
public final @Nonnull String getSearchUrl() {
return getNumber()+"/";
}
/**
* Unique ID of this build.
* Usually the decimal form of {@link #number}, but may be a formatted timestamp for historical builds.
*/
@Exported
public @Nonnull String getId() {
return id != null ? id : Integer.toString(number);
}
/**
* Get the root directory of this {@link Run} on the master.
* Files related to this {@link Run} should be stored below this directory.
* @return Root directory of this {@link Run} on the master. Never null
*/
@Override
public @Nonnull File getRootDir() {
return new File(project.getBuildDir(), Integer.toString(number));
}
/**
* Gets an object responsible for storing and retrieving build artifacts.
* If {@link #pickArtifactManager} has previously been called on this build,
* and a nondefault manager selected, that will be returned.
* Otherwise (including if we are loading a historical build created prior to this feature) {@link StandardArtifactManager} is used.
*
This method should be used when existing artifacts are to be loaded, displayed, or removed.
* If adding artifacts, use {@link #pickArtifactManager} instead.
* @return an appropriate artifact manager
* @since 1.532
*/
public final @Nonnull ArtifactManager getArtifactManager() {
return artifactManager != null ? artifactManager : new StandardArtifactManager(this);
}
/**
* Selects an object responsible for storing and retrieving build artifacts.
* The first time this is called on a running build, {@link ArtifactManagerConfiguration} is checked
* to see if one will handle this build.
* If so, that manager is saved in the build and it will be used henceforth.
* If no manager claimed the build, {@link StandardArtifactManager} is used.
*
This method should be used when a build step expects to archive some artifacts.
* If only displaying existing artifacts, use {@link #getArtifactManager} instead.
* @return an appropriate artifact manager
* @throws IOException if a custom manager was selected but the selection could not be saved
* @since 1.532
*/
public final synchronized @Nonnull ArtifactManager pickArtifactManager() throws IOException {
if (artifactManager != null) {
return artifactManager;
} else {
for (ArtifactManagerFactory f : ArtifactManagerConfiguration.get().getArtifactManagerFactories()) {
ArtifactManager mgr = f.managerFor(this);
if (mgr != null) {
artifactManager = mgr;
save();
return mgr;
}
}
return new StandardArtifactManager(this);
}
}
/**
* Gets the directory where the artifacts are archived.
* @deprecated Should only be used from {@link StandardArtifactManager} or subclasses.
*/
@Deprecated
public File getArtifactsDir() {
return new File(getRootDir(),"archive");
}
/**
* Gets the artifacts (relative to {@link #getArtifactsDir()}.
* @return The list can be empty but never null
*/
@Exported
public @Nonnull List getArtifacts() {
return getArtifactsUpTo(Integer.MAX_VALUE);
}
/**
* Gets the first N artifacts.
* @return The list can be empty but never null
*/
public @Nonnull List getArtifactsUpTo(int artifactsNumber) {
SerializableArtifactList sal;
VirtualFile root = getArtifactManager().root();
try {
sal = root.run(new AddArtifacts(root, artifactsNumber));
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
sal = new SerializableArtifactList();
}
ArtifactList r = new ArtifactList();
r.updateFrom(sal);
r.computeDisplayName();
return r;
}
/**
* Check if the {@link Run} contains artifacts.
* The strange method name is so that we can access it from EL.
* @return true if this run has any artifacts
*/
public boolean getHasArtifacts() {
return !getArtifactsUpTo(1).isEmpty();
}
private static final class AddArtifacts extends MasterToSlaveCallable {
private static final long serialVersionUID = 1L;
private final VirtualFile root;
private final int artifactsNumber;
AddArtifacts(VirtualFile root, int artifactsNumber) {
this.root = root;
this.artifactsNumber = artifactsNumber;
}
@Override
public SerializableArtifactList call() throws IOException {
SerializableArtifactList sal = new SerializableArtifactList();
addArtifacts(root, "", "", sal, null, artifactsNumber);
return sal;
}
}
private static int addArtifacts(@Nonnull VirtualFile dir,
@Nonnull String path, @Nonnull String pathHref,
@Nonnull SerializableArtifactList r, @CheckForNull SerializableArtifact parent, int upTo) throws IOException {
VirtualFile[] kids = dir.list();
Arrays.sort(kids);
int n = 0;
for (VirtualFile sub : kids) {
String child = sub.getName();
String childPath = path + child;
String childHref = pathHref + Util.rawEncode(child);
String length = sub.isFile() ? String.valueOf(sub.length()) : "";
boolean collapsed = (kids.length==1 && parent!=null);
SerializableArtifact a;
if (collapsed) {
// Collapse single items into parent node where possible:
a = new SerializableArtifact(parent.name + '/' + child, childPath,
sub.isDirectory() ? null : childHref, length,
parent.treeNodeId);
r.tree.put(a, r.tree.remove(parent));
} else {
// Use null href for a directory:
a = new SerializableArtifact(child, childPath,
sub.isDirectory() ? null : childHref, length,
"n" + ++r.idSeq);
r.tree.put(a, parent!=null ? parent.treeNodeId : 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 SerializableArtifact(child, a.relativePath, a.href, length, 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(SystemProperties.getString("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(SystemProperties.getString("hudson.model.Run.ArtifactList.treeCutoff", "40"));
// ..and then "too many"
/** {@link Run.Artifact} without the implicit link to {@link Run} */
private static final class SerializableArtifact implements Serializable {
private static final long serialVersionUID = 1L;
final String name;
final String relativePath;
final String href;
final String length;
final String treeNodeId;
SerializableArtifact(String name, String relativePath, String href, String length, String treeNodeId) {
this.name = name;
this.relativePath = relativePath;
this.href = href;
this.length = length;
this.treeNodeId = treeNodeId;
}
}
/** {@link Run.ArtifactList} without the implicit link to {@link Run} */
private static final class SerializableArtifactList extends ArrayList {
private static final long serialVersionUID = 1L;
private LinkedHashMap tree = new LinkedHashMap<>();
private int idSeq = 0;
}
public final class ArtifactList extends ArrayList {
private static final long serialVersionUID = 1L;
/**
* 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 tree = new LinkedHashMap<>();
void updateFrom(SerializableArtifactList clone) {
Map artifacts = new HashMap<>(); // need to share objects between tree and list, since computeDisplayName mutates displayPath
for (SerializableArtifact sa : clone) {
Artifact a = new Artifact(sa);
artifacts.put(a.relativePath, a);
add(a);
}
tree = new LinkedHashMap<>();
for (Map.Entry entry : clone.tree.entrySet()) {
SerializableArtifact sa = entry.getKey();
Artifact a = artifacts.get(sa.relativePath);
if (a == null) {
a = new Artifact(sa);
}
tree.put(a, entry.getValue());
}
}
public Map 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 names = new HashMap<>();
for (int i = 0; i < tokens.length; i++) {
String[] token = tokens[i];
String displayName = combineLast(token,len[i]);
Integer j = names.put(displayName, i);
if(j!=null) {
collision = true;
if(j>=0)
len[j]++;
len[i]++;
names.put(displayName,-1); // occupy this name but don't let len[i] incremented with additional collisions
}
}
} while(collision && depth++ names = new HashSet();
// for (String[] token : tokens) {
// if(!names.add(combineLast(token,n)))
// continue OUTER; // collision. Increase n and try again
// }
//
// // this n successfully disambiguates
// 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); i0) buf.append('/');
buf.append(token[i]);
}
return buf.toString();
}
}
/**
* A build artifact.
*/
@ExportedBean
public class Artifact {
/**
* Relative path name from artifacts root.
*/
@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;
/**
*length of this artifact for files.
*/
private String length;
Artifact(SerializableArtifact clone) {
this(clone.name, clone.relativePath, clone.href, clone.length, clone.treeNodeId);
}
/*package for test*/ Artifact(String name, String relativePath, String href, String len, String treeNodeId) {
this.name = name;
this.relativePath = relativePath;
this.href = href;
this.treeNodeId = treeNodeId;
this.length = len;
}
/**
* Gets the artifact file.
* @deprecated May not be meaningful with custom artifact managers. Use {@link ArtifactManager#root} plus {@link VirtualFile#child} with {@link #relativePath} instead.
*/
@Deprecated
public @Nonnull 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 getLength() {
return length;
}
public long getFileSize(){
try {
return Long.decode(length);
}
catch (NumberFormatException e) {
LOGGER.log(FINE, "Cannot determine file size of the artifact {0}. The length {1} is not a valid long value", new Object[] {this, length});
return 0;
}
}
public String getTreeNodeId() {
return treeNodeId;
}
@Override
public String toString() {
return relativePath;
}
}
/**
* get the fingerprints associated with this build
*
* @return The fingerprints
*/
@Nonnull
@Exported(name = "fingerprint", inline = true, visibility = -1)
public Collection getBuildFingerprints() {
FingerprintAction fingerprintAction = getAction(FingerprintAction.class);
if (fingerprintAction != null) {
return fingerprintAction.getFingerprints().values();
}
return Collections.emptyList();
}
/**
* Returns the log file.
* @return The file may reference both uncompressed or compressed logs
* @deprecated Assumes file-based storage of the log, which is not necessarily the case for Pipelines after JEP-210. Use other methods giving various kinds of streams such as {@link Run#getLogReader()}, {@link Run#getLogInputStream()}, or {@link Run#getLogText()}.
*/
@Deprecated
public @Nonnull File getLogFile() {
File rawF = new File(getRootDir(), "log");
if (rawF.isFile()) {
return rawF;
}
File gzF = new File(getRootDir(), "log.gz");
if (gzF.isFile()) {
return gzF;
}
//If both fail, return the standard, uncompressed log file
return rawF;
}
/**
* 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.
* If the log file does not exist, the error message will be returned to the output.
* @since 1.349
*/
public @Nonnull InputStream getLogInputStream() throws IOException {
File logFile = getLogFile();
if (logFile.exists() ) {
// Checking if a ".gz" file was return
try {
InputStream fis = Files.newInputStream(logFile.toPath());
if (logFile.getName().endsWith(".gz")) {
return new GZIPInputStream(fis);
} else {
return fis;
}
} catch (InvalidPathException e) {
throw new IOException(e);
}
}
String message = "No such file: " + logFile;
return new ByteArrayInputStream(charset != null ? message.getBytes(charset) : message.getBytes());
}
public @Nonnull Reader getLogReader() throws IOException {
if (charset==null) return new InputStreamReader(getLogInputStream());
else return new InputStreamReader(getLogInputStream(),charset);
}
/**
* Used from {@code console.jelly} to write annotated log to the given output.
*
* @since 1.349
*/
public void writeLogTo(long offset, @Nonnull XMLOutput out) throws IOException {
getLogText().writeHtmlTo(offset, out.asWriter());
}
/**
* Writes the complete log from the start to finish to the {@link OutputStream}.
*
* If someone is still writing to the log, this method will not return until the whole log
* file gets written out.
*
* The method does not close the {@link OutputStream}.
*/
public void writeWholeLogTo(@Nonnull OutputStream out) throws IOException, InterruptedException {
long pos = 0;
AnnotatedLargeText logText;
logText = getLogText();
pos = logText.writeLogTo(pos, out);
while (!logText.isComplete()) {
// Instead of us hitting the log file as many times as possible, instead we get the information once every
// second to avoid CPU usage getting very high.
Thread.sleep(1000);
logText = getLogText();
pos = logText.writeLogTo(pos, out);
}
}
/**
* Used to URL-bind {@link AnnotatedLargeText}.
* @return A {@link Run} log with annotations
*/
public @Nonnull AnnotatedLargeText getLogText() {
return new AnnotatedLargeText(getLogFile(),getCharset(),!isLogUpdated(),this);
}
@Override
protected @Nonnull SearchIndexBuilder makeSearchIndex() {
SearchIndexBuilder builder = super.makeSearchIndex()
.add("console")
.add("changes");
for (Action a : getAllActions()) {
if(a.getIconFileName()!=null)
builder.add(a.getUrlName());
}
return builder;
}
public @Nonnull Api getApi() {
return new Api(this);
}
@Override
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 {
try {
getArtifactManager().delete();
} catch (InterruptedException x) {
throw new IOException(x);
}
}
/**
* Deletes this build and its entire log
*
* @throws IOException
* if we fail to delete.
*/
public void delete() throws IOException {
File rootDir = getRootDir();
if (!rootDir.isDirectory()) {
//No root directory found to delete. Somebody seems to have nuked
//it externally. Logging a warning before dropping the build
LOGGER.warning(String.format(
"%s: %s looks to have already been deleted, assuming build dir was already cleaned up",
this, rootDir
));
//Still firing the delete listeners; just no need to clean up rootDir
RunListener.fireDeleted(this);
synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
removeRunFromParent();
}
return;
}
//The root dir exists and is a directory that needs to be purged
RunListener.fireDeleted(this);
if (artifactManager != null) {
deleteArtifacts();
} // for StandardArtifactManager, deleting the whole build dir suffices
synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName());
if (tmp.exists()) {
Util.deleteRecursive(tmp);
}
try {
Files.move(
Util.fileToPath(rootDir),
Util.fileToPath(tmp),
StandardCopyOption.ATOMIC_MOVE
);
} catch (UnsupportedOperationException | SecurityException ex) {
throw new IOException(rootDir + " is in use");
}
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();
}
LOGGER.log(FINE, "{0}: {1} successfully deleted", new Object[] {this, rootDir});
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(@Nonnull CheckPoint id) {
Run,?>.RunExecution exec = RunnerStack.INSTANCE.peek();
if (exec == null) {
return;
}
exec.checkpoints.report(id);
}
/**
* @see CheckPoint#block()
*/
/*package*/ static void waitForCheckpoint(@Nonnull CheckPoint id, @CheckForNull BuildListener listener, @CheckForNull String waiter) throws InterruptedException {
while(true) {
Run,?>.RunExecution exec = RunnerStack.INSTANCE.peek();
if (exec == null) {
return;
}
Run b = exec.getBuild().getPreviousBuildInProgress();
if(b==null) return; // no pending earlier build
Run.RunExecution runner = b.runner;
if(runner==null) {
// polled at the wrong moment. try again.
Thread.sleep(0);
continue;
}
if(runner.checkpoints.waitForCheckPoint(id, listener, waiter))
return; // confirmed that the previous build reached the check point
// the previous build finished without ever reaching the check point. try again.
}
}
/**
* @deprecated as of 1.467
* Please use {@link RunExecution}
*/
@Deprecated
protected abstract class Runner extends RunExecution {}
/**
* Object that lives while the build is executed, to keep track of things that
* are needed only during the build.
*/
public abstract class RunExecution {
/**
* 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 RunExecution}s to
* coordinate and serialize their executions where necessary.
*/
private final Set checkpoints = new HashSet<>();
private boolean allDone;
protected synchronized void report(@Nonnull CheckPoint identifier) {
checkpoints.add(identifier);
notifyAll();
}
protected synchronized boolean waitForCheckPoint(@Nonnull CheckPoint identifier, @CheckForNull BuildListener listener, @CheckForNull String waiter) throws InterruptedException {
final Thread t = Thread.currentThread();
final String oldName = t.getName();
t.setName(oldName + " : waiting for " + identifier + " on " + getFullDisplayName() + " from " + waiter);
try {
boolean first = true;
while (!allDone && !checkpoints.contains(identifier)) {
if (first && listener != null && waiter != null) {
listener.getLogger().println(Messages.Run__is_waiting_for_a_checkpoint_on_(waiter, getFullDisplayName()));
}
wait();
first = false;
}
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();
private final Map