package com.gorillalogic.monkeytalk.utils.exec; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteException; import org.apache.commons.exec.ExecuteWatchdog; import org.apache.commons.exec.LogOutputStream; import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.exec.ShutdownHookProcessDestroyer; import com.gorillalogic.monkeytalk.utils.FileUtils; public class Exec { /** how long to wait after process finishes for stdout and stderr to drain */ private final static long PAUSE_FOR_STREAM_DRAIN = 5000; private static long buildTimeout(Long timeout) { return (timeout == null ? ExecuteWatchdog.INFINITE_TIMEOUT : timeout.longValue()); } private static CommandLine buildCommandLine(String cmd) { if (cmd != null) { return CommandLine.parse(cmd); } return null; } private static CommandLine buildCommandLine(String[] cmds) { CommandLine cmd = null; if (cmds != null) { for (String s : cmds) { if (s == null || s.length() == 0) { // error for any null or blank command parts return null; } if (cmd == null) { cmd = new CommandLine(s); } else { cmd.addArgument(s, false); } } } return cmd; } @SuppressWarnings("rawtypes") private static ExecResult exec(CommandLine cmd, long timeout, Map env) { if (cmd == null) { return ExecResult.ERROR; } LogOutputStream out = new LogOutputStream() { private final StringBuilder sb = new StringBuilder(); @Override protected void processLine(String line, int level) { sb.append(sb.length() > 0 ? "\n" : "").append(line); } @Override public String toString() { return sb.toString(); } }; LogOutputStream err = new LogOutputStream() { private final StringBuilder sb = new StringBuilder(); @Override protected void processLine(String line, int level) { sb.append(sb.length() > 0 ? "\n" : "").append(line); } @Override public String toString() { return sb.toString(); } }; DefaultExecutor exec = new DefaultExecutor(); exec.setStreamHandler(new PumpStreamHandler(out, err)); exec.setProcessDestroyer(new ShutdownHookProcessDestroyer()); ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout); exec.setWatchdog(watchdog); int exitval = -1; try { exitval = exec.execute(cmd, env); } catch (ExecuteException ex) { if (watchdog.killedProcess()) { return new ExecResult(ExecStatus.ERROR, ex.getExitValue(), out.toString(), "killed after " + timeout + "ms", true); } String exceptionMessage = ex.getMessage(); if (exceptionMessage!=null && exceptionMessage.startsWith("Process exited with an error:")) { exceptionMessage=err.toString(); } return new ExecResult(ExecStatus.ERROR, ex.getExitValue(), out.toString(), exceptionMessage); } catch (IOException ex) { return new ExecResult(ExecStatus.ERROR, exitval, out.toString(), ex.getMessage()); } return new ExecResult(ExecStatus.OK, exitval, out.toString(), err.toString()); } private static String execAndBlock(CommandLine cmd, long timeout) throws IOException { if (cmd == null) { return null; } LogOutputStream out = new LogOutputStream() { private final StringBuilder sb = new StringBuilder(); @Override protected void processLine(String line, int level) { sb.append(sb.length() > 0 ? "\n" : "").append(line); } @Override public String toString() { return sb.toString(); } }; DefaultExecutor exec = new DefaultExecutor(); exec.setStreamHandler(new PumpStreamHandler(out, out)); exec.setProcessDestroyer(new ShutdownHookProcessDestroyer()); ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout); exec.setWatchdog(watchdog); try { exec.execute(cmd); } catch (ExecuteException ex) { if (watchdog.killedProcess()) { return out.toString() + (out.toString().length() > 0 ? " : " : "") + "killed after " + timeout + "ms"; } return out.toString() + (out.toString().length() > 0 ? " : " : "") + "error " + ex.getExitValue(); } return out.toString(); } /** * Execute the given command, block indefinitely for completion, and return the result * (including exit code, stdout, and stderr). * * @param cmd * the command to execute * @return the result */ public static ExecResult run(String cmd) { return exec(buildCommandLine(cmd), ExecuteWatchdog.INFINITE_TIMEOUT, null); } /** * Execute the given command, block for the given timeout, and return the result (including exit * code, stdout, and stderr). If timeout is {@code null}, block indefinitely. * * @param cmd * the command to execute * @param timeout * the timeout (in ms) * @return the result */ public static ExecResult run(String cmd, Long timeout) { return exec(buildCommandLine(cmd), buildTimeout(timeout), null); } /** * Execute the given command set, block indefinitely for completion, and return the result * (including exit code, stdout, and stderr). * * @param cmds * the command set * @return the result */ public static ExecResult run(String[] cmds) { return exec(buildCommandLine(cmds), ExecuteWatchdog.INFINITE_TIMEOUT, null); } /** * Execute the given command set, block for the given timeout, and return the result (including * exit code, stdout, and stderr). If timeout is {@code null}, block indefinitely. * * @param cmds * the command set * @param timeout * the timeout (in ms) * @return the result */ public static ExecResult run(String[] cmds, Long timeout) { return exec(buildCommandLine(cmds), buildTimeout(timeout), null); } /** * Execute the given command set with the given environment variables, block for the given * timeout, and return the result (including exit code, stdout, and stderr). If timeout is * {@code null}, block indefinitely. * * @param cmds * the command set * @param timeout * the timeout (in ms) * @param env * the environment * @return the result */ @SuppressWarnings({ "rawtypes", "unchecked" }) public static ExecResult run(String[] cmds, Long timeout, String[] env) { Map m = null; if (env != null) { m = new HashMap(); for (String e : env) { String[] parts = e.split("="); m.put(parts[0], parts[1]); } } return exec(buildCommandLine(cmds), buildTimeout(timeout), m); } /** * Execute the given command, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmd * the command to execute * @return the result */ public static String runAndBlock(String cmd) throws IOException { return execAndBlock(buildCommandLine(new String[] { cmd }), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmd1 * the first command part * @param cmd2 * the second command part * @return the result */ public static String runAndBlock(String cmd1, String cmd2) throws IOException { return execAndBlock(buildCommandLine(new String[] { cmd1, cmd2 }), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmd1 * the first command part * @param cmd2 * the second command part * @param cmd3 * the third command part * @return the result */ public static String runAndBlock(String cmd1, String cmd2, String cmd3) throws IOException { return execAndBlock(buildCommandLine(new String[] { cmd1, cmd2, cmd3 }), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmd1 * the first command part * @param cmd2 * the second command part * @param cmd3 * the third command part * @param cmd4 * the forth command part * @return the result */ public static String runAndBlock(String cmd1, String cmd2, String cmd3, String cmd4) throws IOException { return execAndBlock(buildCommandLine(new String[] { cmd1, cmd2, cmd3, cmd4 }), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmd1 * the first command part * @param cmd2 * the second command part * @param cmd3 * the third command part * @param cmd4 * the forth command part * @param cmd5 * the fifth command part * @return the result */ public static String runAndBlock(String cmd1, String cmd2, String cmd3, String cmd4, String cmd5) throws IOException { return execAndBlock(buildCommandLine(new String[] { cmd1, cmd2, cmd3, cmd4, cmd5 }), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block indefinitely for completion, and return the result as a * String (a combo of stdout and stderr). * * @param cmds * the command set to execute * @return the result */ public static String runAndBlock(String[] cmds) throws IOException { return execAndBlock(buildCommandLine(cmds), ExecuteWatchdog.INFINITE_TIMEOUT); } /** * Execute the given command set, block for the given timeout, and return the result as a String * (a combo of stdout and stderr). If timeout is {@code null}, block indefinitely. * * @param cmds * the command set to execute * @param timeout * the timeout (in ms) * @return the result */ public static String runAndBlock(String[] cmds, Long timeout) throws IOException { return execAndBlock(buildCommandLine(cmds), buildTimeout(timeout)); } public static AsyncExecResult runAsync(String cmd) { return runAsync(new String[] { cmd }, null); } public static AsyncExecResult runAsync(String[] cmd) { return runAsync(cmd, null); } public static AsyncExecResult runAsync(String[] cmd, Long timeout) { try { final Process p = Runtime.getRuntime().exec(cmd); final AsyncExecResult asyncResult = new AsyncExecResult(p); final Long timeout2 = timeout == null ? null : new Long(timeout); new Thread(new Runnable() { public void run() { try { manageProcess(p, timeout2, asyncResult); } catch (Exception e) { asyncResult.setResult(new ExecResult(ExecStatus.ERROR, e.getMessage())); } } }).start(); return asyncResult; } catch (Exception ex) { AsyncExecResult asyncResult = new AsyncExecResult(null); asyncResult.setResult(new ExecResult(ExecStatus.ERROR, ex.getMessage())); System.err.println("Error caught in Exec.runAsync(): " + ex.getMessage() + " -- returning ERROR, stack trace for diagnostic purposes: "); ex.printStackTrace(); return asyncResult; } } private static ExecResult manageProcess(Process p, Long timeout, AsyncExecResult asyncResult) throws Exception { ExecStatus status = ExecStatus.OK; String message; FileUtils.StreamEater stdoutEater = new FileUtils.StreamEater(p.getInputStream()); FileUtils.StreamEater stderrEater = new FileUtils.StreamEater(p.getErrorStream()); if (timeout == null) { p.waitFor(); } else { ExecWorker worker = new ExecWorker(p); worker.start(); try { worker.join(timeout); if (worker.getExitValue() == null) { throw new Exception("Exec() timed out"); } } catch (InterruptedException ex) { worker.interrupt(); Thread.currentThread().interrupt(); throw ex; } finally { p.destroy(); } } int exitValue = p.exitValue(); try { Thread t=stderrEater.getEaterThread(); t.join(PAUSE_FOR_STREAM_DRAIN); if (t.isAlive()) { t.interrupt(); } t=stdoutEater.getEaterThread(); t.join(PAUSE_FOR_STREAM_DRAIN); if (t.isAlive()) { t.interrupt(); } } catch (InterruptedException ex) { // ignore } String stderr = stderrEater.toString(); String stdout = stdoutEater.toString(); if (exitValue != 0) { status = ExecStatus.ERROR; message = stderr; if (message == null || message.length() == 0) { message = stdout; } } else { status = ExecStatus.OK; message = stdout; if (message == null || message.length() == 0) { message = stderr; } } if (message == null) { message = ""; } ExecResult result = new ExecResult(status, exitValue, stdout, stderr); if (asyncResult != null) { asyncResult.setResult(result); } return result; } private static class ExecWorker extends Thread { private final Process process; private Integer exitValue; private ExecWorker(Process process) { this.process = process; } private Integer getExitValue() { return exitValue; } public void run() { try { exitValue = process.waitFor(); } catch (InterruptedException ignore) { return; } } } }