package hudson.plugins.tfs;
import hudson.Extension;
import hudson.Util;
import hudson.console.AnnotatedLargeText;
import hudson.model.Action;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.queue.QueueTaskFuture;
import hudson.plugins.tfs.model.GitCodePushedEventArgs;
import hudson.plugins.tfs.util.ActionHelper;
import hudson.plugins.tfs.util.MediaType;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.StreamTaskListener;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.triggers.SCMTriggerItem;
import org.apache.commons.jelly.XMLOutput;
import org.kohsuke.stapler.DataBoundConstructor;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Triggers a build when we receive a TFS/Team Services Git code push event.
*/
public class TeamPushTrigger extends Trigger<Job<?, ?>> {
private static final Logger LOGGER = Logger.getLogger(TeamPushTrigger.class.getName());
@DataBoundConstructor
public TeamPushTrigger() {
}
public TeamPushTrigger(final Job<?, ?> job) {
this.job = job;
}
public void execute(final GitCodePushedEventArgs gitCodePushedEventArgs, final List<Action> actions, final boolean bypassPolling) {
// TODO: Consider executing the poll + queue asynchronously
final Runner runner = new Runner(gitCodePushedEventArgs, actions, bypassPolling);
runner.run();
}
public File getLogFile() {
return new File(job.getRootDir(), "team-polling.log");
}
// TODO: This was inspired by SCMTrigger.Runner; it would be worth extracting something for re-use
public class Runner implements Runnable {
private final GitCodePushedEventArgs gitCodePushedEventArgs;
private final List<Action> actions;
private final boolean bypassPolling;
public Runner(final GitCodePushedEventArgs gitCodePushedEventArgs, final List<Action> actions, final boolean bypassPolling) {
this.gitCodePushedEventArgs = gitCodePushedEventArgs;
this.actions = actions;
this.bypassPolling = bypassPolling;
}
private SCMTriggerItem job() {
return SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(job);
}
private boolean runPolling() {
final String failedToRecord = "Failed to record SCM polling for " + job;
try {
final StreamTaskListener listener = new StreamTaskListener(getLogFile(), MediaType.UTF_8);
try {
final PrintStream logger = listener.getLogger();
final long startTimeMillis = System.currentTimeMillis();
final Date date = new Date(startTimeMillis);
logger.println("Started on "+ DateFormat.getDateTimeInstance().format(date));
final boolean result = job().poll(listener).hasChanges();
final long endTimeMillis = System.currentTimeMillis();
logger.println("Done. Took "+ Util.getTimeSpanString(endTimeMillis - startTimeMillis));
if (result) {
logger.println("Changes found");
}
else {
logger.println("No changes");
}
return result;
}
catch (final Error e) {
e.printStackTrace(listener.error(failedToRecord));
LOGGER.log(Level.SEVERE, failedToRecord,e);
throw e;
}
catch (final RuntimeException e) {
e.printStackTrace(listener.error(failedToRecord));
LOGGER.log(Level.SEVERE, failedToRecord,e);
throw e;
}
finally {
listener.close();
}
}
catch (final IOException e) {
LOGGER.log(Level.SEVERE, failedToRecord, e);
return false;
}
}
@Override
public void run() {
boolean shouldSchedule = bypassPolling;
String changesDetected = "";
if (!bypassPolling) {
if (runPolling()) {
changesDetected = "SCM changes detected in " + job.getFullDisplayName() + ". ";
shouldSchedule = true;
}
}
else {
changesDetected = "Polling bypassed for " + job.getFullDisplayName() + ". ";;
}
if (shouldSchedule) {
final SCMTriggerItem p = job();
final String name = "#" + p.getNextBuildNumber();
final String pushedBy = gitCodePushedEventArgs.pushedBy;
TeamPushCause cause;
final File logFile = getLogFile();
if (logFile.isFile()) {
try {
cause = new TeamPushCause(logFile, pushedBy);
}
catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to parse the polling log", e);
cause = new TeamPushCause(pushedBy);
}
}
else {
cause = new TeamPushCause(pushedBy);
}
final int quietPeriod = p.getQuietPeriod();
final CauseAction causeAction = new CauseAction(cause);
final Action[] actionArray = ActionHelper.create(actions, causeAction);
final QueueTaskFuture<?> queueTaskFuture = p.scheduleBuild2(quietPeriod, actionArray);
if (queueTaskFuture != null) {
LOGGER.info(changesDetected + "Triggering " + name);
}
else {
LOGGER.info(changesDetected + "Job is already in the queue");
}
}
}
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
@Extension
public static class DescriptorImpl extends TriggerDescriptor {
@Override
public boolean isApplicable(final Item item) {
return item instanceof Job
&& SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(item) != null
&& item instanceof ParameterizedJobMixIn.ParameterizedJob;
}
@Override
public String getDisplayName() {
return "Build when a change is pushed to TFS/Team Services";
}
}
@Override
public Collection<? extends Action> getProjectActions() {
if (job == null) {
return Collections.emptyList();
}
return Collections.singleton(new TeamPollingAction());
}
public final class TeamPollingAction implements Action {
@Override
public String getIconFileName() {
return "/plugin/tfs/48x48/logo.png";
}
@Override
public String getDisplayName() {
return "TFS/Team Services hook log";
}
@Override
public String getUrlName() {
return "TeamPollLog";
}
// the following methods are called from TeamPushTrigger/TeamPollingAction/index.jelly
@SuppressWarnings("unused")
public Job<?, ?> getOwner() {
return job;
}
@SuppressWarnings("unused")
public String getLog() throws IOException {
return Util.loadFile(getLogFile());
}
@SuppressWarnings("unused")
public void writeLogTo(XMLOutput out) throws IOException {
final File logFile = getLogFile();
final AnnotatedLargeText<TeamPollingAction> text =
new AnnotatedLargeText<TeamPollingAction>(logFile, MediaType.UTF_8, true, this);
final Writer writer = out.asWriter();
text.writeHtmlTo(0, writer);
}
}
}