From e703cc829c8e95fc0cd8ef577e98a5749871d888 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 12 Feb 2018 17:16:17 -0500 Subject: [PATCH 1/6] [JENKINS-31096] Define API for gathering command output in a local encoding. --- .../plugins/durabletask/Controller.java | 4 + .../plugins/durabletask/DurableTask.java | 21 +++ .../durabletask/FileMonitoringTask.java | 78 ++++++++-- .../durabletask/BourneShellScriptTest.java | 140 ++++++++++++++++++ 4 files changed, 233 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/Controller.java b/src/main/java/org/jenkinsci/plugins/durabletask/Controller.java index e110bc49..9fbc01ca 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/Controller.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/Controller.java @@ -49,6 +49,8 @@ public abstract class Controller implements Serializable { * @param workspace the workspace in use * @param sink where to send new log output * @return true if something was written and the controller should be resaved, false if everything is idle + * @see DurableTask#charset + * @see DurableTask#defaultCharset */ public abstract boolean writeLog(FilePath workspace, OutputStream sink) throws IOException, InterruptedException; @@ -88,6 +90,8 @@ public abstract class Controller implements Serializable { * @param workspace the workspace in use * @param launcher a way to start processes * @return the output of the process as raw bytes (may be empty but not null) + * @see DurableTask#charset + * @see DurableTask#defaultCharset */ public @Nonnull byte[] getOutput(@Nonnull FilePath workspace, @Nonnull Launcher launcher) throws IOException, InterruptedException { throw new IOException("Did not implement getOutput in " + getClass().getName()); diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java index d86b5bb9..22711cec 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java @@ -31,6 +31,8 @@ import hudson.model.AbstractDescribableImpl; import hudson.model.TaskListener; import java.io.IOException; +import java.nio.charset.Charset; +import javax.annotation.Nonnull; /** * A task which may be run asynchronously on a build node and withstand disconnection of the slave agent. @@ -63,4 +65,23 @@ public void captureOutput() throws UnsupportedOperationException { throw new UnsupportedOperationException("Capturing of output is not implemented in " + getClass().getName()); } + /** + * Requests that a specified charset be used to transcode process output. + * The encoding of {@link Controller#writeLog} and {@link Controller#getOutput} is then presumed to be UTF-8. + * If not called, no translation is performed. + * @param cs the character set in which process output is expected to be + */ + public void charset(@Nonnull Charset cs) { + // by default, ignore + } + + /** + * Requests that the node’s system charset be used to transcode process output. + * The encoding of {@link Controller#writeLog} and {@link Controller#getOutput} is then presumed to be UTF-8. + * If not called, no translation is performed. + */ + public void defaultCharset() { + // by default, ignore + } + } diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java index 7ef7d3d7..480db070 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java @@ -36,18 +36,24 @@ import hudson.util.StreamTaskListener; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.io.StringWriter; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import jenkins.MasterToSlaveFileCallable; -import org.apache.commons.io.IOUtils; +import org.apache.commons.io.FileUtils; /** * A task which forks some external command and then waits for log and status files to be updated/created. @@ -58,12 +64,19 @@ public abstract class FileMonitoringTask extends DurableTask { private static final String COOKIE = "JENKINS_SERVER_COOKIE"; + /** + * Charset name to use for transcoding, or the empty string for node system default, or null for no transcoding. + */ + private @CheckForNull String charset; + private static String cookieFor(FilePath workspace) { return "durable-" + Util.getDigestOf(workspace.getRemote()); } @Override public final Controller launch(EnvVars env, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { - return launchWithCookie(workspace, launcher, listener, env, COOKIE, cookieFor(workspace)); + FileMonitoringController controller = launchWithCookie(workspace, launcher, listener, env, COOKIE, cookieFor(workspace)); + controller.charset = charset; + return controller; } protected FileMonitoringController launchWithCookie(FilePath workspace, Launcher launcher, TaskListener listener, EnvVars envVars, String cookieVariable, String cookieValue) throws IOException, InterruptedException { @@ -71,6 +84,14 @@ protected FileMonitoringController launchWithCookie(FilePath workspace, Launcher return doLaunch(workspace, launcher, listener, envVars); } + @Override public final void charset(Charset cs) { + charset = cs.name(); + } + + @Override public final void defaultCharset() { + charset = ""; + } + /** * Should start a process which sends output to {@linkplain FileMonitoringController#getLogFile(FilePath) log file} * in the workspace and finally writes its exit code to {@linkplain FileMonitoringController#getResultFile(FilePath) result file}. @@ -110,6 +131,9 @@ protected static class FileMonitoringController extends Controller { */ private long lastLocation; + /** @see FileMonitoringTask#charset */ + private @CheckForNull String charset; + protected FileMonitoringController(FilePath ws) throws IOException, InterruptedException { // can't keep ws reference because Controller is expected to be serializable ws.mkdirs(); @@ -120,7 +144,7 @@ protected FileMonitoringController(FilePath ws) throws IOException, InterruptedE @Override public final boolean writeLog(FilePath workspace, OutputStream sink) throws IOException, InterruptedException { FilePath log = getLogFile(workspace); - Long newLocation = log.act(new WriteLog(lastLocation, new RemoteOutputStream(sink))); + Long newLocation = log.act(new WriteLog(lastLocation, new RemoteOutputStream(sink), charset)); if (newLocation != null) { LOGGER.log(Level.FINE, "copied {0} bytes from {1}", new Object[] {newLocation - lastLocation, log}); lastLocation = newLocation; @@ -132,9 +156,11 @@ protected FileMonitoringController(FilePath ws) throws IOException, InterruptedE private static class WriteLog extends MasterToSlaveFileCallable { private final long lastLocation; private final OutputStream sink; - WriteLog(long lastLocation, OutputStream sink) { + private final @CheckForNull String charset; + WriteLog(long lastLocation, OutputStream sink, String charset) { this.lastLocation = lastLocation; this.sink = sink; + this.charset = charset; } @Override public Long invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { long len = f.length(); @@ -149,7 +175,12 @@ private static class WriteLog extends MasterToSlaveFileCallable { // TODO is this efficient for large amounts of output? Would it be better to stream data, or return a byte[] from the callable? byte[] buf = new byte[(int) toRead]; raf.readFully(buf); - sink.write(buf); + ByteBuffer transcoded = maybeTranscode(buf, charset); + if (transcoded == null) { + sink.write(buf); + } else { + Channels.newChannel(sink).write(transcoded); + } } finally { raf.close(); } @@ -174,13 +205,40 @@ private static class WriteLog extends MasterToSlaveFileCallable { } } - @Override public byte[] getOutput(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { - // TODO could perhaps be more efficient for large files to send a MasterToSlaveFileCallable - try (InputStream is = getOutputFile(workspace).read()) { - return IOUtils.toByteArray(is); + /** + * Transcode process output to UTF-8 if necessary. + * @param data output presumed to be in local encoding + * @param charset a particular encoding name, or the empty string for the system default encoding, or null to skip transcoding + * @return a newly allocate buffer of UTF-8 encoded data ({@link CodingErrorAction#REPLACE} is used), + * or null if not performing transcoding because it was not requested or the data was already thought to be in UTF-8 + */ + private static @CheckForNull ByteBuffer maybeTranscode(@Nonnull byte[] data, @CheckForNull String charset) { + if (charset == null) { // no transcoding requested, do raw copy and YMMV + return null; + } else { + Charset cs = charset.isEmpty() ? Charset.defaultCharset() : Charset.forName(charset); + if (cs.equals(StandardCharsets.UTF_8)) { // transcoding unnecessary as output was already UTF-8 + return null; + } else { // decode output in specified charset and reëncode in UTF-8 + return StandardCharsets.UTF_8.encode(cs.decode(ByteBuffer.wrap(data))); + } } } + @Override public byte[] getOutput(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { + return getOutputFile(workspace).act(new MasterToSlaveFileCallable() { + @Override public byte[] invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + byte[] buf = FileUtils.readFileToByteArray(f); + ByteBuffer transcoded = maybeTranscode(buf, charset); + if (transcoded == null) { + return buf; + } else { + return transcoded.array(); + } + } + }); + } + @Override public final void stop(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { launcher.kill(Collections.singletonMap(COOKIE, cookieFor(workspace))); } diff --git a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java index 78ff543a..4ae04e56 100644 --- a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java +++ b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java @@ -34,8 +34,11 @@ import hudson.util.VersionNumber; import java.io.ByteArrayOutputStream; import java.io.File; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.logging.Level; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.output.TeeOutputStream; import static org.hamcrest.Matchers.*; import org.jenkinsci.test.acceptance.docker.Docker; @@ -236,4 +239,141 @@ private void runOnDocker(DumbSlave s) throws Exception { runOnDocker(new DumbSlave("docker", "/home/jenkins/agent", new SimpleCommandLauncher("docker run -i --rm --name agent --init jenkinsci/slave:3.7-1 java -jar /usr/share/jenkins/slave.jar"))); } + @Issue("JENKINS-31096") + @Test public void encoding() throws Exception { + JavaContainer container = dockerUbuntu.get(); + DumbSlave s = new DumbSlave("docker", "/home/test", new SSHLauncher(container.ipBound(22), container.port(22), "test", "test", "", "-Dfile.encoding=ISO-8859-1")); + j.jenkins.addNode(s); + j.waitOnline(s); + assertEquals("ISO-8859-1", s.getChannel().call(new DetectCharset())); + FilePath dockerWS = s.getWorkspaceRoot(); + dockerWS.child("latin").write("¡Ole!", "ISO-8859-1"); + dockerWS.child("eastern").write("Čau!", "ISO-8859-2"); + dockerWS.child("mixed").write("¡Čau → there!", "UTF-8"); + Launcher dockerLauncher = s.createLauncher(listener); + // control: no transcoding + Controller c = new BourneShellScript("cat latin").launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(baos.toString("ISO-8859-1"), containsString("¡Ole!")); + c.cleanup(dockerWS); + // and with output capture: + BourneShellScript dt = new BourneShellScript("cat latin"); + dt.captureOutput(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + c.writeLog(dockerWS, System.err); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(new String(c.getOutput(dockerWS, launcher), "ISO-8859-1"), containsString("¡Ole!")); + c.cleanup(dockerWS); + // test: specify particular charset (UTF-8) + dt = new BourneShellScript("cat mixed"); + dt.charset(StandardCharsets.UTF_8); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(baos.toString("UTF-8"), containsString("¡Čau → there!")); + c.cleanup(dockerWS); + // and with output capture: + dt = new BourneShellScript("cat mixed"); + dt.charset(StandardCharsets.UTF_8); + dt.captureOutput(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + c.writeLog(dockerWS, System.err); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("¡Čau → there!")); + c.cleanup(dockerWS); + // test: specify particular charset (unrelated) + dt = new BourneShellScript("cat eastern"); + dt.charset(Charset.forName("ISO-8859-2")); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(baos.toString("UTF-8"), containsString("Čau!")); + c.cleanup(dockerWS); + // and with output capture: + dt = new BourneShellScript("cat eastern"); + dt.charset(Charset.forName("ISO-8859-2")); + dt.captureOutput(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + c.writeLog(dockerWS, System.err); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("Čau!")); + c.cleanup(dockerWS); + // test: specify agent default charset + dt = new BourneShellScript("cat latin"); + dt.defaultCharset(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(baos.toString("UTF-8"), containsString("¡Ole!")); + c.cleanup(dockerWS); + // and with output capture: + dt = new BourneShellScript("cat latin"); + dt.defaultCharset(); + dt.captureOutput(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + c.writeLog(dockerWS, System.err); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("¡Ole!")); + c.cleanup(dockerWS); + // test: inappropriate charset, some replacement characters + dt = new BourneShellScript("cat mixed"); + dt.charset(StandardCharsets.US_ASCII); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(baos.toString("UTF-8"), containsString("����au ��� there!")); + c.cleanup(dockerWS); + // and with output capture: + dt = new BourneShellScript("cat mixed"); + dt.charset(StandardCharsets.US_ASCII); + dt.captureOutput(); + c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + Thread.sleep(100); + } + c.writeLog(dockerWS, System.err); + assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("����au ��� there!")); + c.cleanup(dockerWS); + s.toComputer().disconnect(new OfflineCause.UserCause(null, null)); + } + private static class DetectCharset extends MasterToSlaveCallable { + @Override public String call() throws RuntimeException { + return Charset.defaultCharset().name(); + } + } + } From cc53b1838554ba96a62efc1e0e78fdeb02673c14 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 13 Feb 2018 12:50:01 -0500 Subject: [PATCH 2/6] ByteBuffer.array is not what I wanted. --- .../plugins/durabletask/FileMonitoringTask.java | 6 ++++-- .../plugins/durabletask/BourneShellScriptTest.java | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java index 480db070..477e99a6 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java @@ -209,7 +209,7 @@ private static class WriteLog extends MasterToSlaveFileCallable { * Transcode process output to UTF-8 if necessary. * @param data output presumed to be in local encoding * @param charset a particular encoding name, or the empty string for the system default encoding, or null to skip transcoding - * @return a newly allocate buffer of UTF-8 encoded data ({@link CodingErrorAction#REPLACE} is used), + * @return a buffer of UTF-8 encoded data ({@link CodingErrorAction#REPLACE} is used), * or null if not performing transcoding because it was not requested or the data was already thought to be in UTF-8 */ private static @CheckForNull ByteBuffer maybeTranscode(@Nonnull byte[] data, @CheckForNull String charset) { @@ -233,7 +233,9 @@ private static class WriteLog extends MasterToSlaveFileCallable { if (transcoded == null) { return buf; } else { - return transcoded.array(); + byte[] buf2 = new byte[transcoded.remaining()]; + transcoded.get(buf2); + return buf2; } } }); diff --git a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java index 4ae04e56..f43ea9d6 100644 --- a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java +++ b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java @@ -270,7 +270,7 @@ private void runOnDocker(DumbSlave s) throws Exception { } c.writeLog(dockerWS, System.err); assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(new String(c.getOutput(dockerWS, launcher), "ISO-8859-1"), containsString("¡Ole!")); + assertEquals("¡Ole!", new String(c.getOutput(dockerWS, launcher), "ISO-8859-1")); c.cleanup(dockerWS); // test: specify particular charset (UTF-8) dt = new BourneShellScript("cat mixed"); @@ -294,7 +294,7 @@ private void runOnDocker(DumbSlave s) throws Exception { } c.writeLog(dockerWS, System.err); assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("¡Čau → there!")); + assertEquals("¡Čau → there!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); c.cleanup(dockerWS); // test: specify particular charset (unrelated) dt = new BourneShellScript("cat eastern"); @@ -318,7 +318,7 @@ private void runOnDocker(DumbSlave s) throws Exception { } c.writeLog(dockerWS, System.err); assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("Čau!")); + assertEquals("Čau!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); c.cleanup(dockerWS); // test: specify agent default charset dt = new BourneShellScript("cat latin"); @@ -342,7 +342,7 @@ private void runOnDocker(DumbSlave s) throws Exception { } c.writeLog(dockerWS, System.err); assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("¡Ole!")); + assertEquals("¡Ole!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); c.cleanup(dockerWS); // test: inappropriate charset, some replacement characters dt = new BourneShellScript("cat mixed"); @@ -366,7 +366,7 @@ private void runOnDocker(DumbSlave s) throws Exception { } c.writeLog(dockerWS, System.err); assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(new String(c.getOutput(dockerWS, launcher), "UTF-8"), containsString("����au ��� there!")); + assertEquals("����au ��� there!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); c.cleanup(dockerWS); s.toComputer().disconnect(new OfflineCause.UserCause(null, null)); } From d04d08ce3d87011413c654dea873f427759bf6be Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 19 Feb 2018 13:46:11 -0500 Subject: [PATCH 3/6] Introduce constant for SYSTEM_DEFAULT_CHARSET as suggested by @svanoort. --- .../plugins/durabletask/FileMonitoringTask.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java index 477e99a6..29508c33 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java @@ -64,8 +64,11 @@ public abstract class FileMonitoringTask extends DurableTask { private static final String COOKIE = "JENKINS_SERVER_COOKIE"; + /** Value of {@link #charset} used to mean the node’s system default. */ + private static final String SYSTEM_DEFAULT_CHARSET = "SYSTEM_DEFAULT"; + /** - * Charset name to use for transcoding, or the empty string for node system default, or null for no transcoding. + * Charset name to use for transcoding, or {@link #SYSTEM_DEFAULT_CHARSET}, or null for no transcoding. */ private @CheckForNull String charset; @@ -89,7 +92,7 @@ protected FileMonitoringController launchWithCookie(FilePath workspace, Launcher } @Override public final void defaultCharset() { - charset = ""; + charset = SYSTEM_DEFAULT_CHARSET; } /** @@ -216,7 +219,7 @@ private static class WriteLog extends MasterToSlaveFileCallable { if (charset == null) { // no transcoding requested, do raw copy and YMMV return null; } else { - Charset cs = charset.isEmpty() ? Charset.defaultCharset() : Charset.forName(charset); + Charset cs = charset.equals(SYSTEM_DEFAULT_CHARSET) ? Charset.defaultCharset() : Charset.forName(charset); if (cs.equals(StandardCharsets.UTF_8)) { // transcoding unnecessary as output was already UTF-8 return null; } else { // decode output in specified charset and reëncode in UTF-8 From 6da5620d8d0d2ffeae60e36645a2fb67516e5d6e Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 7 Jun 2018 18:26:23 -0400 Subject: [PATCH 4/6] Suggestions from @oleg-nenashev. --- .../plugins/durabletask/DurableTask.java | 8 +- .../durabletask/BourneShellScriptTest.java | 151 ++++-------------- 2 files changed, 41 insertions(+), 118 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java index 22711cec..5247447d 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java @@ -32,6 +32,8 @@ import hudson.model.TaskListener; import java.io.IOException; import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nonnull; /** @@ -40,6 +42,8 @@ */ public abstract class DurableTask extends AbstractDescribableImpl implements ExtensionPoint { + private static final Logger LOGGER = Logger.getLogger(DurableTask.class.getName()); + @Override public DurableTaskDescriptor getDescriptor() { return (DurableTaskDescriptor) super.getDescriptor(); } @@ -72,7 +76,7 @@ public void captureOutput() throws UnsupportedOperationException { * @param cs the character set in which process output is expected to be */ public void charset(@Nonnull Charset cs) { - // by default, ignore + LOGGER.log(Level.WARNING, "The charset method should be overridden in {0}", getClass().getName()); } /** @@ -81,7 +85,7 @@ public void charset(@Nonnull Charset cs) { * If not called, no translation is performed. */ public void defaultCharset() { - // by default, ignore + LOGGER.log(Level.WARNING, "The defaultCharset method should be overridden in {0}", getClass().getName()); } } diff --git a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java index f43ea9d6..474e5f88 100644 --- a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java +++ b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java @@ -35,7 +35,6 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.logging.Level; import jenkins.security.MasterToSlaveCallable; @@ -251,129 +250,49 @@ private void runOnDocker(DumbSlave s) throws Exception { dockerWS.child("eastern").write("Čau!", "ISO-8859-2"); dockerWS.child("mixed").write("¡Čau → there!", "UTF-8"); Launcher dockerLauncher = s.createLauncher(listener); - // control: no transcoding - Controller c = new BourneShellScript("cat latin").launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(baos.toString("ISO-8859-1"), containsString("¡Ole!")); - c.cleanup(dockerWS); - // and with output capture: - BourneShellScript dt = new BourneShellScript("cat latin"); - dt.captureOutput(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); - } - c.writeLog(dockerWS, System.err); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertEquals("¡Ole!", new String(c.getOutput(dockerWS, launcher), "ISO-8859-1")); - c.cleanup(dockerWS); - // test: specify particular charset (UTF-8) - dt = new BourneShellScript("cat mixed"); - dt.charset(StandardCharsets.UTF_8); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); - } - baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(baos.toString("UTF-8"), containsString("¡Čau → there!")); - c.cleanup(dockerWS); - // and with output capture: - dt = new BourneShellScript("cat mixed"); - dt.charset(StandardCharsets.UTF_8); - dt.captureOutput(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); - } - c.writeLog(dockerWS, System.err); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertEquals("¡Čau → there!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); - c.cleanup(dockerWS); - // test: specify particular charset (unrelated) - dt = new BourneShellScript("cat eastern"); - dt.charset(Charset.forName("ISO-8859-2")); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); - } - baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(baos.toString("UTF-8"), containsString("Čau!")); - c.cleanup(dockerWS); - // and with output capture: - dt = new BourneShellScript("cat eastern"); - dt.charset(Charset.forName("ISO-8859-2")); - dt.captureOutput(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); + assertEncoding("control: no transcoding", "latin", null, "¡Ole!", "ISO-8859-1", dockerWS, dockerLauncher); + assertEncoding("test: specify particular charset (UTF-8)", "mixed", "UTF-8", "¡Čau → there!", "UTF-8", dockerWS, dockerLauncher); + assertEncoding("test: specify particular charset (unrelated)", "eastern", "ISO-8859-2", "Čau!", "UTF-8", dockerWS, dockerLauncher); + assertEncoding("test: specify agent default charset", "latin", "", "¡Ole!", "UTF-8", dockerWS, dockerLauncher); + assertEncoding("test: inappropriate charset, some replacement characters", "mixed", "US-ASCII", "����au ��� there!", "UTF-8", dockerWS, dockerLauncher); + s.toComputer().disconnect(new OfflineCause.UserCause(null, null)); + } + private static class DetectCharset extends MasterToSlaveCallable { + @Override public String call() throws RuntimeException { + return Charset.defaultCharset().name(); } - c.writeLog(dockerWS, System.err); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertEquals("Čau!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); - c.cleanup(dockerWS); - // test: specify agent default charset - dt = new BourneShellScript("cat latin"); - dt.defaultCharset(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); + } + private void assertEncoding(String description, String file, String charset, String expected, String expectedEncoding, FilePath dockerWS, Launcher dockerLauncher) throws Exception { + assertEncoding(description, file, charset, expected, expectedEncoding, false, dockerWS, dockerLauncher); + assertEncoding(description, file, charset, expected, expectedEncoding, true, dockerWS, dockerLauncher); + } + private void assertEncoding(String description, String file, String charset, String expected, String expectedEncoding, boolean output, FilePath dockerWS, Launcher dockerLauncher) throws Exception { + BourneShellScript dt = new BourneShellScript("cat " + file); + if (charset != null) { + if (charset.isEmpty()) { + dt.defaultCharset(); + } else { + dt.charset(Charset.forName(charset)); + } } - baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(baos.toString("UTF-8"), containsString("¡Ole!")); - c.cleanup(dockerWS); - // and with output capture: - dt = new BourneShellScript("cat latin"); - dt.defaultCharset(); - dt.captureOutput(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); + if (output) { + dt.captureOutput(); } - c.writeLog(dockerWS, System.err); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertEquals("¡Ole!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); - c.cleanup(dockerWS); - // test: inappropriate charset, some replacement characters - dt = new BourneShellScript("cat mixed"); - dt.charset(StandardCharsets.US_ASCII); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + Controller c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { Thread.sleep(100); } - baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertThat(baos.toString("UTF-8"), containsString("����au ��� there!")); - c.cleanup(dockerWS); - // and with output capture: - dt = new BourneShellScript("cat mixed"); - dt.charset(StandardCharsets.US_ASCII); - dt.captureOutput(); - c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); - while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { - Thread.sleep(100); + assertEquals(description, 0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + if (output) { + c.writeLog(dockerWS, System.err); + assertEquals(description, expected, new String(c.getOutput(dockerWS, launcher), expectedEncoding)); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + c.writeLog(dockerWS, baos); + assertThat(description, baos.toString(expectedEncoding), containsString(expected)); } - c.writeLog(dockerWS, System.err); - assertEquals(0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); - assertEquals("����au ��� there!", new String(c.getOutput(dockerWS, launcher), "UTF-8")); c.cleanup(dockerWS); - s.toComputer().disconnect(new OfflineCause.UserCause(null, null)); - } - private static class DetectCharset extends MasterToSlaveCallable { - @Override public String call() throws RuntimeException { - return Charset.defaultCharset().name(); - } + } } From 32de5bf24ba981bfb627a0f7790da71fce37128a Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Wed, 25 Jul 2018 17:13:35 -0400 Subject: [PATCH 5/6] For purposes of lastLocation we must count bytes, not characters, so transcoding must be done on the master side. --- .../durabletask/FileMonitoringTask.java | 98 +++++++++++++------ .../durabletask/BourneShellScriptTest.java | 25 +++-- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java index eb5aec9e..303d1c97 100644 --- a/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java +++ b/src/main/java/org/jenkinsci/plugins/durabletask/FileMonitoringTask.java @@ -38,24 +38,28 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.RandomAccessFile; import java.io.StringWriter; import java.nio.ByteBuffer; -import java.nio.channels.Channels; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.MasterToSlaveFileCallable; +import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.FileUtils; import org.apache.commons.io.output.CountingOutputStream; +import org.apache.commons.io.output.WriterOutputStream; /** * A task which forks some external command and then waits for log and status files to be updated/created. @@ -139,6 +143,12 @@ protected static class FileMonitoringController extends Controller { /** @see FileMonitoringTask#charset */ private @CheckForNull String charset; + /** + * {@link #transcodingCharset} on the remote side when using {@link #writeLog}. + * May be a wrapper for null; initialized on demand. + */ + private transient volatile AtomicReference writeLogCs; + protected FileMonitoringController(FilePath ws) throws IOException, InterruptedException { // can't keep ws reference because Controller is expected to be serializable ws.mkdirs(); @@ -148,12 +158,31 @@ protected FileMonitoringController(FilePath ws) throws IOException, InterruptedE } @Override public final boolean writeLog(FilePath workspace, OutputStream sink) throws IOException, InterruptedException { + if (writeLogCs == null) { + if (SYSTEM_DEFAULT_CHARSET.equals(charset)) { + String cs = workspace.act(new TranscodingCharsetForSystemDefault()); + writeLogCs = new AtomicReference<>(cs == null ? null : Charset.forName(cs)); + } else { + // Does not matter what system default charset on the remote side is, so save the Remoting call. + writeLogCs = new AtomicReference<>(transcodingCharset(charset)); + } + LOGGER.log(Level.FINE, "remote transcoding charset: {0}", writeLogCs); + } FilePath log = getLogFile(workspace); - CountingOutputStream cos = new CountingOutputStream(sink); + OutputStream transcodedSink; + if (writeLogCs.get() == null) { + transcodedSink = sink; + } else { + // WriterOutputStream constructor taking Charset calls .replaceWith("?") which we do not want: + CharsetDecoder decoder = writeLogCs.get().newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE); + transcodedSink = new WriterOutputStream(new OutputStreamWriter(sink, StandardCharsets.UTF_8), decoder, 1024, true); + } + CountingOutputStream cos = new CountingOutputStream(transcodedSink); try { - log.act(new WriteLog(lastLocation, new RemoteOutputStream(cos), charset)); + log.act(new WriteLog(lastLocation, new RemoteOutputStream(cos))); return cos.getByteCount() > 0; } finally { // even if RemoteOutputStream write was interrupted, record what we actually received + transcodedSink.flush(); // writeImmediately flag does not seem to work long written = cos.getByteCount(); if (written > 0) { LOGGER.log(Level.FINE, "copied {0} bytes from {1}", new Object[] {written, log}); @@ -164,11 +193,9 @@ protected FileMonitoringController(FilePath ws) throws IOException, InterruptedE private static class WriteLog extends MasterToSlaveFileCallable { private final long lastLocation; private final OutputStream sink; - private final @CheckForNull String charset; - WriteLog(long lastLocation, OutputStream sink, String charset) { + WriteLog(long lastLocation, OutputStream sink) { this.lastLocation = lastLocation; this.sink = sink; - this.charset = charset; } @Override public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { long len = f.length(); @@ -181,18 +208,20 @@ private static class WriteLog extends MasterToSlaveFileCallable { } byte[] buf = new byte[(int) toRead]; raf.readFully(buf); - ByteBuffer transcoded = maybeTranscode(buf, charset); - if (transcoded == null) { - sink.write(buf); - } else { - Channels.newChannel(sink).write(transcoded); - } + sink.write(buf); } } return null; } } + private static class TranscodingCharsetForSystemDefault extends MasterToSlaveCallable { + @Override public String call() throws RuntimeException { + Charset cs = transcodingCharset(SYSTEM_DEFAULT_CHARSET); + return cs != null ? cs.name() : null; + } + } + /** Avoids excess round-tripping when reading status file. */ static class StatusCheck extends MasterToSlaveFileCallable { @Override @@ -226,6 +255,22 @@ public Integer invoke(File f, VirtualChannel channel) throws IOException, Interr return status.act(STATUS_CHECK_INSTANCE); } + @Override public byte[] getOutput(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { + return getOutputFile(workspace).act(new MasterToSlaveFileCallable() { + @Override public byte[] invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + byte[] buf = FileUtils.readFileToByteArray(f); + ByteBuffer transcoded = maybeTranscode(buf, charset); + if (transcoded == null) { + return buf; + } else { + byte[] buf2 = new byte[transcoded.remaining()]; + transcoded.get(buf2); + return buf2; + } + } + }); + } + /** * Transcode process output to UTF-8 if necessary. * @param data output presumed to be in local encoding @@ -234,34 +279,27 @@ public Integer invoke(File f, VirtualChannel channel) throws IOException, Interr * or null if not performing transcoding because it was not requested or the data was already thought to be in UTF-8 */ private static @CheckForNull ByteBuffer maybeTranscode(@Nonnull byte[] data, @CheckForNull String charset) { - if (charset == null) { // no transcoding requested, do raw copy and YMMV + Charset cs = transcodingCharset(charset); + if (cs == null) { + return null; + } else { + return StandardCharsets.UTF_8.encode(cs.decode(ByteBuffer.wrap(data))); + } + } + + private static @CheckForNull Charset transcodingCharset(@CheckForNull String charset) { + if (charset == null) { return null; } else { Charset cs = charset.equals(SYSTEM_DEFAULT_CHARSET) ? Charset.defaultCharset() : Charset.forName(charset); if (cs.equals(StandardCharsets.UTF_8)) { // transcoding unnecessary as output was already UTF-8 return null; } else { // decode output in specified charset and reëncode in UTF-8 - return StandardCharsets.UTF_8.encode(cs.decode(ByteBuffer.wrap(data))); + return cs; } } } - @Override public byte[] getOutput(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { - return getOutputFile(workspace).act(new MasterToSlaveFileCallable() { - @Override public byte[] invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - byte[] buf = FileUtils.readFileToByteArray(f); - ByteBuffer transcoded = maybeTranscode(buf, charset); - if (transcoded == null) { - return buf; - } else { - byte[] buf2 = new byte[transcoded.remaining()]; - transcoded.get(buf2); - return buf2; - } - } - }); - } - @Override public final void stop(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { launcher.kill(Collections.singletonMap(COOKIE, cookieFor(workspace))); } diff --git a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java index 474e5f88..ca3ab524 100644 --- a/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java +++ b/src/test/java/org/jenkinsci/plugins/durabletask/BourneShellScriptTest.java @@ -34,8 +34,10 @@ import hudson.util.VersionNumber; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.OutputStream; import java.nio.charset.Charset; import java.util.Collections; +import java.util.Locale; import java.util.logging.Level; import jenkins.security.MasterToSlaveCallable; import org.apache.commons.io.output.TeeOutputStream; @@ -69,7 +71,7 @@ public class BourneShellScriptTest { assumeThat("Docker must be at least 1.13.0 for this test (uses --init)", new VersionNumber(baos.toString().trim()), greaterThanOrEqualTo(new VersionNumber("1.13.0"))); } - @Rule public LoggerRule logging = new LoggerRule().record(BourneShellScript.class, Level.FINE); + @Rule public LoggerRule logging = new LoggerRule().recordPackage(BourneShellScript.class, Level.FINE); private StreamTaskListener listener; private FilePath ws; @@ -246,9 +248,9 @@ private void runOnDocker(DumbSlave s) throws Exception { j.waitOnline(s); assertEquals("ISO-8859-1", s.getChannel().call(new DetectCharset())); FilePath dockerWS = s.getWorkspaceRoot(); - dockerWS.child("latin").write("¡Ole!", "ISO-8859-1"); - dockerWS.child("eastern").write("Čau!", "ISO-8859-2"); - dockerWS.child("mixed").write("¡Čau → there!", "UTF-8"); + dockerWS.child("latin").write("¡Ole!\n", "ISO-8859-1"); + dockerWS.child("eastern").write("Čau!\n", "ISO-8859-2"); + dockerWS.child("mixed").write("¡Čau → there!\n", "UTF-8"); Launcher dockerLauncher = s.createLauncher(listener); assertEncoding("control: no transcoding", "latin", null, "¡Ole!", "ISO-8859-1", dockerWS, dockerLauncher); assertEncoding("test: specify particular charset (UTF-8)", "mixed", "UTF-8", "¡Čau → there!", "UTF-8", dockerWS, dockerLauncher); @@ -267,7 +269,8 @@ private void assertEncoding(String description, String file, String charset, Str assertEncoding(description, file, charset, expected, expectedEncoding, true, dockerWS, dockerLauncher); } private void assertEncoding(String description, String file, String charset, String expected, String expectedEncoding, boolean output, FilePath dockerWS, Launcher dockerLauncher) throws Exception { - BourneShellScript dt = new BourneShellScript("cat " + file); + System.err.println(description + " (output=" + output + ")"); // TODO maybe this should just be moved into a new class and @RunWith(Parameterized.class) for clarity + BourneShellScript dt = new BourneShellScript("set +x; cat " + file + "; sleep 1; tr '[a-z]' '[A-Z]' < " + file); if (charset != null) { if (charset.isEmpty()) { dt.defaultCharset(); @@ -279,17 +282,19 @@ private void assertEncoding(String description, String file, String charset, Str dt.captureOutput(); } Controller c = dt.launch(new EnvVars(), dockerWS, dockerLauncher, listener); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStream tee = new TeeOutputStream(baos, System.err); while (c.exitStatus(dockerWS, dockerLauncher, listener) == null) { + c.writeLog(dockerWS, tee); Thread.sleep(100); } + c.writeLog(dockerWS, tee); assertEquals(description, 0, c.exitStatus(dockerWS, dockerLauncher, listener).intValue()); + String fullExpected = expected + "\n" + expected.toUpperCase(Locale.ENGLISH) + "\n"; if (output) { - c.writeLog(dockerWS, System.err); - assertEquals(description, expected, new String(c.getOutput(dockerWS, launcher), expectedEncoding)); + assertEquals(description, fullExpected, new String(c.getOutput(dockerWS, launcher), expectedEncoding)); } else { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - c.writeLog(dockerWS, baos); - assertThat(description, baos.toString(expectedEncoding), containsString(expected)); + assertThat(description, baos.toString(expectedEncoding), containsString(fullExpected)); } c.cleanup(dockerWS); From 1aa8974f4307d2825d33c8a64b24bf6f5f896523 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 6 Aug 2018 16:03:16 -0400 Subject: [PATCH 6/6] Updated parent and reincrementalified. --- .mvn/extensions.xml | 2 +- pom.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 510f24fb..a2d496cc 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,6 +2,6 @@ io.jenkins.tools.incrementals git-changelist-maven-extension - 1.0-beta-3 + 1.0-beta-4 diff --git a/pom.xml b/pom.xml index 31db1176..b1c6ebe5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,11 @@ org.jenkins-ci.plugins plugin - 3.14 + 3.19 durable-task - 1.24-SNAPSHOT + ${revision}${changelist} hpi Durable Task Plugin Library offering an extension point for processes which can run outside of Jenkins yet be monitored. @@ -20,7 +20,7 @@ - 1.23 + 1.24 -SNAPSHOT 2.7.3 7