ProcessManager.java
001 package gate.util;
002 
003 import java.io.BufferedInputStream;
004 import java.io.File;
005 import java.io.IOException;
006 import java.io.InputStream;
007 import java.io.OutputStream;
008 
009 /**
010  * Class that supports running an external process and either silently
011  * consuming its standard output and error streams, or copying them to Java's
012  * stdout and stderr.
013  *
014  * This implementation reads the output and error streams in separate threads,
015  * but tries to reuse these threads from one external call to the next, unlike
016  * other approaches I've seen (which all spawn a pair of new threads for every
017  * external call).  As a result, instances of this class are <b>not thread
018  * safe</b>.  You must use a different instance of ProcessManager in each
019  * thread that you use to run external processes.
020  */
021 public class ProcessManager {
022   /**
023    * Debug flag.
024    */
025   private static final boolean DEBUG = false;
026 
027   /**
028    * StreamGobbler thread for standard output.
029    */
030   private StreamGobbler stdout;
031 
032   /**
033    * StreamGobbler thread for standard error.
034    */
035   private StreamGobbler stderr;
036   
037   private Process proc;
038 
039   /**
040    * Construct a ProcessManager object and start the gobbler threads.
041    */
042   public ProcessManager() {
043     stdout = new StreamGobbler();
044     Thread t = new Thread(stdout);
045     t.setDaemon(true);
046     t.start();
047 
048     stderr = new StreamGobbler();
049     t = new Thread(stderr);
050     t.setDaemon(true);
051     t.start();
052   }
053   
054   public synchronized boolean isProcessRunning() {
055     if (proc == nullreturn false;
056     
057     try {
058       proc.exitValue();
059       return false;
060     }
061     catch (IllegalThreadStateException e) {
062       return true;
063     }
064   }
065   
066   /**
067    * If a process is currently running then this method destroys it so
068    * that the ProcessManager instance can be used to start another
069    * process
070    */
071   public synchronized void destroyProcess() throws IOException {
072     if(isProcessRunning()) proc.destroy();
073 
074     // wait for the gobblers to finish their jobs
075     while(!stderr.isDone() || !stdout.isDone()) {
076       try {
077         if(DEBUG) {
078           System.err.println("Gobblers not done, waiting...");
079         }
080         this.wait();
081       catch(InterruptedException ie) {
082         // if interrupted, try waiting again
083       }
084     }
085 
086     // make it really obvious the process has finished
087     proc = null;
088 
089     // reset the gobblers
090     if(DEBUG) {
091       System.err.println("Gobblers done - resetting");
092     }
093     stdout.reset();
094     stderr.reset();
095 
096     // if there was an exception during running, throw that
097     Exception ex = null;
098     if(stdout.hasException()) {
099       ex = stdout.getException();
100       stderr.getException()// to reset exception cache
101     else if(stderr.hasException()) {
102       ex = stderr.getException();
103     }
104 
105     if(ex != null) {
106       if(DEBUG) {
107         System.err.println("Rethrowing exception");
108       }
109       if(ex instanceof IOException) {
110         throw (IOException)ex;
111       else if(ex instanceof RuntimeException) {
112         throw (RuntimeException)ex;
113       else throw new GateRuntimeException(ex);
114     }
115   }
116 
117   /**
118    * Sometimes you want to start an external process and pass data to it
119    * at different intervals. A good example is a tool with large startup
120    * time where you can feed it one sentence at a time and get a
121    * response. If you keep the process running you can use it over
122    * multiple documents.
123    */
124   public synchronized OutputStream startProcess(String[] argv, File dir,
125           OutputStream out, OutputStream errthrows IOException {
126 
127     if(isProcessRunning())
128       throw new IOException("The previous process is still running");
129 
130     // Start the process. This may throw an exception
131     if(DEBUG) {
132       System.err.println("Starting process");
133     }
134 
135     proc = Runtime.getRuntime().exec(argv, null, dir);
136 
137     // set up the stream gobblers for stdout and stderr
138     if(DEBUG) {
139       System.err.println("Configuring gobblers");
140     }
141     stdout.setInputStream(proc.getInputStream());
142     stdout.setOutputStream(out);
143 
144     stderr.setInputStream(proc.getErrorStream());
145     stderr.setOutputStream(err);
146 
147     // start the gobblers
148     if(DEBUG) {
149       System.err.println("Waking up gobblers");
150     }
151     this.notifyAll();
152 
153     return proc.getOutputStream();
154   }
155 
156   /**
157    * Run the given external process.  If an exception results from starting the
158    * process, or while reading the output from the process, it will be thrown.
159    * Otherwise, the exit value from the process is returned.
160    *
161    @param argv the process command line, suitable for passing to
162    <code>Runtime.exec</code>.
163    @param dumpOutput should we copy the process output and error streams to
164    * the Java output and error streams or just consume them silently?
165    */
166   public synchronized int runProcess(String[] argv, boolean dumpOutput)
167                           throws IOException {
168     return runProcess(argv, null, (dumpOutput ? System.out : null)(dumpOutput ? System.err : null));
169   }
170   
171   public synchronized int runProcess(String[] argv, OutputStream out, OutputStream err)
172   throws IOException {
173     return runProcess(argv, null, out, err);
174   }
175   
176   /**
177    * Run the given external process.  If an exception results from starting the
178    * process, or while reading the output from the process, it will be thrown.
179    * Otherwise, the exit value from the process is returned.
180    *
181    @param argv the process command line, suitable for passing to
182    <code>Runtime.exec</code>.
183    */
184   public synchronized int runProcess(String[] argv, File dir, OutputStream out, OutputStream err)
185                           throws IOException {
186 
187     if (isProcessRunning()) throw new IOException("The previous process is still running");
188     
189     // Start the process.  This may throw an exception    
190     if(DEBUG) {
191       System.err.println("Starting process");
192     }
193     
194     proc = Runtime.getRuntime().exec(argv, null, dir);
195         
196     // set up the stream gobblers for stdout and stderr
197     if(DEBUG) {
198       System.err.println("Configuring gobblers");
199     }
200     stdout.setInputStream(proc.getInputStream());
201     stdout.setOutputStream(out);
202 
203     stderr.setInputStream(proc.getErrorStream());
204     stderr.setOutputStream(err);
205 
206     // start the gobblers
207     if(DEBUG) {
208       System.err.println("Waking up gobblers");
209     }
210     this.notifyAll();
211 
212     // wait for the gobblers to finish their jobs
213     while(!stderr.isDone() || !stdout.isDone()) {
214       try {
215         if(DEBUG) {
216           System.err.println("Gobblers not done, waiting...");
217         }
218         this.wait();
219       }
220       catch(InterruptedException ie) {
221         // if interrupted, try waiting again
222       }
223     }
224 
225     // get the return code from the process
226     Integer returnCode = null;
227     while(returnCode == null) {
228       try {
229         returnCode = new Integer(proc.waitFor());
230       }
231       catch(InterruptedException ie) {
232         // if interrupted, just try waiting again
233       }
234     }
235     
236     // make it really obvious the process has finished
237     proc = null;
238     
239     // reset the gobblers
240     if(DEBUG) {
241       System.err.println("Gobblers done - resetting");
242     }
243     stdout.reset();
244     stderr.reset();
245     
246     // if there was an exception during running, throw that
247     Exception ex = null;
248     if(stdout.hasException()) {
249       ex = stdout.getException();
250       stderr.getException()// to reset exception cache
251     }
252     else if(stderr.hasException()) {
253       ex = stderr.getException();
254     }
255 
256     if(ex != null) {
257       if(DEBUG) {
258         System.err.println("Rethrowing exception");
259       }
260       if(ex instanceof IOException) {
261         throw (IOException)ex;
262       }
263       else if(ex instanceof RuntimeException) {
264         throw (RuntimeException)ex;
265       }
266       else throw new GateRuntimeException(ex);
267     }
268     // otherwise return the exit code
269     else {
270       return returnCode.intValue();
271     }
272   }
273 
274   /**
275    * Thread body that takes a stream and either consumes it silently or echoes
276    * it to another stream.
277    */
278   private class StreamGobbler implements Runnable {
279     /**
280      * The input stream to gobble.  If this is null, the thread is idle.
281      */
282     private InputStream inputStream = null;
283 
284     /**
285      * The output stream to echo to.  If this is null the gobbler silently
286      * discards the input stream contents.
287      */
288     private OutputStream outputStream = null;;
289 
290     /**
291      * Buffer used for reading and writing.
292      */
293     private byte[] buf = new byte[4096];
294 
295     /**
296      * Are we finished?  This is set to true once the stream has been emptied.
297      */
298     private boolean done = false;
299 
300     /**
301      * If an exception is thrown during gobbling, it is stored here.
302      */
303     private Exception exception = null;
304 
305     /**
306      * Set the stream to gobble.  This should not be called while the thread is
307      * active.
308      */
309     void setInputStream(InputStream is) {
310       inputStream = is;
311     }
312 
313     /**
314      * Set the output stream to redirect output to.  A value of
315      <code>null</code> indicates that we should discard the output without
316      * echoing it.
317      */
318     void setOutputStream(OutputStream os) {
319       outputStream = os;
320     }
321 
322     /**
323      * Has an exception been thrown since {@link #getException()} was last
324      * called?
325      */
326     boolean hasException() {
327       return (exception != null);
328     }
329 
330     /**
331      * Return the last exception thrown.  This also resets the cached exception
332      * to <code>null</code>.
333      */
334     Exception getException() {
335       Exception ex = exception;
336       exception = null;
337       return ex;
338     }
339 
340     boolean isDone() {
341       return done;
342     }
343 
344     /**
345      * Reset state.
346      */
347     void reset() {
348       done = false;
349       exception = null;
350       inputStream = null;
351     }
352 
353     /**
354      * Main body of the thread.  Waits until we have been given a stream to
355      * gobble, then reads it until there is no more input available.
356      */
357     @Override
358     public void run() {
359       if(DEBUG) {
360         System.err.println("StreamGobbler starting");
361       }
362       // wait until we have a stream to gobble
363       synchronized(ProcessManager.this) {
364         while(inputStream == null) {
365           try {
366             if(DEBUG) {
367               System.err.println("Waiting for stream...");
368             }
369             ProcessManager.this.wait();
370           }
371           catch(InterruptedException ie) {
372           }
373         }
374       }
375 
376       while(true) {
377         // read the stream until end of file or an exception is thrown.
378         BufferedInputStream bis = new BufferedInputStream(inputStream);
379         int bytesRead = -1;
380         try {
381           if(DEBUG) {
382             System.err.println("Gobbling stream");
383           }
384           while((bytesRead = bis.read(buf)) != -1) {
385             // echo to outputStream if necessary
386             if(outputStream != null) {
387               outputStream.write(buf, 0, bytesRead);
388             }
389           }
390         }
391         catch(Exception ex) {
392           // any exception is stored to be retrieved by the ProcessManager
393           exception = ex;
394           if(DEBUG) {
395             System.err.println("Exception thrown");
396           }
397         }
398         
399         try {
400           bis.close();
401         }
402         catch(IOException ioe) {
403           // oh well, it's not the end of the world
404         }
405 
406         done = true;
407         inputStream = null;
408         outputStream = null;
409 
410         // wake up ProcessManager to say we've finished, and start waiting for
411         // the next round.
412         synchronized(ProcessManager.this) {
413           if(DEBUG) {
414             System.err.println("Waking process manager");
415           }
416           ProcessManager.this.notifyAll();
417           while(inputStream == null) {
418             try {
419               if(DEBUG) {
420                 System.err.println("Waiting for stream (2)");
421               }
422               ProcessManager.this.wait();
423             }
424             catch(InterruptedException ie) {
425             }
426           }
427         }
428       }
429     }
430   }
431 }