/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., CloudBees, Inc. * * 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 hudson.AbortException; import hudson.EnvVars; import hudson.Functions; import hudson.Launcher; import hudson.Util; import hudson.FilePath; import hudson.console.AnnotatedLargeText; import hudson.console.ExpandableDetailsNote; import hudson.slaves.WorkspaceList; import hudson.slaves.NodeProperty; import hudson.slaves.WorkspaceList.Lease; import hudson.matrix.MatrixConfiguration; import hudson.model.Fingerprint.BuildPtr; import hudson.model.Fingerprint.RangeSet; import hudson.model.listeners.SCMListener; import hudson.scm.ChangeLogParser; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.SCM; import hudson.scm.NullChangeLogParser; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.Fingerprinter.FingerprintAction; import hudson.tasks.Publisher; import hudson.tasks.BuildStepMonitor; import hudson.tasks.BuildTrigger; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.AdaptedIterator; import hudson.util.Iterators; import hudson.util.LogTaskListener; import hudson.util.VariableResolver; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.xml.sax.SAXException; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Base implementation of {@link Run}s that build software. * * For now this is primarily the common part of {@link Build} and MavenBuild. * * @author Kohsuke Kawaguchi * @see AbstractProject */ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Run<P,R> implements Queue.Executable { /** * Set if we want the blame information to flow from upstream to downstream build. */ private static final boolean upstreamCulprits = Boolean.getBoolean("hudson.upstreamCulprits"); /** * Name of the slave this project was built on. * Null or "" if built by the master. (null happens when we read old record that didn't have this information.) */ private String builtOn; /** * The file path on the node that performed a build. Kept as a string since {@link FilePath} is not serializable into XML. * @since 1.319 */ private String workspace; /** * Version of Hudson that built this. */ private String hudsonVersion; /** * SCM used for this build. * Maybe null, for historical reason, in which case CVS is assumed. */ private ChangeLogParser scm; /** * Changes in this build. */ private volatile transient ChangeLogSet<? extends Entry> changeSet; /** * Cumulative list of people who contributed to the build problem. * * <p> * This is a list of {@link User#getId() user ids} who made a change * since the last non-broken build. Can be null (which should be * treated like empty set), because of the compatibility. * * <p> * This field is semi-final --- once set the value will never be modified. * * @since 1.137 */ private volatile Set<String> culprits; /** * During the build this field remembers {@link BuildWrapper.Environment}s created by * {@link BuildWrapper}. This design is bit ugly but forced due to compatibility. */ protected transient List<Environment> buildEnvironments; protected AbstractBuild(P job) throws IOException { super(job); } protected AbstractBuild(P job, Calendar timestamp) { super(job, timestamp); } protected AbstractBuild(P project, File buildDir) throws IOException { super(project, buildDir); } public final P getProject() { return getParent(); } /** * Returns a {@link Slave} on which this build was done. * * @return * null, for example if the slave that this build run no longer exists. */ public Node getBuiltOn() { if (builtOn==null || builtOn.equals("")) return Hudson.getInstance(); else return Hudson.getInstance().getNode(builtOn); } /** * Returns the name of the slave it was built on; null or "" if built by the master. * (null happens when we read old record that didn't have this information.) */ @Exported(name="builtOn") public String getBuiltOnStr() { return builtOn; } /** * Used to render the side panel "Back to project" link. * * <p> * In a rare situation where a build can be reached from multiple paths, * returning different URLs from this method based on situations might * be desirable. * * <p> * If you override this method, you'll most likely also want to override * {@link #getDisplayName()}. */ public String getUpUrl() { return Functions.getNearestAncestorUrl(Stapler.getCurrentRequest(),getParent())+'/'; } /** * Gets the directory where this build is being built. * * <p> * Note to implementors: to control where the workspace is created, override * {@link AbstractRunner#decideWorkspace(Node,WorkspaceList)}. * * @return * null if the workspace is on a slave that's not connected. Note that once the build is completed, * the workspace may be used to build something else, so the value returned from this method may * no longer show a workspace as it was used for this build. * @since 1.319 */ public final FilePath getWorkspace() { if (workspace==null) return null; Node n = getBuiltOn(); if (n==null) return null; return n.createPath(workspace); } /** * Normally, a workspace is assigned by {@link Runner}, but this lets you set the workspace in case * {@link AbstractBuild} is created without a build. */ protected void setWorkspace(FilePath ws) { this.workspace = ws.getRemote(); } /** * Returns the root directory of the checked-out module. * <p> * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt> * and so on exists. */ public final FilePath getModuleRoot() { FilePath ws = getWorkspace(); if (ws==null) return null; return getParent().getScm().getModuleRoot(ws,this); } /** * Returns the root directories of all checked-out modules. * <p> * Some SCMs support checking out multiple modules into the same workspace. * In these cases, the returned array will have a length greater than one. * @return The roots of all modules checked out from the SCM. */ public FilePath[] getModuleRoots() { FilePath ws = getWorkspace(); if (ws==null) return null; return getParent().getScm().getModuleRoots(ws,this); } /** * List of users who committed a change since the last non-broken build till now. * * <p> * This list at least always include people who made changes in this build, but * if the previous build was a failure it also includes the culprit list from there. * Culprits of unstable build are also included * see <a href="http://issues.hudson-ci.org/browse/HUDSON-4617">HUDSON-4617</a> for details * @return * can be empty but never null. */ @Exported public Set<User> getCulprits() { if (culprits==null) { Set<User> r = new HashSet<User>(); R p = getPreviousCompletedBuild(); if (p !=null && isBuilding()) { Result pr = p.getResult(); if (pr!=null && pr.isWorseOrEqualTo(Result.UNSTABLE)) { // we are still building, so this is just the current latest information, // but we seems to be failing so far, so inherit culprits from the previous build. // isBuilding() check is to avoid recursion when loading data from old Hudson, which doesn't record // this information r.addAll(p.getCulprits()); } } for (Entry e : getChangeSet()) r.add(e.getAuthor()); if (upstreamCulprits) { // If we have dependencies since the last successful build, add their authors to our list R previousBuild = getPreviousSuccessfulBuild(); if (previousBuild != null) { Map <AbstractProject,AbstractBuild.DependencyChange> depmap = getDependencyChanges(previousBuild); for (AbstractBuild.DependencyChange dep : depmap.values()) { for (AbstractBuild<?,?> b : dep.getBuilds()) { for (Entry entry : b.getChangeSet()) { r.add(entry.getAuthor()); } } } } } return r; } return new AbstractSet<User>() { public Iterator<User> iterator() { return new AdaptedIterator<String,User>(culprits.iterator()) { protected User adapt(String id) { return User.get(id); } }; } public int size() { return culprits.size(); } }; } /** * Returns true if this user has made a commit to this build. * * @since 1.191 */ public boolean hasParticipant(User user) { for (ChangeLogSet.Entry e : getChangeSet()) if (e.getAuthor()==user) return true; return false; } /** * Gets the version of Hudson that was used to build this job. * * @since 1.246 */ public String getHudsonVersion() { return hudsonVersion; } @Override public synchronized void delete() throws IOException { // Need to check if deleting this build affects lastSuccessful/lastStable symlinks R lastSuccessful = getProject().getLastSuccessfulBuild(), lastStable = getProject().getLastStableBuild(); super.delete(); try { if (lastSuccessful == this) updateSymlink("lastSuccessful", getProject().getLastSuccessfulBuild()); if (lastStable == this) updateSymlink("lastStable", getProject().getLastStableBuild()); } catch (InterruptedException ex) { LOGGER.warning("Interrupted update of lastSuccessful/lastStable symlinks for " + getProject().getDisplayName()); // handle it later Thread.currentThread().interrupt(); } } private void updateSymlink(String name, AbstractBuild<?,?> newTarget) throws InterruptedException { if (newTarget != null) newTarget.createSymlink(new LogTaskListener(LOGGER, Level.WARNING), name); else new File(getProject().getBuildDir(), "../"+name).delete(); } private void createSymlink(TaskListener listener, String name) throws InterruptedException { Util.createSymlink(getProject().getBuildDir(),"builds/"+getId(),"../"+name,listener); } protected abstract class AbstractRunner extends Runner { /** * Since configuration can be changed while a build is in progress, * create a launcher once and stick to it for the entire build duration. */ protected Launcher launcher; /** * Output/progress of this build goes here. */ protected BuildListener listener; /** * Returns the current {@link Node} on which we are buildling. */ protected final Node getCurrentNode() { return Executor.currentExecutor().getOwner().getNode(); } /** * Allocates the workspace from {@link WorkspaceList}. * * @param n * Passed in for the convenience. The node where the build is running. * @param wsl * Passed in for the convenience. The returned path must be registered to this object. */ protected Lease decideWorkspace(Node n, WorkspaceList wsl) throws InterruptedException, IOException { // TODO: this cast is indicative of abstraction problem return wsl.allocate(n.getWorkspaceFor((TopLevelItem)getProject())); } public Result run(BuildListener listener) throws Exception { Node node = getCurrentNode(); assert builtOn==null; builtOn = node.getNodeName(); hudsonVersion = Hudson.VERSION; this.listener = listener; launcher = createLauncher(listener); if (!Hudson.getInstance().getNodes().isEmpty()) listener.getLogger().println(node instanceof Hudson ? Messages.AbstractBuild_BuildingOnMaster() : Messages.AbstractBuild_BuildingRemotely(builtOn)); final Lease lease = decideWorkspace(node,Computer.currentComputer().getWorkspaceList()); try { workspace = lease.path.getRemote(); node.getFileSystemProvisioner().prepareWorkspace(AbstractBuild.this,lease.path,listener); if (project.isCleanWorkspaceRequired()) { listener.getLogger().println("Cleaning the workspace because project is configured to clean the workspace before each build."); if (!project.cleanWorkspace()){ listener.getLogger().println("Workspace cleaning was attempted but SCM blocked the cleaning."); } } checkout(listener); if (!preBuild(listener,project.getProperties())) return Result.FAILURE; Result result = doRun(listener); Computer c = node.toComputer(); if (c==null || c.isOffline()) { // As can be seen in HUDSON-5073, when a build fails because of the slave connectivity problem, // error message doesn't point users to the slave. So let's do it here. listener.hyperlink("/computer/"+builtOn+"/log","Looks like the node went offline during the build. Check the slave log for the details."); // grab the end of the log file. This might not work very well if the slave already // starts reconnecting. Fixing this requires a ring buffer in slave logs. AnnotatedLargeText<Computer> log = c.getLogText(); StringWriter w = new StringWriter(); log.writeHtmlTo(Math.max(0,c.getLogFile().length()-10240),w); listener.getLogger().print(ExpandableDetailsNote.encodeTo("details",w.toString())); listener.getLogger().println(); } // kill run-away processes that are left // use multiple environment variables so that people can escape this massacre by overriding an environment // variable for some processes launcher.kill(getCharacteristicEnvVars()); // this is ugly, but for historical reason, if non-null value is returned // it should become the final result. if (result==null) result = getResult(); if (result==null) result = Result.SUCCESS; return result; } finally { lease.release(); this.listener = null; } } /** * Creates a {@link Launcher} that this build will use. This can be overridden by derived types * to decorate the resulting {@link Launcher}. * * @param listener * Always non-null. Connected to the main build output. */ protected Launcher createLauncher(BuildListener listener) throws IOException, InterruptedException { Launcher l = getCurrentNode().createLauncher(listener); if (project instanceof BuildableItemWithBuildWrappers) { BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; for (BuildWrapper bw : biwbw.getBuildWrappersList()) l = bw.decorateLauncher(AbstractBuild.this,l,listener); } buildEnvironments = new ArrayList<Environment>(); for (NodeProperty nodeProperty: Hudson.getInstance().getGlobalNodeProperties()) { Environment environment = nodeProperty.setUp(AbstractBuild.this, l, listener); if (environment != null) { buildEnvironments.add(environment); } } for (NodeProperty nodeProperty: Computer.currentComputer().getNode().getNodeProperties()) { Environment environment = nodeProperty.setUp(AbstractBuild.this, l, listener); if (environment != null) { buildEnvironments.add(environment); } } return l; } private void checkout(BuildListener listener) throws Exception { for (int retryCount = project.getScmCheckoutRetryCount(); ; retryCount--) { // for historical reasons, null in the scm field means CVS, so we need to explicitly set this to something // in case check out fails and leaves a broken changelog.xml behind. // see http://www.nabble.com/CVSChangeLogSet.parse-yields-SAXParseExceptions-when-parsing-bad-*AccuRev*-changelog.xml-files-td22213663.html AbstractBuild.this.scm = new NullChangeLogParser(); try { if (project.checkout(AbstractBuild.this, launcher, listener, new File(getRootDir(), "changelog.xml"))) { // check out succeeded SCM scm = project.getScm(); AbstractBuild.this.scm = scm.createChangeLogParser(); AbstractBuild.this.changeSet = AbstractBuild.this.calcChangeSet(); for (SCMListener l : Hudson.getInstance().getSCMListeners()) { l.onChangeLogParsed(AbstractBuild.this, listener, changeSet); } return; } } catch (AbortException e) { listener.error(e.getMessage()); } catch (IOException e) { // checkout error not yet reported e.printStackTrace(listener.getLogger()); } // all attempts failed if (retryCount == 0){ throw new RunnerAbortedException(); } listener.getLogger().println("Retrying after 10 seconds"); Thread.sleep(10000); } } /** * The portion of a build that is specific to a subclass of {@link AbstractBuild} * goes here. * * @return * null to continue the build normally (that means the doRun method * itself run successfully) * Return a non-null value to abort the build right there with the specified result code. */ protected abstract Result doRun(BuildListener listener) throws Exception, RunnerAbortedException; /** * @see #post(BuildListener) */ protected abstract void post2(BuildListener listener) throws Exception; public final void post(BuildListener listener) throws Exception { try { post2(listener); //Resolve issue with invalid symlinks for maven (see http://issues.hudson-ci.org/browse/HUDSON-8340) if (getResult().isBetterOrEqualTo(Result.UNSTABLE)) createSymlink(listener, "lastSuccessful"); if (getResult().isBetterOrEqualTo(Result.SUCCESS)) createSymlink(listener, "lastStable"); } finally { // update the culprit list HashSet<String> r = new HashSet<String>(); for (User u : getCulprits()) r.add(u.getId()); culprits = r; CheckPoint.CULPRITS_DETERMINED.report(); } } public void cleanUp(BuildListener listener) throws Exception { BuildTrigger.execute(AbstractBuild.this, listener); buildEnvironments = null; } /** * @deprecated as of 1.356 * Use {@link #performAllBuildSteps(BuildListener, Map, boolean)} */ protected final void performAllBuildStep(BuildListener listener, Map<?,? extends BuildStep> buildSteps, boolean phase) throws InterruptedException, IOException { performAllBuildSteps(listener,buildSteps.values(),phase); } protected final boolean performAllBuildSteps(BuildListener listener, Map<?,? extends BuildStep> buildSteps, boolean phase) throws InterruptedException, IOException { return performAllBuildSteps(listener,buildSteps.values(),phase); } /** * @deprecated as of 1.356 * Use {@link #performAllBuildSteps(BuildListener, Iterable, boolean)} */ protected final void performAllBuildStep(BuildListener listener, Iterable<? extends BuildStep> buildSteps, boolean phase) throws InterruptedException, IOException { performAllBuildSteps(listener,buildSteps,phase); } /** * Runs all the given build steps, even if one of them fail. * * @param phase * true for the post build processing, and false for the final "run after finished" execution. */ protected final boolean performAllBuildSteps(BuildListener listener, Iterable<? extends BuildStep> buildSteps, boolean phase) throws InterruptedException, IOException { boolean r = true; for (BuildStep bs : buildSteps) { if (bs instanceof Publisher && ((Publisher) bs).needsToRun(getResult()) && ((((Publisher) bs).needsToRunAfterFinalized()) ^ phase)) { try { r &= perform(bs, listener); } catch (Exception e) { String msg = "Publisher " + bs.getClass().getName() + " aborted due to exception"; e.printStackTrace(listener.error(msg)); LOGGER.log(Level.WARNING, msg, e); setResult(Result.FAILURE); } } } return r; } /** * Calls a build step. */ protected final boolean perform(BuildStep bs, BuildListener listener) throws InterruptedException, IOException { BuildStepMonitor mon; try { mon = bs.getRequiredMonitorService(); } catch (AbstractMethodError e) { mon = BuildStepMonitor.BUILD; } return mon.perform(bs, AbstractBuild.this, launcher, listener); } protected final boolean preBuild(BuildListener listener,Map<?,? extends BuildStep> steps) { return preBuild(listener,steps.values()); } protected final boolean preBuild(BuildListener listener,Collection<? extends BuildStep> steps) { return preBuild(listener,(Iterable<? extends BuildStep>)steps); } protected final boolean preBuild(BuildListener listener,Iterable<? extends BuildStep> steps) { for (BuildStep bs : steps) if (!bs.prebuild(AbstractBuild.this,listener)) return false; return true; } } /** * Gets the changes incorporated into this build. * * @return never null. */ @Exported public ChangeLogSet<? extends Entry> getChangeSet() { if (scm==null) { // for historical reason, null means CVS. try { Class<?> c = Hudson.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.scm.CVSChangeLogParser"); scm = (ChangeLogParser)c.newInstance(); } catch (ClassNotFoundException e) { // if CVS isn't available, fall back to something non-null. scm = new NullChangeLogParser(); } catch (InstantiationException e) { scm = new NullChangeLogParser(); throw (Error)new InstantiationError().initCause(e); } catch (IllegalAccessException e) { scm = new NullChangeLogParser(); throw (Error)new IllegalAccessError().initCause(e); } } if (changeSet==null) // cached value try { changeSet = calcChangeSet(); } finally { // defensive check. if the calculation fails (such as through an exception), // set a dummy value so that it'll work the next time. the exception will // be still reported, giving the plugin developer an opportunity to fix it. if (changeSet==null) changeSet=ChangeLogSet.createEmpty(this); } return changeSet; } /** * Returns true if the changelog is already computed. */ public boolean hasChangeSetComputed() { File changelogFile = new File(getRootDir(), "changelog.xml"); return changelogFile.exists(); } private ChangeLogSet<? extends Entry> calcChangeSet() { File changelogFile = new File(getRootDir(), "changelog.xml"); if (!changelogFile.exists()) return ChangeLogSet.createEmpty(this); try { return scm.parse(this,changelogFile); } catch (IOException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } return ChangeLogSet.createEmpty(this); } @Override public EnvVars getEnvironment(TaskListener log) throws IOException, InterruptedException { EnvVars env = super.getEnvironment(log); FilePath ws = getWorkspace(); if (ws!=null) // if this is done very early on in the build, workspace may not be decided yet. see HUDSON-3997 env.put("WORKSPACE", ws.getRemote()); // servlet container may have set CLASSPATH in its launch script, // so don't let that inherit to the new child process. // see http://www.nabble.com/Run-Job-with-JDK-1.4.2-tf4468601.html env.put("CLASSPATH",""); JDK jdk = project.getJDK(); if (jdk != null) { Computer computer = Computer.currentComputer(); if (computer != null) { // just in case were not in a build jdk = jdk.forNode(computer.getNode(), log); } jdk.buildEnvVars(env); } project.getScm().buildEnvVars(this,env); if (buildEnvironments!=null) for (Environment e : buildEnvironments) e.buildEnvVars(env); for (EnvironmentContributingAction a : Util.filter(getActions(),EnvironmentContributingAction.class)) a.buildEnvVars(this,env); EnvVars.resolve(env); return env; } public Calendar due() { return getTimestamp(); } /** * Builds up a set of variable names that contain sensitive values that * should not be exposed. The expection is that this set is populated with * keys returned by {@link #getBuildVariables()} that should have their * values masked for display purposes. * * @since 1.378 */ public Set<String> getSensitiveBuildVariables() { Set<String> s = new HashSet<String>(); ParametersAction parameters = getAction(ParametersAction.class); if (parameters != null) { for (ParameterValue p : parameters) { if (p.isSensitive()) { s.add(p.getName()); } } } // Allow BuildWrappers to determine if any of their data is sensitive if (project instanceof BuildableItemWithBuildWrappers) { for (BuildWrapper bw : ((BuildableItemWithBuildWrappers) project).getBuildWrappersList()) { bw.makeSensitiveBuildVariables(this, s); } } return s; } /** * Provides additional variables and their values to {@link Builder}s. * * <p> * This mechanism is used by {@link MatrixConfiguration} to pass * the configuration values to the current build. It is up to * {@link Builder}s to decide whether it wants to recognize the values * or how to use them. * * <p> * This also includes build parameters if a build is parameterized. * * @return * The returned map is mutable so that subtypes can put more values. */ public Map<String,String> getBuildVariables() { Map<String,String> r = new HashMap<String, String>(); ParametersAction parameters = getAction(ParametersAction.class); if (parameters!=null) { // this is a rather round about way of doing this... for (ParameterValue p : parameters) { String v = p.createVariableResolver(this).resolve(p.getName()); if (v!=null) r.put(p.getName(),v); } } customizeBuildVariables(r); // allow the BuildWrappers to contribute additional build variables if (project instanceof BuildableItemWithBuildWrappers) { for (BuildWrapper bw : ((BuildableItemWithBuildWrappers) project).getBuildWrappersList()) bw.makeBuildVariables(this,r); } return r; } /** * @since 2.1.0 */ protected void customizeBuildVariables(final Map<String, String> vars) { // nop } /** * Creates {@link VariableResolver} backed by {@link #getBuildVariables()}. */ public final VariableResolver<String> getBuildVariableResolver() { return new VariableResolver.ByMap<String>(getBuildVariables()); } /** * Gets {@link AbstractTestResultAction} associated with this build if any. */ public AbstractTestResultAction getTestResultAction() { return getAction(AbstractTestResultAction.class); } /** * Invoked by {@link Executor} to performs a build. */ public abstract void run(); // // // fingerprint related stuff // // @Override public String getWhyKeepLog() { // if any of the downstream project is configured with 'keep dependency component', // we need to keep this log OUTER: for (AbstractProject<?,?> p : getParent().getDownstreamProjects()) { if (!p.isKeepDependencies()) continue; AbstractBuild<?,?> fb = p.getFirstBuild(); if (fb==null) continue; // no active record // is there any active build that depends on us? for (int i : getDownstreamRelationship(p).listNumbersReverse()) { // TODO: this is essentially a "find intersection between two sparse sequences" // and we should be able to do much better. if (i<fb.getNumber()) continue OUTER; // all the other records are younger than the first record, so pointless to search. AbstractBuild<?,?> b = p.getBuildByNumber(i); if (b!=null) return Messages.AbstractBuild_KeptBecause(b); } } return super.getWhyKeepLog(); } /** * Gets the dependency relationship from this build (as the source) * and that project (as the sink.) * * @return * range of build numbers that represent which downstream builds are using this build. * The range will be empty if no build of that project matches this, but it'll never be null. */ public RangeSet getDownstreamRelationship(AbstractProject that) { RangeSet rs = new RangeSet(); FingerprintAction f = getAction(FingerprintAction.class); if (f==null) return rs; // look for fingerprints that point to this build as the source, and merge them all for (Fingerprint e : f.getFingerprints().values()) { if (upstreamCulprits) { // With upstreamCulprits, we allow downstream relationships // from intermediate jobs rs.add(e.getRangeSet(that)); } else { BuildPtr o = e.getOriginal(); if (o!=null && o.is(this)) rs.add(e.getRangeSet(that)); } } return rs; } /** * Works like {@link #getDownstreamRelationship(AbstractProject)} but returns * the actual build objects, in ascending order. * @since 1.150 */ public Iterable<AbstractBuild<?,?>> getDownstreamBuilds(final AbstractProject<?,?> that) { final Iterable<Integer> nums = getDownstreamRelationship(that).listNumbers(); return new Iterable<AbstractBuild<?, ?>>() { public Iterator<AbstractBuild<?, ?>> iterator() { return Iterators.removeNull( new AdaptedIterator<Integer,AbstractBuild<?,?>>(nums) { protected AbstractBuild<?, ?> adapt(Integer item) { return that.getBuildByNumber(item); } }); } }; } /** * Gets the dependency relationship from this build (as the sink) * and that project (as the source.) * * @return * Build number of the upstream build that feed into this build, * or -1 if no record is available. */ public int getUpstreamRelationship(AbstractProject that) { FingerprintAction f = getAction(FingerprintAction.class); if (f==null) return -1; int n = -1; // look for fingerprints that point to the given project as the source, and merge them all for (Fingerprint e : f.getFingerprints().values()) { if (upstreamCulprits) { // With upstreamCulprits, we allow upstream relationships // from intermediate jobs Fingerprint.RangeSet rangeset = e.getRangeSet(that); if (!rangeset.isEmpty()) { n = Math.max(n, rangeset.listNumbersReverse().iterator().next()); } } else { BuildPtr o = e.getOriginal(); if (o!=null && o.belongsTo(that)) n = Math.max(n,o.getNumber()); } } return n; } /** * Works like {@link #getUpstreamRelationship(AbstractProject)} but returns the * actual build object. * * @return * null if no such upstream build was found, or it was found but the * build record is already lost. */ public AbstractBuild<?,?> getUpstreamRelationshipBuild(AbstractProject<?,?> that) { int n = getUpstreamRelationship(that); if (n==-1) return null; return that.getBuildByNumber(n); } /** * Gets the downstream builds of this build, which are the builds of the * downstream projects that use artifacts of this build. * * @return * For each project with fingerprinting enabled, returns the range * of builds (which can be empty if no build uses the artifact from this build.) */ public Map<AbstractProject,RangeSet> getDownstreamBuilds() { Map<AbstractProject,RangeSet> r = new HashMap<AbstractProject,RangeSet>(); for (AbstractProject p : getParent().getDownstreamProjects()) { if (p.isFingerprintConfigured()) r.put(p,getDownstreamRelationship(p)); } return r; } /** * Gets the upstream builds of this build, which are the builds of the * upstream projects whose artifacts feed into this build. * * @see #getTransitiveUpstreamBuilds() */ public Map<AbstractProject,Integer> getUpstreamBuilds() { return _getUpstreamBuilds(getParent().getUpstreamProjects()); } /** * Works like {@link #getUpstreamBuilds()} but also includes all the transitive * dependencies as well. */ public Map<AbstractProject,Integer> getTransitiveUpstreamBuilds() { return _getUpstreamBuilds(getParent().getTransitiveUpstreamProjects()); } private Map<AbstractProject, Integer> _getUpstreamBuilds(Collection<AbstractProject> projects) { Map<AbstractProject,Integer> r = new HashMap<AbstractProject,Integer>(); for (AbstractProject p : projects) { int n = getUpstreamRelationship(p); if (n>=0) r.put(p,n); } return r; } /** * Gets the changes in the dependency between the given build and this build. */ public Map<AbstractProject,DependencyChange> getDependencyChanges(AbstractBuild from) { if (from==null) return Collections.emptyMap(); // make it easy to call this from views FingerprintAction n = this.getAction(FingerprintAction.class); FingerprintAction o = from.getAction(FingerprintAction.class); if (n==null || o==null) return Collections.emptyMap(); Map<AbstractProject,Integer> ndep = n.getDependencies(); Map<AbstractProject,Integer> odep = o.getDependencies(); Map<AbstractProject,DependencyChange> r = new HashMap<AbstractProject,DependencyChange>(); for (Map.Entry<AbstractProject,Integer> entry : odep.entrySet()) { AbstractProject p = entry.getKey(); Integer oldNumber = entry.getValue(); Integer newNumber = ndep.get(p); if (newNumber!=null && oldNumber.compareTo(newNumber)<0) { r.put(p,new DependencyChange(p,oldNumber,newNumber)); } } return r; } /** * Represents a change in the dependency. */ public static final class DependencyChange { /** * The dependency project. */ //TODO: review and check whether we can do it private public final AbstractProject project; /** * Version of the dependency project used in the previous build. */ //TODO: review and check whether we can do it private public final int fromId; /** * {@link Build} object for {@link #fromId}. Can be null if the log is gone. */ //TODO: review and check whether we can do it private public final AbstractBuild from; /** * Version of the dependency project used in this build. */ //TODO: review and check whether we can do it private public final int toId; //TODO: review and check whether we can do it private public final AbstractBuild to; public DependencyChange(AbstractProject<?,?> project, int fromId, int toId) { this.project = project; this.fromId = fromId; this.toId = toId; this.from = project.getBuildByNumber(fromId); this.to = project.getBuildByNumber(toId); } public AbstractProject getProject() { return project; } public int getFromId() { return fromId; } public AbstractBuild getFrom() { return from; } public int getToId() { return toId; } public AbstractBuild getTo() { return to; } /** * Gets the {@link AbstractBuild} objects (fromId,toId]. * <p> * This method returns all such available builds in the ascending order * of IDs, but due to log rotations, some builds may be already unavailable. */ public List<AbstractBuild> getBuilds() { List<AbstractBuild> r = new ArrayList<AbstractBuild>(); AbstractBuild<?,?> b = (AbstractBuild)project.getNearestBuild(fromId); if (b!=null && b.getNumber()==fromId) b = b.getNextBuild(); // fromId exclusive while (b!=null && b.getNumber()<=toId) { r.add(b); b = b.getNextBuild(); } return r; } } // // web methods // /** * Stops this build if it's still going. * * If we use this/executor/stop URL, it causes 404 if the build is already killed, * as {@link #getExecutor()} returns null. */ public synchronized void doStop(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { Executor e = getExecutor(); if (e!=null) e.doStop(req,rsp); else // nothing is building rsp.forwardToPreviousPage(req); } private static final Logger LOGGER = Logger.getLogger(AbstractBuild.class.getName()); }