/******************************************************************************* * * Copyright (c) 2004-2011 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Brian Westrich, Jean-Baptiste Quenot, Stephen Connolly, Tom Huybrechts, Nikita Levyankov * * *******************************************************************************/ package hudson.triggers; import hudson.DependencyRunner; import hudson.DependencyRunner.ProjectRunnable; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.ExtensionPoint; import static hudson.init.InitMilestone.JOB_LOADED; import hudson.init.Initializer; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Build; import hudson.model.ComputerSet; import hudson.model.Describable; import hudson.model.Hudson; import hudson.model.Item; import hudson.model.PeriodicWork; import hudson.model.Project; import hudson.model.TopLevelItem; import hudson.model.TopLevelItemDescriptor; import hudson.scheduler.CronTab; import hudson.scheduler.CronTabList; import hudson.util.CascadingUtil; import hudson.util.DoubleLaunchChecker; import java.io.InvalidObjectException; import java.io.ObjectStreamException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Timer; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import org.antlr.runtime.RecognitionException; import org.eclipse.hudson.model.project.property.TriggerProjectProperty; /** * Triggers a {@link Build}. * * <p> To register a custom {@link Trigger} from a plugin, put {@link Extension} * on your {@link TriggerDescriptor} class. * * @author Kohsuke Kawaguchi */ public abstract class Trigger<J extends Item> implements Describable<Trigger<?>>, ExtensionPoint { /** * Called when a {@link Trigger} is loaded into memory and started. * * @param project given so that the persisted form of this object won't have * to have a back pointer. * @param newInstance True if this is a newly created trigger first attached * to the {@link Project}. False if this is invoked for a {@link Project} * loaded from disk. */ public void start(J job, boolean newInstance) { if (!jobs.contains(job)){ jobs.add(job); } this.job = job; } /** * Executes the triggered task. * * This method is invoked when {@link #Trigger(String)} is used to create an * instance, and the crontab matches the current time. */ public void run() { } /** * Called before a {@link Trigger} is removed. Under some circumstances, * this may be invoked more than once for a given {@link Trigger}, so be * prepared for that. * * <p> When the configuration is changed for a project, all triggers are * removed once and then added back. */ public void stop() { } /** * Add the job if it should be part of the job list in this trigger * @param job * @since 3.2.2 */ public void addJob(J job){ if (!jobs.contains(job)){ jobs.add(job); } } /** * Remove the job if it should not be part of the job list in this trigger * @param job * @since 3.2.2 */ public void removeJob(J job){ if (jobs.contains(job)){ jobs.remove(job); } } /** * Check if the job list in this trigger contains this particular job * @param job * @return boolean * @since 3.2.2 */ public boolean hasJob(J job){ return jobs.contains(job); } /** * Returns an action object if this {@link Trigger} has an action to * contribute to a {@link Project}. * * @deprecated as of 1.341 Use {@link #getProjectActions()} instead. */ public Action getProjectAction() { return null; } /** * {@link Action}s to be displayed in the job page. * * @return can be empty but never null * @since 1.341 */ public Collection<? extends Action> getProjectActions(AbstractProject job) { // delegate to getJobAction (singular) for backward compatible behavior Action a = getProjectAction(); if (a == null) { return Collections.emptyList(); } return Collections.singletonList(a); } public TriggerDescriptor getDescriptor() { return (TriggerDescriptor) Hudson.getInstance().getDescriptorOrDie(getClass()); } protected final String spec; protected transient CronTabList tabs; /** * @deprecated as of 3.1.2 use the list {@link #jobs} instead */ protected transient J job; // Theorectically each trigger should contain only one job. But in a cascading environment // if the tigger is defined in the parent, then this list represent the parent job and the // children jobs. protected transient List<J> jobs = new ArrayList<J>(); /** * Creates a new {@link Trigger} that gets {@link #run() run} periodically. * This is useful when your trigger does some polling work. */ protected Trigger(String cronTabSpec) throws RecognitionException { this.spec = cronTabSpec; this.tabs = CronTabList.create(cronTabSpec); } /** * Creates a new {@link Trigger} without using cron. */ protected Trigger() { this.spec = ""; this.tabs = new CronTabList(Collections.<CronTab>emptyList()); } /** * Gets the crontab specification. * * If you are not using cron service, just ignore it. */ public final String getSpec() { return spec; } protected Object readResolve() throws ObjectStreamException { try { tabs = CronTabList.create(spec); } catch (RecognitionException e) { InvalidObjectException x = new InvalidObjectException(e.getMessage()); x.initCause(e); throw x; } jobs = new ArrayList<J>(); return this; } protected String getJobNames(){ String jobnames = ""; for (J job : jobs){ jobnames += job.getName() + " "; } return jobnames; } /** * Runs every minute to check {@link TimerTrigger} and schedules build. */ @Extension public static class Cron extends PeriodicWork { private final Calendar cal = new GregorianCalendar(); public long getRecurrencePeriod() { return MIN; } public void doRun() { while (new Date().getTime() - cal.getTimeInMillis() > 1000) { LOGGER.fine("cron checking " + cal.getTime().toLocaleString()); try { checkTriggers(cal); } catch (Throwable e) { LOGGER.log(Level.WARNING, "Cron thread throw an exception", e); // bug in the code. Don't let the thread die. e.printStackTrace(); } cal.add(Calendar.MINUTE, 1); } } } private static Future previousSynchronousPolling; public static void checkTriggers(final Calendar cal) { Hudson inst = Hudson.getInstance(); // Are we using synchronous polling? SCMTrigger.DescriptorImpl scmd = inst.getDescriptorByType(SCMTrigger.DescriptorImpl.class); if (scmd.synchronousPolling) { LOGGER.fine("using synchronous polling"); // Check that previous synchronous polling job is done to prevent piling up too many jobs if (previousSynchronousPolling == null || previousSynchronousPolling.isDone()) { // Process SCMTriggers in the order of dependencies. Note that the crontab spec expressed per-project is // ignored, only the global setting is honored. The polling job is submitted only if the previous job has // terminated. // FIXME allow to set a global crontab spec previousSynchronousPolling = scmd.getExecutor().submit(new DependencyRunner(new ProjectRunnable() { public void run(AbstractProject p) { for (Trigger t : (Collection<Trigger>) p.getTriggers().values()) { if (t instanceof SCMTrigger) { LOGGER.fine("synchronously triggering SCMTrigger for jobs " + t.getJobNames()); t.run(); } } } })); } else { LOGGER.fine("synchronous polling has detected unfinished jobs, will not trigger additional jobs."); } } // Process all triggers, except SCMTriggers when synchronousPolling is set for (AbstractProject<?, ?> p : inst.getAllItems(AbstractProject.class)) { for (Trigger t : p.getTriggers().values()) { //Fix: 457113 - Unnecessary calls of Trigger.run() if (p.hasCascadingProject()){ TriggerProjectProperty triggerProjectProperty = CascadingUtil.getTriggerProjectProperty(p, t.getDescriptor().getJsonSafeClassName()); if (!triggerProjectProperty.isOverridden()){ continue; } } if (!(t instanceof SCMTrigger && scmd.synchronousPolling)) { LOGGER.fine("cron checking " + p.getName()); if (t.tabs.check(cal)) { LOGGER.config("cron triggered " + p.getName()); try { t.run(); } catch (Throwable e) { // t.run() is a plugin, and some of them throw RuntimeException and other things. // don't let that cancel the polling activity. report and move on. LOGGER.log(Level.WARNING, t.getClass().getName() + ".run() failed for " + p.getName(), e); } } } } } } private static final Logger LOGGER = Logger.getLogger(Trigger.class.getName()); /** * This timer is available for all the components inside Hudson to schedule * some work. * * Initialized and cleaned up by {@link Hudson}, but value kept here for * compatibility. * * If plugins want to run periodic jobs, they should implement * {@link PeriodicWork}. */ public static Timer timer; @Initializer(after = JOB_LOADED) public static void init() { new DoubleLaunchChecker().schedule(); // start all PeridocWorks for (PeriodicWork p : PeriodicWork.all()) { timer.scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod()); } // start monitoring nodes, although there's no hurry. timer.schedule(new SafeTimerTask() { public void doRun() { ComputerSet.initialize(); } }, 1000 * 10); } /** * Returns all the registered {@link Trigger} descriptors. */ public static DescriptorExtensionList<Trigger<?>, TriggerDescriptor> all() { return (DescriptorExtensionList) Hudson.getInstance().getDescriptorList(Trigger.class); } /** * Returns a subset of {@link TriggerDescriptor}s that applys to the given * item. */ public static List<TriggerDescriptor> for_(Item i) { List<TriggerDescriptor> r = new ArrayList<TriggerDescriptor>(); for (TriggerDescriptor t : all()) { if (!t.isApplicable(i)) { continue; } if (i instanceof TopLevelItem) {// ugly TopLevelItemDescriptor tld = ((TopLevelItem) i).getDescriptor(); // tld shouldn't be really null in contract, but we often write test Describables that // doesn't have a Descriptor. if (tld != null && !tld.isApplicable(t)) { continue; } } r.add(t); } return r; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Trigger trigger = (Trigger) o; if (spec != null ? !spec.equals(trigger.spec) : trigger.spec != null) { return false; } return true; } @Override public int hashCode() { return spec != null ? spec.hashCode() : 0; } }