View Javadoc
1   package com.randomnoun.common;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.io.ByteArrayOutputStream;
8   import java.io.File;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.io.OutputStream;
12  import java.util.Map;
13  
14  /** Utility class for running processes, including timeouts, and slightly better exceptions that include more information
15   * about process failure. 
16   * 
17   */
18  public class ProcessUtil {
19      
20  	/// maximum output; if >0, removes passwords & limits to this amount
21  	private int maxOutputChars = 8000;
22  	
23  	public static int NO_MAX_OUTPUT_CHARS = 0;
24  	
25  	public void setMaxOutputChars(int maxOutputChars) {
26  		this.maxOutputChars = maxOutputChars;
27  	}
28  	
29  	/** Encapsulates an error from executing a command through System.exec()
30  	 * 
31  	 */
32  	public class ProcessException extends Exception {
33  		
34  		/** Generated serialVersionUID */
35  		private static final long serialVersionUID = -6301630237335589674L;
36  		
37  		private String command;
38  		private int exitCode;
39  		private String stdout;
40  		private String stderr;
41  		private String exitCause;
42  
43  		/** Create a new executable exception
44  		 * 
45  		 * @param command Command being executed
46  		 * @param hostname The host the command was being executed on
47  		 * @param exitCode The exit code of the program
48  		 * @param stdout The standard output of the program
49  		 * @param stderr The error output of the program
50  		 */
51  		public ProcessException(String command, String exitCause, int exitCode, String stdout, String stderr) {
52  			super("Error executing '" + command + "'" + ", cause='" + exitCause + "'");
53  			
54  			this.command = command;
55  			this.exitCode = exitCode;
56  			this.exitCause = exitCause;
57  			this.stdout = stdout;
58  			this.stderr = stderr;
59  		}
60  		public String getStdout() { return stdout; }
61  		public String getStderr() { return stderr; }
62  		public String getCommand() { return command; }
63  		public int getExitCode() { return exitCode; }
64  		public String getExitCause() { return exitCause; }
65  		public String getMessage() {
66  			// trim stdout/stderr
67  			if (maxOutputChars>0) { stdout = Text.getDisplayString("stdout", stdout, 8000); }
68  			if (maxOutputChars>0) { stderr = Text.getDisplayString("stderr", stderr, 8000); }
69  			return super.getMessage() +
70  			  "; " + 
71  			  (stdout==null ? "" : ", stdout='" + stdout + "'\n") +
72  		      (stderr==null ? "" : ", stderr='" + stderr + "'\n") +
73  		      "exitCode=" + exitCode;
74  		}
75  	}
76  	
77  	
78  
79  	public String exec(String[] command) throws ProcessException {
80  		return exec(command, -1, null, null, null);
81  	}
82  	
83  	public String exec(String[] command, Map<String, String> env) throws ProcessException {
84  		return exec(command, -1, null, env, null);
85  	}
86  
87  	public String exec(String[] command, long timeout, InputStream stdin, Map<String, String> envMap, /*String[] env, */ File dir) throws ProcessException {
88  		Process process;
89  		try {
90  			ProcessBuilder pb = new ProcessBuilder(command);
91  			if (dir != null) {
92  				pb.directory(dir);
93  			}
94  			if (envMap != null) {
95  				pb.environment().clear();
96  				pb.environment().putAll(envMap);
97  			}
98  			process = pb.start();
99  		} 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 }