1   package com.mccrory.scott.base;
2   
3   ////////////////////////////////////////////////////////////////////////////////
4   // Copyright (C) 2002  Scott McCrory
5   //
6   // This program is free software; you can redistribute it and/or
7   // modify it under the terms of the GNU General Public License
8   // as published by the Free Software Foundation; either version 2
9   // of the License, or (at your option) any later version.
10  //
11  // This program is distributed in the hope that it will be useful,
12  // but WITHOUT ANY WARRANTY; without even the implied warranty of
13  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  // GNU General Public License for more details.
15  //
16  // You should have received a copy of the GNU General Public License
17  // along with this program; if not, write to the Free Software
18  // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
19  ////////////////////////////////////////////////////////////////////////////////
20  
21  import java.io.IOException;
22  import java.io.ObjectInputStream;
23  import java.io.ObjectOutputStream;
24  import java.io.OutputStream;
25  import java.io.PrintWriter;
26  import java.io.StringWriter;
27  import java.util.Date;
28  import java.util.StringTokenizer;
29  
30  /**
31   * <P>Makes running external executables easier, optionally under a watched thread.
32   * 
33   * In addition, probably the most useful feature of ExecRunner is using it to
34   * run a command-line program and obtain its stdout and stderr results in two
35   * strings.  This is done with exec(String) - see that method for an example.
36   *
37   * With acknowledgements to Michael C. Daconta, author of "Java Pitfalls,
38   * Time Saving Solutions, and Workarounds to Improve Programs." and his
39   * article in JavaWorld "When Runtime.exec() Won't".</P>
40   *
41   * @author <a href="mailto:smccrory@users.sourceforge.net">Scott McCrory</a>.
42   * @version CVS $Id: ExecRunner.java,v 1.20 2002/08/04 22:04:53 smccrory Exp $
43   */
44  public class ExecRunner {
45  
46      /** Win NT/2K/MEPro require cmd.exe to run programs **/
47      private static final String WINDOWS_NT_2000_COMMAND_1 = "cmd.exe";
48  
49      /** Win NT/2K/MEPro require the /C to specify what to run **/
50      private static final String WINDOWS_NT_2000_COMMAND_2 = "/C";
51  
52      /** Win 9X/MEHome require cmd.exe to run programs **/
53      private static final String WINDOWS_9X_ME_COMMAND_1 = "command.exe";
54  
55      /** Win 9X/MEHome require the /C to specify what to run **/
56      private static final String WINDOWS_9X_ME_COMMAND_2 = "/C";
57  
58      /** String to send to STDERR if program exceeds max run time **/
59      private static final String MAX_RUN_TIME_EXCEEDED_STRING =
60          "MAX_RUN_TIME_EXCEEDED";
61  
62      /** String to capture STDOUT **/
63      private String out = new String();
64  
65      /** String to capture STDERR **/
66      private String err = new String();
67  
68      /** Default max run time (in seconds) **/
69      private int maxRunTimeSecs = 0;
70  
71      /** Flag to indicate if we've exceeded max run time **/
72      private boolean maxRunTimeExceeded = false;
73  
74      /** The name of this class to log context without introspection **/
75      private static final String CLASS_NAME = "ExecRunner";
76  
77      /** The version of this class (filled in by CVS) **/
78      private static final String VERSION = "CVS $Revision: 1.20 $";
79  
80      /**
81       * Basic ExecRunner constructor.
82       *
83       */
84      public ExecRunner() {
85          super();
86      }
87  
88      /**
89       * ExecRunner constructor which also conveniently runs exec(String).
90       *
91       * @param command The program or command to run
92       * @throws ExceptionInInitializerError thrown if a problem occurs
93       */
94      public ExecRunner(String command) throws ExceptionInInitializerError {
95          this();
96          try {
97              exec(command);
98          }
99          catch (IOException ioe) {
100             throw new ExceptionInInitializerError(ioe.getMessage());
101         }
102         catch (InterruptedException inte) {
103             throw new ExceptionInInitializerError(inte.getMessage());
104         }
105 
106     }
107 
108     /**
109      * We override the <code>clone</code> method here to prevent cloning of our class.
110      *
111      * @throws CloneNotSupportedException To indicate cloning is not allowed
112      * @return Nothing ever really returned since we throw a CloneNotSupportedException
113      **/
114     public final Object clone() throws CloneNotSupportedException {
115 
116         throw new java.lang.CloneNotSupportedException();
117 
118     }
119 
120     /**
121      * The <B>exec(String)</B> method runs a process inside of a watched thread.
122      * It returns the client's exit code and feeds its STDOUT and STDERR to
123      * ExecRunner's out and err strings, where you then use getOutString()
124      * and getErrString() to obtain these values.  Example:
125      *
126      * <pre>
127      * // Execute the program and grab the results
128      * String out = "";
129      * String err = "";
130      * try {
131      *   
132      *     ExecRunner er = new ExecRunner();
133      *     er.setMaxRunTimeSecs(maxRunTime);
134      *     er.exec(program);
135      *     if (!er.getMaxRunTimeExceeded()) {
136      *         out = er.getOutString();
137      *         err = er.getErrString();
138      *     }
139      *     else {
140      *         log.error("Maximum run time exceeded!");
141      *         continue;
142      *     }
143      *  
144      * }
145      * catch (Exception e) {
146      *     log.error("Error executing " + program + ": " + e.getMessage());
147      *     continue;
148      * }
149      * </pre>
150      *
151      * @return The command's return code
152      * @param command The program or command to run
153      * @throws IOException thrown if a problem occurs
154      * @throws InterruptedException thrown if a problem occurs
155      */
156     public int exec(String command) throws IOException, InterruptedException {
157 
158         StringWriter swOut = new StringWriter();
159         PrintWriter pwOut = new PrintWriter(swOut, true);
160 
161         StringWriter swErr = new StringWriter();
162         PrintWriter pwErr = new PrintWriter(swErr, true);
163 
164         int rc = exec(command, pwOut, pwErr);
165 
166         out = swOut.toString();
167         err = swErr.toString();
168 
169         return rc;
170 
171     }
172 
173     /**
174      * Convenience method for calling exec with OutputStreams.
175      *
176      * @return The command's return code
177      * @param command The program or command to run
178      * @param stdoutStream java.io.OutputStream 
179      * @param stderrStream java.io.OutputStream
180      * @throws IOException thrown if a problem occurs
181      * @throws InterruptedException thrown if a problem occurs
182      **/
183     public int exec(
184         String command,
185         OutputStream stdoutStream,
186         OutputStream stderrStream)
187         throws IOException, InterruptedException {
188 
189         PrintWriter pwOut = new PrintWriter(stdoutStream, true);
190         PrintWriter pwErr = new PrintWriter(stderrStream, true);
191 
192         return exec(command, pwOut, pwErr);
193 
194     }
195 
196     /**
197      * The <code>exec(String, PrintWriter, PrintWriter)</code> method runs
198      * a process inside of a watched thread.  It returns the client's exit code
199      * and feeds its STDOUT and STDERR to the passed-in streams.
200      *
201      * @return The command's return code
202      * @param command The program or command to run
203      * @param stdoutWriter java.io.PrintWriter 
204      * @param stderrWriter java.io.PrintWriter 
205      * @throws IOException thrown if a problem occurs
206      * @throws InterruptedException thrown if a problem occurs
207      **/
208     public int exec(
209         String command,
210         PrintWriter stdoutWriter,
211         PrintWriter stderrWriter)
212         throws IOException, InterruptedException {
213 
214         // Default exit value is non-zero to indicate a problem.
215         int exitVal = 1;
216 
217         ////////////////////////////////////////////////////////////////
218         Runtime rt = Runtime.getRuntime();
219         Process proc;
220         String[] cmd = null;
221 
222         // First get the start time & calculate comparison numbers
223         Date startTime = new Date();
224         long startTimeMs = startTime.getTime();
225         long maxTimeMs = startTimeMs + (maxRunTimeSecs * 1000);
226 
227         ////////////////////////////////////////////////////////////////
228         // First determine the OS to build the right command string
229         String osName = System.getProperty("os.name");
230         if (osName.equals("Windows NT") || osName.equals("Windows 2000")) {
231             cmd = new String[3];
232             cmd[0] = WINDOWS_NT_2000_COMMAND_1;
233             cmd[1] = WINDOWS_NT_2000_COMMAND_2;
234             cmd[2] = command;
235         }
236         else if (
237             osName.equals("Windows 95")
238                 || osName.equals("Windows 98")
239                 || osName.equals("Windows ME")) {
240             cmd = new String[3];
241             cmd[0] = WINDOWS_9X_ME_COMMAND_1;
242             cmd[1] = WINDOWS_9X_ME_COMMAND_2;
243             cmd[2] = command;
244         }
245         else {
246             // Linux (and probably other *nixes) prefers to be called
247             // with each argument supplied separately, so we first
248             // Tokenize it across spaces as the boundary.
249             StringTokenizer st = new StringTokenizer(command, " ");
250             cmd = new String[st.countTokens()];
251             int token = 0;
252             while (st.hasMoreTokens()) {
253                 String tokenString = st.nextToken();
254                 //System.out.println(tokenString);
255                 cmd[token++] = tokenString;
256             }
257         }
258 
259         // Execute the command and start the two output gobblers
260         if (cmd != null && cmd.length > 0) {
261             //System.out.println("**Checkpoint** :" + cmd.length);
262             proc = rt.exec(cmd);
263         }
264         else {
265             throw new IOException("Insufficient commands!");
266         }
267 
268         StreamGobbler outputGobbler =
269             new StreamGobbler(proc.getInputStream(), stdoutWriter);
270         StreamGobbler errorGobbler =
271             new StreamGobbler(proc.getErrorStream(), stderrWriter);
272         outputGobbler.start();
273         errorGobbler.start();
274 
275         // Wait for the program to finish running and return the
276         // exit value obtained from the executable
277         while (true) {
278 
279             try {
280                 exitVal = proc.exitValue();
281                 break;
282             }
283             catch (IllegalThreadStateException e) {
284 
285                 // If we get this exception, then the process isn't
286                 // done executing and we determine if our time is up.
287                 if (maxRunTimeSecs > 0) {
288 
289                     Date endTime = new Date();
290                     long endTimeMs = endTime.getTime();
291                     if (endTimeMs > maxTimeMs) {
292                         // Time's up - kill the process and the gobblers and return
293                         proc.destroy();
294                         maxRunTimeExceeded = true;
295                         stderrWriter.println(MAX_RUN_TIME_EXCEEDED_STRING);
296                         outputGobbler.quit();
297                         errorGobbler.quit();
298                         return exitVal;
299 
300                     }
301                     else {
302                         // Time is not up yet so wait 100 ms before testing again
303                         Thread.sleep(100);
304                     }
305                 }
306 
307             }
308 
309         }
310 
311         ////////////////////////////////////////////////////////////////
312         // Wait for output gobblers to finish forwarding the output
313         while (outputGobbler.isAlive() || errorGobbler.isAlive()) {
314         }
315 
316         ////////////////////////////////////////////////////////////////
317         // All done, flush the streams and return the exit value
318         stdoutWriter.flush();
319         stderrWriter.flush();
320         return exitVal;
321 
322     }
323 
324     /**
325      * Returns the error string if exec(String) was invoked.
326      *
327      * @return The error string if exec(String) was invoked.
328      */
329     public String getErrString() {
330         return err;
331     }
332 
333     /**
334      * Returns whether the maximum runtime was exceeded or not.
335      *
336      * @return boolean indicating whether the maximum runtime was exceeded or not.
337      */
338     public boolean getMaxRunTimeExceeded() {
339         return maxRunTimeExceeded;
340     }
341 
342     /**
343      * Returns the maximum run time in seconds for this object.
344      *
345      * @return the maximum run time in seconds for this object.
346      */
347     public int getMaxRunTimeSecs() {
348         return maxRunTimeSecs;
349     }
350 
351     /**
352      * Returns the output string if exec(String) was invoked.
353      *
354      * @return The output string if exec(String) was invoked.
355      */
356     public String getOutString() {
357         return out;
358     }
359 
360     /**
361      * This is for unit testing of the class.
362      *
363      * @param args an array of command-line arguments
364      * @throws IOException thrown if a problem occurs
365      **/
366     public static void main(String[] args) throws IOException {
367 
368         try {
369 
370             ExecRunner er = new ExecRunner();
371 
372             /////////////////////////////////////////////////////////////////////
373             // Linux: Test the exec operation with just STDOUT and STDERR
374             //System.out.println("Testing ExecRunner with STDOUT and STDERR...");
375             //er.exec("ls -l", System.out, System.err);
376             //System.out.println("Complete");
377 
378             /////////////////////////////////////////////////////////////////////
379             // Windows: Test the exec operation with just STDOUT and STDERR
380             System.out.println("Testing ExecRunner with StringWriter...");
381 
382             er = new ExecRunner();
383             er.setMaxRunTimeSecs(1);
384             er.exec("dir /s c:\\");
385             //er.exec("ls -l");
386 
387             System.out.println("<STDOUT>\n" + er.getOutString() + "</STDOUT>");
388             System.out.println("<STDERR>\n" + er.getErrString() + "</STDERR>");
389             System.out.println("Testing Done");
390 
391             /////////////////////////////////////////////////////////////////////
392             // Exit nicely
393             System.exit(0);
394 
395         }
396         catch (Exception e) {
397 
398             e.printStackTrace();
399             System.exit(1);
400 
401         }
402     }
403 
404     /**
405      * We override the <code>readObject</code> method here to prevent
406      * deserialization of our class for security reasons.
407      *
408      * @param in java.io.ObjectInputStream
409      * @throws IOException thrown if a problem occurs
410      **/
411     private final void readObject(ObjectInputStream in) throws IOException {
412 
413         throw new IOException("Object cannot be deserialized");
414 
415     }
416 
417     /**
418      * Sets the maximum run time in seconds.
419      * If you do not want to limit the executable's run time, simply pass in
420      * a 0 (which is also the default).
421      *
422      * @param max Maximim number of seconds to let program run
423      */
424     public void setMaxRunTimeSecs(int max) {
425 
426         maxRunTimeSecs = max;
427 
428     }
429 
430     /**
431      * We override the <code>writeObject</code> method here to prevent
432      * serialization of our class for security reasons.
433      *
434      * @param out java.io.ObjectOutputStream
435      * @throws IOException thrown if a problem occurs
436      **/
437     private final void writeObject(ObjectOutputStream out) throws IOException {
438 
439         throw new IOException("Object cannot be serialized");
440 
441     }
442 
443 }