View Javadoc
1   package auxtestlib;
2   
3   import java.io.ByteArrayOutputStream;
4   import java.io.File;
5   import java.io.IOException;
6   import java.io.InputStream;
7   import java.util.ArrayList;
8   import java.util.List;
9   import java.util.Timer;
10  import java.util.TimerTask;
11  
12  /**
13   * Class that runs a command and captures its output.
14   */
15  public class CommandRunner {
16  	/**
17  	 * Class that represents the output of a command.
18  	 */
19  	public static class CommandOutput {
20  		/**
21  		 * Output generated by the command.
22  		 */
23  		public String output;
24  
25  		/**
26  		 * Error messages generated.
27  		 */
28  		public String error;
29  
30  		/**
31  		 * Output generated by the command (in bytes).
32  		 */
33  		public byte outputBytes[];
34  
35  		/**
36  		 * Output written by the commandin the stderr (in bytes).
37  		 */
38  		public byte errorBytes[];
39  
40  		/**
41  		 * Program's exit code.
42  		 */
43  		public int exitCode;
44  
45  		/**
46  		 * Has the program timed out?
47  		 */
48  		public boolean timedOut;
49  	}
50  
51  	/**
52  	 * Creates a new instance.
53  	 */
54  	public CommandRunner() {
55  		/*
56  		 * Nothing to do.
57  		 */
58  	}
59  
60  	/**
61  	 * Runs a command and captures the output. This method will return
62  	 * immediately (the process keeps running in the background). The process
63  	 * can me monitored and accessed through the {@link ProcessInterface} class.
64  	 * @param cmds the command and its arguments
65  	 * @param directory the directory where the command should bd executed
66  	 * @param limit execution time limit (in seconds)
67  	 * @return an interface to control the process
68  	 * @throws IOException failed to launch the process
69  	 */
70  	public ProcessInterface run_command_async(String[] cmds, File directory,
71  			int limit) throws IOException {
72  		if (cmds == null) {
73  			throw new IllegalArgumentException("cmds == null");
74  		}
75  
76  		if (directory == null) {
77  			throw new IllegalArgumentException("directory == null");
78  		}
79  
80  		if (limit <= 0) {
81  			throw new IllegalArgumentException("limit <= 0");
82  		}
83  
84  		ProcessBuilder pb = new ProcessBuilder(cmds);
85  		pb.directory(directory);
86  		Process p = pb.start();
87  		return ProcessInterface.makeProcessInterface(p, limit);
88  	}
89  
90  	/**
91  	 * @param cmds deprecated
92  	 * @param directory deprecated
93  	 * @param limit deprecated
94  	 * @return deprecated
95  	 * @throws IOException deprecated
96  	 * @deprecated use {@link #run_command_async(String[], File, int)}
97  	 */
98  	@Deprecated
99  	public ProcessInterface runCommandAsync(String[] cmds, File directory,
100 			int limit) throws IOException {
101 		return run_command_async(cmds, directory, limit);
102 	}
103 	
104 	/**
105 	 * This method is a shortcut for the
106 	 * {@link #run_command_async(String[], File, int)}. It will invoke the
107 	 * command, wait for it to run and returns the command's output.
108 	 * @param cmds the command and its arguments
109 	 * @param directory the directory where the command should bd executed
110 	 * @param limit execution time limit (in seconds)
111 	 * @return the commands output in the stdout
112 	 * @throws IOException failed to launch the process
113 	 */
114 	public CommandOutput run_command(String cmds[], File directory, int limit)
115 			throws IOException {
116 		ProcessInterface pi = runCommandAsync(cmds, directory, limit);
117 
118 		/*
119 		 * Wait until the process dies.
120 		 */
121 		while (pi.isRunning()) {
122 			try {
123 				Thread.sleep(ProcessInterface.PROCESS_POLLING);
124 			} catch (InterruptedException e) {
125 				/*
126 				 * We'll ignore this.
127 				 */
128 			}
129 		}
130 
131 		return pi.getOutput();
132 	}
133 
134 	/**
135 	 * @param cmds deprecated
136 	 * @param directory deprecated
137 	 * @param limit deprecated
138 	 * @return deprecated
139 	 * @throws IOException deprecated
140 	 * @deprecated use {@link #run_command(String[], File, int)}
141 	 */
142 	@Deprecated
143 	public CommandOutput runCommand(String cmds[], File directory, int limit)
144 			throws IOException {
145 		return run_command(cmds, directory, limit);
146 	}
147 	
148 	/**
149 	 * Thread that keeps reading an input stream and saves the output. The
150 	 * thread will automatically stop when the stream is closed.
151 	 */
152 	static class Capturer extends Thread {
153 		/**
154 		 * The input stream.
155 		 */
156 		private final InputStream m_input_stream;
157 
158 		/**
159 		 * Buffer where the text is kept.
160 		 */
161 		private final StringBuffer m_result;
162 
163 		/**
164 		 * Data read from the stream (without text conversion).
165 		 */
166 		private final ByteArrayOutputStream m_result_bytes;
167 
168 		/**
169 		 * Creates and starts the thread.
170 		 * 
171 		 * @param is the stream to read
172 		 */
173 		Capturer(InputStream is) {
174 			assert is != null;
175 
176 			m_input_stream = is;
177 			m_result = new StringBuffer();
178 			m_result_bytes = new ByteArrayOutputStream();
179 			start();
180 		}
181 
182 		@Override
183 		public void run() {
184 			int read;
185 			try {
186 				while ((read = m_input_stream.read()) != -1) {
187 					synchronized (this) {
188 						m_result_bytes.write(read);
189 						m_result.append((char) read);
190 					}
191 				}
192 			} catch (IOException e) {
193 				/*
194 				 * We'll ignore I/O exceptions.
195 				 */
196 			}
197 		}
198 
199 		/**
200 		 * Obtains a copy of the captured text.
201 		 * 
202 		 * @return the text
203 		 */
204 		synchronized String text() {
205 			return m_result.toString();
206 		}
207 
208 		/**
209 		 * Obtains a copy of the captured bytes.
210 		 * 
211 		 * @return the captured bytes
212 		 */
213 		synchronized byte[] bytes() {
214 			return m_result_bytes.toByteArray();
215 		}
216 	}
217 
218 	/**
219 	 * Interface provided to access the process while it is running.
220 	 */
221 	public static class ProcessInterface {
222 		/**
223 		 * Polling interval to check whether the process has finished (in
224 		 * milliseconds).
225 		 */
226 		private static final int PROCESS_POLLING = 200;
227 
228 		/**
229 		 * The process itself.
230 		 */
231 		private final Process m_process;
232 
233 		/**
234 		 * Is the process still running?
235 		 */
236 		private boolean m_running;
237 
238 		/**
239 		 * What was the exit code for the process?
240 		 */
241 		private int m_exit_code;
242 
243 		/**
244 		 * Stdout capturer.
245 		 */
246 		private final Capturer m_out;
247 
248 		/**
249 		 * Stderr capturer.
250 		 */
251 		private final Capturer m_err;
252 
253 		/**
254 		 * Has the program timed out?
255 		 */
256 		private boolean m_timed_out;
257 
258 		/**
259 		 * Listeners of the process interface.
260 		 */
261 		private final List<ProcessInterfaceListener> m_listeners;
262 
263 		/**
264 		 * Creates a new interface for the process. These objects are linked to
265 		 * their respective runners.
266 		 * 
267 		 * @param process the process that is running
268 		 * @param limit the time limit to run the program (in seconds).
269 		 * 
270 		 * @return the process interface
271 		 */
272 		private static ProcessInterface makeProcessInterface(Process process,
273 				int limit) {
274 			return new ProcessInterface(process, limit);
275 		}
276 
277 		/**
278 		 * Creates a new interface for the process. These objects are linked to
279 		 * their respective runners.
280 		 * 
281 		 * @param process the process that is running
282 		 * @param limit the time limit to run the program (in seconds).
283 		 */
284 		private ProcessInterface(Process process, int limit) {
285 			assert process != null;
286 
287 			this.m_process = process;
288 			m_running = true;
289 			m_exit_code = 0;
290 			m_out = new Capturer(process.getInputStream());
291 			m_err = new Capturer(process.getErrorStream());
292 			m_listeners = new ArrayList<>();
293 			m_timed_out = false;
294 
295 			Timer timer = new Timer();
296 			timer.schedule(new TimerTask() {
297 				@Override
298 				public void run() {
299 					synchronized (ProcessInterface.this) {
300 						update_state();
301 						if (!m_running) {
302 							cancel();
303 						}
304 					}
305 				}
306 			}, PROCESS_POLLING, PROCESS_POLLING);
307 
308 			final TimerTask timeout_task = new TimerTask() {
309 				@Override
310 				public void run() {
311 					synchronized (this) {
312 						if (killProcess()) {
313 							m_timed_out = true;
314 						}
315 					}
316 				}
317 			};
318 
319 			addProcessInterfaceListener(new ProcessInterfaceListener() {
320 				@Override
321 				public void processFinished(ProcessInterface process) {
322 					timeout_task.cancel();
323 				}
324 			});
325 
326 			timer.schedule(timeout_task, limit * 1000);
327 		}
328 
329 		/**
330 		 * Adds a listener to the process interface.
331 		 * @param listener the listener
332 		 */
333 		public synchronized void add_process_interface_listener(
334 				ProcessInterfaceListener listener) {
335 			if (listener == null) {
336 				throw new IllegalArgumentException("listener == null");
337 			}
338 
339 			m_listeners.add(listener);
340 		}
341 
342 		/**
343 		 * @param listener deprecated
344 		 * @deprecated use
345 		 * 	{@link #add_process_interface_listener(ProcessInterfaceListener)}
346 		 */
347 		@Deprecated
348 		public synchronized void addProcessInterfaceListener(
349 				ProcessInterfaceListener listener) {
350 			add_process_interface_listener(listener);
351 		}
352 
353 		/**
354 		 * Removes a listener from the process interface.
355 		 * @param listener the listener
356 		 */
357 		public synchronized void remove_process_interface_listener(
358 				ProcessInterfaceListener listener) {
359 			if (listener == null) {
360 				throw new IllegalArgumentException("listener == null");
361 			}
362 
363 			m_listeners.remove(listener);
364 		}
365 
366 		/**
367 		 * @param listener deprecated
368 		 * @deprecated use
369 		 * {@link #remove_process_interface_listener(ProcessInterfaceListener)}
370 		 */
371 		@Deprecated
372 		public synchronized void removeProcessInterfaceListener(
373 				ProcessInterfaceListener listener) {
374 			remove_process_interface_listener(listener);
375 		}
376 		
377 		/**
378 		 * Updates the state of the process. Since the
379 		 * <code>java.lang.Process</code> class doesn't provide any way of
380 		 * observing its state, we must probe regularly. This method should be
381 		 * called for that purpose.
382 		 */
383 		private synchronized void update_state() {
384 			if (!m_running) {
385 				return;
386 			}
387 
388 			try {
389 				m_exit_code = m_process.exitValue();
390 				m_running = false;
391 				for (ProcessInterfaceListener l : new ArrayList<>(
392 						m_listeners)) {
393 					l.processFinished(this);
394 				}
395 			} catch (IllegalThreadStateException e) {
396 				/*
397 				 * Process is still running.
398 				 */
399 			}
400 		}
401 
402 		/**
403 		 * Requests the process to be killed (if it is running).
404 		 * 
405 		 * @return was the process killed (<code>true</code>) or was it already
406 		 * dead (<code>false</code>)?
407 		 */
408 		public synchronized boolean killProcess() {
409 			if (!m_running) {
410 				return false;
411 			}
412 
413 			m_process.destroy();
414 			while (true) {
415 				try {
416 					m_process.exitValue();
417 					break;
418 				} catch (IllegalThreadStateException e) {
419 					/*
420 					 * The process is still running.
421 					 */
422 				}
423 			}
424 
425 			update_state();
426 			assert !m_running;
427 			return true;
428 		}
429 
430 		/**
431 		 * Determines whether the process is still running.
432 		 * 
433 		 * @return is the process running?
434 		 */
435 		public synchronized boolean isRunning() {
436 			return m_running;
437 		}
438 
439 		/**
440 		 * Obtains the output of the command. Can only be invoked if the process
441 		 * has been stopped.
442 		 * 
443 		 * @return the output
444 		 * 
445 		 * @throws IllegalStateException if the process is still running
446 		 */
447 		public synchronized CommandOutput getOutput() {
448 			if (m_running) {
449 				throw new IllegalStateException("Process still running.");
450 			}
451 
452 			CommandOutput co = new CommandOutput();
453 
454 			co.exitCode = m_exit_code;
455 			co.output = m_out.text();
456 			co.error = m_err.text();
457 			co.outputBytes = m_out.bytes();
458 			co.errorBytes = m_err.bytes();
459 			co.timedOut = m_timed_out;
460 			return co;
461 		}
462 
463 		/**
464 		 * Obtains the text currently written to the stdout by the process.
465 		 * 
466 		 * @return the text written
467 		 */
468 		public synchronized String getOutputText() {
469 			return m_out.text();
470 		}
471 
472 		/**
473 		 * Obtains the text currently written to the stderr by the process.
474 		 * 
475 		 * @return the text written
476 		 */
477 		public synchronized String getErrorText() {
478 			return m_err.text();
479 		}
480 	}
481 
482 	/**
483 	 * Interface implemented by classes that observe a process interface.
484 	 */
485 	interface ProcessInterfaceListener {
486 		/**
487 		 * The process has finished.
488 		 * 
489 		 * @param process the process
490 		 */
491 		void processFinished(ProcessInterface process);
492 	}
493 }