diff --git a/changelog.html b/changelog.html
index a46ab9009fe30b5eacbbd6cdc4c2e6382b4f66fc..9bff3a9bc99f882bd52190c8cb8d16689e6fd8f7 100644
--- a/changelog.html
+++ b/changelog.html
@@ -81,6 +81,9 @@ Upcoming changes
(issue 7275)
+ Improved the process forking abstractions so that plugins can more easily read from child processes.
+ (issue 7809)
diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java
index d216aff9bda8b449603694bbb579bdb72e36b975..d48892ea5061b3ed618ef735a1ab3c50b5cc1a0b 100644
--- a/core/src/main/java/hudson/Launcher.java
+++ b/core/src/main/java/hudson/Launcher.java
@@ -1,7 +1,7 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +24,6 @@
package hudson;
import hudson.Proc.LocalProc;
-import hudson.Proc.RemoteProc;
import hudson.model.Computer;
import hudson.model.Hudson;
import hudson.model.TaskListener;
@@ -44,7 +43,9 @@ import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InterruptedIOException;
import java.io.OutputStream;
+import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
@@ -144,8 +145,19 @@ public abstract class Launcher {
protected boolean[] masks;
protected FilePath pwd;
protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr;
- protected InputStream stdin = new NullInputStream(0);
+ protected InputStream stdin = NULL_INPUT_STREAM;
protected String[] envs;
+ /**
+ * True to reverse the I/O direction.
+ *
+ * For example, if {@link #reverseStdout}==true, then we expose
+ * {@link InputStream} from {@link Proc} and expect the client to read from it,
+ * whereas normally we take {@link OutputStream} via {@link #stdout(OutputStream)}
+ * and feed stdout into that output.
+ *
+ * @since 1.399
+ */
+ protected boolean reverseStdin, reverseStdout, reverseStderr;
public ProcStarter cmds(String... args) {
return cmds(Arrays.asList(args));
@@ -266,6 +278,50 @@ public abstract class Launcher {
return envs;
}
+ /**
+ * Indicates that the caller will pump {@code stdout} from the child process
+ * via {@link Proc#getStdout()} (whereas by default you call {@link #stdout(OutputStream)}
+ * and let Jenkins pump stdout into your {@link OutputStream} of choosing.
+ *
+ *
+ * When this method is called, {@link Proc#getStdout()} will read the combined output
+ * of {@code stdout/stderr} from the child process, unless {@link #readStderr()} is called
+ * separately, which lets the caller read those two streams separately.
+ *
+ * @since 1.399
+ */
+ public ProcStarter readStdout() {
+ reverseStdout = true;
+ stdout = stderr = null;
+ return this;
+ }
+
+ /**
+ * In addition to the effect of {@link #readStdout()}, indicate that the caller will pump {@code stderr}
+ * from the child process separately from {@code stdout}. The stderr will be readable from
+ * {@link Proc#getStderr()} while {@link Proc#getStdout()} reads from stdout.
+ *
+ * @since 1.399
+ */
+ public ProcStarter readStderr() {
+ reverseStdout = true;
+ reverseStderr = true;
+ return this;
+ }
+
+ /**
+ * Indicates that the caller will directly write to the child process {@code stin} }via
+ * {@link Proc#getStdin()} (whereas by default you call {@link #stdin(InputStream)}
+ * and let Jenkins pump your {@link InputStream} of choosing to stdin.
+ * @since 1.399
+ */
+ public ProcStarter writeStdin() {
+ reverseStdin = true;
+ stdin = null;
+ return this;
+ }
+
+
/**
* Starts the new process as configured.
*/
@@ -284,7 +340,11 @@ public abstract class Launcher {
* Copies a {@link ProcStarter}.
*/
public ProcStarter copy() {
- return new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs);
+ ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs);
+ rhs.reverseStdin = this.reverseStdin;
+ rhs.reverseStderr = this.reverseStderr;
+ rhs.reverseStdout = this.reverseStdout;
+ return rhs;
}
}
@@ -635,7 +695,11 @@ public abstract class Launcher {
for ( int idx = 0 ; idx < jobCmd.length; idx++ )
jobCmd[idx] = jobEnv.expand(ps.commands.get(idx));
- return new LocalProc(jobCmd, Util.mapToEnv(jobEnv), ps.stdin, ps.stdout, ps.stderr, toFile(ps.pwd));
+ return new LocalProc(jobCmd, Util.mapToEnv(jobEnv),
+ ps.reverseStdin ?LocalProc.SELFPUMP_INPUT:ps.stdin,
+ ps.reverseStdout?LocalProc.SELFPUMP_OUTPUT:ps.stdout,
+ ps.reverseStderr?LocalProc.SELFPUMP_OUTPUT:ps.stderr,
+ toFile(ps.pwd));
}
private File toFile(FilePath f) {
@@ -716,10 +780,14 @@ public abstract class Launcher {
public Proc launch(ProcStarter ps) throws IOException {
final OutputStream out = ps.stdout == null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stdout));
final OutputStream err = ps.stderr==null ? null : new RemoteOutputStream(new CloseProofOutputStream(ps.stderr));
- final InputStream in = ps.stdin==null ? null : new RemoteInputStream(ps.stdin);
+ final InputStream in = (ps.stdin==null || ps.stdin==NULL_INPUT_STREAM) ? null : new RemoteInputStream(ps.stdin,false);
final String workDir = ps.pwd==null ? null : ps.pwd.getRemote();
- return new RemoteProc(getChannel().callAsync(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, out, err, workDir, listener)));
+ try {
+ return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, workDir, listener)));
+ } catch (InterruptedException e) {
+ throw (IOException)new InterruptedIOException().initCause(e);
+ }
}
public Channel launchChannel(String[] cmd, OutputStream err, FilePath _workDir, Map envOverrides) throws IOException, InterruptedException {
@@ -762,9 +830,64 @@ public abstract class Launcher {
private static final long serialVersionUID = 1L;
}
+
+ public static final class ProcImpl extends Proc {
+ private final RemoteProcess process;
+ private final IOTriplet io;
+
+ public ProcImpl(RemoteProcess process) {
+ this.process = process;
+ this.io = process.getIOtriplet();
+ }
+
+ @Override
+ public void kill() throws IOException, InterruptedException {
+ process.kill();
+ }
+
+ @Override
+ public int join() throws IOException, InterruptedException {
+ return process.join();
+ }
+
+ @Override
+ public boolean isAlive() throws IOException, InterruptedException {
+ return process.isAlive();
+ }
+
+ @Override
+ public InputStream getStdout() {
+ return io.stdout;
+ }
+
+ @Override
+ public InputStream getStderr() {
+ return io.stderr;
+ }
+
+ @Override
+ public OutputStream getStdin() {
+ return io.stdin;
+ }
+ }
+ }
+
+ private static class IOTriplet implements Serializable {
+ InputStream stdout,stderr;
+ OutputStream stdin;
+ private static final long serialVersionUID = 1L;
+ }
+ /**
+ * Remoting interface of a remote process
+ */
+ static interface RemoteProcess {
+ int join() throws InterruptedException, IOException;
+ void kill() throws IOException, InterruptedException;
+ boolean isAlive() throws IOException, InterruptedException;
+ IOTriplet getIOtriplet();
}
- private static class RemoteLaunchCallable implements Callable {
+ private static class RemoteLaunchCallable implements Callable {
private final List cmd;
private final boolean[] masks;
private final String[] env;
@@ -773,8 +896,9 @@ public abstract class Launcher {
private final OutputStream err;
private final String workDir;
private final TaskListener listener;
+ private final boolean reverseStdin, reverseStdout, reverseStderr;
- RemoteLaunchCallable(List cmd, boolean[] masks, String[] env, InputStream in, OutputStream out, OutputStream err, String workDir, TaskListener listener) {
+ RemoteLaunchCallable(List cmd, boolean[] masks, String[] env, InputStream in, boolean reverseStdin, OutputStream out, boolean reverseStdout, OutputStream err, boolean reverseStderr, String workDir, TaskListener listener) {
this.cmd = new ArrayList(cmd);
this.masks = masks;
this.env = env;
@@ -783,26 +907,51 @@ public abstract class Launcher {
this.err = err;
this.workDir = workDir;
this.listener = listener;
+ this.reverseStdin = reverseStdin;
+ this.reverseStdout = reverseStdout;
+ this.reverseStderr = reverseStderr;
}
- public Integer call() throws IOException {
+ public RemoteProcess call() throws IOException {
Launcher.ProcStarter ps = new LocalLauncher(listener).launch();
ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err);
if(workDir!=null) ps.pwd(workDir);
+ if (reverseStdin) ps.writeStdin();
+ if (reverseStdout) ps.readStdout();
+ if (reverseStderr) ps.readStderr();
- Proc p = ps.start();
- try {
- return p.join();
- } catch (InterruptedException e) {
- return -1;
- } finally {
- // make sure I/O is delivered to the remote before we return
- try {
- Channel.current().syncIO();
- } catch (Throwable _) {
- // this includes a failure to sync, slave.jar too old, etc
+ final Proc p = ps.start();
+
+ return Channel.current().export(RemoteProcess.class,new RemoteProcess() {
+ public int join() throws InterruptedException, IOException {
+ try {
+ return p.join();
+ } finally {
+ // make sure I/O is delivered to the remote before we return
+ try {
+ Channel.current().syncIO();
+ } catch (Throwable _) {
+ // this includes a failure to sync, slave.jar too old, etc
+ }
+ }
}
- }
+
+ public void kill() throws IOException, InterruptedException {
+ p.kill();
+ }
+
+ public boolean isAlive() throws IOException, InterruptedException {
+ return p.isAlive();
+ }
+
+ public IOTriplet getIOtriplet() {
+ IOTriplet r = new IOTriplet();
+ if (reverseStdout) r.stdout = new RemoteInputStream(p.getStdout());
+ if (reverseStderr) r.stderr = new RemoteInputStream(p.getStderr());
+ if (reverseStdin) r.stdin = new RemoteOutputStream(p.getStdin());
+ return r;
+ }
+ });
}
private static final long serialVersionUID = 1L;
@@ -873,5 +1022,7 @@ public abstract class Launcher {
*/
public static boolean showFullPath = false;
+ private static final NullInputStream NULL_INPUT_STREAM = new NullInputStream(0);
+
private static final Logger LOGGER = Logger.getLogger(Launcher.class.getName());
}
diff --git a/core/src/main/java/hudson/Proc.java b/core/src/main/java/hudson/Proc.java
index 2bfa33d7d539320c4d333c927651572279a26487..0f3ffd2983092c5d38472abd021d37fb9721566e 100644
--- a/core/src/main/java/hudson/Proc.java
+++ b/core/src/main/java/hudson/Proc.java
@@ -1,7 +1,7 @@
/*
* The MIT License
*
- * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,11 +23,14 @@
*/
package hudson;
+import hudson.Launcher.ProcStarter;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.util.IOException2;
+import hudson.util.NullStream;
import hudson.util.StreamCopyThread;
import hudson.util.ProcessTree;
+import org.apache.commons.io.input.NullInputStream;
import java.io.File;
import java.io.IOException;
@@ -71,7 +74,10 @@ public abstract class Proc {
public abstract void kill() throws IOException, InterruptedException;
/**
- * Waits for the completion of the process and until we finish reading everything that the process has produced
+ * Waits for the completion of the process.
+ *
+ * Unless the caller opts to pump the streams via {@link #getStdout()} etc.,
+ * this method also blocks until we finish reading everything that the process has produced
* to stdout/stderr.
*
*
@@ -85,6 +91,45 @@ public abstract class Proc {
*/
public abstract int join() throws IOException, InterruptedException;
+ /**
+ * Returns an {@link InputStream} to read from {@code stdout} of the child process.
+ *
+ * When this method returns null, {@link Proc} will internally pump the output from
+ * the child process to your {@link OutputStream} of choosing.
+ *
+ * @return
+ * null unless {@link ProcStarter#readStdout()} is used to indicate
+ * that the caller intends to pump the stream by itself.
+ * @since 1.399
+ */
+ public abstract InputStream getStdout();
+
+ /**
+ * Returns an {@link InputStream} to read from {@code stderr} of the child process.
+ *
+ * When this method returns null, {@link Proc} will internally pump the output from
+ * the child process to your {@link OutputStream} of choosing.
+ *
+ * @return
+ * null unless {@link ProcStarter#readStderr()} is used to indicate
+ * that the caller intends to pump the stream by itself.
+ * @since 1.399
+ */
+ public abstract InputStream getStderr();
+
+ /**
+ * Returns an {@link OutputStream} to write to {@code stdin} of the child process.
+ *
+ * When this method returns null, {@link Proc} will internally pump the {@link InputStream}
+ * of your choosing to the child process.
+ *
+ * @return
+ * null unless {@link ProcStarter#writeStdin()} is used to indicate
+ * that the caller intends to pump the stream by itself.
+ * @since 1.399
+ */
+ public abstract OutputStream getStdin();
+
private static final ExecutorService executor = Executors.newCachedThreadPool();
/**
* Like {@link #join} but can be given a maximum time to wait.
@@ -133,6 +178,9 @@ public abstract class Proc {
private final EnvVars cookie;
private final String name;
+ private final InputStream stdout,stderr;
+ private final OutputStream stdin;
+
public LocalProc(String cmd, Map env, OutputStream out, File workDir) throws IOException {
this(cmd,Util.mapToEnv(env),out,workDir);
}
@@ -163,12 +211,12 @@ public abstract class Proc {
*/
public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out,OutputStream err,File workDir) throws IOException {
this( calcName(cmd),
- stderr(environment(new ProcessBuilder(cmd),env).directory(workDir),err),
+ stderr(environment(new ProcessBuilder(cmd),env).directory(workDir), err==null || err== SELFPUMP_OUTPUT),
in, out, err );
}
- private static ProcessBuilder stderr(ProcessBuilder pb, OutputStream stderr) {
- if(stderr==null) pb.redirectErrorStream(true);
+ private static ProcessBuilder stderr(ProcessBuilder pb, boolean redirectError) {
+ if(redirectError) pb.redirectErrorStream(true);
return pb;
}
@@ -191,17 +239,39 @@ public abstract class Proc {
this.cookie = EnvVars.createCookie();
procBuilder.environment().putAll(cookie);
this.proc = procBuilder.start();
+
InputStream procInputStream = proc.getInputStream();
- copier = new StreamCopyThread(name+": stdout copier", procInputStream, out);
- copier.start();
- if(in!=null)
- new StdinCopyThread(name+": stdin copier",in,proc.getOutputStream()).start();
- else
+ if (out==SELFPUMP_OUTPUT) {
+ stdout = procInputStream;
+ copier = null;
+ } else {
+ copier = new StreamCopyThread(name+": stdout copier", procInputStream, out);
+ copier.start();
+ stdout = null;
+ }
+
+ if (in == null) {
+ // nothing to feed to stdin
+ stdin = null;
proc.getOutputStream().close();
+ } else
+ if (in==SELFPUMP_INPUT) {
+ stdin = proc.getOutputStream();
+ } else {
+ new StdinCopyThread(name+": stdin copier",in,proc.getOutputStream()).start();
+ stdin = null;
+ }
+
InputStream procErrorStream = proc.getErrorStream();
if(err!=null) {
- copier2 = new StreamCopyThread(name+": stderr copier", procErrorStream, err);
- copier2.start();
+ if (err==SELFPUMP_OUTPUT) {
+ stderr = procErrorStream;
+ copier2 = null;
+ } else {
+ stderr = null;
+ copier2 = new StreamCopyThread(name+": stderr copier", procErrorStream, err);
+ copier2.start();
+ }
} else {
// the javadoc is unclear about what getErrorStream() returns when ProcessBuilder.redirectErrorStream(true),
//
@@ -212,9 +282,22 @@ public abstract class Proc {
procErrorStream.close();
}
copier2 = null;
+ stderr = null;
}
}
+ public InputStream getStdout() {
+ return stdout;
+ }
+
+ public InputStream getStderr() {
+ return stderr;
+ }
+
+ public OutputStream getStdin() {
+ return stdin;
+ }
+
/**
* Waits for the completion of the process.
*/
@@ -234,9 +317,9 @@ public abstract class Proc {
// see http://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build
// problems like that shows up as infinite wait in join(), which confuses great many users.
// So let's do a timed wait here and try to diagnose the problem
- copier.join(10*1000);
+ if (copier!=null) copier.join(10*1000);
if(copier2!=null) copier2.join(10*1000);
- if(copier.isAlive() || (copier2!=null && copier2.isAlive())) {
+ if((copier!=null && copier.isAlive()) || (copier2!=null && copier2.isAlive())) {
// looks like handles are leaking.
// closing these handles should terminate the threads.
String msg = "Process leaked file descriptors. See http://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build for more information";
@@ -335,10 +418,15 @@ public abstract class Proc {
}
return buf.toString();
}
+
+ public static final InputStream SELFPUMP_INPUT = new NullInputStream(0);
+ public static final OutputStream SELFPUMP_OUTPUT = new NullStream();
}
/**
* Remotely launched process via {@link Channel}.
+ *
+ * @deprecated as of 1.399
*/
public static final class RemoteProc extends Proc {
private final Future process;
@@ -373,6 +461,21 @@ public abstract class Proc {
public boolean isAlive() throws IOException, InterruptedException {
return !process.isDone();
}
+
+ @Override
+ public InputStream getStdout() {
+ return null;
+ }
+
+ @Override
+ public InputStream getStderr() {
+ return null;
+ }
+
+ @Override
+ public OutputStream getStdin() {
+ return null;
+ }
}
private static final Logger LOGGER = Logger.getLogger(Proc.class.getName());
diff --git a/core/src/main/java/hudson/os/windows/WindowsRemoteLauncher.java b/core/src/main/java/hudson/os/windows/WindowsRemoteLauncher.java
index 5e4115f3afe3c7350438befca475df075acc5e6d..9b4d16c8127462880997be88ab3ef06a0bd83809 100644
--- a/core/src/main/java/hudson/os/windows/WindowsRemoteLauncher.java
+++ b/core/src/main/java/hudson/os/windows/WindowsRemoteLauncher.java
@@ -14,6 +14,7 @@ import org.jvnet.hudson.remcom.WindowsRemoteProcessLauncher;
import java.io.BufferedOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
@@ -89,6 +90,21 @@ public class WindowsRemoteLauncher extends Launcher {
proc.destroy();
}
}
+
+ @Override
+ public InputStream getStdout() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InputStream getStderr() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public OutputStream getStdin() {
+ throw new UnsupportedOperationException();
+ }
};
}
diff --git a/core/src/test/java/hudson/LauncherTest.java b/core/src/test/java/hudson/LauncherTest.java
index 44afce152a9267cd7fd53424c879122daed5e2f4..07d20a4b0865f2aae967d8026ce70f7e866b762c 100644
--- a/core/src/test/java/hudson/LauncherTest.java
+++ b/core/src/test/java/hudson/LauncherTest.java
@@ -28,11 +28,12 @@ import hudson.util.ProcessTree;
import hudson.util.StreamTaskListener;
import hudson.remoting.Callable;
import org.apache.commons.io.FileUtils;
+import org.jvnet.hudson.test.Bug;
import java.io.File;
public class LauncherTest extends ChannelTestCase {
- //@Bug(4611)
+ @Bug(4611)
public void testRemoteKill() throws Exception {
if (File.pathSeparatorChar != ':') {
System.err.println("Skipping, currently Unix-specific test");
diff --git a/remoting/src/main/java/hudson/remoting/RemoteInputStream.java b/remoting/src/main/java/hudson/remoting/RemoteInputStream.java
index 26bb3250c940f9d430b7dc3dbc3e713634f0f272..754ef35afb07c72d25b213d43c82177af68898e7 100644
--- a/remoting/src/main/java/hudson/remoting/RemoteInputStream.java
+++ b/remoting/src/main/java/hudson/remoting/RemoteInputStream.java
@@ -39,13 +39,28 @@ import java.io.ObjectInputStream;
*/
public class RemoteInputStream extends InputStream implements Serializable {
private transient InputStream core;
+ private boolean autoUnexport;
+ /**
+ * Short for {@code RemoteInputStream(core,true)}.
+ */
public RemoteInputStream(InputStream core) {
+ this(core,true);
+ }
+
+ /**
+ * @param autoUnexport
+ * If true, the {@link InputStream} will be automatically unexported when
+ * the callable that took it with returns. If false, it'll not unexported
+ * until the close method is called.
+ */
+ public RemoteInputStream(InputStream core, boolean autoUnexport) {
this.core = core;
+ this.autoUnexport = autoUnexport;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
- int id = Channel.current().export(core);
+ int id = Channel.current().export(core,autoUnexport);
oos.writeInt(id);
}
diff --git a/test/src/main/java/org/jvnet/hudson/test/FakeLauncher.java b/test/src/main/java/org/jvnet/hudson/test/FakeLauncher.java
index f91c05cc4a2c0ee5313d9b420ebea861367ca6a6..63961f1e23b9e2b1fd22242b48432910962880cd 100644
--- a/test/src/main/java/org/jvnet/hudson/test/FakeLauncher.java
+++ b/test/src/main/java/org/jvnet/hudson/test/FakeLauncher.java
@@ -4,6 +4,8 @@ import hudson.Launcher.ProcStarter;
import hudson.Proc;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
/**
* Fake a process launch.
@@ -47,5 +49,23 @@ public interface FakeLauncher {
public int join() throws IOException, InterruptedException {
return exitCode;
}
+
+ @Override
+ public InputStream getStdout() {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InputStream getStderr() {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public OutputStream getStdin() {
+ // TODO
+ throw new UnsupportedOperationException();
+ }
}
}
diff --git a/test/src/test/java/hudson/ProcTest.java b/test/src/test/java/hudson/ProcTest.java
index 82f5ccba13f3efa9c4c455cc06b422e1b2489acd..5eb775276878341c9e9a527d56bde505d5ae368f 100644
--- a/test/src/test/java/hudson/ProcTest.java
+++ b/test/src/test/java/hudson/ProcTest.java
@@ -1,5 +1,6 @@
package hudson;
+import hudson.Launcher.LocalLauncher;
import hudson.Launcher.RemoteLauncher;
import hudson.Proc.RemoteProc;
import hudson.remoting.Callable;
@@ -12,6 +13,7 @@ import hudson.util.StreamTaskListener;
import org.jvnet.hudson.test.Bug;
import org.jvnet.hudson.test.HudsonTestCase;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -26,13 +28,7 @@ public class ProcTest extends HudsonTestCase {
*/
@Bug(7809)
public void testRemoteProcOutputSync() throws Exception {
- DumbSlave s = createSlave();
- s.toComputer().connect(false).get();
- VirtualChannel ch=null;
- while (ch==null) {
- ch = s.toComputer().getChannel();
- Thread.sleep(100);
- }
+ VirtualChannel ch = createSlaveChannel();
// keep the pipe fairly busy
final Pipe p = Pipe.createRemoteToLocal();
@@ -63,6 +59,17 @@ public class ProcTest extends HudsonTestCase {
ch.close();
}
+ private VirtualChannel createSlaveChannel() throws Exception {
+ DumbSlave s = createSlave();
+ s.toComputer().connect(false).get();
+ VirtualChannel ch=null;
+ while (ch==null) {
+ ch = s.toComputer().getChannel();
+ Thread.sleep(100);
+ }
+ return ch;
+ }
+
private static class ChannelFiller implements Callable {
private final OutputStream o;
@@ -77,4 +84,38 @@ public class ProcTest extends HudsonTestCase {
return null;
}
}
+
+ @Bug(7809)
+ public void testIoPumpingWithLocalLaunch() throws Exception {
+ doIoPumpingTest(new LocalLauncher(new StreamTaskListener(System.out, Charset.defaultCharset())));
+ }
+
+ @Bug(7809)
+ public void testIoPumpingWithRemoteLaunch() throws Exception {
+ doIoPumpingTest(new RemoteLauncher(
+ new StreamTaskListener(System.out, Charset.defaultCharset()),
+ createSlaveChannel(), true));
+ }
+
+ private void doIoPumpingTest(Launcher l) throws IOException, InterruptedException {
+ String[] ECHO_BACK_CMD = {"cat"}; // TODO: what is the echo back command for Windows? "cmd /C copy CON CON"?
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ l.launch().cmds(ECHO_BACK_CMD).stdin(new ByteArrayInputStream("Hello".getBytes())).stdout(out).join();
+ assertEquals("Hello",out.toString());
+
+ Proc p = l.launch().cmds(ECHO_BACK_CMD).stdin(new ByteArrayInputStream("Hello".getBytes())).readStdout().start();
+ p.join();
+ assertEquals("Hello", org.apache.commons.io.IOUtils.toString(p.getStdout()));
+ assertNull(p.getStderr());
+ assertNull(p.getStdin());
+
+
+ p = l.launch().cmds(ECHO_BACK_CMD).writeStdin().readStdout().start();
+ p.getStdin().write("Hello".getBytes());
+ p.getStdin().close();
+ p.join();
+ assertEquals("Hello", org.apache.commons.io.IOUtils.toString(p.getStdout()));
+ assertNull(p.getStderr());
+ }
}