Skip to content

Commit ebd7111

Browse files
jglickoleg-nenashev
authored andcommitted
[JENKINS-52729] Launcher.ProcStarter.stdout(TaskListener) discards remotability (#3563)
* [JENKINS-52729] Demonstrating issue with Launcher.ProcStarter.stdout(TaskListener). * [JENKINS-52729] Properly remote the TaskListener. * Defend test against a possible problem on Windows. * FindBugs * Fix a Windows test failure. There is no C:\Windows\System32\echo.exe; it is a cmd.exe built-in. * Clarify in Javadoc that TaskListener is remotable. * A call to stdout(TaskListener) should be countermanded by a call to stdout(OutputStream), which was true for local but not remote launchers.
1 parent 78e012a commit ebd7111

File tree

3 files changed

+143
-7
lines changed

3 files changed

+143
-7
lines changed

core/src/main/java/hudson/Launcher.java

+19-6
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public final class ProcStarter {
166166
@CheckForNull
167167
protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr;
168168
@CheckForNull
169+
private TaskListener stdoutListener;
170+
@CheckForNull
169171
protected InputStream stdin = NULL_INPUT_STREAM;
170172
@CheckForNull
171173
protected String[] envs = null;
@@ -285,17 +287,20 @@ public FilePath pwd() {
285287
*/
286288
public ProcStarter stdout(@CheckForNull OutputStream out) {
287289
this.stdout = out;
290+
stdoutListener = null;
288291
return this;
289292
}
290293

291294
/**
292295
* Sends the stdout to the given {@link TaskListener}.
293296
*
294-
* @param out Task listener
297+
* @param out Task listener (must be safely remotable)
295298
* @return {@code this}
296299
*/
297300
public ProcStarter stdout(@Nonnull TaskListener out) {
298-
return stdout(out.getLogger());
301+
stdout = out.getLogger();
302+
stdoutListener = out;
303+
return this;
299304
}
300305

301306
/**
@@ -490,6 +495,7 @@ public int join() throws IOException, InterruptedException {
490495
@Nonnull
491496
public ProcStarter copy() {
492497
ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet);
498+
rhs.stdoutListener = stdoutListener;
493499
rhs.reverseStdin = this.reverseStdin;
494500
rhs.reverseStderr = this.reverseStderr;
495501
rhs.reverseStdout = this.reverseStdout;
@@ -1041,15 +1047,15 @@ public VirtualChannel getChannel() {
10411047
}
10421048

10431049
public Proc launch(ProcStarter ps) throws IOException {
1044-
final OutputStream out = ps.stdout == null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
1050+
final OutputStream out = ps.stdout == null || ps.stdoutListener != null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
10451051
final OutputStream err = ps.stderr==null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stderr));
10461052
final InputStream in = (ps.stdin==null || ps.stdin==NULL_INPUT_STREAM) ? null : new RemoteInputStream(ps.stdin,false);
10471053

10481054
final FilePath psPwd = ps.pwd;
10491055
final String workDir = psPwd==null ? null : psPwd.getRemote();
10501056

10511057
try {
1052-
return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener)));
1058+
return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener, ps.stdoutListener)));
10531059
} catch (InterruptedException e) {
10541060
throw (IOException)new InterruptedIOException().initCause(e);
10551061
}
@@ -1265,14 +1271,15 @@ private static class RemoteLaunchCallable extends MasterToSlaveCallable<RemotePr
12651271
private final @CheckForNull OutputStream err;
12661272
private final @CheckForNull String workDir;
12671273
private final @Nonnull TaskListener listener;
1274+
private final @CheckForNull TaskListener stdoutListener;
12681275
private final boolean reverseStdin, reverseStdout, reverseStderr;
12691276
private final boolean quiet;
12701277

12711278
RemoteLaunchCallable(@Nonnull List<String> cmd, @CheckForNull boolean[] masks, @CheckForNull String[] env,
12721279
@CheckForNull InputStream in, boolean reverseStdin,
12731280
@CheckForNull OutputStream out, boolean reverseStdout,
12741281
@CheckForNull OutputStream err, boolean reverseStderr,
1275-
boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener) {
1282+
boolean quiet, @CheckForNull String workDir, @Nonnull TaskListener listener, @CheckForNull TaskListener stdoutListener) {
12761283
this.cmd = new ArrayList<>(cmd);
12771284
this.masks = masks;
12781285
this.env = env;
@@ -1281,6 +1288,7 @@ private static class RemoteLaunchCallable extends MasterToSlaveCallable<RemotePr
12811288
this.err = err;
12821289
this.workDir = workDir;
12831290
this.listener = listener;
1291+
this.stdoutListener = stdoutListener;
12841292
this.reverseStdin = reverseStdin;
12851293
this.reverseStdout = reverseStdout;
12861294
this.reverseStderr = reverseStderr;
@@ -1290,7 +1298,12 @@ private static class RemoteLaunchCallable extends MasterToSlaveCallable<RemotePr
12901298
public RemoteProcess call() throws IOException {
12911299
final Channel channel = getOpenChannelOrFail();
12921300
Launcher.ProcStarter ps = new LocalLauncher(listener).launch();
1293-
ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err).quiet(quiet);
1301+
ps.cmds(cmd).masks(masks).envs(env).stdin(in).stderr(err).quiet(quiet);
1302+
if (stdoutListener != null) {
1303+
ps.stdout(stdoutListener.getLogger());
1304+
} else {
1305+
ps.stdout(out);
1306+
}
12941307
if(workDir!=null) ps.pwd(workDir);
12951308
if (reverseStdin) ps.writeStdin();
12961309
if (reverseStdout) ps.readStdout();

core/src/main/java/hudson/model/TaskListener.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import hudson.console.ConsoleNote;
2727
import hudson.console.HyperlinkNote;
28+
import hudson.remoting.Channel;
2829
import hudson.util.NullStream;
2930
import hudson.util.StreamTaskListener;
3031

@@ -59,7 +60,9 @@
5960
*
6061
* <p>
6162
* {@link StreamTaskListener} is the most typical implementation of this interface.
62-
* All the {@link TaskListener} implementations passed to plugins from Hudson core are remotable.
63+
*
64+
* <p>
65+
* Implementations are generally expected to be remotable via {@link Channel}.
6366
*
6467
* @author Kohsuke Kawaguchi
6568
*/

test/src/test/java/hudson/LauncherTest.java

+120
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,38 @@
2323
*/
2424
package hudson;
2525

26+
import hudson.console.LineTransformationOutputStream;
2627
import hudson.model.AbstractBuild;
2728
import hudson.model.AbstractProject;
29+
import hudson.model.BuildListener;
2830
import hudson.model.FreeStyleBuild;
2931
import hudson.model.FreeStyleProject;
3032
import hudson.model.Node;
3133
import hudson.model.ParametersDefinitionProperty;
3234
import hudson.model.Slave;
3335
import hudson.model.StringParameterDefinition;
3436
import hudson.model.TaskListener;
37+
import hudson.remoting.Channel;
3538
import hudson.tasks.BatchFile;
3639
import hudson.tasks.BuildStepDescriptor;
3740
import hudson.tasks.Builder;
3841
import hudson.tasks.CommandInterpreter;
3942
import hudson.tasks.Shell;
43+
import hudson.util.StreamTaskListener;
44+
import java.io.ByteArrayOutputStream;
45+
import java.io.File;
46+
import java.io.FileOutputStream;
4047
import java.io.IOException;
48+
import java.io.OutputStream;
49+
import java.io.PrintStream;
50+
import java.nio.charset.StandardCharsets;
4151

4252
import java.util.HashMap;
4353
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.*;
4458

4559
import org.junit.Rule;
4660
import org.junit.Test;
@@ -153,4 +167,110 @@ private static final class QuietBatchFile extends BatchFile {
153167
}
154168
}
155169

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+
156276
}

0 commit comments

Comments
 (0)