diff --git a/pom.xml b/pom.xml index 046b8250..4b400f20 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ org.jenkins-ci.plugins plugin - 3.19 + 3.25 org.jenkins-ci.plugins.workflow @@ -67,10 +67,12 @@ 2.121.1 8 2.15 - 2.32 - 2.14 - 2.28 + 2.58 + 2.21 + 2.30 true + 4.0.0-beta3 + 2.2.6 @@ -98,7 +100,7 @@ org.jenkins-ci.plugins structs - 1.14 + 1.17 org.jenkins-ci.plugins @@ -121,13 +123,13 @@ org.jenkins-ci.plugins.workflow workflow-job - 2.10 + 2.26 test org.jenkins-ci.plugins.workflow workflow-durable-task-step - 2.3 + 2.24 test @@ -182,7 +184,7 @@ org.jenkins-ci.plugins script-security - 1.28 + 1.46 test @@ -204,5 +206,34 @@ tests test + + org.jenkins-ci.plugins + git + ${git-plugin.version} + test + + + org.jenkins-ci.plugins + scm-api + ${scm-api-plugin.version} + tests + test + + + org.jenkins-ci.plugins + git + ${git-plugin.version} + tests + test + + + + + org.jenkins-ci.plugins + scm-api + ${scm-api-plugin.version} + + + diff --git a/src/main/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepExecution.java b/src/main/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepExecution.java index 20f2f210..e8fd3bb4 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepExecution.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepExecution.java @@ -10,17 +10,25 @@ import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; +import hudson.remoting.Callable; import hudson.remoting.Channel; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; import java.util.List; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import jenkins.model.CauseOfInterruption; +import jenkins.security.SlaveToMasterCallable; import jenkins.util.Timer; import org.jenkinsci.plugins.workflow.flow.FlowExecution; import org.jenkinsci.plugins.workflow.graph.FlowNode; @@ -37,7 +45,7 @@ public class TimeoutStepExecution extends AbstractStepExecutionImpl { private BodyExecution body; private transient ScheduledFuture killer; - private long timeout = 0; + private final long timeout; private long end = 0; /** Used to track whether this is timing out on inactivity without needing to reference {@link #step}. */ @@ -46,10 +54,15 @@ public class TimeoutStepExecution extends AbstractStepExecutionImpl { /** whether we are forcing the body to end */ private boolean forcible; + /** Token for {@link #activity} callbacks. */ + private final String id; + TimeoutStepExecution(TimeoutStep step, StepContext context) { super(context); this.step = step; this.activity = step.isActivity(); + id = activity ? UUID.randomUUID().toString() : null; + timeout = step.getUnit().toMillis(step.getTime()); } @Override @@ -62,13 +75,12 @@ public boolean start() throws Exception { bodyInvoker = bodyInvoker.withContext( BodyInvoker.mergeConsoleLogFilters( context.get(ConsoleLogFilter.class), - new ConsoleLogFilterImpl(new ResetCallbackImpl()) + new ConsoleLogFilterImpl2(id, timeout) ) ); } body = bodyInvoker.start(); - timeout = step.getUnit().toMillis(step.getTime()); resetTimer(); return false; // execution is asynchronous } @@ -230,10 +242,127 @@ public String getShortDescription() { } } + private static final class ResetTimer extends SlaveToMasterCallable { + + private static final long serialVersionUID = 1L; + + private final @Nonnull String id; + + ResetTimer(String id) { + this.id = id; + } + + @Override public Void call() throws RuntimeException { + StepExecution.applyAll(TimeoutStepExecution.class, e -> { + if (id.equals(e.id)) { + e.resetTimer(); + } + return null; + }); + return null; + } + + } + + private static class ConsoleLogFilterImpl2 extends ConsoleLogFilter implements /* TODO Remotable */ Serializable { + private static final long serialVersionUID = 1L; + + private final @Nonnull String id; + private final long timeout; + private transient @CheckForNull Channel channel; + + ConsoleLogFilterImpl2(String id, long timeout) { + this.id = id; + this.timeout = timeout; + } + + private Object readResolve() { + channel = Channel.current(); + return this; + } + + @Override + public OutputStream decorateLogger(@SuppressWarnings("rawtypes") Run build, final OutputStream logger) + throws IOException, InterruptedException { + // TODO if channel == null, we can safely ResetTimer.call synchronously from eol and skip the Tick + AtomicBoolean active = new AtomicBoolean(); + OutputStream decorated = new LineTransformationOutputStream() { + @Override + protected void eol(byte[] b, int len) throws IOException { + logger.write(b, 0, len); + active.set(true); + } + + @Override + public void flush() throws IOException { + super.flush(); + logger.flush(); + } + + @Override + public void close() throws IOException { + super.close(); + logger.close(); + } + }; + new Tick(active, new WeakReference<>(decorated), timeout, channel, id).schedule(); + return decorated; + } + } + + private static final class Tick implements Runnable { + private final AtomicBoolean active; + private final Reference stream; + private final long timeout; + private final @CheckForNull Channel channel; + private final @Nonnull String id; + Tick(AtomicBoolean active, Reference stream, long timeout, Channel channel, String id) { + this.active = active; + this.stream = stream; + this.timeout = timeout; + this.channel = channel; + this.id = id; + } + @Override + public void run() { + if (stream.get() == null) { + // Not only idle but gone—stop the timer. + return; + } + boolean currentlyActive = active.getAndSet(false); + if (currentlyActive) { + Callable resetTimer = new ResetTimer(id); + if (channel != null) { + try { + channel.call(resetTimer); + } catch (Exception x) { + LOGGER.log(Level.WARNING, null, x); + } + } else { + resetTimer.call(); + } + schedule(); + } else { + // Idle at the moment, but check well before the timeout expires in case new output appears. + schedule(timeout / 10); + } + } + void schedule() { + schedule(timeout / 2); // less than the full timeout, to give some grace period, but in the same ballpark to avoid overhead + } + private void schedule(long delay) { + Timer.get().schedule(this, delay, TimeUnit.MILLISECONDS); + } + } + + /** @deprecated only here for serial compatibility */ + @Deprecated public interface ResetCallback extends Serializable { void logWritten(); } + /** @deprecated only here for serial compatibility */ + @Deprecated private class ResetCallbackImpl implements ResetCallback { private static final long serialVersionUID = 1L; @Override public void logWritten() { @@ -241,6 +370,8 @@ private class ResetCallbackImpl implements ResetCallback { } } + /** @deprecated only here for serial compatibility */ + @Deprecated private static class ConsoleLogFilterImpl extends ConsoleLogFilter implements /* TODO Remotable */ Serializable { private static final long serialVersionUID = 1L; diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/EnvStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/EnvStepTest.java index 959ef530..6512733b 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/steps/EnvStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/EnvStepTest.java @@ -90,9 +90,9 @@ public class EnvStepTest { WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p"); p.setDefinition(new CpsFlowDefinition( "parallel a: {\n" + - " node {withEnv(['TOOL=aloc']) {semaphore 'a'; isUnix() ? sh('echo TOOL=$TOOL') : bat('echo TOOL=%TOOL%')}}\n" + + " node {withEnv(['TOOL=aloc']) {semaphore 'a'; isUnix() ? sh('echo a TOOL=$TOOL') : bat('echo a TOOL=%TOOL%')}}\n" + "}, b: {\n" + - " node {withEnv(['TOOL=bloc']) {semaphore 'b'; isUnix() ? sh('echo TOOL=$TOOL') : bat('echo TOOL=%TOOL%')}}\n" + + " node {withEnv(['TOOL=bloc']) {semaphore 'b'; isUnix() ? sh('echo b TOOL=$TOOL') : bat('echo b TOOL=%TOOL%')}}\n" + "}", true)); WorkflowRun b = p.scheduleBuild2(0).getStartCondition().get(); SemaphoreStep.waitForStart("a/1", b); @@ -100,8 +100,8 @@ public class EnvStepTest { SemaphoreStep.success("a/1", null); SemaphoreStep.success("b/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b)); - story.j.assertLogContains("[a] TOOL=aloc", b); - story.j.assertLogContains("[b] TOOL=bloc", b); + story.j.assertLogContains("a TOOL=aloc", b); + story.j.assertLogContains("b TOOL=bloc", b); } }); } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepTest.java b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepTest.java index 92af2dc0..f8ac74a1 100644 --- a/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepTest.java +++ b/src/test/java/org/jenkinsci/plugins/workflow/steps/TimeoutStepTest.java @@ -24,6 +24,7 @@ package org.jenkinsci.plugins.workflow.steps; +import hudson.Functions; import hudson.model.Result; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; @@ -33,6 +34,7 @@ import java.util.concurrent.TimeUnit; import jenkins.model.CauseOfInterruption; import jenkins.model.InterruptedBuildAction; +import jenkins.plugins.git.GitSampleRepoRule; import org.jenkinsci.plugins.workflow.actions.ErrorAction; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode; @@ -43,6 +45,7 @@ import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; import org.junit.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assume.*; import org.junit.runners.model.Statement; import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.Issue; @@ -57,6 +60,8 @@ public class TimeoutStepTest extends Assert { @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); + @Rule public GitSampleRepoRule git = new GitSampleRepoRule(); + @Test public void configRoundTrip() { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { @@ -239,6 +244,42 @@ public void evaluate() throws Throwable { }); } + @Test + public void activityRemote() { + assumeFalse(Functions.isWindows()); // TODO create analogue using bat + story.then(r -> { + r.createSlave(); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("" + + "node('!master') {\n" + + " timeout(time:5, unit:'SECONDS', activity: true) {\n" + + " sh 'set +x; echo NotHere; sleep 3; echo NotHereYet; sleep 3; echo JustHere; sleep 10; echo ShouldNot'\n" + + " }\n" + + "}\n", true)); + WorkflowRun b = r.assertBuildStatus(Result.ABORTED, p.scheduleBuild2(0)); + story.j.assertLogContains("JustHere", b); + story.j.assertLogNotContains("ShouldNot", b); + }); + } + + @Issue("JENKINS-54078") + @Test public void activityGit() { + story.then(r -> { + r.createSlave(); + git.init(); + git.write("file", "content"); + git.git("commit", "--all", "--message=init"); + WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("" + + "node('!master') {\n" + + " timeout(time: 5, unit: 'MINUTES', activity: true) {\n" + + " git($/" + git + "/$)\n" + + " }\n" + + "}\n", true)); + r.buildAndAssertSuccess(p); + }); + } + @Issue("JENKINS-26163") @Test public void restarted() throws Exception {