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 {