/* * 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 attributes = new HashMap<>(); /** * 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 @Nonnull Result run(@Nonnull BuildListener listener ) throws Exception, RunnerAbortedException; /** * Performs the post-build action. *

* 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(@Nonnull BuildListener listener ) throws Exception; /** * Performs final clean up action. *

* This method is called after {@link #post(BuildListener)}, * after the build result is fully finalized. This is the point * where the build is already considered completed. *

* Among other things, this is often a necessary pre-condition * before invoking other builds that depend on this build. */ public abstract void cleanUp(@Nonnull BuildListener listener) throws Exception; public @Nonnull RunT getBuild() { return _this(); } public @Nonnull JobT getProject() { return _this().getParent(); } /** * Bag of stuff to allow plugins to store state for the duration of a build * without persisting it. * * @since 1.473 */ public @Nonnull Map getAttributes() { return attributes; } } /** * Used in {@link Run.RunExecution#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 { private static final long serialVersionUID = 1L; } /** * @deprecated as of 1.467 * Use {@link #execute(hudson.model.Run.RunExecution)} */ @Deprecated protected final void run(@Nonnull Runner job) { execute(job); } protected final void execute(@Nonnull RunExecution job) { if(result!=null) return; // already built. OutputStream logger = null; 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(); try { try { Computer computer = Computer.currentComputer(); Charset charset = null; if (computer != null) { charset = computer.getDefaultCharset(); this.charset = charset.name(); } logger = createLogger(); listener = createBuildListener(job, logger, charset); listener.started(getCauses()); Authentication auth = Jenkins.getAuthentication(); if (auth.equals(ACL.SYSTEM)) { listener.getLogger().println(Messages.Run_running_as_SYSTEM()); } else { String id = auth.getName(); if (!auth.equals(Jenkins.ANONYMOUS)) { final User usr = User.getById(id, false); if (usr != null) { // Encode user hyperlink for existing users id = ModelHyperlinkNote.encodeTo(usr); } } listener.getLogger().println(Messages.Run_running_as_(id)); } RunListener.fireStarted(this,listener); setResult(job.run(listener)); LOGGER.log(FINEST, "{0} main build action completed: {1}", new Object[] {this, result}); CheckPoint.MAIN_COMPLETED.report(); } catch (ThreadDeath t) { throw t; } catch( AbortException e ) {// orderly abortion. result = Result.FAILURE; listener.error(e.getMessage()); LOGGER.log(FINE, "Build "+this+" aborted",e); } catch( RunnerAbortedException e ) {// orderly abortion. result = Result.FAILURE; LOGGER.log(FINE, "Build "+this+" aborted",e); } catch( InterruptedException e) { // aborted result = Executor.currentExecutor().abortResult(); listener.getLogger().println(Messages.Run_BuildAborted()); Executor.currentExecutor().recordCauseOfInterruption(Run.this,listener); LOGGER.log(Level.INFO, this + " aborted", e); } catch( Throwable e ) { handleFatalBuildProblem(listener,e); result = Result.FAILURE; } // even if the main build fails fatally, try to run post build processing job.post(listener); } catch (ThreadDeath t) { throw t; } catch( Throwable e ) { handleFatalBuildProblem(listener,e); result = Result.FAILURE; } finally { long end = System.currentTimeMillis(); duration = Math.max(end - start, 0); // @see HUDSON-5844 // advance the state. // the significance of doing this is that Jenkins // will now see this build as completed. // things like triggering other builds requires this as pre-condition. // see issue #980. LOGGER.log(FINER, "moving into POST_PRODUCTION on {0}", this); state = State.POST_PRODUCTION; if (listener != null) { RunListener.fireCompleted(this,listener); try { job.cleanUp(listener); } catch (Exception e) { handleFatalBuildProblem(listener,e); // too late to update the result now } listener.finished(result); listener.closeQuietly(); } try { save(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to save build record",e); } } try { getParent().logRotate(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to rotate log",e); } } finally { onEndBuilding(); if (logger != null) { try { logger.close(); } catch (IOException x) { LOGGER.log(Level.WARNING, "failed to close log for " + Run.this, x); } } } } private OutputStream createLogger() throws IOException { // 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 try { return Files.newOutputStream(getLogFile().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (InvalidPathException e) { throw new IOException(e); } } private StreamBuildListener createBuildListener(@Nonnull RunExecution job, OutputStream logger, Charset charset) throws IOException, InterruptedException { RunT build = job.getBuild(); // Global log filters for (ConsoleLogFilter filter : ConsoleLogFilter.all()) { logger = filter.decorateLogger(build, logger); } // Project specific log filters if (project instanceof BuildableItemWithBuildWrappers && build instanceof AbstractBuild) { BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; for (BuildWrapper bw : biwbw.getBuildWrappersList()) { logger = bw.decorateLogger((AbstractBuild) build, logger); } } return new StreamBuildListener(logger,charset); } /** * @deprecated After JENKINS-37862 this no longer does anything. */ @Deprecated public final void updateSymlinks(@Nonnull TaskListener listener) throws InterruptedException {} /** * Handles a fatal build problem (exception) that occurred during the build. */ private void handleFatalBuildProblem(@Nonnull BuildListener listener, @Nonnull Throwable e) { if(listener!=null) { LOGGER.log(FINE, getDisplayName()+" failed to build",e); if(e instanceof IOException) Util.displayIOException((IOException)e,listener); Functions.printStackTrace(e, listener.fatalError(e.getMessage())); } else { LOGGER.log(SEVERE, getDisplayName()+" failed to build and we don't even have a listener",e); } } /** * Called when a job started building. */ protected void onStartBuilding() { LOGGER.log(FINER, "moving to BUILDING on {0}", this); state = State.BUILDING; startTime = System.currentTimeMillis(); if (runner!=null) RunnerStack.INSTANCE.push(runner); RunListener.fireInitialize(this); } /** * Called when a job finished building normally or abnormally. */ protected void onEndBuilding() { // signal that we've finished building. state = State.COMPLETED; LOGGER.log(FINER, "moving to COMPLETED on {0}", this); if (runner!=null) { // MavenBuilds may be created without their corresponding runners. runner.checkpoints.allDone(); runner = null; RunnerStack.INSTANCE.pop(); } if (result == null) { result = Result.FAILURE; LOGGER.log(WARNING, "{0}: No build result is set, so marking as failure. This should not happen.", this); } RunListener.fireFinalized(this); } /** * Save the settings to a file. */ public synchronized void save() throws IOException { if(BulkChange.contains(this)) return; getDataFile().write(this); SaveableListener.fireOnChange(this, getDataFile()); } private @Nonnull XmlFile getDataFile() { return new XmlFile(XSTREAM,new File(getRootDir(),"build.xml")); } private Object writeReplace() { return XmlFile.replaceIfNotAtTopLevel(this, () -> new Replacer(this)); } private static class Replacer { private final String id; Replacer(Run r) { id = r.getExternalizableId(); } private Object readResolve() { return fromExternalizableId(id); } } /** * Gets the log of the build as a string. * @return Returns the log or an empty string if it has not been found * @deprecated since 2007-11-11. * Use {@link #getLog(int)} instead as it avoids loading * the whole log into memory unnecessarily. */ @Deprecated public @Nonnull 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 @Nonnull List getLog(int maxLines) throws IOException { if (maxLines == 0) { return Collections.emptyList(); } int lines = 0; long filePointer; final List lastLines = new ArrayList<>(Math.min(maxLines, 128)); final List bytes = new ArrayList<>(); try (RandomAccessFile fileHandler = new RandomAccessFile(getLogFile(), "r")) { long fileLength = fileHandler.length() - 1; for (filePointer = fileLength; filePointer != -1 && maxLines != lines; filePointer--) { fileHandler.seek(filePointer); byte readByte = fileHandler.readByte(); if (readByte == 0x0A) { if (filePointer < fileLength) { lines = lines + 1; lastLines.add(convertBytesToString(bytes)); bytes.clear(); } } else if (readByte != 0xD) { bytes.add(readByte); } } } if (lines != maxLines) { lastLines.add(convertBytesToString(bytes)); } Collections.reverse(lastLines); // 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 (lines == maxLines) { lastLines.set(0, "[...truncated " + Functions.humanReadableByteSize(filePointer)+ "...]"); } return ConsoleNote.removeNotes(lastLines); } private String convertBytesToString(List bytes) { Collections.reverse(bytes); Byte[] byteArray = bytes.toArray(new Byte[0]); return new String(ArrayUtils.toPrimitive(byteArray), getCharset()); } public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { rsp.sendRedirect2(req.getContextPath()+"/images/48x48/"+getBuildStatusUrl()); } public @Nonnull String getBuildStatusUrl() { return getIconColor().getImage(); } public String getBuildStatusIconClassName() { return getIconColor().getIconClassName(); } public static class Summary { /** * Is this build worse or better, compared to the previous build? */ public boolean isWorse; public String message; public Summary(boolean worse, String message) { this.isWorse = worse; this.message = message; } } /** * Used to implement {@link #getBuildStatusSummary}. * @since 1.575 */ public static abstract class StatusSummarizer implements ExtensionPoint { /** * Possibly summarizes the reasons for a build’s status. * @param run a completed build * @param trend the result of {@link ResultTrend#getResultTrend(hudson.model.Run)} on {@code run} (precomputed for efficiency) * @return a summary, or null to fall back to other summarizers or built-in behavior */ public abstract @CheckForNull Summary summarize(@Nonnull Run run, @Nonnull ResultTrend trend); } /** * Gets an object which represents the single line summary of the status of this build * (especially in comparison with the previous build.) * @see StatusSummarizer */ public @Nonnull Summary getBuildStatusSummary() { if (isBuilding()) { return new Summary(false, Messages.Run_Summary_Unknown()); } ResultTrend trend = ResultTrend.getResultTrend(this); for (StatusSummarizer summarizer : ExtensionList.lookup(StatusSummarizer.class)) { Summary summary = summarizer.summarize(this, trend); if (summary != null) { return summary; } } switch (trend) { case ABORTED : return new Summary(false, Messages.Run_Summary_Aborted()); case NOT_BUILT : return new Summary(false, Messages.Run_Summary_NotBuilt()); case FAILURE : return new Summary(true, Messages.Run_Summary_BrokenSinceThisBuild()); case STILL_FAILING : RunT since = getPreviousNotFailedBuild(); if(since==null) return new Summary(false, Messages.Run_Summary_BrokenForALongTime()); RunT failedBuild = since.getNextBuild(); return new Summary(false, Messages.Run_Summary_BrokenSince(failedBuild.getDisplayName())); case NOW_UNSTABLE: case STILL_UNSTABLE : return new Summary(false, Messages.Run_Summary_Unstable()); case UNSTABLE : return new Summary(true, Messages.Run_Summary_Unstable()); case SUCCESS : return new Summary(false, Messages.Run_Summary_Stable()); case FIXED : return new Summary(false, Messages.Run_Summary_BackToNormal()); } return new Summary(false, Messages.Run_Summary_Unknown()); } /** * Serves the artifacts. * @throws AccessDeniedException Access denied */ public @Nonnull DirectoryBrowserSupport doArtifact() { if(Functions.isArtifactsPermissionEnabled()) { checkPermission(ARTIFACTS); } return new DirectoryBrowserSupport(this, getArtifactManager().root(), Messages.Run_ArtifactsBrowserTitle(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"); try (InputStream input = getLogInputStream(); OutputStream os = rsp.getCompressedOutputStream(req); PlainTextConsoleOutputStream out = new PlainTextConsoleOutputStream(os)) { IOUtils.copy(input, out); } } /** * Handles incremental log output. * @deprecated as of 1.352 * Use {@code getLogText().doProgressiveText(req,rsp)} */ @Deprecated public void doProgressiveLog( StaplerRequest req, StaplerResponse rsp) throws IOException { getLogText().doProgressText(req,rsp); } /** * Checks whether keep status can be toggled. * Normally it can, but if there is a complex reason (from subclasses) why this build must be kept, the toggle is meaningless. * @return true if {@link #doToggleLogKeep} and {@link #keepLog(boolean)} and {@link #keepLog()} are options * @since 1.510 */ public boolean canToggleLogKeep() { if (!keepLog && isKeepLog()) { // Definitely prevented. return false; } // TODO may be that keepLog is on (perhaps toggler earlier) yet isKeepLog() would be true anyway. // In such a case this will incorrectly return true and logKeep.jelly will allow the toggle. // However at least then (after redirecting to the same page) the toggle button will correctly disappear. return true; } @RequirePOST public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { keepLog(!keepLog); rsp.forwardToPreviousPage(req); } /** * Marks this build to be kept. */ @CLIMethod(name="keep-build") public final void keepLog() throws IOException { keepLog(true); } public void keepLog(boolean newValue) throws IOException { checkPermission(newValue ? UPDATE : DELETE); keepLog = newValue; save(); } /** * Deletes the build when the button is pressed. */ @RequirePOST public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { checkPermission(DELETE); // We should not simply delete the build if it has been explicitly // marked to be preserved, or if the build should not be deleted // due to dependencies! String why = getWhyKeepLog(); if (why!=null) { sendError(Messages.Run_UnableToDelete(getFullDisplayName(), why), req, rsp); return; } try{ delete(); } catch(IOException ex){ req.setAttribute("stackTraces", Functions.printThrowable(ex)); req.getView(this, "delete-retry.jelly").forward(req, rsp); return; } rsp.sendRedirect2(req.getContextPath()+'/' + getParent().getUrl()); } public void setDescription(String description) throws IOException { checkPermission(UPDATE); this.description = description; save(); } /** * Accepts the new description. */ @RequirePOST 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(TaskListener)} instead. */ @Deprecated public Map getEnvVars() { LOGGER.log(WARNING, "deprecated call to Run.getEnvVars\n\tat {0}", new Throwable().getStackTrace()[1]); try { return getEnvironment(new LogTaskListener(LOGGER, Level.INFO)); } catch (IOException | InterruptedException e) { return new EnvVars(); } } /** * @deprecated as of 1.305 use {@link #getEnvironment(TaskListener)} */ @Deprecated public EnvVars getEnvironment() throws IOException, InterruptedException { LOGGER.log(WARNING, "deprecated call to Run.getEnvironment\n\tat {0}", new Throwable().getStackTrace()[1]); return getEnvironment(new LogTaskListener(LOGGER, Level.INFO)); } /** * Returns the map that contains environmental variables to be used for launching * processes for this build. * *

* {@link hudson.tasks.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. * *

* Unlike earlier {@link #getEnvVars()}, this map contains the whole environment, * not just the overrides, so one can introspect values to change its behavior. * * @return the map with the environmental variables. * @since 1.305 */ public @Nonnull EnvVars getEnvironment(@Nonnull TaskListener listener) throws IOException, InterruptedException { Computer c = Computer.currentComputer(); Node n = c==null ? null : c.getNode(); EnvVars env = getParent().getEnvironment(n,listener); env.putAll(getCharacteristicEnvVars()); // apply them in a reverse order so that higher ordinal ones can modify values added by lower ordinal ones for (EnvironmentContributor ec : EnvironmentContributor.all().reverseView()) ec.buildEnvironmentFor(this,env,listener); if (!(this instanceof AbstractBuild)) { for (EnvironmentContributingAction a : getActions(EnvironmentContributingAction.class)) { a.buildEnvironment(this, env); } } // else for compatibility reasons, handled in override after buildEnvironments 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 @Nonnull final EnvVars getCharacteristicEnvVars() { EnvVars env = getParent().getCharacteristicEnvVars(); env.put("BUILD_NUMBER",String.valueOf(number)); env.put("BUILD_ID",getId()); env.put("BUILD_TAG","jenkins-"+getParent().getFullName().replace('/', '-')+"-"+number); return env; } /** * Produces an identifier for this run unique in the system. * @return the {@link Job#getFullName}, then {@code #}, then {@link #getNumber} * @see #fromExternalizableId */ public @Nonnull String getExternalizableId() { return project.getFullName() + "#" + getNumber(); } /** * Tries to find a run from an persisted identifier. * @param id as produced by {@link #getExternalizableId} * @return the same run, or null if the job or run was not found * @throws IllegalArgumentException if the ID is malformed */ public @CheckForNull static Run fromExternalizableId(String id) throws IllegalArgumentException { int hash = id.lastIndexOf('#'); if (hash <= 0) { throw new IllegalArgumentException("Invalid id"); } String jobName = id.substring(0, hash); int number; try { number = Integer.parseInt(id.substring(hash + 1)); } catch (NumberFormatException x) { throw new IllegalArgumentException(x); } Jenkins j = Jenkins.getInstanceOrNull(); if (j == null) { return null; } Job job = j.getItemByFullName(jobName, Job.class); if (job == null) { return null; } 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 */ @Exported public long getEstimatedDuration() { return project.getEstimatedDuration(); } @POST public @Nonnull HttpResponse doConfigSubmit( StaplerRequest req ) throws IOException, ServletException, FormException { checkPermission(UPDATE); try (BulkChange bc = new BulkChange(this)) { JSONObject json = req.getSubmittedForm(); submit(json); bc.commit(); } return FormApply.success("."); } protected void submit(JSONObject json) throws IOException { setDisplayName(Util.fixEmptyAndTrim(json.getString("displayName"))); setDescription(json.getString("description")); } public static final XStream XSTREAM = new XStream2(); /** * Alias to {@link #XSTREAM} so that one can access additional methods on {@link XStream2} more easily. */ public static final XStream2 XSTREAM2 = (XStream2)XSTREAM; static { XSTREAM.alias("build",FreeStyleBuild.class); XSTREAM.registerConverter(Result.conv); } private static final Logger LOGGER = Logger.getLogger(Run.class.getName()); /** * Sort by date. Newer ones first. */ public static final Comparator ORDER_BY_DATE = new Comparator() { public int compare(@Nonnull Run lhs, @Nonnull Run rhs) { long lt = lhs.getTimeInMillis(); long rt = rhs.getTimeInMillis(); return Long.compare(rt, lt); } }; /** * {@link FeedAdapter} to produce feed from the summary of this build. */ public static final FeedAdapter FEED_ADAPTER = new DefaultFeedAdapter(); /** * {@link FeedAdapter} to produce feeds to show one build per project. */ public static final FeedAdapter 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.dev.java.net,2008:"+e.getParent().getAbsoluteUrl(); } }; /** * {@link BuildBadgeAction} that shows the build is being kept. */ public final class KeepLogBuildBadge implements BuildBadgeAction { public @CheckForNull String getIconFileName() { return null; } public @CheckForNull String getDisplayName() { return null; } public @CheckForNull String getUrlName() { return null; } public @CheckForNull 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, PermissionScope.RUN); public static final Permission UPDATE = new Permission(PERMISSIONS,"Update",Messages._Run_UpdatePermission_Description(),Permission.UPDATE, PermissionScope.RUN); /** See {@link hudson.Functions#isArtifactsPermissionEnabled} */ public static final Permission ARTIFACTS = new Permission(PERMISSIONS,"Artifacts",Messages._Run_ArtifactsPermission_Description(), null, Functions.isArtifactsPermissionEnabled(), new PermissionScope[]{PermissionScope.RUN}); private static class DefaultFeedAdapter implements FeedAdapter { public String getEntryTitle(Run entry) { return entry.getFullDisplayName()+" ("+entry.getBuildStatusSummary().message+")"; } public String getEntryUrl(Run entry) { return entry.getUrl(); } public String getEntryID(Run entry) { return "tag:" + "hudson.dev.java.net," + entry.getTimestamp().get(Calendar.YEAR) + ":" + entry.getParent().getFullName()+':'+entry.getId(); } public String getEntryDescription(Run entry) { return entry.getDescription(); } public Calendar getEntryTimestamp(Run entry) { return entry.getTimestamp(); } public String getEntryAuthor(Run entry) { return JenkinsLocationConfiguration.get().getAdminAddress(); } } @Override public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { Object returnedResult = super.getDynamic(token, req, rsp); if (returnedResult == null){ //check transient actions too for(Action action: getTransientActions()){ String urlName = action.getUrlName(); if (urlName == null) { continue; } if (urlName.equals(token)) { return action; } } // 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. returnedResult = new RedirectUp(); } return returnedResult; } @Override @Restricted(NoExternalUse.class) public Object getTarget() { if (!SKIP_PERMISSION_CHECK) { // This is a bit weird, but while the Run's PermissionScope does not have READ, delegate to the parent if (!getParent().hasPermission(Item.DISCOVER)) { return null; } getParent().checkPermission(Item.READ); } return this; } /** * Escape hatch for StaplerProxy-based access control */ @Restricted(NoExternalUse.class) public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(Run.class.getName() + ".skipPermissionCheck"); 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 javascript to redirect browsers. rsp.setStatus(HttpServletResponse.SC_NOT_FOUND); rsp.setContentType("text/html;charset=UTF-8"); PrintWriter out = rsp.getWriter(); out.println("" + "" + "" + "" + "" + "Not found"); out.flush(); } } }