/hudson-core/src/main/java/hudson/Proc.java

http://github.com/hudson/hudson · Java · 376 lines · 228 code · 34 blank · 114 comment · 16 complexity · e256bdec1bd4afb79ce6ad7a35dd0310 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson;
  25. import hudson.model.TaskListener;
  26. import hudson.util.IOException2;
  27. import hudson.util.StreamCopyThread;
  28. import hudson.util.ProcessTree;
  29. import java.io.File;
  30. import java.io.IOException;
  31. import java.io.InputStream;
  32. import java.io.OutputStream;
  33. import java.util.Locale;
  34. import java.util.Map;
  35. import java.util.concurrent.CancellationException;
  36. import java.util.concurrent.CountDownLatch;
  37. import java.util.concurrent.ExecutionException;
  38. import java.util.concurrent.ExecutorService;
  39. import java.util.concurrent.Executors;
  40. import java.util.concurrent.Future;
  41. import java.util.concurrent.TimeUnit;
  42. import java.util.logging.Level;
  43. import java.util.logging.Logger;
  44. /**
  45. * External process wrapper.
  46. *
  47. * <p>
  48. * Used for launching, monitoring, waiting for a process.
  49. *
  50. * @author Kohsuke Kawaguchi
  51. */
  52. public abstract class Proc {
  53. protected Proc() {}
  54. /**
  55. * Checks if the process is still alive.
  56. */
  57. public abstract boolean isAlive() throws IOException, InterruptedException;
  58. /**
  59. * Terminates the process.
  60. *
  61. * @throws IOException
  62. * if there's an error killing a process
  63. * and a stack trace could help the trouble-shooting.
  64. */
  65. public abstract void kill() throws IOException, InterruptedException;
  66. /**
  67. * Waits for the completion of the process and until we finish reading everything that the process has produced
  68. * to stdout/stderr.
  69. *
  70. * <p>
  71. * If the thread is interrupted while waiting for the completion
  72. * of the process, this method terminates the process and
  73. * exits with a non-zero exit code.
  74. *
  75. * @throws IOException
  76. * if there's an error launching/joining a process
  77. * and a stack trace could help the trouble-shooting.
  78. */
  79. public abstract int join() throws IOException, InterruptedException;
  80. private static final ExecutorService executor = Executors.newCachedThreadPool();
  81. /**
  82. * Like {@link #join} but can be given a maximum time to wait.
  83. * @param timeout number of time units
  84. * @param unit unit of time
  85. * @param listener place to send messages if there are problems, incl. timeout
  86. * @return exit code from the process
  87. * @throws IOException for the same reasons as {@link #join}
  88. * @throws InterruptedException for the same reasons as {@link #join}
  89. * @since 1.363
  90. */
  91. public final int joinWithTimeout(final long timeout, final TimeUnit unit,
  92. final TaskListener listener) throws IOException, InterruptedException {
  93. final CountDownLatch latch = new CountDownLatch(1);
  94. try {
  95. executor.submit(new Runnable() {
  96. public void run() {
  97. try {
  98. if (!latch.await(timeout, unit)) {
  99. listener.error("Timeout after " + timeout + " " +
  100. unit.toString().toLowerCase(Locale.ENGLISH));
  101. kill();
  102. }
  103. } catch (InterruptedException x) {
  104. listener.error(x.toString());
  105. } catch (IOException x) {
  106. listener.error(x.toString());
  107. } catch (RuntimeException x) {
  108. listener.error(x.toString());
  109. }
  110. }
  111. });
  112. return join();
  113. } finally {
  114. latch.countDown();
  115. }
  116. }
  117. /**
  118. * Locally launched process.
  119. */
  120. public static final class LocalProc extends Proc {
  121. private final Process proc;
  122. private final Thread copier,copier2;
  123. private final OutputStream out;
  124. private final EnvVars cookie;
  125. private final String name;
  126. public LocalProc(String cmd, Map<String,String> env, OutputStream out, File workDir) throws IOException {
  127. this(cmd,Util.mapToEnv(env),out,workDir);
  128. }
  129. public LocalProc(String[] cmd, Map<String,String> env,InputStream in, OutputStream out) throws IOException {
  130. this(cmd,Util.mapToEnv(env),in,out);
  131. }
  132. public LocalProc(String cmd,String[] env,OutputStream out, File workDir) throws IOException {
  133. this( Util.tokenize(cmd), env, out, workDir );
  134. }
  135. public LocalProc(String[] cmd,String[] env,OutputStream out, File workDir) throws IOException {
  136. this(cmd,env,null,out,workDir);
  137. }
  138. public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
  139. this(cmd,env,in,out,null);
  140. }
  141. public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out, File workDir) throws IOException {
  142. this(cmd,env,in,out,null,workDir);
  143. }
  144. /**
  145. * @param err
  146. * null to redirect stderr to stdout.
  147. */
  148. public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out,OutputStream err,File workDir) throws IOException {
  149. this( calcName(cmd),
  150. stderr(environment(new ProcessBuilder(cmd),env).directory(workDir),err),
  151. in, out, err );
  152. }
  153. private static ProcessBuilder stderr(ProcessBuilder pb, OutputStream stderr) {
  154. if(stderr==null) pb.redirectErrorStream(true);
  155. return pb;
  156. }
  157. private static ProcessBuilder environment(ProcessBuilder pb, String[] env) {
  158. if(env!=null) {
  159. Map<String, String> m = pb.environment();
  160. m.clear();
  161. for (String e : env) {
  162. int idx = e.indexOf('=');
  163. m.put(e.substring(0,idx),e.substring(idx+1,e.length()));
  164. }
  165. }
  166. return pb;
  167. }
  168. private LocalProc( String name, ProcessBuilder procBuilder, InputStream in, OutputStream out, OutputStream err ) throws IOException {
  169. Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
  170. this.name = name;
  171. this.out = out;
  172. this.cookie = EnvVars.createCookie();
  173. procBuilder.environment().putAll(cookie);
  174. this.proc = procBuilder.start();
  175. copier = new StreamCopyThread(name+": stdout copier", proc.getInputStream(), out);
  176. copier.start();
  177. if(in!=null)
  178. new StdinCopyThread(name+": stdin copier",in,proc.getOutputStream()).start();
  179. else
  180. proc.getOutputStream().close();
  181. if(err!=null) {
  182. copier2 = new StreamCopyThread(name+": stderr copier", proc.getErrorStream(), err);
  183. copier2.start();
  184. } else {
  185. // while this is not discussed in javadoc, even with ProcessBuilder.redirectErrorStream(true),
  186. // Process.getErrorStream() still returns a distinct reader end of a pipe that needs to be closed.
  187. // this is according to the source code of JVM
  188. proc.getErrorStream().close();
  189. copier2 = null;
  190. }
  191. }
  192. /**
  193. * Waits for the completion of the process.
  194. */
  195. @Override
  196. public int join() throws InterruptedException, IOException {
  197. // show what we are waiting for in the thread title
  198. // since this involves some native work, let's have some soak period before enabling this by default
  199. Thread t = Thread.currentThread();
  200. String oldName = t.getName();
  201. if (SHOW_PID) {
  202. ProcessTree.OSProcess p = ProcessTree.get().get(proc);
  203. t.setName(oldName+" "+(p!=null?"waiting for pid="+p.getPid():"waiting for "+name));
  204. }
  205. try {
  206. int r = proc.waitFor();
  207. // see http://wiki.hudson-ci.org/display/HUDSON/Spawning+processes+from+build
  208. // problems like that shows up as infinite wait in join(), which confuses great many users.
  209. // So let's do a timed wait here and try to diagnose the problem
  210. copier.join(10*1000);
  211. if(copier2!=null) copier2.join(10*1000);
  212. if(copier.isAlive() || (copier2!=null && copier2.isAlive())) {
  213. // looks like handles are leaking.
  214. // closing these handles should terminate the threads.
  215. String msg = "Process leaked file descriptors. See http://wiki.hudson-ci.org/display/HUDSON/Spawning+processes+from+build for more information";
  216. Throwable e = new Exception().fillInStackTrace();
  217. LOGGER.log(Level.WARNING,msg,e);
  218. // doing proc.getInputStream().close() hangs in FileInputStream.close0()
  219. // it could be either because another thread is blocking on read, or
  220. // it could be a bug in Windows JVM. Who knows.
  221. // so I'm abandoning the idea of closing the stream
  222. // try {
  223. // proc.getInputStream().close();
  224. // } catch (IOException x) {
  225. // LOGGER.log(Level.FINE,"stdin termination failed",x);
  226. // }
  227. // try {
  228. // proc.getErrorStream().close();
  229. // } catch (IOException x) {
  230. // LOGGER.log(Level.FINE,"stderr termination failed",x);
  231. // }
  232. out.write(msg.getBytes());
  233. out.write('\n');
  234. }
  235. return r;
  236. } catch (InterruptedException e) {
  237. // aborting. kill the process
  238. destroy();
  239. throw e;
  240. } finally {
  241. t.setName(oldName);
  242. }
  243. }
  244. @Override
  245. public boolean isAlive() throws IOException, InterruptedException {
  246. try {
  247. proc.exitValue();
  248. return false;
  249. } catch (IllegalThreadStateException e) {
  250. return true;
  251. }
  252. }
  253. @Override
  254. public void kill() throws InterruptedException, IOException {
  255. destroy();
  256. join();
  257. }
  258. /**
  259. * Destroys the child process without join.
  260. */
  261. private void destroy() throws InterruptedException {
  262. ProcessTree.get().killAll(proc,cookie);
  263. }
  264. /**
  265. * {@link Process#getOutputStream()} is buffered, so we need to eagerly flash
  266. * the stream to push bytes to the process.
  267. */
  268. private static class StdinCopyThread extends Thread {
  269. private final InputStream in;
  270. private final OutputStream out;
  271. public StdinCopyThread(String threadName, InputStream in, OutputStream out) {
  272. super(threadName);
  273. this.in = in;
  274. this.out = out;
  275. }
  276. @Override
  277. public void run() {
  278. try {
  279. try {
  280. byte[] buf = new byte[8192];
  281. int len;
  282. while ((len = in.read(buf)) > 0) {
  283. out.write(buf, 0, len);
  284. out.flush();
  285. }
  286. } finally {
  287. in.close();
  288. out.close();
  289. }
  290. } catch (IOException e) {
  291. // TODO: what to do?
  292. }
  293. }
  294. }
  295. private static String calcName(String[] cmd) {
  296. StringBuilder buf = new StringBuilder();
  297. for (String token : cmd) {
  298. if(buf.length()>0) buf.append(' ');
  299. buf.append(token);
  300. }
  301. return buf.toString();
  302. }
  303. }
  304. /**
  305. * Retemoly launched process via {@link Channel}.
  306. */
  307. public static final class RemoteProc extends Proc {
  308. private final Future<Integer> process;
  309. public RemoteProc(Future<Integer> process) {
  310. this.process = process;
  311. }
  312. @Override
  313. public void kill() throws IOException, InterruptedException {
  314. process.cancel(true);
  315. }
  316. @Override
  317. public int join() throws IOException, InterruptedException {
  318. try {
  319. return process.get();
  320. } catch (InterruptedException e) {
  321. // aborting. kill the process
  322. process.cancel(true);
  323. throw e;
  324. } catch (ExecutionException e) {
  325. if(e.getCause() instanceof IOException)
  326. throw (IOException)e.getCause();
  327. throw new IOException2("Failed to join the process",e);
  328. } catch (CancellationException x) {
  329. return -1;
  330. }
  331. }
  332. @Override
  333. public boolean isAlive() throws IOException, InterruptedException {
  334. return !process.isDone();
  335. }
  336. }
  337. private static final Logger LOGGER = Logger.getLogger(Proc.class.getName());
  338. /**
  339. * Debug switch to have the thread display the process it's waiting for.
  340. */
  341. public static boolean SHOW_PID = false;
  342. }