/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Brian Westrich, Erik Ramfelt, Ertan Deniz, Jean-Baptiste Quenot, * Luca Domenico Milanesio, R. Tyler Ballance, Stephen Connolly, Tom Huybrechts, * id:cactusman, Yahoo! Inc., Andrew Bayer, Manufacture Francaise des Pneumatiques * Michelin, Romain Seguy * * 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 antlr.ANTLRException; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.AbortException; import hudson.CopyOnWrite; import hudson.EnvVars; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.model.Cause.LegacyCodeCause; import hudson.model.Descriptor.FormException; import hudson.model.Fingerprint.RangeSet; import hudson.model.Node.Mode; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; import hudson.model.labels.LabelAtom; import hudson.model.labels.LabelExpression; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SCMPollListener; import hudson.model.queue.CauseOfBlockage; import hudson.model.queue.QueueTaskFuture; import hudson.model.queue.SubTask; import hudson.model.queue.SubTaskContributor; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.NullSCM; import hudson.scm.PollingResult; import static hudson.scm.PollingResult.*; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import hudson.scm.SCMS; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.Permission; import hudson.slaves.Cloud; import hudson.slaves.WorkspaceList; import hudson.tasks.BuildStep; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildTrigger; import hudson.tasks.BuildWrapperDescriptor; import hudson.tasks.Publisher; import hudson.triggers.SCMTrigger; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.AlternativeUiTextProvider; import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; import hudson.util.FormValidation; import hudson.util.TimeUnit2; import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.servlet.ServletException; import jenkins.model.BlockedBecauseOfBuildInProgress; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ParameterizedJobMixIn; import jenkins.model.Uptime; import jenkins.model.lazy.LazyBuildMixIn; import jenkins.scm.DefaultSCMCheckoutStrategyImpl; import jenkins.scm.SCMCheckoutStrategy; import jenkins.scm.SCMCheckoutStrategyDescriptor; import jenkins.scm.SCMDecisionHandler; import jenkins.util.TimeDuration; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; import org.jenkinsci.bytecode.AdaptField; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.ForwardToView; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.interceptor.RequirePOST; /** * Base implementation of {@link Job}s that build software. * * For now this is primarily the common part of {@link Project} and MavenModule. * * @author Kohsuke Kawaguchi * @see AbstractBuild */ @SuppressWarnings("rawtypes") public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Job<P,R> implements BuildableItem, LazyBuildMixIn.LazyLoadingJob<P,R>, ParameterizedJobMixIn.ParameterizedJob { /** * {@link SCM} associated with the project. * To allow derived classes to link {@link SCM} config to elsewhere, * access to this variable should always go through {@link #getScm()}. */ private volatile SCM scm = new NullSCM(); /** * Controls how the checkout is done. */ private volatile SCMCheckoutStrategy scmCheckoutStrategy; /** * State returned from {@link SCM#poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)}. */ private volatile transient SCMRevisionState pollingBaseline = null; private transient LazyBuildMixIn<P,R> buildMixIn; /** * All the builds keyed by their build number. * Kept here for binary compatibility only; otherwise use {@link #buildMixIn}. * External code should use {@link #getBuildByNumber(int)} or {@link #getLastBuild()} and traverse via * {@link Run#getPreviousBuild()} */ @Restricted(NoExternalUse.class) protected transient RunMap<R> builds; /** * The quiet period. Null to delegate to the system default. */ private volatile Integer quietPeriod = null; /** * The retry count. Null to delegate to the system default. */ private volatile Integer scmCheckoutRetryCount = null; /** * If this project is configured to be only built on a certain label, * this value will be set to that label. * * For historical reasons, this is called 'assignedNode'. Also for * a historical reason, null to indicate the affinity * with the master node. * * @see #canRoam */ private String assignedNode; /** * True if this project can be built on any node. * * <p> * This somewhat ugly flag combination is so that we can migrate * existing Hudson installations nicely. */ private volatile boolean canRoam; /** * True to suspend new builds. */ protected volatile boolean disabled; /** * True to keep builds of this project in queue when downstream projects are * building. False by default to keep from breaking existing behavior. */ protected volatile boolean blockBuildWhenDownstreamBuilding = false; /** * True to keep builds of this project in queue when upstream projects are * building. False by default to keep from breaking existing behavior. */ protected volatile boolean blockBuildWhenUpstreamBuilding = false; /** * Identifies {@link JDK} to be used. * Null if no explicit configuration is required. * * <p> * Can't store {@link JDK} directly because {@link Jenkins} and {@link Project} * are saved independently. * * @see Jenkins#getJDK(String) */ private volatile String jdk; private volatile BuildAuthorizationToken authToken = null; /** * List of all {@link Trigger}s for this project. */ @AdaptField(was=List.class) protected volatile DescribableList<Trigger<?>,TriggerDescriptor> triggers = new DescribableList<Trigger<?>,TriggerDescriptor>(this); private static final AtomicReferenceFieldUpdater<AbstractProject,DescribableList> triggersUpdater = AtomicReferenceFieldUpdater.newUpdater(AbstractProject.class,DescribableList.class,"triggers"); /** * {@link Action}s contributed from subsidiary objects associated with * {@link AbstractProject}, such as from triggers, builders, publishers, etc. * * We don't want to persist them separately, and these actions * come and go as configuration change, so it's kept separate. */ @CopyOnWrite protected transient volatile List<Action> transientActions = new Vector<Action>(); private boolean concurrentBuild; /** * See {@link #setCustomWorkspace(String)}. * * @since 1.410 */ private String customWorkspace; protected AbstractProject(ItemGroup parent, String name) { super(parent,name); buildMixIn = createBuildMixIn(); builds = buildMixIn.getRunMap(); final Jenkins j = Jenkins.getInstance(); final List<Node> nodes = j != null ? j.getNodes() : null; if(nodes!=null && !nodes.isEmpty()) { // if a new job is configured with Hudson that already has agent nodes // make it roamable by default canRoam = true; } } private LazyBuildMixIn<P,R> createBuildMixIn() { return new LazyBuildMixIn<P,R>() { @SuppressWarnings("unchecked") // untypable @Override protected P asJob() { return (P) AbstractProject.this; } @Override protected Class<R> getBuildClass() { return AbstractProject.this.getBuildClass(); } }; } @Override public LazyBuildMixIn<P,R> getLazyBuildMixIn() { return buildMixIn; } private ParameterizedJobMixIn<P,R> getParameterizedJobMixIn() { return new ParameterizedJobMixIn<P,R>() { @SuppressWarnings("unchecked") // untypable @Override protected P asJob() { return (P) AbstractProject.this; } }; } @Override public synchronized void save() throws IOException { super.save(); updateTransientActions(); } @Override public void onCreatedFromScratch() { super.onCreatedFromScratch(); buildMixIn.onCreatedFromScratch(); builds = buildMixIn.getRunMap(); // solicit initial contributions, especially from TransientProjectActionFactory updateTransientActions(); } @Override public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { super.onLoad(parent, name); if (buildMixIn == null) { buildMixIn = createBuildMixIn(); } buildMixIn.onLoad(parent, name); builds = buildMixIn.getRunMap(); triggers().setOwner(this); for (Trigger t : triggers()) { try { t.start(this, Items.currentlyUpdatingByXml()); } catch (Throwable e) { LOGGER.log(Level.WARNING, "could not start trigger while loading project '" + getFullName() + "'", e); } } if(scm==null) scm = new NullSCM(); // perhaps it was pointing to a plugin that no longer exists. if(transientActions==null) transientActions = new Vector<Action>(); // happens when loaded from disk updateTransientActions(); } @WithBridgeMethods(List.class) protected DescribableList<Trigger<?>,TriggerDescriptor> triggers() { if (triggers == null) { triggersUpdater.compareAndSet(this,null,new DescribableList<Trigger<?>,TriggerDescriptor>(this)); } return triggers; } @Override public EnvVars getEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException { EnvVars env = super.getEnvironment(node, listener); JDK jdkTool = getJDK(); if (jdkTool != null) { if (node != null) { // just in case were not in a build jdkTool = jdkTool.forNode(node, listener); } jdkTool.buildEnvVars(env); } else if (!JDK.isDefaultName(jdk)) { listener.getLogger().println("No JDK named ‘" + jdk + "’ found"); } return env; } @Override protected void performDelete() throws IOException, InterruptedException { // prevent a new build while a delete operation is in progress makeDisabled(true); FilePath ws = getWorkspace(); if(ws!=null) { Node on = getLastBuiltOn(); getScm().processWorkspaceBeforeDeletion(this, ws, on); if(on!=null) on.getFileSystemProvisioner().discardWorkspace(this,ws); } super.performDelete(); } /** * Does this project perform concurrent builds? * @since 1.319 */ @Exported public boolean isConcurrentBuild() { return concurrentBuild; } public void setConcurrentBuild(boolean b) throws IOException { concurrentBuild = b; save(); } /** * If this project is configured to be always built on this node, * return that {@link Node}. Otherwise null. */ public @CheckForNull Label getAssignedLabel() { if(canRoam) return null; if(assignedNode==null) return Jenkins.getInstance().getSelfLabel(); return Jenkins.getInstance().getLabel(assignedNode); } /** * Set of labels relevant to this job. * * This method is used to determine what agents are relevant to jobs, for example by {@link View}s. * It does not affect the scheduling. This information is informational and the best-effort basis. * * @since 1.456 * @return * Minimally it should contain {@link #getAssignedLabel()}. The set can contain null element * to correspond to the null return value from {@link #getAssignedLabel()}. */ public Set<Label> getRelevantLabels() { return Collections.singleton(getAssignedLabel()); } /** * Gets the textual representation of the assigned label as it was entered by the user. */ public String getAssignedLabelString() { if (canRoam || assignedNode==null) return null; try { LabelExpression.parseExpression(assignedNode); return assignedNode; } catch (ANTLRException e) { // must be old label or host name that includes whitespace or other unsafe chars return LabelAtom.escape(assignedNode); } } /** * Sets the assigned label. */ public void setAssignedLabel(Label l) throws IOException { if(l==null) { canRoam = true; assignedNode = null; } else { canRoam = false; if(l== Jenkins.getInstance().getSelfLabel()) assignedNode = null; else assignedNode = l.getExpression(); } save(); } /** * Assigns this job to the given node. A convenience method over {@link #setAssignedLabel(Label)}. */ public void setAssignedNode(Node l) throws IOException { setAssignedLabel(l.getSelfLabel()); } /** * Get the term used in the UI to represent this kind of {@link AbstractProject}. * Must start with a capital letter. */ @Override public String getPronoun() { return AlternativeUiTextProvider.get(PRONOUN, this,Messages.AbstractProject_Pronoun()); } /** * Gets the human readable display name to be rendered in the "Build Now" link. * * @since 1.401 */ public String getBuildNowText() { // For compatibility, still use the deprecated replacer if specified. return AlternativeUiTextProvider.get(BUILD_NOW_TEXT, this, getParameterizedJobMixIn().getBuildNowText()); } /** * Gets the nearest ancestor {@link TopLevelItem} that's also an {@link AbstractProject}. * * <p> * Some projects (such as matrix projects, Maven projects, or promotion processes) form a tree of jobs * that acts as a single unit. This method can be used to find the top most dominating job that * covers such a tree. * * @return never null. * @see AbstractBuild#getRootBuild() */ public AbstractProject<?,?> getRootProject() { if (this instanceof TopLevelItem) { return this; } else { ItemGroup p = this.getParent(); if (p instanceof AbstractProject) return ((AbstractProject) p).getRootProject(); return this; } } /** * Gets the directory where the module is checked out. * * @return * null if the workspace is on an agent that's not connected. * @deprecated as of 1.319 * To support concurrent builds of the same project, this method is moved to {@link AbstractBuild}. * For backward compatibility, this method returns the right {@link AbstractBuild#getWorkspace()} if called * from {@link Executor}, and otherwise the workspace of the last build. * * <p> * If you are calling this method during a build from an executor, switch it to {@link AbstractBuild#getWorkspace()}. * If you are calling this method to serve a file from the workspace, doing a form validation, etc., then * use {@link #getSomeWorkspace()} */ @Deprecated public final FilePath getWorkspace() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getWorkspace() : null; } /** * Various deprecated methods in this class all need the 'current' build. This method returns * the build suitable for that purpose. * * @return An AbstractBuild for deprecated methods to use. */ private AbstractBuild getBuildForDeprecatedMethods() { Executor e = Executor.currentExecutor(); if(e!=null) { Executable exe = e.getCurrentExecutable(); if (exe instanceof AbstractBuild) { AbstractBuild b = (AbstractBuild) exe; if(b.getProject()==this) return b; } } R lb = getLastBuild(); if(lb!=null) return lb; return null; } /** * Gets a workspace for some build of this project. * * <p> * This is useful for obtaining a workspace for the purpose of form field validation, where exactly * which build the workspace belonged is less important. The implementation makes a cursory effort * to find some workspace. * * @return * null if there's no available workspace. * @since 1.319 */ public final @CheckForNull FilePath getSomeWorkspace() { R b = getSomeBuildWithWorkspace(); if (b!=null) return b.getWorkspace(); for (WorkspaceBrowser browser : ExtensionList.lookup(WorkspaceBrowser.class)) { FilePath f = browser.getWorkspace(this); if (f != null) return f; } return null; } /** * Gets some build that has a live workspace. * * @return null if no such build exists. */ public final R getSomeBuildWithWorkspace() { int cnt=0; for (R b = getLastBuild(); cnt<5 && b!=null; b=b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws!=null) return b; } return null; } private R getSomeBuildWithExistingWorkspace() throws IOException, InterruptedException { int cnt=0; for (R b = getLastBuild(); cnt<5 && b!=null; b=b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws!=null && ws.exists()) return b; } return null; } /** * 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. * * @deprecated as of 1.319 * See {@link #getWorkspace()} for a migration strategy. */ @Deprecated public FilePath getModuleRoot() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getModuleRoot() : null; } /** * 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. * * @deprecated as of 1.319 * See {@link #getWorkspace()} for a migration strategy. */ @Deprecated public FilePath[] getModuleRoots() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getModuleRoots() : null; } public int getQuietPeriod() { return quietPeriod!=null ? quietPeriod : Jenkins.getInstance().getQuietPeriod(); } public SCMCheckoutStrategy getScmCheckoutStrategy() { return scmCheckoutStrategy == null ? new DefaultSCMCheckoutStrategyImpl() : scmCheckoutStrategy; } public void setScmCheckoutStrategy(SCMCheckoutStrategy scmCheckoutStrategy) throws IOException { this.scmCheckoutStrategy = scmCheckoutStrategy; save(); } public int getScmCheckoutRetryCount() { return scmCheckoutRetryCount !=null ? scmCheckoutRetryCount : Jenkins.getInstance().getScmCheckoutRetryCount(); } // ugly name because of EL public boolean getHasCustomQuietPeriod() { return quietPeriod!=null; } /** * Sets the custom quiet period of this project, or revert to the global default if null is given. */ public void setQuietPeriod(Integer seconds) throws IOException { this.quietPeriod = seconds; save(); } public boolean hasCustomScmCheckoutRetryCount(){ return scmCheckoutRetryCount != null; } @Override public boolean isBuildable() { return !isDisabled() && !isHoldOffBuildUntilSave(); } /** * Used in <tt>sidepanel.jelly</tt> to decide whether to display * the config/delete/build links. */ public boolean isConfigurable() { return true; } public boolean blockBuildWhenDownstreamBuilding() { return blockBuildWhenDownstreamBuilding; } public void setBlockBuildWhenDownstreamBuilding(boolean b) throws IOException { blockBuildWhenDownstreamBuilding = b; save(); } public boolean blockBuildWhenUpstreamBuilding() { return blockBuildWhenUpstreamBuilding; } public void setBlockBuildWhenUpstreamBuilding(boolean b) throws IOException { blockBuildWhenUpstreamBuilding = b; save(); } public boolean isDisabled() { return disabled; } /** * Validates the retry count Regex */ public FormValidation doCheckRetryCount(@QueryParameter String value)throws IOException,ServletException{ // retry count is optional so this is ok if(value == null || value.trim().equals("")) return FormValidation.ok(); if (!value.matches("[0-9]*")) { return FormValidation.error("Invalid retry count"); } return FormValidation.ok(); } /** * Marks the build as disabled. * The method will ignore the disable command if {@link #supportsMakeDisabled()} * returns false. The enable command will be executed in any case. * @param b true - disable, false - enable * @since 1.585 Do not disable projects if {@link #supportsMakeDisabled()} returns false */ public void makeDisabled(boolean b) throws IOException { if(disabled==b) return; // noop if (b && !supportsMakeDisabled()) return; // do nothing if the disabling is unsupported this.disabled = b; if(b) Jenkins.getInstance().getQueue().cancel(this); save(); ItemListener.fireOnUpdated(this); } /** * Specifies whether this project may be disabled by the user. * By default, it can be only if this is a {@link TopLevelItem}; * would be false for matrix configurations, etc. * @return true if the GUI should allow {@link #doDisable} and the like * @since 1.475 */ public boolean supportsMakeDisabled() { return this instanceof TopLevelItem; } public void disable() throws IOException { makeDisabled(true); } public void enable() throws IOException { makeDisabled(false); } @Override public BallColor getIconColor() { if(isDisabled()) return isBuilding() ? BallColor.DISABLED_ANIME : BallColor.DISABLED; else return super.getIconColor(); } /** * effectively deprecated. Since using updateTransientActions correctly * under concurrent environment requires a lock that can too easily cause deadlocks. * * <p> * Override {@link #createTransientActions()} instead. */ protected void updateTransientActions() { transientActions = createTransientActions(); } protected List<Action> createTransientActions() { Vector<Action> ta = new Vector<Action>(); for (JobProperty<? super P> p : Util.fixNull(properties)) ta.addAll(p.getJobActions((P)this)); for (TransientProjectActionFactory tpaf : TransientProjectActionFactory.all()) { try { ta.addAll(Util.fixNull(tpaf.createFor(this))); // be defensive against null } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not load actions from " + tpaf + " for " + this, e); } } return ta; } /** * Returns the live list of all {@link Publisher}s configured for this project. * * <p> * This method couldn't be called <tt>getPublishers()</tt> because existing methods * in sub-classes return different inconsistent types. */ public abstract DescribableList<Publisher,Descriptor<Publisher>> getPublishersList(); @Override public void addProperty(JobProperty<? super P> jobProp) throws IOException { super.addProperty(jobProp); updateTransientActions(); } public List<ProminentProjectAction> getProminentActions() { return getActions(ProminentProjectAction.class); } @Override public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { super.doConfigSubmit(req,rsp); updateTransientActions(); // notify the queue as the project might be now tied to different node Jenkins.getInstance().getQueue().scheduleMaintenance(); // this is to reflect the upstream build adjustments done above Jenkins.getInstance().rebuildDependencyGraphAsync(); } /** * @deprecated * Use {@link #scheduleBuild(Cause)}. Since 1.283 */ @Deprecated public boolean scheduleBuild() { return getParameterizedJobMixIn().scheduleBuild(); } /** * @deprecated * Use {@link #scheduleBuild(int, Cause)}. Since 1.283 */ @Deprecated public boolean scheduleBuild(int quietPeriod) { return getParameterizedJobMixIn().scheduleBuild(quietPeriod); } /** * Schedules a build of this project. * * @return * true if the project is added to the queue. * false if the task was rejected from the queue (such as when the system is being shut down.) */ public boolean scheduleBuild(Cause c) { return getParameterizedJobMixIn().scheduleBuild(c); } public boolean scheduleBuild(int quietPeriod, Cause c) { return getParameterizedJobMixIn().scheduleBuild(quietPeriod, c); } /** * Schedules a build. * * Important: the actions should be persistable without outside references (e.g. don't store * references to this project). To provide parameters for a parameterized project, add a ParametersAction. If * no ParametersAction is provided for such a project, one will be created with the default parameter values. * * @param quietPeriod the quiet period to observer * @param c the cause for this build which should be recorded * @param actions a list of Actions that will be added to the build * @return whether the build was actually scheduled */ public boolean scheduleBuild(int quietPeriod, Cause c, Action... actions) { return scheduleBuild2(quietPeriod,c,actions)!=null; } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. * * @param actions * For the convenience of the caller, this array can contain null, and those will be silently ignored. */ @WithBridgeMethods(Future.class) public QueueTaskFuture<R> scheduleBuild2(int quietPeriod, Cause c, Action... actions) { return scheduleBuild2(quietPeriod,c,Arrays.asList(actions)); } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. * * @param actions * For the convenience of the caller, this collection can contain null, and those will be silently ignored. * @since 1.383 */ @SuppressWarnings("unchecked") @WithBridgeMethods(Future.class) public QueueTaskFuture<R> scheduleBuild2(int quietPeriod, Cause c, Collection<? extends Action> actions) { List<Action> queueActions = new ArrayList<Action>(actions); if (c != null) { queueActions.add(new CauseAction(c)); } return getParameterizedJobMixIn().scheduleBuild2(quietPeriod, queueActions.toArray(new Action[queueActions.size()])); } /** * Schedules a build, and returns a {@link Future} object * to wait for the completion of the build. * * <p> * Production code shouldn't be using this, but for tests this is very convenient, so this isn't marked * as deprecated. */ @SuppressWarnings("deprecation") @WithBridgeMethods(Future.class) public QueueTaskFuture<R> scheduleBuild2(int quietPeriod) { return scheduleBuild2(quietPeriod, new LegacyCodeCause()); } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. */ @WithBridgeMethods(Future.class) public QueueTaskFuture<R> scheduleBuild2(int quietPeriod, Cause c) { return scheduleBuild2(quietPeriod, c, new Action[0]); } /** * Schedules a polling of this project. */ public boolean schedulePolling() { if(isDisabled()) return false; SCMTrigger scmt = getTrigger(SCMTrigger.class); if(scmt==null) return false; scmt.run(); return true; } /** * Returns true if the build is in the queue. */ @Override public boolean isInQueue() { return Jenkins.getInstance().getQueue().contains(this); } @Override public Queue.Item getQueueItem() { return Jenkins.getInstance().getQueue().getItem(this); } /** * Gets the JDK that this project is configured with, or null. */ public JDK getJDK() { return Jenkins.getInstance().getJDK(jdk); } /** * Overwrites the JDK setting. */ public void setJDK(JDK jdk) throws IOException { this.jdk = jdk.getName(); save(); } public BuildAuthorizationToken getAuthToken() { return authToken; } @Override public RunMap<R> _getRuns() { return buildMixIn._getRuns(); } @Override public void removeRun(R run) { buildMixIn.removeRun(run); } /** * {@inheritDoc} * * More efficient implementation. */ @Override public R getBuild(String id) { return buildMixIn.getBuild(id); } /** * {@inheritDoc} * * More efficient implementation. */ @Override public R getBuildByNumber(int n) { return buildMixIn.getBuildByNumber(n); } /** * {@inheritDoc} * * More efficient implementation. */ @Override public R getFirstBuild() { return buildMixIn.getFirstBuild(); } @Override public @CheckForNull R getLastBuild() { return buildMixIn.getLastBuild(); } @Override public R getNearestBuild(int n) { return buildMixIn.getNearestBuild(n); } @Override public R getNearestOldBuild(int n) { return buildMixIn.getNearestOldBuild(n); } /** * Type token for the corresponding build type. * The build class must have two constructors: * one taking this project type; * and one taking this project type, then {@link File}. */ protected abstract Class<R> getBuildClass(); /** * Creates a new build of this project for immediate execution. */ protected synchronized R newBuild() throws IOException { return buildMixIn.newBuild(); } /** * Loads an existing build record from disk. */ protected R loadBuild(File dir) throws IOException { return buildMixIn.loadBuild(dir); } /** * {@inheritDoc} * * <p> * Note that this method returns a read-only view of {@link Action}s. * {@link BuildStep}s and others who want to add a project action * should do so by implementing {@link BuildStep#getProjectActions(AbstractProject)}. * * @see TransientProjectActionFactory */ @SuppressWarnings("deprecation") @Override public List<Action> getActions() { // add all the transient actions, too List<Action> actions = new Vector<Action>(super.getActions()); actions.addAll(transientActions); // return the read only list to cause a failure on plugins who try to add an action here return Collections.unmodifiableList(actions); } /** * Gets the {@link Node} where this project was last built on. * * @return * null if no information is available (for example, * if no build was done yet.) */ public Node getLastBuiltOn() { // where was it built on? AbstractBuild b = getLastBuild(); if(b==null) return null; else return b.getBuiltOn(); } public Object getSameNodeConstraint() { return this; // in this way, any member that wants to run with the main guy can nominate the project itself } public final Task getOwnerTask() { return this; } @Nonnull public Authentication getDefaultAuthentication() { // backward compatible behaviour. return ACL.SYSTEM; } @Nonnull @Override public Authentication getDefaultAuthentication(Queue.Item item) { return getDefaultAuthentication(); } /** * {@inheritDoc} * * <p> * A project must be blocked if its own previous build is in progress, * or if the blockBuildWhenUpstreamBuilding option is true and an upstream * project is building, but derived classes can also check other conditions. */ @Override public boolean isBuildBlocked() { return getCauseOfBlockage()!=null; } public String getWhyBlocked() { CauseOfBlockage cb = getCauseOfBlockage(); return cb!=null ? cb.getShortDescription() : null; } /** * @deprecated use {@link BlockedBecauseOfBuildInProgress} instead. */ @Deprecated public static class BecauseOfBuildInProgress extends BlockedBecauseOfBuildInProgress { public BecauseOfBuildInProgress(@Nonnull AbstractBuild<?, ?> build) { super(build); } } /** * Because the downstream build is in progress, and we are configured to wait for that. */ public static class BecauseOfDownstreamBuildInProgress extends CauseOfBlockage { public final AbstractProject<?,?> up; public BecauseOfDownstreamBuildInProgress(AbstractProject<?,?> up) { this.up = up; } @Override public String getShortDescription() { return Messages.AbstractProject_DownstreamBuildInProgress(up.getName()); } } /** * Because the upstream build is in progress, and we are configured to wait for that. */ public static class BecauseOfUpstreamBuildInProgress extends CauseOfBlockage { public final AbstractProject<?,?> up; public BecauseOfUpstreamBuildInProgress(AbstractProject<?,?> up) { this.up = up; } @Override public String getShortDescription() { return Messages.AbstractProject_UpstreamBuildInProgress(up.getName()); } } @Override public CauseOfBlockage getCauseOfBlockage() { // Block builds until they are done with post-production if (isLogUpdated() && !isConcurrentBuild()) { final R lastBuild = getLastBuild(); if (lastBuild != null) { return new BlockedBecauseOfBuildInProgress(lastBuild); } else { // The build has been likely deleted after the isLogUpdated() call. // Another cause may be an API implemetation glitсh in the implementation for AbstractProject. // Anyway, we should let the code go then. LOGGER.log(Level.FINE, "The last build has been deleted during the non-concurrent cause creation. The build is not blocked anymore"); } } if (blockBuildWhenDownstreamBuilding()) { AbstractProject<?,?> bup = getBuildingDownstream(); if (bup!=null) return new BecauseOfDownstreamBuildInProgress(bup); } if (blockBuildWhenUpstreamBuilding()) { AbstractProject<?,?> bup = getBuildingUpstream(); if (bup!=null) return new BecauseOfUpstreamBuildInProgress(bup); } return null; } /** * Returns the project if any of the downstream project is either * building, waiting, pending or buildable. * <p> * This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ public AbstractProject getBuildingDownstream() { Set<Task> unblockedTasks = Jenkins.getInstance().getQueue().getUnblockedTasks(); for (AbstractProject tup : getTransitiveDownstreamProjects()) { if (tup!=this && (tup.isBuilding() || unblockedTasks.contains(tup))) return tup; } return null; } /** * Returns the project if any of the upstream project is either * building or is in the queue. * <p> * This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ public AbstractProject getBuildingUpstream() { Set<Task> unblockedTasks = Jenkins.getInstance().getQueue().getUnblockedTasks(); for (AbstractProject tup : getTransitiveUpstreamProjects()) { if (tup!=this && (tup.isBuilding() || unblockedTasks.contains(tup))) return tup; } return null; } public List<SubTask> getSubTasks() { List<SubTask> r = new ArrayList<SubTask>(); r.add(this); for (SubTaskContributor euc : SubTaskContributor.all()) r.addAll(euc.forProject(this)); for (JobProperty<? super P> p : properties) r.addAll(p.getSubTasks()); return r; } public @CheckForNull R createExecutable() throws IOException { if(isDisabled()) return null; return newBuild(); } public void checkAbortPermission() { checkPermission(CANCEL); } public boolean hasAbortPermission() { return hasPermission(CANCEL); } /** * Gets the {@link Resource} that represents the workspace of this project. * Useful for locking and mutual exclusion control. * * @deprecated as of 1.319 * Projects no longer have a fixed workspace, ands builds will find an available workspace via * {@link WorkspaceList} for each build (furthermore, that happens after a build is started.) * So a {@link Resource} representation for a workspace at the project level no longer makes sense. * * <p> * If you need to lock a workspace while you do some computation, see the source code of * {@link #pollSCMChanges(TaskListener)} for how to obtain a lock of a workspace through {@link WorkspaceList}. */ @Deprecated public Resource getWorkspaceResource() { return new Resource(getFullDisplayName()+" workspace"); } /** * List of necessary resources to perform the build of this project. */ public ResourceList getResourceList() { final Set<ResourceActivity> resourceActivities = getResourceActivities(); final List<ResourceList> resourceLists = new ArrayList<ResourceList>(1 + resourceActivities.size()); for (ResourceActivity activity : resourceActivities) { if (activity != this && activity != null) { // defensive infinite recursion and null check resourceLists.add(activity.getResourceList()); } } return ResourceList.union(resourceLists); } /** * Set of child resource activities of the build of this project (override in child projects). * @return The set of child resource activities of the build of this project. */ protected Set<ResourceActivity> getResourceActivities() { return Collections.emptySet(); } public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException, InterruptedException { SCM scm = getScm(); if(scm==null) return true; // no SCM FilePath workspace = build.getWorkspace(); workspace.mkdirs(); boolean r = scm.checkout(build, launcher, workspace, listener, changelogFile); if (r) { // Only calcRevisionsFromBuild if checkout was successful. Note that modern SCM implementations // won't reach this line anyway, as they throw AbortExceptions on checkout failure. calcPollingBaseline(build, launcher, listener); } return r; } /** * Pushes the baseline up to the newly checked out revision. */ private void calcPollingBaseline(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { SCMRevisionState baseline = build.getAction(SCMRevisionState.class); if (baseline==null) { try { baseline = getScm().calcRevisionsFromBuild(build, launcher, listener); } catch (AbstractMethodError e) { baseline = SCMRevisionState.NONE; // pre-1.345 SCM implementations, which doesn't use the baseline in polling } if (baseline!=null) build.addAction(baseline); } pollingBaseline = baseline; } /** * Checks if there's any update in SCM, and returns true if any is found. * * @deprecated as of 1.346 * Use {@link #poll(TaskListener)} instead. */ @Deprecated public boolean pollSCMChanges( TaskListener listener ) { return poll(listener).hasChanges(); } /** * Checks if there's any update in SCM, and returns true if any is found. * * <p> * The implementation is responsible for ensuring mutual exclusion between polling and builds * if necessary. * * @since 1.345 */ public PollingResult poll( TaskListener listener ) { SCM scm = getScm(); if (scm==null) { listener.getLogger().println(Messages.AbstractProject_NoSCM()); return NO_CHANGES; } if (!isBuildable()) { listener.getLogger().println(Messages.AbstractProject_Disabled()); return NO_CHANGES; } SCMDecisionHandler veto = SCMDecisionHandler.firstShouldPollVeto(this); if (veto != null) { listener.getLogger().println(Messages.AbstractProject_PollingVetoed(veto)); return NO_CHANGES; } R lb = getLastBuild(); if (lb==null) { listener.getLogger().println(Messages.AbstractProject_NoBuilds()); return isInQueue() ? NO_CHANGES : BUILD_NOW; } if (pollingBaseline==null) { R success = getLastSuccessfulBuild(); // if we have a persisted baseline, we'll find it by this for (R r=lb; r!=null; r=r.getPreviousBuild()) { SCMRevisionState s = r.getAction(SCMRevisionState.class); if (s!=null) { pollingBaseline = s; break; } if (r==success) break; // searched far enough } // NOTE-NO-BASELINE: // if we don't have baseline yet, it means the data is built by old Hudson that doesn't set the baseline // as action, so we need to compute it. This happens later. } try { SCMPollListener.fireBeforePolling(this, listener); PollingResult r = _poll(listener, scm); SCMPollListener.firePollingSuccess(this,listener, r); return r; } catch (AbortException e) { listener.getLogger().println(e.getMessage()); listener.fatalError(Messages.AbstractProject_Aborted()); LOGGER.log(Level.FINE, "Polling "+this+" aborted",e); SCMPollListener.firePollingFailed(this, listener,e); return NO_CHANGES; } catch (IOException e) { e.printStackTrace(listener.fatalError(e.getMessage())); SCMPollListener.firePollingFailed(this, listener,e); return NO_CHANGES; } catch (InterruptedException e) { e.printStackTrace(listener.fatalError(Messages.AbstractProject_PollingABorted())); SCMPollListener.firePollingFailed(this, listener,e); return NO_CHANGES; } catch (RuntimeException e) { SCMPollListener.firePollingFailed(this, listener,e); throw e; } catch (Error e) { SCMPollListener.firePollingFailed(this, listener,e); throw e; } } /** * {@link #poll(TaskListener)} method without the try/catch block that does listener notification and . */ private PollingResult _poll(TaskListener listener, SCM scm) throws IOException, InterruptedException { if (scm.requiresWorkspaceForPolling()) { R b = getSomeBuildWithExistingWorkspace(); if (b == null) b = getLastBuild(); // lock the workspace for the given build FilePath ws=b.getWorkspace(); WorkspaceOfflineReason workspaceOfflineReason = workspaceOffline( b ); if ( workspaceOfflineReason != null ) { // workspace offline for (WorkspaceBrowser browser : ExtensionList.lookup(WorkspaceBrowser.class)) { ws = browser.getWorkspace(this); if (ws != null) { return pollWithWorkspace(listener, scm, b, ws, browser.getWorkspaceList()); } } // At this point we start thinking about triggering a build just to get a workspace, // because otherwise there's no way we can detect changes. // However, first there are some conditions in which we do not want to do so. // give time for agents to come online if we are right after reconnection (JENKINS-8408) long running = Jenkins.getInstance().getInjector().getInstance(Uptime.class).getUptime(); long remaining = TimeUnit2.MINUTES.toMillis(10)-running; if (remaining>0 && /* this logic breaks tests of polling */!Functions.getIsUnitTest()) { listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(remaining/1000)); listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); return NO_CHANGES; } // Do not trigger build, if no suitable agent is online if (workspaceOfflineReason.equals(WorkspaceOfflineReason.all_suitable_nodes_are_offline)) { // No suitable executor is online listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(running/1000)); listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); return NO_CHANGES; } Label label = getAssignedLabel(); if (label != null && label.isSelfLabel()) { // if the build is fixed on a node, then attempting a build will do us // no good. We should just wait for the agent to come back. listener.getLogger().print(Messages.AbstractProject_NoWorkspace()); listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); return NO_CHANGES; } listener.getLogger().println( ws==null ? Messages.AbstractProject_WorkspaceOffline() : Messages.AbstractProject_NoWorkspace()); if (isInQueue()) { listener.getLogger().println(Messages.AbstractProject_AwaitingBuildForWorkspace()); return NO_CHANGES; } // build now, or nothing will ever be built listener.getLogger().print(Messages.AbstractProject_NewBuildForWorkspace()); listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); return BUILD_NOW; } else { WorkspaceList l = b.getBuiltOn().toComputer().getWorkspaceList(); return pollWithWorkspace(listener, scm, b, ws, l); } } else { // polling without workspace LOGGER.fine("Polling SCM changes of " + getName()); if (pollingBaseline==null) // see NOTE-NO-BASELINE above calcPollingBaseline(getLastBuild(),null,listener); PollingResult r = scm.poll(this, null, null, listener, pollingBaseline); pollingBaseline = r.remote; return r; } } private PollingResult pollWithWorkspace(TaskListener listener, SCM scm, R lb, @Nonnull FilePath ws, WorkspaceList l) throws InterruptedException, IOException { // if doing non-concurrent build, acquire a workspace in a way that causes builds to block for this workspace. // this prevents multiple workspaces of the same job --- the behavior of Hudson < 1.319. // // OTOH, if a concurrent build is chosen, the user is willing to create a multiple workspace, // so better throughput is achieved over time (modulo the initial cost of creating that many workspaces) // by having multiple workspaces Node node = lb.getBuiltOn(); Launcher launcher = ws.createLauncher(listener).decorateByEnv(getEnvironment(node,listener)); WorkspaceList.Lease lease = l.acquire(ws, !concurrentBuild); try { String nodeName = node != null ? node.getSelfLabel().getName() : "[node_unavailable]"; listener.getLogger().println("Polling SCM changes on " + nodeName); LOGGER.fine("Polling SCM changes of " + getName()); if (pollingBaseline==null) // see NOTE-NO-BASELINE above calcPollingBaseline(lb,launcher,listener); PollingResult r = scm.poll(this, launcher, ws, listener, pollingBaseline); pollingBaseline = r.remote; return r; } finally { lease.release(); } } enum WorkspaceOfflineReason { nonexisting_workspace, builton_node_gone, builton_node_no_executors, all_suitable_nodes_are_offline, use_ondemand_slave } /** * Returns true if all suitable nodes for the job are offline. * */ private boolean isAllSuitableNodesOffline(R build) { Label label = getAssignedLabel(); List<Node> allNodes = Jenkins.getInstance().getNodes(); if (label != null) { //Invalid label. Put in queue to make administrator fix if(label.getNodes().isEmpty()) { return false; } //Returns true, if all suitable nodes are offline return label.isOffline(); } else { if(canRoam) { for (Node n : Jenkins.getInstance().getNodes()) { Computer c = n.toComputer(); if (c != null && c.isOnline() && c.isAcceptingTasks() && n.getMode() == Mode.NORMAL) { // Some executor is online that is ready and this job can run anywhere return false; } } //We can roam, check that the master is set to be used as much as possible, and not tied jobs only. if(Jenkins.getInstance().getMode() == Mode.EXCLUSIVE) { return true; } else { return false; } } } return true; } private WorkspaceOfflineReason workspaceOffline(R build) throws IOException, InterruptedException { FilePath ws = build.getWorkspace(); Label label = getAssignedLabel(); if (isAllSuitableNodesOffline(build)) { Collection<Cloud> applicableClouds = label == null ? Jenkins.getInstance().clouds : label.getClouds(); return applicableClouds.isEmpty() ? WorkspaceOfflineReason.all_suitable_nodes_are_offline : WorkspaceOfflineReason.use_ondemand_slave; } if (ws==null || !ws.exists()) { return WorkspaceOfflineReason.nonexisting_workspace; } Node builtOn = build.getBuiltOn(); if (builtOn == null) { // node built-on doesn't exist anymore return WorkspaceOfflineReason.builton_node_gone; } if (builtOn.toComputer() == null) { // node still exists, but has 0 executors - o.s.l.t. return WorkspaceOfflineReason.builton_node_no_executors; } return null; } /** * Returns true if this user has made a commit to this project. * * @since 1.191 */ public boolean hasParticipant(User user) { for( R build = getLastBuild(); build!=null; build=build.getPreviousBuild()) if(build.hasParticipant(user)) return true; return false; } @Exported public SCM getScm() { return scm; } public void setScm(SCM scm) throws IOException { this.scm = scm; save(); } /** * Adds a new {@link Trigger} to this {@link Project} if not active yet. */ public void addTrigger(Trigger<?> trigger) throws IOException { addToList(trigger,triggers()); } public void removeTrigger(TriggerDescriptor trigger) throws IOException { removeFromList(trigger,triggers()); } protected final synchronized <T extends Describable<T>> void addToList( T item, List<T> collection ) throws IOException { //No support to replace item in position, remove then add removeFromList(item.getDescriptor(), collection); collection.add(item); save(); updateTransientActions(); } protected final synchronized <T extends Describable<T>> void removeFromList(Descriptor<T> item, List<T> collection) throws IOException { final Iterator<T> iCollection = collection.iterator(); while(iCollection.hasNext()) { final T next = iCollection.next(); if(next.getDescriptor()==item) { // found it iCollection.remove(); save(); updateTransientActions(); return; } } } @SuppressWarnings("unchecked") @Override public Map<TriggerDescriptor,Trigger<?>> getTriggers() { return triggers().toMap(); } /** * Gets the specific trigger, or null if the propert is not configured for this job. */ public <T extends Trigger> T getTrigger(Class<T> clazz) { for (Trigger p : triggers()) { if(clazz.isInstance(p)) return clazz.cast(p); } return null; } // // // fingerprint related // // /** * True if the builds of this project produces {@link Fingerprint} records. */ public abstract boolean isFingerprintConfigured(); /** * Gets the other {@link AbstractProject}s that should be built * when a build of this project is completed. */ @Exported public final List<AbstractProject> getDownstreamProjects() { return Jenkins.getInstance().getDependencyGraph().getDownstream(this); } @Exported public final List<AbstractProject> getUpstreamProjects() { return Jenkins.getInstance().getDependencyGraph().getUpstream(this); } /** * Returns only those upstream projects that defines {@link BuildTrigger} to this project. * This is a subset of {@link #getUpstreamProjects()} * <p>No longer used in the UI. * @return A List of upstream projects that has a {@link BuildTrigger} to this project. */ public final List<AbstractProject> getBuildTriggerUpstreamProjects() { ArrayList<AbstractProject> result = new ArrayList<AbstractProject>(); for (AbstractProject<?,?> ap : getUpstreamProjects()) { BuildTrigger buildTrigger = ap.getPublishersList().get(BuildTrigger.class); if (buildTrigger != null) if (buildTrigger.getChildProjects(ap).contains(this)) result.add(ap); } return result; } /** * Gets all the upstream projects including transitive upstream projects. * * @since 1.138 */ public final Set<AbstractProject> getTransitiveUpstreamProjects() { return Jenkins.getInstance().getDependencyGraph().getTransitiveUpstream(this); } /** * Gets all the downstream projects including transitive downstream projects. * * @since 1.138 */ public final Set<AbstractProject> getTransitiveDownstreamProjects() { return Jenkins.getInstance().getDependencyGraph().getTransitiveDownstream(this); } /** * Gets the dependency relationship map between this project (as the source) * and that project (as the sink.) * * @return * can be empty but not null. build number of this project to the build * numbers of that project. */ public SortedMap<Integer, RangeSet> getRelationship(AbstractProject that) { TreeMap<Integer,RangeSet> r = new TreeMap<Integer,RangeSet>(REVERSE_INTEGER_COMPARATOR); checkAndRecord(that, r, this.getBuilds()); // checkAndRecord(that, r, that.getBuilds()); return r; } /** * Helper method for getDownstreamRelationship. * * For each given build, find the build number range of the given project and put that into the map. */ private void checkAndRecord(AbstractProject that, TreeMap<Integer, RangeSet> r, Collection<R> builds) { for (R build : builds) { RangeSet rs = build.getDownstreamRelationship(that); if(rs==null || rs.isEmpty()) continue; int n = build.getNumber(); RangeSet value = r.get(n); if(value==null) r.put(n,rs); else value.add(rs); } } /** * Builds the dependency graph. * Since 1.558, not abstract and by default includes dependencies contributed by {@link #triggers()}. */ protected void buildDependencyGraph(DependencyGraph graph) { triggers().buildDependencyGraph(this, graph); } @Override protected SearchIndexBuilder makeSearchIndex() { return getParameterizedJobMixIn().extendSearchIndex(super.makeSearchIndex()); } @Override protected HistoryWidget createHistoryWidget() { return buildMixIn.createHistoryWidget(); } public boolean isParameterized() { return getParameterizedJobMixIn().isParameterized(); } // // // actions // // /** * Schedules a new build command. */ public void doBuild( StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay ) throws IOException, ServletException { getParameterizedJobMixIn().doBuild(req, rsp, delay); } /** @deprecated use {@link #doBuild(StaplerRequest, StaplerResponse, TimeDuration)} */ @Deprecated public void doBuild(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { doBuild(req, rsp, TimeDuration.fromString(req.getParameter("delay"))); } /** * Computes the delay by taking the default value and the override in the request parameter into the account. * * @deprecated as of 1.489 * Inject {@link TimeDuration}. */ @Deprecated public int getDelay(StaplerRequest req) throws ServletException { String delay = req.getParameter("delay"); if (delay==null) return getQuietPeriod(); try { // TODO: more unit handling if(delay.endsWith("sec")) delay=delay.substring(0,delay.length()-3); if(delay.endsWith("secs")) delay=delay.substring(0,delay.length()-4); return Integer.parseInt(delay); } catch (NumberFormatException e) { throw new ServletException("Invalid delay parameter value: "+delay); } } /** * Supports build trigger with parameters via an HTTP GET or POST. * Currently only String parameters are supported. */ public void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException { getParameterizedJobMixIn().doBuildWithParameters(req, rsp, delay); } /** @deprecated use {@link #doBuildWithParameters(StaplerRequest, StaplerResponse, TimeDuration)} */ @Deprecated public void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { doBuildWithParameters(req, rsp, TimeDuration.fromString(req.getParameter("delay"))); } /** * Schedules a new SCM polling command. */ public void doPolling( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { BuildAuthorizationToken.checkPermission((Job) this, authToken, req, rsp); schedulePolling(); rsp.sendRedirect("."); } /** * Cancels a scheduled build. */ @RequirePOST public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { getParameterizedJobMixIn().doCancelQueue(req, rsp); } @Override protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { super.submit(req,rsp); JSONObject json = req.getSubmittedForm(); makeDisabled(json.optBoolean("disable")); jdk = json.optString("jdk", null); if(json.optBoolean("hasCustomQuietPeriod", json.has("quiet_period"))) { quietPeriod = json.optInt("quiet_period"); } else { quietPeriod = null; } if(json.optBoolean("hasCustomScmCheckoutRetryCount", json.has("scmCheckoutRetryCount"))) { scmCheckoutRetryCount = json.optInt("scmCheckoutRetryCount"); } else { scmCheckoutRetryCount = null; } blockBuildWhenDownstreamBuilding = json.optBoolean("blockBuildWhenDownstreamBuilding"); blockBuildWhenUpstreamBuilding = json.optBoolean("blockBuildWhenUpstreamBuilding"); if(req.hasParameter("customWorkspace.directory")) { // Workaround for JENKINS-25221 while plugins are being updated. LOGGER.log(Level.WARNING, "label assignment is using legacy 'customWorkspace.directory'"); customWorkspace = Util.fixEmptyAndTrim(req.getParameter("customWorkspace.directory")); } else if(json.optBoolean("hasCustomWorkspace", json.has("customWorkspace"))) { customWorkspace = Util.fixEmptyAndTrim(json.optString("customWorkspace")); } else { customWorkspace = null; } if (json.has("scmCheckoutStrategy")) scmCheckoutStrategy = req.bindJSON(SCMCheckoutStrategy.class, json.getJSONObject("scmCheckoutStrategy")); else scmCheckoutStrategy = null; if(json.optBoolean("hasSlaveAffinity", json.has("label"))) { assignedNode = Util.fixEmptyAndTrim(json.optString("label")); } else if(req.hasParameter("_.assignedLabelString")) { // Workaround for JENKINS-25372 while plugin is being updated. // Keep this condition second for JENKINS-25533 LOGGER.log(Level.WARNING, "label assignment is using legacy '_.assignedLabelString'"); assignedNode = Util.fixEmptyAndTrim(req.getParameter("_.assignedLabelString")); } else { assignedNode = null; } canRoam = assignedNode==null; keepDependencies = json.has("keepDependencies"); concurrentBuild = json.optBoolean("concurrentBuild"); authToken = BuildAuthorizationToken.create(req); setScm(SCMS.parseSCM(req,this)); for (Trigger t : triggers()) t.stop(); triggers.replaceBy(buildDescribable(req, Trigger.for_(this))); for (Trigger t : triggers()) t.start(this,true); } /** * @deprecated * As of 1.261. Use {@link #buildDescribable(StaplerRequest, List)} instead. */ @Deprecated protected final <T extends Describable<T>> List<T> buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors, String prefix) throws FormException, ServletException { return buildDescribable(req,descriptors); } protected final <T extends Describable<T>> List<T> buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors) throws FormException, ServletException { JSONObject data = req.getSubmittedForm(); List<T> r = new Vector<T>(); for (Descriptor<T> d : descriptors) { String safeName = d.getJsonSafeClassName(); if (req.getParameter(safeName) != null) { T instance = d.newInstance(req, data.getJSONObject(safeName)); r.add(instance); } } return r; } /** * Serves the workspace files. */ public DirectoryBrowserSupport doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException { checkPermission(Item.WORKSPACE); FilePath ws = getSomeWorkspace(); if ((ws == null) || (!ws.exists())) { // if there's no workspace, report a nice error message // Would be good if when asked for *plain*, do something else! // (E.g. return 404, or send empty doc.) // Not critical; client can just check if content type is not text/plain, // which also serves to detect old versions of Hudson. req.getView(this,"noWorkspace.jelly").forward(req,rsp); return null; } else { Computer c = ws.toComputer(); String title; if (c == null) { title = Messages.AbstractProject_WorkspaceTitle(getDisplayName()); } else { title = Messages.AbstractProject_WorkspaceTitleOnComputer(getDisplayName(), c.getDisplayName()); } return new DirectoryBrowserSupport(this, ws, title, "folder.png", true); } } /** * Wipes out the workspace. */ @RequirePOST public HttpResponse doDoWipeOutWorkspace() throws IOException, ServletException, InterruptedException { checkPermission(Functions.isWipeOutPermissionEnabled() ? WIPEOUT : BUILD); R b = getSomeBuildWithWorkspace(); FilePath ws = b!=null ? b.getWorkspace() : null; if (ws!=null && getScm().processWorkspaceBeforeDeletion(this, ws, b.getBuiltOn())) { ws.deleteRecursive(); for (WorkspaceListener wl : WorkspaceListener.all()) { wl.afterDelete(this); } return new HttpRedirect("."); } else { // If we get here, that means the SCM blocked the workspace deletion. return new ForwardToView(this,"wipeOutWorkspaceBlocked.jelly"); } } @CLIMethod(name="disable-job") @RequirePOST public HttpResponse doDisable() throws IOException, ServletException { checkPermission(CONFIGURE); makeDisabled(true); return new HttpRedirect("."); } @CLIMethod(name="enable-job") @RequirePOST public HttpResponse doEnable() throws IOException, ServletException { checkPermission(CONFIGURE); makeDisabled(false); return new HttpRedirect("."); } /** * RSS feed for changes in this project. */ public void doRssChangelog( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { class FeedItem { ChangeLogSet.Entry e; int idx; public FeedItem(Entry e, int idx) { this.e = e; this.idx = idx; } AbstractBuild<?,?> getBuild() { return e.getParent().build; } } List<FeedItem> entries = new ArrayList<FeedItem>(); for(R r=getLastBuild(); r!=null; r=r.getPreviousBuild()) { int idx=0; for( ChangeLogSet.Entry e : r.getChangeSet()) entries.add(new FeedItem(e,idx++)); } RSS.forwardToRss( getDisplayName()+' '+getScm().getDescriptor().getDisplayName()+" changes", getUrl()+"changes", entries, new FeedAdapter<FeedItem>() { public String getEntryTitle(FeedItem item) { return "#"+item.getBuild().number+' '+item.e.getMsg()+" ("+item.e.getAuthor()+")"; } public String getEntryUrl(FeedItem item) { return item.getBuild().getUrl()+"changes#detail"+item.idx; } public String getEntryID(FeedItem item) { return getEntryUrl(item); } public String getEntryDescription(FeedItem item) { StringBuilder buf = new StringBuilder(); for(String path : item.e.getAffectedPaths()) buf.append(path).append('\n'); return buf.toString(); } public Calendar getEntryTimestamp(FeedItem item) { return item.getBuild().getTimestamp(); } public String getEntryAuthor(FeedItem entry) { return JenkinsLocationConfiguration.get().getAdminAddress(); } }, req, rsp ); } /** * {@link AbstractProject} subtypes should implement this base class as a descriptor. * * @since 1.294 */ public static abstract class AbstractProjectDescriptor extends TopLevelItemDescriptor { /** * {@link AbstractProject} subtypes can override this method to veto some {@link Descriptor}s * from showing up on their configuration screen. This is often useful when you are building * a workflow/company specific project type, where you want to limit the number of choices * given to the users. * * <p> * Some {@link Descriptor}s define their own schemes for controlling applicability * (such as {@link BuildStepDescriptor#isApplicable(Class)}), * This method works like AND in conjunction with them; * Both this method and that method need to return true in order for a given {@link Descriptor} * to show up for the given {@link Project}. * * <p> * The default implementation returns true for everything. * * @see BuildStepDescriptor#isApplicable(Class) * @see BuildWrapperDescriptor#isApplicable(AbstractProject) * @see TriggerDescriptor#isApplicable(Item) */ @Override public boolean isApplicable(Descriptor descriptor) { return true; } @Restricted(DoNotUse.class) public FormValidation doCheckAssignedLabelString(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String value) { // Provide a legacy interface in case plugins are not going through p:config-assignedLabel // see: JENKINS-25372 LOGGER.log(Level.WARNING, "checking label via legacy '_.assignedLabelString'"); return doCheckLabel(project, value); } public FormValidation doCheckLabel(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String value) { return validateLabelExpression(value, project); } /** * Validate label expression string. * * @param project May be specified to perform project specific validation. * @since 1.590 */ public static @Nonnull FormValidation validateLabelExpression(String value, @CheckForNull AbstractProject<?, ?> project) { if (Util.fixEmpty(value)==null) return FormValidation.ok(); // nothing typed yet try { Label.parseExpression(value); } catch (ANTLRException e) { return FormValidation.error(e, Messages.AbstractProject_AssignedLabelString_InvalidBooleanExpression(e.getMessage())); } Jenkins j = Jenkins.getInstance(); Label l = j.getLabel(value); if (l.isEmpty()) { for (LabelAtom a : l.listAtoms()) { if (a.isEmpty()) { LabelAtom nearest = LabelAtom.findNearest(a.getName()); return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch_DidYouMean(a.getName(),nearest.getDisplayName())); } } return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch()); } if (project != null) { for (AbstractProject.LabelValidator v : j .getExtensionList(AbstractProject.LabelValidator.class)) { FormValidation result = v.check(project, l); if (!FormValidation.Kind.OK.equals(result.kind)) { return result; } } } return FormValidation.okWithMarkup(Messages.AbstractProject_LabelLink( j.getRootUrl(), Util.escape(l.getName()), l.getUrl(), l.getNodes().size(), l.getClouds().size()) ); } public FormValidation doCheckCustomWorkspace(@QueryParameter String customWorkspace){ if(Util.fixEmptyAndTrim(customWorkspace)==null) return FormValidation.error(Messages.AbstractProject_CustomWorkspaceEmpty()); else return FormValidation.ok(); } public AutoCompletionCandidates doAutoCompleteUpstreamProjects(@QueryParameter String value) { AutoCompletionCandidates candidates = new AutoCompletionCandidates(); List<Job> jobs = Jenkins.getInstance().getItems(Job.class); for (Job job: jobs) { if (job.getFullName().startsWith(value)) { if (job.hasPermission(Item.READ)) { candidates.add(job.getFullName()); } } } return candidates; } @Restricted(DoNotUse.class) public AutoCompletionCandidates doAutoCompleteAssignedLabelString(@QueryParameter String value) { // Provide a legacy interface in case plugins are not going through p:config-assignedLabel // see: JENKINS-25372 LOGGER.log(Level.WARNING, "autocompleting label via legacy '_.assignedLabelString'"); return doAutoCompleteLabel(value); } public AutoCompletionCandidates doAutoCompleteLabel(@QueryParameter String value) { AutoCompletionCandidates c = new AutoCompletionCandidates(); Set<Label> labels = Jenkins.getInstance().getLabels(); List<String> queries = new AutoCompleteSeeder(value).getSeeds(); for (String term : queries) { for (Label l : labels) { if (l.getName().startsWith(term)) { c.add(l.getName()); } } } return c; } public List<SCMCheckoutStrategyDescriptor> getApplicableSCMCheckoutStrategyDescriptors(AbstractProject p) { return SCMCheckoutStrategyDescriptor._for(p); } /** * Utility class for taking the current input value and computing a list * of potential terms to match against the list of defined labels. */ static class AutoCompleteSeeder { private String source; AutoCompleteSeeder(String source) { this.source = source; } List<String> getSeeds() { ArrayList<String> terms = new ArrayList<String>(); boolean trailingQuote = source.endsWith("\""); boolean leadingQuote = source.startsWith("\""); boolean trailingSpace = source.endsWith(" "); if (trailingQuote || (trailingSpace && !leadingQuote)) { terms.add(""); } else { if (leadingQuote) { int quote = source.lastIndexOf('"'); if (quote == 0) { terms.add(source.substring(1)); } else { terms.add(""); } } else { int space = source.lastIndexOf(' '); if (space > -1) { terms.add(source.substring(space+1)); } else { terms.add(source); } } } return terms; } } } /** * Finds a {@link AbstractProject} that has the name closest to the given name. * @see Items#findNearest */ public static @CheckForNull AbstractProject findNearest(String name) { return findNearest(name,Jenkins.getInstance()); } /** * Finds a {@link AbstractProject} whose name (when referenced from the specified context) is closest to the given name. * * @since 1.419 * @see Items#findNearest */ public static @CheckForNull AbstractProject findNearest(String name, ItemGroup context) { return Items.findNearest(AbstractProject.class, name, context); } private static final Comparator<Integer> REVERSE_INTEGER_COMPARATOR = new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { return o2-o1; } }; private static final Logger LOGGER = Logger.getLogger(AbstractProject.class.getName()); /** * @deprecated Just use {@link #CANCEL}. */ @Deprecated public static final Permission ABORT = CANCEL; /** * @deprecated Use {@link ParameterizedJobMixIn#BUILD_NOW_TEXT}. */ @Deprecated public static final Message<AbstractProject> BUILD_NOW_TEXT = new Message<AbstractProject>(); /** * Used for CLI binding. */ @CLIResolver public static AbstractProject resolveForCLI( @Argument(required=true,metaVar="NAME",usage="Job name") String name) throws CmdLineException { AbstractProject item = Jenkins.getInstance().getItemByFullName(name, AbstractProject.class); if (item==null) { AbstractProject project = AbstractProject.findNearest(name); throw new CmdLineException(null, project == null ? Messages.AbstractItem_NoSuchJobExistsWithoutSuggestion(name) : Messages.AbstractItem_NoSuchJobExists(name, project.getFullName())); } return item; } public String getCustomWorkspace() { return customWorkspace; } /** * User-specified workspace directory, or null if it's up to Jenkins. * * <p> * Normally a project uses the workspace location assigned by its parent container, * but sometimes people have builds that have hard-coded paths. * * <p> * This is not {@link File} because it may have to hold a path representation on another OS. * * <p> * If this path is relative, it's resolved against {@link Node#getRootPath()} on the node where this workspace * is prepared. * * @since 1.410 */ public void setCustomWorkspace(String customWorkspace) throws IOException { this.customWorkspace= Util.fixEmptyAndTrim(customWorkspace); save(); } /** * Plugins may want to contribute additional restrictions on the use of specific labels for specific projects. * This extension point allows such restrictions. * * @since 1.540 */ public static abstract class LabelValidator implements ExtensionPoint { /** * Check the use of the label within the specified context. * * @param project the project that wants to restrict itself to the specified label. * @param label the label that the project wants to restrict itself to. * @return the {@link FormValidation} result. */ @Nonnull public abstract FormValidation check(@Nonnull AbstractProject<?, ?> project, @Nonnull Label label); } }