-
-
Notifications
You must be signed in to change notification settings - Fork 105
Watching plus Unicode #65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5c20d5e
b4ff3a5
cb05308
13885df
af31da4
bd27dc6
5eccfbb
756a87d
6c424e0
0dcfd8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,13 +30,15 @@ | |
| import hudson.EnvVars; | ||
| import hudson.FilePath; | ||
| import hudson.Launcher; | ||
| import hudson.Main; | ||
| import hudson.Util; | ||
| import hudson.model.TaskListener; | ||
| import hudson.util.DaemonThreadFactory; | ||
| import hudson.util.FormValidation; | ||
| import hudson.util.LogTaskListener; | ||
| import hudson.util.NamingThreadFactory; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.nio.charset.Charset; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Set; | ||
|
|
@@ -48,8 +50,10 @@ | |
| import javax.annotation.CheckForNull; | ||
| import javax.annotation.Nonnull; | ||
| import jenkins.util.Timer; | ||
| import org.apache.commons.io.IOUtils; | ||
| import org.jenkinsci.plugins.durabletask.Controller; | ||
| import org.jenkinsci.plugins.durabletask.DurableTask; | ||
| import org.jenkinsci.plugins.durabletask.Handler; | ||
| import org.jenkinsci.plugins.workflow.FilePathUtils; | ||
| import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; | ||
| import org.jenkinsci.plugins.workflow.steps.Step; | ||
|
|
@@ -129,15 +133,20 @@ public FormValidation doCheckReturnStatus(@QueryParameter boolean returnStdout, | |
|
|
||
| } | ||
|
|
||
| interface ExecutionRemotable { | ||
| void exited(int code, byte[] output) throws Exception; | ||
| } | ||
|
|
||
| /** | ||
| * Represents one task that is believed to still be running. | ||
| */ | ||
| @SuppressFBWarnings(value="SE_TRANSIENT_FIELD_NOT_RESTORED", justification="recurrencePeriod is set in onResume, not deserialization") | ||
| static final class Execution extends AbstractStepExecutionImpl implements Runnable { | ||
| static final class Execution extends AbstractStepExecutionImpl implements Runnable, ExecutionRemotable { | ||
|
|
||
| private static final long MIN_RECURRENCE_PERIOD = 250; // ¼s | ||
| private static final long MAX_RECURRENCE_PERIOD = 15000; // 15s | ||
| private static final float RECURRENCE_PERIOD_BACKOFF = 1.2f; | ||
| private static final long WATCHING_RECURRENCE_PERIOD = Main.isUnitTest ? /* 5s */5_000: /* 5m */300_000; | ||
|
|
||
| private static final ScheduledThreadPoolExecutor THREAD_POOL = new ScheduledThreadPoolExecutor(25, new NamingThreadFactory(new DaemonThreadFactory(), DurableTaskStep.class.getName())); | ||
| static { | ||
|
|
@@ -155,6 +164,7 @@ static final class Execution extends AbstractStepExecutionImpl implements Runnab | |
| private String remote; | ||
| private boolean returnStdout; // serialized default is false | ||
| private boolean returnStatus; // serialized default is false | ||
| private boolean watching; | ||
|
|
||
| Execution(StepContext context, DurableTaskStep step) { | ||
| super(context); | ||
|
|
@@ -171,14 +181,21 @@ static final class Execution extends AbstractStepExecutionImpl implements Runnab | |
| if (returnStdout) { | ||
| durableTask.captureOutput(); | ||
| } | ||
| TaskListener listener = context.get(TaskListener.class); | ||
| if (step.encoding != null) { | ||
| durableTask.charset(Charset.forName(step.encoding)); | ||
| } else { | ||
| durableTask.defaultCharset(); | ||
| } | ||
| controller = durableTask.launch(context.get(EnvVars.class), ws, context.get(Launcher.class), context.get(TaskListener.class)); | ||
| controller = durableTask.launch(context.get(EnvVars.class), ws, context.get(Launcher.class), listener); | ||
| this.remote = ws.getRemote(); | ||
| setupTimer(); | ||
| try { | ||
| controller.watch(ws, new HandlerImpl(this, ws, listener), listener); | ||
| watching = true; | ||
| } catch (UnsupportedOperationException x) { | ||
| LOGGER.log(Level.WARNING, null, x); | ||
| } | ||
| setupTimer(watching ? WATCHING_RECURRENCE_PERIOD : MIN_RECURRENCE_PERIOD); | ||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -189,6 +206,19 @@ static final class Execution extends AbstractStepExecutionImpl implements Runnab | |
| LOGGER.log(Level.FINE, "Jenkins is not running, no such node {0}, or it is offline", node); | ||
| return null; | ||
| } | ||
| if (watching) { | ||
| try { | ||
| controller.watch(ws, new HandlerImpl(this, ws, listener()), listener()); | ||
| recurrencePeriod = WATCHING_RECURRENCE_PERIOD; | ||
| } catch (UnsupportedOperationException x) { | ||
| getContext().onFailure(x); | ||
| } catch (Exception x) { // as below | ||
| LOGGER.log(Level.FINE, node + " is evidently offline now", x); | ||
| ws = null; | ||
| recurrencePeriod = MIN_RECURRENCE_PERIOD; | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| boolean directory; | ||
| try (Timeout timeout = Timeout.limit(10, TimeUnit.SECONDS)) { | ||
|
|
@@ -197,6 +227,7 @@ static final class Execution extends AbstractStepExecutionImpl implements Runnab | |
| // RequestAbortedException, ChannelClosedException, EOFException, wrappers thereof; InterruptedException if it just takes too long. | ||
| LOGGER.log(Level.FINE, node + " is evidently offline now", x); | ||
| ws = null; | ||
| recurrencePeriod = MIN_RECURRENCE_PERIOD; | ||
| if (!printedCannotContactMessage) { | ||
| listener().getLogger().println("Cannot contact " + node + ": " + x); | ||
| printedCannotContactMessage = true; | ||
|
|
@@ -206,6 +237,7 @@ static final class Execution extends AbstractStepExecutionImpl implements Runnab | |
| if (!directory) { | ||
| throw new AbortException("missing workspace " + remote + " on " + node); | ||
| } | ||
| LOGGER.log(Level.FINE, "{0} seems to be online", node); | ||
|
||
| return ws; | ||
| } | ||
|
|
||
|
|
@@ -312,10 +344,20 @@ private void check() { | |
| return; | ||
| } | ||
| if (workspace == null) { | ||
| recurrencePeriod = Math.min((long) (recurrencePeriod * RECURRENCE_PERIOD_BACKOFF), MAX_RECURRENCE_PERIOD); | ||
| return; // slave not yet ready, wait for another day | ||
| } | ||
| TaskListener listener = listener(); | ||
| try (Timeout timeout = Timeout.limit(10, TimeUnit.SECONDS)) { | ||
| if (watching) { | ||
| Integer exitCode = controller.exitStatus(workspace, launcher()); | ||
| if (exitCode == null) { | ||
| LOGGER.log(Level.FINE, "still running in {0} on {1}", new Object[] {remote, node}); | ||
| } else { | ||
| LOGGER.log(Level.FINE, "exited with {0} in {1} on {2}; expect asynchronous exit soon", new Object[] {exitCode, remote, node}); | ||
| // TODO if we get here again and exited has still not been called, assume we lost the notification somehow and end the step | ||
| } | ||
| } else { // legacy mode | ||
| if (controller.writeLog(workspace, listener.getLogger())) { | ||
| getContext().saveState(); | ||
| recurrencePeriod = MIN_RECURRENCE_PERIOD; // got output, maybe we will get more soon | ||
|
|
@@ -340,6 +382,7 @@ private void check() { | |
| recurrencePeriod = 0; | ||
| controller.cleanup(workspace); | ||
| } | ||
| } | ||
| } catch (Exception x) { | ||
| LOGGER.log(Level.FINE, "could not check " + workspace, x); | ||
| ws = null; | ||
|
|
@@ -350,17 +393,67 @@ private void check() { | |
| } | ||
| } | ||
|
|
||
| // called remotely from HandlerImpl | ||
| @Override public void exited(int exitCode, byte[] output) throws Exception { | ||
| try { | ||
| getContext().get(TaskListener.class); | ||
| } catch (IOException | InterruptedException x) { | ||
| LOGGER.log(Level.FINE, "asynchronous exit notification with code " + exitCode + " in " + remote + " on " + node + " ignored since step already seems dead", x); | ||
| return; | ||
| } | ||
| LOGGER.log(Level.FINE, "asynchronous exit notification with code {0} in {1} on {2}", new Object[] {exitCode, remote, node}); | ||
| if (returnStdout && output == null) { | ||
| getContext().onFailure(new IllegalStateException("expected output but got none")); | ||
| return; | ||
| } else if (!returnStdout && output != null) { | ||
| getContext().onFailure(new IllegalStateException("did not expect output but got some")); | ||
| return; | ||
| } | ||
| recurrencePeriod = 0; | ||
| if (returnStatus || exitCode == 0) { | ||
| getContext().onSuccess(returnStatus ? exitCode : returnStdout ? new String(output, StandardCharsets.UTF_8) : null); | ||
| } else { | ||
| if (returnStdout) { | ||
| listener().getLogger().write(output); // diagnostic | ||
| } | ||
| getContext().onFailure(new AbortException("script returned exit code " + exitCode)); | ||
| } | ||
| } | ||
|
|
||
| @Override public void onResume() { | ||
| setupTimer(); | ||
| ws = null; // find it from scratch please, rewatching as needed | ||
| setupTimer(MIN_RECURRENCE_PERIOD); | ||
| } | ||
|
|
||
| private void setupTimer() { | ||
| recurrencePeriod = MIN_RECURRENCE_PERIOD; | ||
| private void setupTimer(long initialRecurrencePeriod) { | ||
| recurrencePeriod = initialRecurrencePeriod; | ||
| task = THREAD_POOL.schedule(this, recurrencePeriod, TimeUnit.MILLISECONDS); | ||
| } | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| } | ||
|
|
||
| private static class HandlerImpl extends Handler { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| private final ExecutionRemotable execution; | ||
| private final TaskListener listener; | ||
|
|
||
| HandlerImpl(Execution execution, FilePath workspace, TaskListener listener) { | ||
| this.execution = workspace.getChannel().export(ExecutionRemotable.class, execution); | ||
| this.listener = listener; | ||
| } | ||
|
|
||
| @Override public void output(InputStream stream) throws Exception { | ||
| IOUtils.copy(stream, listener.getLogger()); | ||
| } | ||
|
|
||
| @Override public void exited(int code, byte[] output) throws Exception { | ||
| execution.exited(code, output); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently it's not documented in jenkinsci/durable-task-plugin#60
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Execution link in the log would be helpful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(for #63)