package hudson.plugins.build_timeout; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; import hudson.Launcher; import hudson.Util; import hudson.console.LineTransformationOutputStream; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Descriptor; import hudson.model.Run.RunnerAbortedException; import hudson.plugins.build_timeout.impl.AbsoluteTimeOutStrategy; import hudson.plugins.build_timeout.impl.ElasticTimeOutStrategy; import hudson.plugins.build_timeout.impl.LikelyStuckTimeOutStrategy; import hudson.plugins.build_timeout.operations.AbortOperation; import hudson.plugins.build_timeout.operations.FailOperation; import hudson.plugins.build_timeout.operations.WriteDescriptionOperation; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.triggers.SafeTimerTask; import hudson.triggers.Trigger; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import hudson.util.IOException2; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; /** * {@link BuildWrapper} that terminates a build if it's taking too long. * * @author Kohsuke Kawaguchi */ public class BuildTimeoutWrapper extends BuildWrapper { @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Diagnostic fields are left mutable so that groovy console can be used to dynamically turn/off probes.") public static long MINIMUM_TIMEOUT_MILLISECONDS = Long.getLong(BuildTimeoutWrapper.class.getName()+ ".MINIMUM_TIMEOUT_MILLISECONDS", 3 * 60 * 1000); private /* final */ BuildTimeOutStrategy strategy; private final String timeoutEnvVar; /** * Fail the build rather than aborting it * @deprecated use {@link FailOperation} instead. */ @Deprecated public transient boolean failBuild; /** * Writing the build description when timeout occurred. * @deprecated use {@link WriteDescriptionOperation} instead. */ @Deprecated public transient boolean writingDescription; private final List<BuildTimeOutOperation> operationList; /** * @return operations to perform at timeout. */ public List<BuildTimeOutOperation> getOperationList() { return operationList; } private static List<BuildTimeOutOperation> createCompatibleOperationList( boolean failBuild, boolean writingDescription ) { BuildTimeOutOperation lastOp = (failBuild)?new FailOperation():new AbortOperation(); if (!writingDescription) { return Arrays.asList(lastOp); } String msg; if (failBuild) { msg = Messages.Timeout_Message("{0}", Messages.Timeout_Failed()); } else { msg = Messages.Timeout_Message("{0}", Messages.Timeout_Aborted()); } BuildTimeOutOperation firstOp = new WriteDescriptionOperation(msg); return Arrays.asList(firstOp, lastOp); } @Deprecated public BuildTimeoutWrapper(BuildTimeOutStrategy strategy, boolean failBuild, boolean writingDescription) { this.strategy = strategy; this.operationList = createCompatibleOperationList(failBuild, writingDescription); this.timeoutEnvVar = null; } @Deprecated public BuildTimeoutWrapper(BuildTimeOutStrategy strategy, List<BuildTimeOutOperation> operationList) { this.strategy = strategy; this.operationList = (operationList != null)?operationList:Collections.<BuildTimeOutOperation>emptyList(); this.timeoutEnvVar = null; } /** * ctor. * * Don't forget to update {@link DescriptorImpl#newInstance(StaplerRequest, JSONObject)} * when you add new arguments. * * @param strategy * @param operationList * @param timeoutEnvVar */ @DataBoundConstructor public BuildTimeoutWrapper(BuildTimeOutStrategy strategy, List<BuildTimeOutOperation> operationList, String timeoutEnvVar) { this.strategy = strategy; this.operationList = (operationList != null)?operationList:Collections.<BuildTimeOutOperation>emptyList(); this.timeoutEnvVar = Util.fixEmptyAndTrim(timeoutEnvVar); } public class EnvironmentImpl extends Environment { private final AbstractBuild<?,?> build; private final BuildListener listener; //Did some opertion failed? protected boolean operationFailed = false; final class TimeoutTimerTask extends SafeTimerTask { public void doRun() { synchronized(EnvironmentImpl.this) { EnvironmentImpl.this.task = null; // mark timer is not active. } List<BuildTimeOutOperation> opList = getOperationList(); if (opList == null || opList.isEmpty()) { // defaults to AbortOperation. opList = Arrays.<BuildTimeOutOperation>asList(new AbortOperation()); } for( BuildTimeOutOperation op: opList ) { try { if (!op.perform(build, listener, effectiveTimeout)) { operationFailed = true; break; } } catch(RuntimeException e) { // if some unexpected exception, // mark the operation failed and pass through the exception. operationFailed = true; throw e; } } } } private TimeoutTimerTask task = null; private final long effectiveTimeout; public EnvironmentImpl(AbstractBuild<?,?> build, BuildListener listener) throws InterruptedException, MacroEvaluationException, IOException { this.build = build; this.listener = listener; this.effectiveTimeout = strategy.getTimeOut(build, listener); reschedule(); } @Override public void buildEnvVars(Map<String, String> env) { if (timeoutEnvVar != null) { env.put(timeoutEnvVar, String.valueOf(effectiveTimeout)); } } public synchronized void reschedule() { if (task != null) { task.cancel(); // avoid memory leaks for the case where this timer is in the future (JENKINS-31627) Trigger.timer.purge(); } task = new TimeoutTimerTask(); Trigger.timer.schedule(task, effectiveTimeout); } public synchronized void rescheduleIfScheduled() { if (task == null) { return; } reschedule(); } @Override public synchronized boolean tearDown(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { if (task != null) { task.cancel(); // avoid memory leaks for the case where this timer is in the future (JENKINS-31627). Trigger.timer.purge(); task = null; } // true to continue build. return !operationFailed; } } @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { try { return new EnvironmentImpl(build, listener); } catch (MacroEvaluationException e) { e.printStackTrace(listener.fatalError("Could not evaluate macro")); throw new IOException2(e.getMessage(), e); } } protected Object readResolve() { if (strategy != null && getOperationList() != null) { // no need to upgrade return this; } if ("elastic".equalsIgnoreCase(timeoutType)) { strategy = new ElasticTimeOutStrategy(timeoutPercentage, timeoutMinutesElasticDefault != null ? timeoutMinutesElasticDefault.intValue() : 60, 3); } else if ("likelyStuck".equalsIgnoreCase(timeoutType)) { strategy = new LikelyStuckTimeOutStrategy(); } else if (strategy == null) { strategy = new AbsoluteTimeOutStrategy(timeoutMinutes); } List<BuildTimeOutOperation> opList = getOperationList(); if (opList == null) { opList = createCompatibleOperationList(failBuild, writingDescription); } return new BuildTimeoutWrapper(strategy, opList, timeoutEnvVar); } @Override public Descriptor<BuildWrapper> getDescriptor() { return DESCRIPTOR; } @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); public static final class DescriptorImpl extends BuildWrapperDescriptor { DescriptorImpl() { super(BuildTimeoutWrapper.class); } /** * create a new instance form user input. * * Usually this is performed with {@link StaplerRequest#bindJSON(Class, JSONObject)}, * but here it is required to construct object manually to call {@link Descriptor#newInstance(StaplerRequest, JSONObject)} * of downstream classes. * * @param req * @param formData * @return * @throws hudson.model.Descriptor.FormException * @see hudson.model.Descriptor#newInstance(org.kohsuke.stapler.StaplerRequest, net.sf.json.JSONObject) */ @Override public BuildTimeoutWrapper newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException { BuildTimeOutStrategy strategy = BuildTimeOutUtility.bindJSONWithDescriptor(req, formData, "strategy", BuildTimeOutStrategy.class); List<BuildTimeOutOperation> operationList = newInstancesFromHeteroList(req, formData, "operationList", getOperations()); String timeoutEnvVar = formData.getString("timeoutEnvVar"); return new BuildTimeoutWrapper(strategy, operationList, timeoutEnvVar); } public String getDisplayName() { return Messages.Descriptor_DisplayName(); } public boolean isApplicable(AbstractProject<?, ?> item) { return true; } public List<BuildTimeOutStrategyDescriptor> getStrategies() { return Jenkins.getInstance().getDescriptorList(BuildTimeOutStrategy.class); } @SuppressWarnings("unchecked") public List<BuildTimeOutOperationDescriptor> getOperations(AbstractProject<?,?> project) { return BuildTimeOutOperationDescriptor.all((Class<? extends AbstractProject<?, ?>>)project.getClass()); } public List<BuildTimeOutOperationDescriptor> getOperations() { return BuildTimeOutOperationDescriptor.all(); } } public BuildTimeOutStrategy getStrategy() { return strategy; } public String getTimeoutEnvVar() { return timeoutEnvVar; } /** * @param build * @param logger * @return * @throws IOException * @throws InterruptedException * @throws RunnerAbortedException * @see hudson.tasks.BuildWrapper#decorateLogger(hudson.model.AbstractBuild, java.io.OutputStream) */ @Override public OutputStream decorateLogger(@SuppressWarnings("rawtypes") final AbstractBuild build, final OutputStream logger) throws IOException, InterruptedException, RunnerAbortedException { if(!getStrategy().wantsCaptureLog()) { // For performance reason, decorates only when // the strategy requires that. return logger; } return new LineTransformationOutputStream() { @Override protected void eol(byte[] b, int len) throws IOException { getStrategy().onWrite(build, b, len); logger.write(b, 0, len); } @Override public void flush() throws IOException { super.flush(); logger.flush(); } @Override public void close() throws IOException { logger.close(); super.close(); } }; } // --- legacy attributes, kept for backward compatibility public transient int timeoutMinutes; public transient int timeoutPercentage; public transient String timeoutType; public transient Integer timeoutMinutesElasticDefault; }