package hudson; import hudson.remoting.Channel; import hudson.util.StreamCopyThread; import hudson.util.IOException2; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; /** * External process wrapper. * *

* Used for launching, monitoring, waiting for a process. * * @author Kohsuke Kawaguchi */ public abstract class Proc { private Proc() {} /** * 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. * *

* 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; /** * Locally launched process. */ public static final class LocalProc extends Proc { private final Process proc; private final Thread copier; private final OutputStream out; public LocalProc(String cmd, Map env, OutputStream out, File workDir) throws IOException { this(cmd,Util.mapToEnv(env),out,workDir); } public LocalProc(String[] cmd, Map 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( calcName(cmd), environment(new ProcessBuilder(cmd),env).directory(workDir).redirectErrorStream(true).start(), in, out ); } private static ProcessBuilder environment(ProcessBuilder pb, String[] env) { Map m = pb.environment(); m.clear(); for (String e : env) { int idx = e.indexOf('='); m.put(e.substring(0,idx),e.substring(idx+1,0)); } return pb; } private LocalProc( String name, Process proc, InputStream in, OutputStream out ) throws IOException { Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name); this.out = out; this.proc = proc; copier = new StreamCopyThread(name+": stdout copier", proc.getInputStream(), out); copier.start(); if(in!=null) new ByteCopier(name+": stdin copier",in,proc.getOutputStream()).start(); else proc.getOutputStream().close(); } /** * Waits for the completion of the process. */ @Override public int join() throws InterruptedException, IOException { try { int r = proc.waitFor(); // see http://hudson.gotdns.com/wiki/display/HUDSON/Spawning+processes+from+build // problems like that shows up as inifinite 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.isAlive()) { // looks like handles are leaking. // closing these handles should terminate the threads. String msg = "Process leaked file descriptors. See http://hudson.gotdns.com/wiki/display/HUDSON/Spawning+processes+from+build for more information"; Throwable e = new Exception().fillInStackTrace(); LOGGER.log(Level.WARNING,msg,e); proc.getInputStream().close(); proc.getErrorStream().close(); out.write(msg.getBytes()); out.write('\n'); } return r; } catch (InterruptedException e) { // aborting. kill the process proc.destroy(); throw e; } } @Override public void kill() throws InterruptedException, IOException { proc.destroy(); join(); } private static class ByteCopier extends Thread { private final InputStream in; private final OutputStream out; public ByteCopier(String threadName, InputStream in, OutputStream out) { super(threadName); this.in = in; this.out = out; } public void run() { try { while(true) { int ch = in.read(); if(ch==-1) break; out.write(ch); } in.close(); out.close(); } catch (IOException e) { // TODO: what to do? } } } private static String calcName(String[] cmd) { StringBuffer buf = new StringBuffer(); 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 process; public RemoteProc(Future process) { this.process = process; } @Override public void kill() throws IOException, InterruptedException { process.cancel(true); join(); } @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); } } } private static final Logger LOGGER = Logger.getLogger(Proc.class.getName()); }