001package com.randomnoun.common; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.io.ByteArrayOutputStream; 008import java.io.File; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.OutputStream; 012import java.util.Map; 013 014/** Utility class for running processes, including timeouts, and slightly better exceptions that include more information 015 * about process failure. 016 * 017 */ 018public class ProcessUtil { 019 020 /// maximum output; if >0, removes passwords & limits to this amount 021 private int maxOutputChars = 8000; 022 023 public static int NO_MAX_OUTPUT_CHARS = 0; 024 025 public void setMaxOutputChars(int maxOutputChars) { 026 this.maxOutputChars = maxOutputChars; 027 } 028 029 /** Encapsulates an error from executing a command through System.exec() 030 * 031 */ 032 public class ProcessException extends Exception { 033 034 /** Generated serialVersionUID */ 035 private static final long serialVersionUID = -6301630237335589674L; 036 037 private String command; 038 private int exitCode; 039 private String stdout; 040 private String stderr; 041 private String exitCause; 042 043 /** Create a new executable exception 044 * 045 * @param command Command being executed 046 * @param hostname The host the command was being executed on 047 * @param exitCode The exit code of the program 048 * @param stdout The standard output of the program 049 * @param stderr The error output of the program 050 */ 051 public ProcessException(String command, String exitCause, int exitCode, String stdout, String stderr) { 052 super("Error executing '" + command + "'" + ", cause='" + exitCause + "'"); 053 054 this.command = command; 055 this.exitCode = exitCode; 056 this.exitCause = exitCause; 057 this.stdout = stdout; 058 this.stderr = stderr; 059 } 060 public String getStdout() { return stdout; } 061 public String getStderr() { return stderr; } 062 public String getCommand() { return command; } 063 public int getExitCode() { return exitCode; } 064 public String getExitCause() { return exitCause; } 065 public String getMessage() { 066 // trim stdout/stderr 067 if (maxOutputChars>0) { stdout = Text.getDisplayString("stdout", stdout, 8000); } 068 if (maxOutputChars>0) { stderr = Text.getDisplayString("stderr", stderr, 8000); } 069 return super.getMessage() + 070 "; " + 071 (stdout==null ? "" : ", stdout='" + stdout + "'\n") + 072 (stderr==null ? "" : ", stderr='" + stderr + "'\n") + 073 "exitCode=" + exitCode; 074 } 075 } 076 077 078 079 public String exec(String[] command) throws ProcessException { 080 return exec(command, -1, null, null, null); 081 } 082 083 public String exec(String[] command, Map<String, String> env) throws ProcessException { 084 return exec(command, -1, null, env, null); 085 } 086 087 public String exec(String[] command, long timeout, InputStream stdin, Map<String, String> envMap, /*String[] env, */ File dir) throws ProcessException { 088 Process process; 089 try { 090 ProcessBuilder pb = new ProcessBuilder(command); 091 if (dir != null) { 092 pb.directory(dir); 093 } 094 if (envMap != null) { 095 pb.environment().clear(); 096 pb.environment().putAll(envMap); 097 } 098 process = pb.start(); 099 } catch (IOException ioe) { 100 throw (ProcessException) new ProcessException(Text.join(command, " "), "IOException", 0, "", "").initCause(ioe); 101 } 102 InputStream stdout = process.getInputStream(); 103 InputStream stderr = process.getErrorStream(); 104 OutputStream processStdin = process.getOutputStream(); 105 ByteArrayOutputStream stdoutByteArrayStream = new ByteArrayOutputStream(); 106 ByteArrayOutputStream stderrByteArrayStream = new ByteArrayOutputStream(); 107 OutputStream stdoutStream = stdoutByteArrayStream; 108 OutputStream stderrStream = stderrByteArrayStream; 109 // OutputStream stdoutStream = new TeeOutputStream(stdoutByteArrayStream, new LoggingOutputStream(stdoutLogger)); 110 // OutputStream stderrStream = new TeeOutputStream(stderrByteArrayStream, new LoggingOutputStream(stderrLogger)); 111 112 // could probably use nio these days 113 Thread copyStdoutThread = StreamUtil.copyThread(stdout, stdoutStream, 1024); 114 Thread copyStderrThread = StreamUtil.copyThread(stderr, stderrStream, 1024); 115 Thread copyStdinThread = null; 116 if (stdin!=null) { 117 copyStdinThread = StreamUtil.copyAndCloseThread(stdin, processStdin, 1024); 118 copyStdinThread.start(); 119 } 120 121 copyStdoutThread.start(); 122 copyStderrThread.start(); 123 int exitCode = -1; 124 boolean throwException = false; 125 long interval = 100; 126 String cause = ""; 127 128 try { 129 // timeouts 130 long timeWaiting = 0; 131 boolean processFinished = false; 132 while ((timeout == -1 || timeWaiting < timeout) && !processFinished) { 133 processFinished = true; 134 Thread.sleep(interval); 135 try { 136 exitCode = process.exitValue(); 137 } catch (IllegalThreadStateException e) { 138 // process hasn't finished yet 139 processFinished = false; 140 } 141 timeWaiting += interval; 142 } 143 144 if (processFinished) { 145 // wait for copy threads to complete 146 copyStdoutThread.join(); 147 copyStderrThread.join(); 148 if (copyStdinThread != null) { 149 copyStdinThread.interrupt(); 150 } 151 if (exitCode != 0) { 152 cause = "non-0 exitCode"; 153 throwException = true; 154 } 155 } else { 156 cause = "timeout"; 157 throwException = true; 158 process.destroy(); 159 if (copyStdinThread != null) { 160 copyStdinThread.interrupt(); 161 } 162 copyStderrThread.interrupt(); 163 copyStdoutThread.interrupt(); 164 } 165 166 } catch (InterruptedException ie) { 167 cause = "InterruptedException"; 168 throwException = true; 169 process.destroy(); 170 if (copyStdinThread!=null) { 171 copyStdinThread.interrupt(); 172 } 173 copyStderrThread.interrupt(); 174 copyStdoutThread.interrupt(); 175 } 176 177 if (throwException) { 178 throw new ProcessException( 179 Text.join(command, " "), cause, exitCode, 180 stdoutByteArrayStream.toString(), stderrByteArrayStream.toString()); 181 } 182 return stdoutByteArrayStream.toString(); 183 } 184 185 186 187 188}