|
23 | 23 | */
|
24 | 24 | package hudson;
|
25 | 25 |
|
| 26 | +import hudson.console.LineTransformationOutputStream; |
26 | 27 | import hudson.model.AbstractBuild;
|
27 | 28 | import hudson.model.AbstractProject;
|
| 29 | +import hudson.model.BuildListener; |
28 | 30 | import hudson.model.FreeStyleBuild;
|
29 | 31 | import hudson.model.FreeStyleProject;
|
30 | 32 | import hudson.model.Node;
|
31 | 33 | import hudson.model.ParametersDefinitionProperty;
|
32 | 34 | import hudson.model.Slave;
|
33 | 35 | import hudson.model.StringParameterDefinition;
|
34 | 36 | import hudson.model.TaskListener;
|
| 37 | +import hudson.remoting.Channel; |
35 | 38 | import hudson.tasks.BatchFile;
|
36 | 39 | import hudson.tasks.BuildStepDescriptor;
|
37 | 40 | import hudson.tasks.Builder;
|
38 | 41 | import hudson.tasks.CommandInterpreter;
|
39 | 42 | import hudson.tasks.Shell;
|
| 43 | +import hudson.util.StreamTaskListener; |
| 44 | +import java.io.ByteArrayOutputStream; |
| 45 | +import java.io.File; |
| 46 | +import java.io.FileOutputStream; |
40 | 47 | import java.io.IOException;
|
| 48 | +import java.io.OutputStream; |
| 49 | +import java.io.PrintStream; |
| 50 | +import java.nio.charset.StandardCharsets; |
41 | 51 |
|
42 | 52 | import java.util.HashMap;
|
43 | 53 | import java.util.Map;
|
| 54 | +import org.apache.commons.io.FileUtils; |
| 55 | +import static org.hamcrest.Matchers.*; |
| 56 | +import static org.junit.Assert.*; |
| 57 | +import static org.junit.Assume.*; |
44 | 58 |
|
45 | 59 | import org.junit.Rule;
|
46 | 60 | import org.junit.Test;
|
@@ -153,4 +167,110 @@ private static final class QuietBatchFile extends BatchFile {
|
153 | 167 | }
|
154 | 168 | }
|
155 | 169 |
|
| 170 | + @Issue("JENKINS-52729") |
| 171 | + @Test public void remotable() throws Exception { |
| 172 | + File log = new File(rule.jenkins.root, "log"); |
| 173 | + TaskListener listener = new RemotableBuildListener(log); |
| 174 | + Launcher.ProcStarter ps = rule.createOnlineSlave().createLauncher(listener).launch(); |
| 175 | + if (Functions.isWindows()) { |
| 176 | + ps.cmds("cmd", "/c", "echo", "hello"); |
| 177 | + } else { |
| 178 | + ps.cmds("echo", "hello"); |
| 179 | + } |
| 180 | + assertEquals(0, ps.stdout(listener).join()); |
| 181 | + assertThat(FileUtils.readFileToString(log, StandardCharsets.UTF_8).replace("\r\n", "\n"), |
| 182 | + containsString("[master → slave0] $ " + (Functions.isWindows() ? "cmd /c " : "") + "echo hello\n" + |
| 183 | + "[master → slave0] hello")); |
| 184 | + } |
| 185 | + private static class RemotableBuildListener implements BuildListener { |
| 186 | + private static final long serialVersionUID = 1; |
| 187 | + /** location of log file streamed to by multiple sources */ |
| 188 | + private final File logFile; |
| 189 | + /** records allocation & deserialization history; e.g., {@code master → agentName} */ |
| 190 | + private final String id; |
| 191 | + private transient PrintStream logger; |
| 192 | + RemotableBuildListener(File logFile) { |
| 193 | + this(logFile, "master"); |
| 194 | + } |
| 195 | + private RemotableBuildListener(File logFile, String id) { |
| 196 | + this.logFile = logFile; |
| 197 | + this.id = id; |
| 198 | + } |
| 199 | + @Override public PrintStream getLogger() { |
| 200 | + if (logger == null) { |
| 201 | + final OutputStream fos; |
| 202 | + try { |
| 203 | + fos = new FileOutputStream(logFile, true); |
| 204 | + logger = new PrintStream(new LineTransformationOutputStream() { |
| 205 | + @Override protected void eol(byte[] b, int len) throws IOException { |
| 206 | + fos.write(("[" + id + "] ").getBytes(StandardCharsets.UTF_8)); |
| 207 | + fos.write(b, 0, len); |
| 208 | + } |
| 209 | + }, true, "UTF-8"); |
| 210 | + } catch (IOException x) { |
| 211 | + throw new AssertionError(x); |
| 212 | + } |
| 213 | + } |
| 214 | + return logger; |
| 215 | + } |
| 216 | + private Object writeReplace() { |
| 217 | + Thread.dumpStack(); |
| 218 | + String name = Channel.current().getName(); |
| 219 | + return new RemotableBuildListener(logFile, id + " → " + name); |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + @Issue("JENKINS-52729") |
| 224 | + @Test public void multipleStdioCalls() throws Exception { |
| 225 | + Node master = rule.jenkins; |
| 226 | + Node agent = rule.createOnlineSlave(); |
| 227 | + for (Node node : new Node[] {master, agent}) { |
| 228 | + assertMultipleStdioCalls("first TaskListener then OutputStream", node, false, (ps, os1, os2, os2Listener) -> { |
| 229 | + ps.stdout(os2Listener).stdout(os1); |
| 230 | + assertEquals(os1, ps.stdout()); |
| 231 | + }, false); |
| 232 | + assertMultipleStdioCalls("first OutputStream then TaskListener", node, false, (ps, os1, os2, os2Listener) -> { |
| 233 | + ps.stdout(os1).stdout(os2Listener); |
| 234 | + assertEquals(os2Listener.getLogger(), ps.stdout()); |
| 235 | + }, true); |
| 236 | + assertMultipleStdioCalls("stdout then stderr", node, true, (ps, os1, os2, os2Listener) -> { |
| 237 | + ps.stdout(os1).stderr(os2); |
| 238 | + assertEquals(os1, ps.stdout()); |
| 239 | + assertEquals(os2, ps.stderr()); |
| 240 | + }, true); |
| 241 | + assertMultipleStdioCalls("stderr then stdout", node, true, (ps, os1, os2, os2Listener) -> { |
| 242 | + ps.stdout(os1).stderr(os2); |
| 243 | + assertEquals(os1, ps.stdout()); |
| 244 | + assertEquals(os2, ps.stderr()); |
| 245 | + }, true); |
| 246 | + } |
| 247 | + } |
| 248 | + @FunctionalInterface |
| 249 | + private interface ProcStarterCustomizer { |
| 250 | + void run(Launcher.ProcStarter ps, OutputStream os1, OutputStream os2, TaskListener os2Listener) throws Exception; |
| 251 | + } |
| 252 | + private void assertMultipleStdioCalls(String message, Node node, boolean emitStderr, ProcStarterCustomizer psCustomizer, boolean outputIn2) throws Exception { |
| 253 | + message = node.getDisplayName() + ": " + message; |
| 254 | + Launcher launcher = node.createLauncher(StreamTaskListener.fromStderr()); |
| 255 | + Launcher.ProcStarter ps = launcher.launch(); |
| 256 | + assumeFalse("should not be platform-dependent, not bothering for now", Functions.isWindows()); |
| 257 | + if (emitStderr) { |
| 258 | + ps.cmds("sh", "-c", "echo hello >&2").quiet(true); |
| 259 | + } else { |
| 260 | + ps.cmds("echo", "hello"); |
| 261 | + } |
| 262 | + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); |
| 263 | + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); |
| 264 | + TaskListener listener = new StreamTaskListener(baos2); |
| 265 | + psCustomizer.run(ps, baos1, baos2, listener); |
| 266 | + assertEquals(message, 0, ps.join()); |
| 267 | + if (outputIn2) { |
| 268 | + assertThat(message, baos2.toString(), containsString("hello")); |
| 269 | + assertThat(message, baos1.toString(), isEmptyString()); |
| 270 | + } else { |
| 271 | + assertThat(message, baos1.toString(), containsString("hello")); |
| 272 | + assertThat(message, baos2.toString(), isEmptyString()); |
| 273 | + } |
| 274 | + } |
| 275 | + |
156 | 276 | }
|
0 commit comments