/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Brian Westrich, Jean-Baptiste Quenot, id:cactusman
* 2015 Kanstantsin Shautsou
*
* 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.triggers;
import antlr.ANTLRException;
import com.google.common.base.Preconditions;
import hudson.Extension;
import hudson.Util;
import hudson.console.AnnotatedLargeText;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.AdministrativeMonitor;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Run;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.util.FlushProofOutputStream;
import hudson.util.FormValidation;
import hudson.util.IOUtils;
import hudson.util.NamingThreadFactory;
import hudson.util.SequentialExecutionQueue;
import hudson.util.StreamTaskListener;
import hudson.util.TimeUnit2;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.model.RunAction2;
import jenkins.scm.SCMDecisionHandler;
import jenkins.triggers.SCMTriggerItem;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.apache.commons.jelly.XMLOutput;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import static java.util.logging.Level.WARNING;
/**
* {@link Trigger} that checks for SCM updates periodically.
*
* You can add UI elements under the SCM section by creating a
* config.jelly or config.groovy in the resources area for
* your class that inherits from SCMTrigger and has the
* @{@link hudson.model.Extension} annotation. The UI should
* be wrapped in an f:section element to denote it.
*
* @author Kohsuke Kawaguchi
*/
public class SCMTrigger extends Trigger<Item> {
private boolean ignorePostCommitHooks;
public SCMTrigger(String scmpoll_spec) throws ANTLRException {
this(scmpoll_spec, false);
}
@DataBoundConstructor
public SCMTrigger(String scmpoll_spec, boolean ignorePostCommitHooks) throws ANTLRException {
super(scmpoll_spec);
this.ignorePostCommitHooks = ignorePostCommitHooks;
}
/**
* This trigger wants to ignore post-commit hooks.
* <p>
* SCM plugins must respect this and not run this trigger for post-commit notifications.
*
* @since 1.493
*/
public boolean isIgnorePostCommitHooks() {
return this.ignorePostCommitHooks;
}
@Override
public void run() {
if (job == null) {
return;
}
run(null);
}
/**
* Run the SCM trigger with additional build actions. Used by SubversionRepositoryStatus
* to trigger a build at a specific revisionn number.
*
* @param additionalActions
* @since 1.375
*/
public void run(Action[] additionalActions) {
if (job == null) {
return;
}
DescriptorImpl d = getDescriptor();
LOGGER.fine("Scheduling a polling for "+job);
if (d.synchronousPolling) {
LOGGER.fine("Running the trigger directly without threading, " +
"as it's already taken care of by Trigger.Cron");
new Runner(additionalActions).run();
} else {
// schedule the polling.
// even if we end up submitting this too many times, that's OK.
// the real exclusion control happens inside Runner.
LOGGER.fine("scheduling the trigger to (asynchronously) run");
d.queue.execute(new Runner(additionalActions));
d.clogCheck();
}
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}
@Override
public Collection<? extends Action> getProjectActions() {
if (job == null) {
return Collections.emptyList();
}
return Collections.singleton(new SCMAction());
}
/**
* Returns the file that records the last/current polling activity.
*/
public File getLogFile() {
return new File(job.getRootDir(),"scm-polling.log");
}
@Extension @Symbol("scm")
public static class DescriptorImpl extends TriggerDescriptor {
private static ThreadFactory threadFactory() {
return new NamingThreadFactory(Executors.defaultThreadFactory(), "SCMTrigger");
}
/**
* Used to control the execution of the polling tasks.
* <p>
* This executor implementation has a semantics suitable for polling. Namely, no two threads will try to poll the same project
* at once, and multiple polling requests to the same job will be combined into one. Note that because executor isn't aware
* of a potential workspace lock between a build and a polling, we may end up using executor threads unwisely --- they
* may block.
*/
private transient final SequentialExecutionQueue queue = new SequentialExecutionQueue(Executors.newSingleThreadExecutor(threadFactory()));
/**
* Whether the projects should be polled all in one go in the order of dependencies. The default behavior is
* that each project polls for changes independently.
*/
public boolean synchronousPolling = false;
/**
* Max number of threads for SCM polling.
* 0 for unbounded.
*/
private int maximumThreads;
public DescriptorImpl() {
load();
resizeThreadPool();
}
public boolean isApplicable(Item item) {
return SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(item) != null;
}
public ExecutorService getExecutor() {
return queue.getExecutors();
}
/**
* Returns true if the SCM polling thread queue has too many jobs
* than it can handle.
*/
public boolean isClogged() {
return queue.isStarving(STARVATION_THRESHOLD);
}
/**
* Checks if the queue is clogged, and if so,
* activate {@link AdministrativeMonitorImpl}.
*/
public void clogCheck() {
AdministrativeMonitor.all().get(AdministrativeMonitorImpl.class).on = isClogged();
}
/**
* Gets the snapshot of {@link Runner}s that are performing polling.
*/
public List<Runner> getRunners() {
return Util.filter(queue.getInProgress(),Runner.class);
}
// originally List<SCMedItem> but known to be used only for logging, in which case the instances are not actually cast to SCMedItem anyway
public List<SCMTriggerItem> getItemsBeingPolled() {
List<SCMTriggerItem> r = new ArrayList<SCMTriggerItem>();
for (Runner i : getRunners())
r.add(i.getTarget());
return r;
}
public String getDisplayName() {
return Messages.SCMTrigger_DisplayName();
}
/**
* Gets the number of concurrent threads used for polling.
*
* @return
* 0 if unlimited.
*/
public int getPollingThreadCount() {
return maximumThreads;
}
/**
* Sets the number of concurrent threads used for SCM polling and resizes the thread pool accordingly
* @param n number of concurrent threads, zero or less means unlimited, maximum is 100
*/
public void setPollingThreadCount(int n) {
// fool proof
if(n<0) n=0;
if(n>100) n=100;
maximumThreads = n;
resizeThreadPool();
}
@Restricted(NoExternalUse.class)
public boolean isPollingThreadCountOptionVisible() {
// unless you have a fair number of projects, this option is likely pointless.
// so let's hide this option for new users to avoid confusing them
// unless it was already changed
// TODO switch to check for SCMTriggerItem
return Jenkins.getInstance().getAllItems(AbstractProject.class).size() > 10
|| getPollingThreadCount() != 0;
}
/**
* Update the {@link ExecutorService} instance.
*/
/*package*/ synchronized void resizeThreadPool() {
queue.setExecutors(
(maximumThreads==0 ? Executors.newCachedThreadPool(threadFactory()) : Executors.newFixedThreadPool(maximumThreads, threadFactory())));
}
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
String t = json.optString("pollingThreadCount",null);
if(t==null || t.length()==0)
setPollingThreadCount(0);
else
setPollingThreadCount(Integer.parseInt(t));
// Save configuration
save();
return true;
}
public FormValidation doCheckPollingThreadCount(@QueryParameter String value) {
if (value != null && "".equals(value.trim()))
return FormValidation.ok();
return FormValidation.validateNonNegativeInteger(value);
}
}
@Extension
public static final class AdministrativeMonitorImpl extends AdministrativeMonitor {
private boolean on;
public boolean isActivated() {
return on;
}
}
/**
* Associated with {@link Run} to show the polling log
* that triggered that build.
*
* @since 1.376
*/
public static class BuildAction implements RunAction2 {
private transient /*final*/ Run<?,?> run;
@Deprecated
public transient /*final*/ AbstractBuild build;
/**
* @since 1.568
*/
public BuildAction(Run<?,?> run) {
this.run = run;
build = run instanceof AbstractBuild ? (AbstractBuild) run : null;
}
@Deprecated
public BuildAction(AbstractBuild build) {
this((Run) build);
}
/**
* @since 1.568
*/
public Run<?,?> getRun() {
return run;
}
/**
* Polling log that triggered the build.
*/
public File getPollingLogFile() {
return new File(run.getRootDir(),"polling.log");
}
public String getIconFileName() {
return "clipboard.png";
}
public String getDisplayName() {
return Messages.SCMTrigger_BuildAction_DisplayName();
}
public String getUrlName() {
return "pollingLog";
}
/**
* Sends out the raw polling log output.
*/
public void doPollingLog(StaplerRequest req, StaplerResponse rsp) throws IOException {
rsp.setContentType("text/plain;charset=UTF-8");
// Prevent jelly from flushing stream so Content-Length header can be added afterwards
FlushProofOutputStream out = new FlushProofOutputStream(rsp.getCompressedOutputStream(req));
try {
getPollingLogText().writeLogTo(0, out);
} finally {
IOUtils.closeQuietly(out);
}
}
public AnnotatedLargeText getPollingLogText() {
return new AnnotatedLargeText<BuildAction>(getPollingLogFile(), Charset.defaultCharset(), true, this);
}
/**
* Used from <tt>polling.jelly</tt> to write annotated polling log to the given output.
*/
public void writePollingLogTo(long offset, XMLOutput out) throws IOException {
// TODO: resurrect compressed log file support
getPollingLogText().writeHtmlTo(offset, out.asWriter());
}
@Override public void onAttached(Run<?, ?> r) {
// unnecessary, existing constructor does this
}
@Override public void onLoad(Run<?, ?> r) {
run = r;
build = run instanceof AbstractBuild ? (AbstractBuild) run : null;
}
}
/**
* Action object for job. Used to display the last polling log.
*/
public final class SCMAction implements Action {
public AbstractProject<?,?> getOwner() {
Item item = getItem();
return item instanceof AbstractProject ? ((AbstractProject) item) : null;
}
/**
* @since 1.568
*/
public Item getItem() {
return job().asItem();
}
public String getIconFileName() {
return "clipboard.png";
}
public String getDisplayName() {
Set<SCMDescriptor<?>> descriptors = new HashSet<SCMDescriptor<?>>();
for (SCM scm : job().getSCMs()) {
descriptors.add(scm.getDescriptor());
}
return descriptors.size() == 1 ? Messages.SCMTrigger_getDisplayName(descriptors.iterator().next().getDisplayName()) : Messages.SCMTrigger_BuildAction_DisplayName();
}
public String getUrlName() {
return "scmPollLog";
}
public String getLog() throws IOException {
return Util.loadFile(getLogFile());
}
/**
* Writes the annotated log to the given output.
* @since 1.350
*/
public void writeLogTo(XMLOutput out) throws IOException {
new AnnotatedLargeText<SCMAction>(getLogFile(),Charset.defaultCharset(),true,this).writeHtmlTo(0,out.asWriter());
}
}
private static final Logger LOGGER = Logger.getLogger(SCMTrigger.class.getName());
/**
* {@link Runnable} that actually performs polling.
*/
public class Runner implements Runnable {
/**
* When did the polling start?
*/
private volatile long startTime;
private Action[] additionalActions;
public Runner() {
this(null);
}
public Runner(Action[] actions) {
Preconditions.checkNotNull(job, "Runner can't be instantiated when job is null");
if (actions == null) {
additionalActions = new Action[0];
} else {
additionalActions = actions;
}
}
/**
* Where the log file is written.
*/
public File getLogFile() {
return SCMTrigger.this.getLogFile();
}
/**
* For which {@link Item} are we polling?
* @since 1.568
*/
public SCMTriggerItem getTarget() {
return job();
}
/**
* When was this polling started?
*/
public long getStartTime() {
return startTime;
}
/**
* Human readable string of when this polling is started.
*/
public String getDuration() {
return Util.getTimeSpanString(System.currentTimeMillis()-startTime);
}
private boolean runPolling() {
try {
// to make sure that the log file contains up-to-date text,
// don't do buffering.
StreamTaskListener listener = new StreamTaskListener(getLogFile());
try {
PrintStream logger = listener.getLogger();
long start = System.currentTimeMillis();
logger.println("Started on "+ DateFormat.getDateTimeInstance().format(new Date()));
boolean result = job().poll(listener).hasChanges();
logger.println("Done. Took "+ Util.getTimeSpanString(System.currentTimeMillis()-start));
if(result)
logger.println("Changes found");
else
logger.println("No changes");
return result;
} catch (Error | RuntimeException e) {
e.printStackTrace(listener.error("Failed to record SCM polling for "+job));
LOGGER.log(Level.SEVERE,"Failed to record SCM polling for "+job,e);
throw e;
} finally {
listener.close();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE,"Failed to record SCM polling for "+job,e);
return false;
}
}
public void run() {
if (job == null) {
return;
}
// we can pre-emtively check the SCMDecisionHandler instances here
// note that job().poll(listener) should also check this
SCMDecisionHandler veto = SCMDecisionHandler.firstShouldPollVeto(job);
if (veto != null) {
try (StreamTaskListener listener = new StreamTaskListener(getLogFile())) {
listener.getLogger().println(
"Skipping polling on " + DateFormat.getDateTimeInstance().format(new Date())
+ " due to veto from " + veto);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to record SCM polling for " + job, e);
}
LOGGER.log(Level.FINE, "Skipping polling for {0} due to veto from {1}",
new Object[]{job.getFullDisplayName(), veto}
);
return;
}
String threadName = Thread.currentThread().getName();
Thread.currentThread().setName("SCM polling for "+job);
try {
startTime = System.currentTimeMillis();
if(runPolling()) {
SCMTriggerItem p = job();
String name = " #"+p.getNextBuildNumber();
SCMTriggerCause cause;
try {
cause = new SCMTriggerCause(getLogFile());
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to parse the polling log",e);
cause = new SCMTriggerCause();
}
Action[] queueActions = new Action[additionalActions.length + 1];
queueActions[0] = new CauseAction(cause);
System.arraycopy(additionalActions, 0, queueActions, 1, additionalActions.length);
if (p.scheduleBuild2(p.getQuietPeriod(), queueActions) != null) {
LOGGER.info("SCM changes detected in "+ job.getFullDisplayName()+". Triggering "+name);
} else {
LOGGER.info("SCM changes detected in "+ job.getFullDisplayName()+". Job is already in the queue");
}
}
} finally {
Thread.currentThread().setName(threadName);
}
}
// as per the requirement of SequentialExecutionQueue, value equality is necessary
@Override
public boolean equals(Object that) {
return that instanceof Runner && job == ((Runner) that)._job();
}
private Item _job() {return job;}
@Override
public int hashCode() {
return job.hashCode();
}
}
@SuppressWarnings("deprecation")
private SCMTriggerItem job() {
return SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(job);
}
public static class SCMTriggerCause extends Cause {
/**
* Only used while ths cause is in the queue.
* Once attached to the build, we'll move this into a file to reduce the memory footprint.
*/
private String pollingLog;
private transient Run run;
public SCMTriggerCause(File logFile) throws IOException {
// TODO: charset of this log file?
this(FileUtils.readFileToString(logFile));
}
public SCMTriggerCause(String pollingLog) {
this.pollingLog = pollingLog;
}
/**
* @deprecated
* Use {@link SCMTrigger.SCMTriggerCause#SCMTriggerCause(String)}.
*/
@Deprecated
public SCMTriggerCause() {
this("");
}
@Override
public void onLoad(Run run) {
this.run = run;
}
@Override
public void onAddedTo(Run build) {
this.run = build;
try {
BuildAction a = new BuildAction(build);
FileUtils.writeStringToFile(a.getPollingLogFile(),pollingLog);
build.replaceAction(a);
} catch (IOException e) {
LOGGER.log(WARNING,"Failed to persist the polling log",e);
}
pollingLog = null;
}
@Override
public String getShortDescription() {
return Messages.SCMTrigger_SCMTriggerCause_ShortDescription();
}
@Restricted(DoNotUse.class)
public Run getRun() {
return this.run;
}
@Override
public boolean equals(Object o) {
return o instanceof SCMTriggerCause;
}
@Override
public int hashCode() {
return 3;
}
}
/**
* How long is too long for a polling activity to be in the queue?
*/
public static long STARVATION_THRESHOLD = SystemProperties.getLong(SCMTrigger.class.getName()+".starvationThreshold", TimeUnit2.HOURS.toMillis(1));
}