/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi
*
*
*******************************************************************************/
package hudson;
import hudson.model.TaskListener;
import hudson.util.IOException2;
import hudson.util.StreamCopyThread;
import hudson.util.ProcessTree;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* External process wrapper.
*
* <p> Used for launching, monitoring, waiting for a process.
*
* @author Kohsuke Kawaguchi
*/
public abstract class Proc {
protected Proc() {
}
/**
* Checks if the process is still alive.
*/
public abstract boolean isAlive() throws IOException, InterruptedException;
/**
* Terminates the process.
*
* @throws IOException if there's an error killing a process and a stack
* trace could help the trouble-shooting.
*/
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 to stdout/stderr.
*
* <p> If the thread is interrupted while waiting for the completion of the
* process, this method terminates the process and exits with a non-zero
* exit code.
*
* @throws IOException if there's an error launching/joining a process and a
* stack trace could help the trouble-shooting.
*/
public abstract int join() throws IOException, InterruptedException;
private static final ExecutorService executor = Executors.newCachedThreadPool();
/**
* Like {@link #join} but can be given a maximum time to wait.
*
* @param timeout number of time units
* @param unit unit of time
* @param listener place to send messages if there are problems, incl.
* timeout
* @return exit code from the process
* @throws IOException for the same reasons as {@link #join}
* @throws InterruptedException for the same reasons as {@link #join}
* @since 1.363
*/
public final int joinWithTimeout(final long timeout, final TimeUnit unit,
final TaskListener listener) throws IOException, InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
try {
executor.submit(new Runnable() {
public void run() {
try {
if (!latch.await(timeout, unit)) {
listener.error("Timeout after " + timeout + " "
+ unit.toString().toLowerCase(Locale.ENGLISH));
kill();
}
} catch (InterruptedException x) {
listener.error(x.toString());
} catch (IOException x) {
listener.error(x.toString());
} catch (RuntimeException x) {
listener.error(x.toString());
}
}
});
return join();
} finally {
latch.countDown();
}
}
/**
* Locally launched process.
*/
public static final class LocalProc extends Proc {
private final Process proc;
private final Thread copier, copier2;
private final OutputStream out;
private final EnvVars cookie;
private final String name;
public LocalProc(String cmd, Map<String, String> env, OutputStream out, File workDir) throws IOException {
this(cmd, Util.mapToEnv(env), out, workDir);
}
public LocalProc(String[] cmd, Map<String, String> env, InputStream in, OutputStream out) throws IOException {
this(cmd, Util.mapToEnv(env), in, out);
}
public LocalProc(String cmd, String[] env, OutputStream out, File workDir) throws IOException {
this(Util.tokenize(cmd), env, out, workDir);
}
public LocalProc(String[] cmd, String[] env, OutputStream out, File workDir) throws IOException {
this(cmd, env, null, out, workDir);
}
public LocalProc(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException {
this(cmd, env, in, out, null);
}
public LocalProc(String[] cmd, String[] env, InputStream in, OutputStream out, File workDir) throws IOException {
this(cmd, env, in, out, null, workDir);
}
/**
* @param err null to redirect stderr to stdout.
*/
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),
in, out, err);
}
private static ProcessBuilder stderr(ProcessBuilder pb, OutputStream stderr) {
if (stderr == null) {
pb.redirectErrorStream(true);
}
return pb;
}
private static ProcessBuilder environment(ProcessBuilder pb, String[] env) {
if (env != null) {
Map<String, String> m = pb.environment();
m.clear();
for (String e : env) {
int idx = e.indexOf('=');
m.put(e.substring(0, idx), e.substring(idx + 1, e.length()));
}
}
return pb;
}
private LocalProc(String name, ProcessBuilder procBuilder, InputStream in, OutputStream out, OutputStream err) throws IOException {
Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
this.name = name;
this.out = out;
this.cookie = EnvVars.createCookie();
procBuilder.environment().putAll(cookie);
this.proc = procBuilder.start();
copier = new StreamCopyThread(name + ": stdout copier", proc.getInputStream(), out);
copier.start();
if (in != null) {
new StdinCopyThread(name + ": stdin copier", in, proc.getOutputStream()).start();
} else {
proc.getOutputStream().close();
}
if (err != null) {
copier2 = new StreamCopyThread(name + ": stderr copier", proc.getErrorStream(), err);
copier2.start();
} else {
// while this is not discussed in javadoc, even with ProcessBuilder.redirectErrorStream(true),
// Process.getErrorStream() still returns a distinct reader end of a pipe that needs to be closed.
// this is according to the source code of JVM
proc.getErrorStream().close();
copier2 = null;
}
}
/**
* Waits for the completion of the process.
*/
@Override
public int join() throws InterruptedException, IOException {
// show what we are waiting for in the thread title
// since this involves some native work, let's have some soak period before enabling this by default
Thread t = Thread.currentThread();
String oldName = t.getName();
if (SHOW_PID) {
ProcessTree.OSProcess p = ProcessTree.get().get(proc);
t.setName(oldName + " " + (p != null ? "waiting for pid=" + p.getPid() : "waiting for " + name));
}
try {
int r = proc.waitFor();
// see http://wiki.hudson-ci.org/display/HUDSON/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 (copier2 != null) {
copier2.join(10 * 1000);
}
if (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.hudson-ci.org/display/HUDSON/Spawning+processes+from+build for more information";
Throwable e = new Exception().fillInStackTrace();
LOGGER.log(Level.WARNING, msg, e);
// doing proc.getInputStream().close() hangs in FileInputStream.close0()
// it could be either because another thread is blocking on read, or
// it could be a bug in Windows JVM. Who knows.
// so I'm abandoning the idea of closing the stream
// try {
// proc.getInputStream().close();
// } catch (IOException x) {
// LOGGER.log(Level.FINE,"stdin termination failed",x);
// }
// try {
// proc.getErrorStream().close();
// } catch (IOException x) {
// LOGGER.log(Level.FINE,"stderr termination failed",x);
// }
out.write(msg.getBytes());
out.write('\n');
}
return r;
} catch (InterruptedException e) {
// aborting. kill the process
destroy();
throw e;
} finally {
t.setName(oldName);
}
}
@Override
public boolean isAlive() throws IOException, InterruptedException {
try {
proc.exitValue();
return false;
} catch (IllegalThreadStateException e) {
return true;
}
}
@Override
public void kill() throws InterruptedException, IOException {
destroy();
join();
}
/**
* Destroys the child process without join.
*/
private void destroy() throws InterruptedException {
ProcessTree.get().killAll(proc, cookie);
}
/**
* {@link Process#getOutputStream()} is buffered, so we need to eagerly
* flash the stream to push bytes to the process.
*/
private static class StdinCopyThread extends Thread {
private final InputStream in;
private final OutputStream out;
public StdinCopyThread(String threadName, InputStream in, OutputStream out) {
super(threadName);
this.in = in;
this.out = out;
}
@Override
public void run() {
try {
try {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
out.flush();
}
} finally {
in.close();
out.close();
}
} catch (IOException e) {
// TODO: what to do?
}
}
}
private static String calcName(String[] cmd) {
StringBuilder buf = new StringBuilder();
for (String token : cmd) {
if (buf.length() > 0) {
buf.append(' ');
}
buf.append(token);
}
return buf.toString();
}
}
/**
* Retemoly launched process via {@link Channel}.
*/
public static final class RemoteProc extends Proc {
private final Future<Integer> process;
public RemoteProc(Future<Integer> process) {
this.process = process;
}
@Override
public void kill() throws IOException, InterruptedException {
process.cancel(true);
}
@Override
public int join() throws IOException, InterruptedException {
try {
return process.get();
} catch (InterruptedException e) {
// aborting. kill the process
process.cancel(true);
throw e;
} catch (ExecutionException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw new IOException2("Failed to join the process", e);
} catch (CancellationException x) {
return -1;
}
}
@Override
public boolean isAlive() throws IOException, InterruptedException {
return !process.isDone();
}
}
private static final Logger LOGGER = Logger.getLogger(Proc.class.getName());
/**
* Debug switch to have the thread display the process it's waiting for.
*/
public static boolean SHOW_PID = false;
}