/* * 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 * * 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 jenkins.util.SystemProperties; import hudson.Util; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; import hudson.console.*; import hudson.model.Descriptor.FormException; import hudson.model.Run.RunExecution; import hudson.model.listeners.RunListener; import hudson.model.listeners.SaveableListener; import hudson.model.queue.Executables; 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.util.FormApply; import hudson.util.LogTaskListener; import hudson.util.ProcessTree; import hudson.util.XStream2; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.nio.charset.Charset; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.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.PeepholePermalink; import jenkins.model.RunAction2; import jenkins.model.StandardArtifactManager; import jenkins.model.lazy.BuildReference; import jenkins.model.lazy.LazyBuildMixIn; 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.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.*; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; /** * A particular execution of {@link Job}. * * <p> * Custom {@link Run} type is always used in conjunction with * a custom {@link Job} type, so there's no separate registration * mechanism for custom {@link Run} types. * * @author Kohsuke Kawaguchi * @see RunListener */ @ExportedBean public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,RunT>> extends Actionable implements ExtensionPoint, Comparable<RunT>, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster { /** * 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; protected transient final @Nonnull JobT project; /** * Build number. * * <p> * In earlier versions < 1.24, this number is not unique nor continuous, * but going forward, it will, and this really replaces the build id. */ public 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. Can be null. */ protected volatile String description; /** * Human-readable name of this build. Can be null. * If non-null, this text is displayed instead of "#NNN", which is the default. * @since 1.390 */ private volatile String displayName; /** * The current build state. */ private volatile transient State state; private static enum State { /** * Build is created/queued but we haven't started building it. */ NOT_STARTED, /** * Build is in progress. */ BUILDING, /** * Build is completed now, and the status is determined, * but log files are still being updated. * * 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 log entries. */ 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. * <p>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); getActions().remove(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<Action> getTransientActions() { List<Action> actions = new ArrayList<Action>(); 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. */ public int compareTo(@Nonnull RunT that) { return this.number - that.number; } /** * 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. * <p/> * 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. * * <p> * 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<BuildBadgeAction> getBadgeActions() { List<BuildBadgeAction> r = getActions(BuildBadgeAction.class); if(isKeepLog()) { 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 Executables#getExecutor */ @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.getInstance().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. * * <p> * If a build sits in the queue for a long time, multiple build requests made during this period * are all rolled up into one build, hence this method may return a list. * * @return * can be empty but never null. read-only. * @since 1.321 */ public @Nonnull List<Cause> getCauses() { CauseAction a = getAction(CauseAction.class); if (a==null) return Collections.emptyList(); return Collections.unmodifiableList(a.getCauses()); } /** * Returns a {@link Cause} of a particular type. * * @since 1.362 */ public @CheckForNull <T extends Cause> T getCause(Class<T> type) { for (Cause c : getCauses()) if (type.isInstance(c)) return type.cast(c); return null; } /** * Returns true if this log file should be kept and not deleted. * * This is used as a signal to the {@link 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 epoc. */ 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 public String getDescription() { return description; } /** * Returns the length-limited description. * @return The length-limited description. */ public @Nonnull String getTruncatedDescription() { final int maxDescrLength = 100; if (description == null || description.length() < maxDescrLength) { return description; } final String ending = "..."; final int sz = description.length(), maxTruncLength = maxDescrLength - ending.length(); boolean inTag = false; int displayChars = 0; int lastTruncatablePoint = -1; for (int i=0; i<sz; i++) { char ch = description.charAt(i); if(ch == '<') { inTag = true; } else if (ch == '>') { inTag = false; if (displayChars <= maxTruncLength) { lastTruncatablePoint = i + 1; } } if (!inTag) { displayChars++; if (displayChars <= maxTruncLength && ch == ' ') { lastTruncatablePoint = i; } } } String truncDesc = description; // Could not find a preferred truncable index, force a trunc at maxTruncLength if (lastTruncatablePoint == -1) lastTruncatablePoint = maxTruncLength; if (displayChars >= maxDescrLength) { truncDesc = truncDesc.substring(0, lastTruncatablePoint) + ending; } return truncDesc; } /** * Gets the string that says how long since this build has started. * * @return * string like "3 minutes" "1 day" etc. */ public @Nonnull String getTimestampString() { long duration = new GregorianCalendar().getTimeInMillis()-timestamp; return Util.getPastTimeString(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() { 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<RunT> createReference() { return new BuildReference<RunT>(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. * * <p> * We basically follow the existing skip list, and wherever we find a non-optimal pointer, we remember them * in 'fixUp' and update them later. */ public final @CheckForNull RunT getPreviousBuildInProgress() { if(previousBuildInProgress==this) return null; // the most common case List<RunT> fixUp = new ArrayList<RunT>(); RunT r = _this(); // 'r' is the source of the pointer (so that we can add it to fix up if we find that the target of the pointer is inefficient.) RunT answer; while (true) { RunT n = r.previousBuildInProgress; if (n==null) {// no field computed yet. n=r.getPreviousBuild(); fixUp.add(r); } if (r==n || n==null) { // this indicates that we know there's no build in progress beyond this point answer = null; break; } if (n.isBuilding()) { // we now know 'n' is the target we wanted answer = n; break; } fixUp.add(r); // r contains the stale 'previousBuildInProgress' back pointer r = n; } // fix up so that the next look up will run faster for (RunT f : fixUp) f.previousBuildInProgress = answer==null ? f : answer; return answer; } /** * Returns the last build that was actually built - i.e., skipping any with Result.NOT_BUILT */ public @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 'numberOfBuilds' builds with a build result >= 'threshold'. * * @param numberOfBuilds the desired number of builds * @param threshold the build result threshold * @return a list with the builds (youngest build first). * May be smaller than 'numberOfBuilds' or even empty * if not enough builds satisfying the threshold have been found. Never null. * @since 1.383 */ public @Nonnull List<RunT> getPreviousBuildsOverThreshold(int numberOfBuilds, @Nonnull Result threshold) { List<RunT> builds = new ArrayList<RunT>(numberOfBuilds); RunT r = getPreviousBuild(); while (r != null && builds.size() < numberOfBuilds) { if (!r.isBuilding() && (r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) { builds.add(r); } r = r.getPreviousBuild(); } return builds; } /** * @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 <b>NEVER</b> 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); } @Override public @CheckForNull Descriptor getDescriptorByName(String className) { return Jenkins.getInstance().getDescriptorByName(className); } /** * 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. * <p>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. * <p>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<Artifact> getArtifacts() { return getArtifactsUpTo(Integer.MAX_VALUE); } /** * Gets the first N artifacts. * @return The list can be empty but never null */ public @Nonnull List<Artifact> getArtifactsUpTo(int artifactsNumber) { ArtifactList r = new ArtifactList(); try { addArtifacts(getArtifactManager().root(), "", "", r, null, artifactsNumber); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); } 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 int addArtifacts(@Nonnull VirtualFile dir, @Nonnull String path, @Nonnull String pathHref, @Nonnull ArtifactList r, @Nonnull Artifact 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); Artifact a; if (collapsed) { // Collapse single items into parent node where possible: a = new Artifact(parent.getFileName() + '/' + child, childPath, sub.isDirectory() ? null : childHref, length, parent.getTreeNodeId()); r.tree.put(a, r.tree.remove(parent)); } else { // Use null href for a directory: a = new Artifact(child, childPath, sub.isDirectory() ? null : childHref, length, "n" + ++r.idSeq); r.tree.put(a, parent!=null ? parent.getTreeNodeId() : null); } if (sub.isDirectory()) { n += addArtifacts(sub, childPath + '/', childHref + '/', r, a, upTo-n); if (n>=upTo) break; } else { // Don't store collapsed path in ArrayList (for correct data in external API) r.add(collapsed ? new Artifact(child, a.relativePath, a.href, 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" public final class ArtifactList extends ArrayList<Artifact> { 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<Artifact,String> tree = new LinkedHashMap<Artifact,String>(); private int idSeq = 0; public Map<Artifact,String> getTree() { return tree; } public void computeDisplayName() { if(size()>LIST_CUTOFF) return; // we are not going to display file names, so no point in computing this int maxDepth = 0; int[] len = new int[size()]; String[][] tokens = new String[size()][]; for( int i=0; i<tokens.length; i++ ) { tokens[i] = get(i).relativePath.split("[\\\\/]+"); maxDepth = Math.max(maxDepth,tokens[i].length); len[i] = 1; } boolean collision; int depth=0; do { collision = false; Map<String,Integer/*index*/> names = new HashMap<String,Integer>(); for (int i = 0; i < tokens.length; i++) { String[] token = tokens[i]; String displayName = combineLast(token,len[i]); Integer j = names.put(displayName, i); if(j!=null) { collision = true; if(j>=0) len[j]++; len[i]++; names.put(displayName,-1); // occupy this name but don't let len[i] incremented with additional collisions } } } while(collision && depth++<maxDepth); for (int i = 0; i < tokens.length; i++) get(i).displayPath = combineLast(tokens[i],len[i]); // OUTER: // for( int n=1; n<maxLen; n++ ) { // // if we just display the last n token, would it be suffice for disambiguation? // Set<String> names = new HashSet<String>(); // for (String[] token : tokens) { // if(!names.add(combineLast(token,n))) // continue OUTER; // collision. Increase n and try again // } // // // this n successfully diambiguates // for (int i = 0; i < tokens.length; i++) { // String[] token = tokens[i]; // get(i).displayPath = combineLast(token,n); // } // return; // } // // it's impossible to get here, as that means // // we have the same artifacts archived twice, but be defensive // for (Artifact a : this) // a.displayPath = a.relativePath; } /** * Combines last N token into the "a/b/c" form. */ private String combineLast(String[] token, int n) { StringBuilder buf = new StringBuilder(); for( int i=Math.max(0,token.length-n); i<token.length; i++ ) { if(buf.length()>0) buf.append('/'); buf.append(token[i]); } return buf.toString(); } } /** * A build artifact. */ @ExportedBean public class Artifact { /** * Relative path name from 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; /*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(){ return Long.decode(length); } public String getTreeNodeId() { return treeNodeId; } @Override public String toString() { return relativePath; } } /** * Returns the log file. * @return The file may reference both uncompressed or compressed logs */ 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 FileInputStream fis = new FileInputStream(logFile); if (logFile.getName().endsWith(".gz")) { return new GZIPInputStream(fis); } else { return fis; } } 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 <tt>console.jelly</tt> to write annotated log to the given output. * * @since 1.349 */ public void writeLogTo(long offset, @Nonnull XMLOutput out) throws IOException { try { getLogText().writeHtmlTo(offset,out.asWriter()); } catch (IOException e) { // try to fall back to the old getLogInputStream() // mainly to support .gz compressed files // In this case, console annotation handling will be turned off. InputStream input = getLogInputStream(); try { IOUtils.copy(input, out.asWriter()); } finally { IOUtils.closeQuietly(input); } } } /** * 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. * <p/> * 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 void checkPermission(@Nonnull Permission p) { getACL().checkPermission(p); } @Override public boolean hasPermission(@Nonnull Permission p) { return getACL().hasPermission(p); } @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()) { throw new IOException(this + ": " + rootDir + " looks to have already been deleted; siblings: " + Arrays.toString(project.getBuildDir().list())); } RunListener.fireDeleted(this); 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); } // TODO on Java 7 prefer: Files.move(rootDir.toPath(), tmp.toPath(), StandardCopyOption.ATOMIC_MOVE) boolean renamingSucceeded = rootDir.renameTo(tmp); Util.deleteRecursive(tmp); // some user reported that they see some left-over .xyz files in the workspace, // so just to make sure we've really deleted it, schedule the deletion on VM exit, too. if(tmp.exists()) tmp.deleteOnExit(); if(!renamingSucceeded) throw new IOException(rootDir+" is in use"); 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<CheckPoint> checkpoints = new HashSet<CheckPoint>(); 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<Object,Object> attributes = new HashMap<Object, Object>(); /** * 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. * <p> * This method is called after {@linkplain #run(BuildListener) the main portion of the build is completed.} * This is a good opportunity to do notifications based on the result * of the build. When this method is called, the build is not really * finalized yet, and the build is still considered in progress --- for example, * even if the build is successful, this build still won't be picked up * by {@link Job#getLastSuccessfulBuild()}. */ public abstract void post(@Nonnull BuildListener listener ) throws Exception; /** * Performs final clean up action. * <p> * This method is called after {@link #post(BuildListener)}, * after the build result is fully finalized. This is the point * where the build is already considered completed. * <p> * Among other things, this is often a necessary pre-condition * before invoking other builds that depend on this build. */ public abstract void cleanUp(@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<Object,Object> 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(RunExecution)} */ @Deprecated protected final void run(@Nonnull Runner job) { execute(job); } protected final void execute(@Nonnull RunExecution job) { if(result!=null) return; // already built. StreamBuildListener listener=null; runner = job; onStartBuilding(); try { // to set the state to COMPLETE in the end, even if the thread dies abnormally. // otherwise the queue state becomes inconsistent long start = System.currentTimeMillis(); try { try { Computer computer = Computer.currentComputer(); Charset charset = null; if (computer != null) { charset = computer.getDefaultCharset(); this.charset = charset.name(); } listener = createBuildListener(job, listener, charset); listener.started(getCauses()); Authentication auth = Jenkins.getAuthentication(); if (!auth.equals(ACL.SYSTEM)) { String name = auth.getName(); if (!auth.equals(Jenkins.ANONYMOUS)) { name = ModelHyperlinkNote.encodeTo(User.get(name)); } listener.getLogger().println(Messages.Run_running_as_(name)); } RunListener.fireStarted(this,listener); updateSymlinks(listener); setResult(job.run(listener)); LOGGER.log(INFO, "{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(); } } private StreamBuildListener createBuildListener(@Nonnull RunExecution job, StreamBuildListener listener, Charset charset) throws IOException, InterruptedException { // 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 OutputStream logger = new FileOutputStream(getLogFile(), true); 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); } } listener = new StreamBuildListener(logger,charset); return listener; } /** * Makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist. * Normally you do not need to call this explicitly, since {@link #execute} does so, * but this may be needed if you are creating synthetic {@link Run}s as part of a container project (such as Maven builds in a module set). * You should also ensure that {@link RunListener#fireStarted} and {@link RunListener#fireCompleted} are called. * @param listener probably unused * @throws InterruptedException probably not thrown * @since 1.530 */ public final void updateSymlinks(@Nonnull TaskListener listener) throws InterruptedException { createSymlink(listener, "lastSuccessful", PermalinkProjectAction.Permalink.LAST_SUCCESSFUL_BUILD); createSymlink(listener, "lastStable", PermalinkProjectAction.Permalink.LAST_STABLE_BUILD); } /** * Backward compatibility. * * We used to have $JENKINS_HOME/jobs/JOBNAME/lastStable and lastSuccessful symlinked to the appropriate * builds, but now those are done in {@link PeepholePermalink}. So here, we simply create symlinks that * resolves to the symlink created by {@link PeepholePermalink}. */ private void createSymlink(@Nonnull TaskListener listener, @Nonnull String name, @Nonnull PermalinkProjectAction.Permalink target) throws InterruptedException { File buildDir = getParent().getBuildDir(); File rootDir = getParent().getRootDir(); String targetDir; if (buildDir.equals(new File(rootDir, "builds"))) { targetDir = "builds" + File.separator + target.getId(); } else { targetDir = buildDir + File.separator + target.getId(); } Util.createSymlink(rootDir, targetDir, name, listener); } /** * 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); e.printStackTrace(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")); } /** * 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<String> getLog(int maxLines) throws IOException { int lineCount = 0; List<String> logLines = new LinkedList<String>(); if (maxLines == 0) { return logLines; } BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(getLogFile()),getCharset())); try { for (String line = reader.readLine(); line != null; line = reader.readLine()) { logLines.add(line); ++lineCount; // If we have too many lines, remove the oldest line. This way we // never have to hold the full contents of a huge log file in memory. // Adding to and removing from the ends of a linked list are cheap // operations. if (lineCount > maxLines) logLines.remove(0); } } finally { reader.close(); } // If the log has been truncated, include that information. // Use set (replaces the first element) rather than add so that // the list doesn't grow beyond the specified maximum number of lines. if (lineCount > maxLines) logLines.set(0, "[...truncated " + (lineCount - (maxLines - 1)) + " lines...]"); return ConsoleNote.removeNotes(logLines); } public void doBuildStatus( StaplerRequest req, StaplerResponse rsp ) throws IOException { rsp.sendRedirect2(req.getContextPath()+"/images/48x48/"+getBuildStatusUrl()); } public @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"); PlainTextConsoleOutputStream out = new PlainTextConsoleOutputStream(rsp.getCompressedOutputStream(req)); InputStream input = getLogInputStream(); try { IOUtils.copy(input, out); out.flush(); } finally { IOUtils.closeQuietly(input); IOUtils.closeQuietly(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; } public void doToggleLogKeep( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { keepLog(!keepLog); rsp.forwardToPreviousPage(req); } /** * Marks this build to keep the log. */ @CLIMethod(name="keep-build") public final void keepLog() throws IOException { keepLog(true); } public void keepLog(boolean newValue) throws IOException { checkPermission(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){ StringWriter writer = new StringWriter(); ex.printStackTrace(new PrintWriter(writer)); req.setAttribute("stackTraces", writer); 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. */ 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<String,String> 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 e) { return new EnvVars(); } catch (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. * * <p> * {@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. * * <p> * 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); 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.getInstance(); 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(); } @RequirePOST 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<Run> ORDER_BY_DATE = new Comparator<Run>() { public int compare(@Nonnull Run lhs, @Nonnull Run rhs) { long lt = lhs.getTimeInMillis(); long rt = rhs.getTimeInMillis(); if(lt>rt) return -1; if(lt<rt) return 1; return 0; } }; /** * {@link FeedAdapter} to produce feed from the summary of this build. */ public static final FeedAdapter<Run> FEED_ADAPTER = new DefaultFeedAdapter(); /** * {@link FeedAdapter} to produce feeds to show one build per project. */ public static final FeedAdapter<Run> FEED_ADAPTER_LATEST = new DefaultFeedAdapter() { /** * The entry unique ID needs to be tied to a project, so that * new builds will replace the old result. */ @Override public String getEntryID(Run e) { // can't use a meaningful year field unless we remember when the job was created. return "tag:hudson.dev.java.net,2008:"+e.getParent().getAbsoluteUrl(); } }; /** * {@link BuildBadgeAction} that shows the logs are 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<Run> { public String getEntryTitle(Run entry) { return entry+" ("+entry.getBuildStatusSummary().message+")"; } public String getEntryUrl(Run entry) { return entry.getUrl(); } public String getEntryID(Run entry) { return "tag:" + "hudson.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; } public static class RedirectUp { public void doDynamic(StaplerResponse rsp) throws IOException { // Compromise to handle both browsers (auto-redirect) and programmatic access // (want accurate 404 response).. send 404 with javscript to redirect browsers. rsp.setStatus(HttpServletResponse.SC_NOT_FOUND); rsp.setContentType("text/html;charset=UTF-8"); PrintWriter out = rsp.getWriter(); out.println("<html><head>" + "<meta http-equiv='refresh' content='1;url=..'/>" + "<script>window.location.replace('..');</script>" + "</head>" + "<body style='background-color:white; color:white;'>" + "Not found</body></html>"); out.flush(); } } }