001package com.hfg.util;
002
003import java.io.*;
004import java.util.List;
005import java.util.ArrayList;
006
007//------------------------------------------------------------------------------
008/**
009 More robust wrapper for Process.exec().
010 <div>
011   @author J. Alex Taylor, hairyfatguy.com
012 </div>
013 */
014//------------------------------------------------------------------------------
015// com.hfg XML/HTML Coding Library
016//
017// This library is free software; you can redistribute it and/or
018// modify it under the terms of the GNU Lesser General Public
019// License as published by the Free Software Foundation; either
020// version 2.1 of the License, or (at your option) any later version.
021//
022// This library is distributed in the hope that it will be useful,
023// but WITHOUT ANY WARRANTY; without even the implied warranty of
024// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
025// Lesser General Public License for more details.
026//
027// You should have received a copy of the GNU Lesser General Public
028// License along with this library; if not, write to the Free Software
029// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
030//
031// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
032// jataylor@hairyfatguy.com
033//------------------------------------------------------------------------------
034// TODO:
035//       - Make the internal threads reusable
036   
037public class Executor
038{
039   //***************************************************************************
040   // PRIVATE FIELDS
041   //***************************************************************************
042
043   private String       mCmd;
044   private List         mEnvVars;
045   private File         mWorkingDir;
046   private String       mSTDINString;
047   private InputStream  mSTDINStream;
048   private File         mShell        = new File("/bin/csh");
049
050   private boolean      mRunning;
051   private String       mSTDOUT;
052   private String       mSTDERR;
053   private int          mExitValue;
054   private long         mExecutionTime;
055
056   private static int   BUFFER_SIZE = 8192;
057
058   //***************************************************************************
059   // CONSTRUCTORS
060   //***************************************************************************
061
062   //---------------------------------------------------------------------------
063   /**
064    Default constructor.
065    */
066   public Executor()
067   {
068   }
069
070   //---------------------------------------------------------------------------
071   /**
072    Constructor specifying the command to execute.
073    */
074   public Executor(String inCommand)
075   {
076      this();
077      setCommand(inCommand);
078   }
079
080   //---------------------------------------------------------------------------
081   /**
082    Constructor specifying the command to execute.
083    */
084   public Executor(String[] inCommand)
085   {
086      this(StringUtil.join(inCommand));
087   }
088
089   //***************************************************************************
090   // PUBLIC METHODS
091   //***************************************************************************
092
093   //---------------------------------------------------------------------------
094   /**
095    Sets the command to execute.
096    */
097   public void setCommand(String inCommand)
098   {
099      mCmd = inCommand;
100   }
101
102   //---------------------------------------------------------------------------
103   /**
104    Returns the command to execute.
105    */
106   public String getCommand()
107   {
108      return mCmd;
109   }
110
111
112
113   //---------------------------------------------------------------------------
114   /**
115    Clears the list of environment variables to be set when executing the command.
116    <i><b>IMPORTANT!</b> If the environment variables are cleared it does NOT mean
117    that no environment variables are present, but rather that the parent process'
118    environment variables are inherited.</i>
119    */
120   public void clearEnvVars()
121   {
122      mEnvVars = null;
123   }
124
125   //---------------------------------------------------------------------------
126   /**
127    Adds the specified environment variable to the list to be set when executing
128    the command.
129    <i><b>IMPORTANT!</b> If environment variables are specified they are specified
130    IN PLACE OF the parent process' environment variables and NOT in addition to
131    them.</i>
132    */
133   public void addEnvVar(String inName, String inValue)
134   {
135      if (null == inName)
136      {
137         throw new ExecutorException("Environment variable names cannot be null!");
138      }
139
140      if (null == mEnvVars) mEnvVars = new ArrayList(5);
141
142      mEnvVars.add(inName + (inValue != null ? "=" + inValue : ""));
143   }
144
145
146
147   //---------------------------------------------------------------------------
148   /**
149    Sets the shell to be used for command execution. The default is '/bin/csh'.
150    To run the command without a &lt;shell&gt; -c '&lt;cmd&gt;' construct, set the shell
151    to null.
152    */
153   public void setShell(File inValue)
154   {
155      if (inValue != null
156          && !inValue.exists())
157      {
158         throw new ExecutorException("The shell '" + inValue + "' does not exist!");
159      }
160
161      mShell = inValue;
162   }
163
164   //---------------------------------------------------------------------------
165   /**
166    Returns the shell to be used for command execution.
167    */
168   public File getShell()
169   {
170      return mShell;
171   }
172
173
174   //---------------------------------------------------------------------------
175   /**
176    Sets the working directory for command execution.
177    */
178   public void setWorkingDir(File inValue)
179   {
180      mWorkingDir = inValue;
181   }
182
183   //---------------------------------------------------------------------------
184   /**
185    Returns the working directory for command execution.
186    */
187   public File getWorkingDir()
188   {
189      return mWorkingDir;
190   }
191
192
193   //---------------------------------------------------------------------------
194   /**
195    Sets the STDIN content for the command to be executed.
196    */
197   public void setSTDIN(String inValue)
198   {
199      mSTDINString = inValue;
200      mSTDINStream = null;
201   }
202
203
204   //---------------------------------------------------------------------------
205   /**
206    Sets the STDIN content for the command to be executed.
207    */
208   public void setSTDIN(InputStream inValue)
209   {
210      mSTDINString = null;
211      mSTDINStream = inValue;
212   }
213
214
215
216   //---------------------------------------------------------------------------
217   /**
218    Returns the STDOUT output produced by the executed command.
219    */
220   public String getSTDOUT()
221   {
222      return mSTDOUT;
223   }
224
225   //---------------------------------------------------------------------------
226   /**
227    Returns the STDERR output produced by the executed command.
228    */
229   public String getSTDERR()
230   {
231      return mSTDERR;
232   }
233
234   //---------------------------------------------------------------------------
235   /**
236    Returns the execution time in milliseconds of executed command.  This is wall
237    time, not CPU time.
238    */
239   public long getExecutionTimeMillis()
240   {
241      return mExecutionTime;
242   }
243
244   //---------------------------------------------------------------------------
245   /**
246    Executes the command.
247
248    @return the exit value of the executed command.
249    @throws ExecutorException
250    */
251   public int exec()
252   throws ExecutorException
253   {
254      setup();
255
256      mRunning = true;
257
258      long startTime = System.currentTimeMillis();
259
260      Job job = new Job();
261      Thread thread = new Thread(job);
262      thread.start();
263
264      while (mRunning)
265      {
266         try
267         {
268            Thread.sleep(50);
269         }
270         catch (InterruptedException e)
271         {
272         }
273      }
274
275      mExecutionTime = System.currentTimeMillis() - startTime;
276
277      return mExitValue;
278   }
279
280
281
282   //***************************************************************************
283   // PRIVATE METHODS
284   //***************************************************************************
285
286   //---------------------------------------------------------------------------
287   private void setup()
288   {
289      mSTDOUT = null;
290      mSTDERR = null;
291   }
292
293   //---------------------------------------------------------------------------
294   private String[] getCmdAsArray()
295   {
296      String[] cmd = {mShell.toString(),
297                      "-f", // The shell ignores the ~/.cshrc (or appropriate
298                            // login script for the shell).
299                      "-c", getCommand()};
300
301      return cmd;
302   }
303
304   //---------------------------------------------------------------------------
305   private String[] getEnvVarsAsArray()
306   {
307      String[] env = null;
308
309      if (mEnvVars != null)
310      {
311         env = new String[mEnvVars.size()];
312
313         for (int i = 0; i < mEnvVars.size(); i++)
314         {
315            env[i] = (String) mEnvVars.get(i);
316         }
317      }
318
319      return env;
320   }
321
322
323   //***************************************************************************
324   // INNER CLASS
325   //***************************************************************************
326
327   private class Job implements Runnable
328   {
329      //------------------------------------------------------------------------
330      public void run()
331      {
332         try
333         {
334            Process process = getProcess();
335
336            // Set STDIN if specified
337            setSTDIN(process);
338
339            // Use separate threads to read from STDOUT and STDERR to avoid
340            // potential blocking problems.
341            JobOutput stdout = new JobOutput(process.getInputStream());
342            Thread stdoutThread = new Thread(stdout);
343            stdoutThread.start();
344
345            JobOutput stderr = new JobOutput(process.getErrorStream());
346            Thread stderrThread = new Thread(stderr);
347            stderrThread.start();
348
349            process.waitFor();
350
351            while (   ! stdout.isDone()
352                   || ! stderr.isDone())
353            {
354               Thread.sleep(25);
355            }
356
357
358            mSTDOUT = stdout.getContent();
359            mSTDERR = stderr.getContent();
360
361            mExitValue = process.exitValue();
362         }
363         catch (Throwable e)
364         {
365            throw new ExecutorException("Command " + StringUtil.singleQuote(getCommand())
366                                        + " produced an exception!", e);
367         }
368         finally
369         {
370            mRunning = false;
371         }
372      }
373
374      //------------------------------------------------------------------------
375      private Process getProcess()
376      throws Exception
377      {
378         Process process = null;
379
380         if (null == mShell)
381         {
382            process = Runtime.getRuntime().exec(getCommand(),
383                                                getEnvVarsAsArray(),
384                                                getWorkingDir());
385         }
386         else
387         {
388            process = Runtime.getRuntime().exec(getCmdAsArray(),
389                                                getEnvVarsAsArray(),
390                                                getWorkingDir());
391         }
392
393         return process;
394      }
395
396      //------------------------------------------------------------------------
397      private void setSTDIN(Process inProcess)
398            throws IOException
399      {
400         if (mSTDINString != null)
401         {
402            PrintWriter writer = new PrintWriter(inProcess.getOutputStream());
403            writer.print(mSTDINString);
404            writer.close();
405         }
406         else if (mSTDINStream != null)
407         {
408            PrintWriter writer = new PrintWriter(inProcess.getOutputStream());
409
410            InputStreamReader reader = new InputStreamReader(mSTDINStream);
411
412            char[] buffer = new char[BUFFER_SIZE];
413            int charsRead = 0;
414
415            while ((charsRead = reader.read(buffer, 0, BUFFER_SIZE)) != -1)
416            {
417               writer.write(buffer, 0, charsRead);
418            }
419
420            writer.close();
421         }
422      }
423   }
424
425
426   //***************************************************************************
427   // INNER CLASS
428   //***************************************************************************
429
430   // Generic class to handle reading the STDOUT and STDERR streams from the process.
431   private class JobOutput implements Runnable
432   {
433      private InputStream  mInputStream;
434      private StringBuffer mContent      = new StringBuffer();
435      private boolean      mDone         = false;
436
437      //************************************************************************
438      // CONSTRUCTORS
439      //************************************************************************
440
441      //------------------------------------------------------------------------
442      public JobOutput(InputStream inProcessStream)
443      {
444         mInputStream = inProcessStream;
445      }
446
447      //************************************************************************
448      // PUBLIC METHODS
449      //************************************************************************
450
451      //------------------------------------------------------------------------
452      public void run()
453      {
454         try
455         {
456            InputStreamReader reader = new InputStreamReader(mInputStream);
457
458            char[] buffer = new char[BUFFER_SIZE];
459            int charsRead = 0;
460
461            while ((charsRead = reader.read(buffer, 0, BUFFER_SIZE)) != -1)
462            {
463               mContent.append(buffer, 0, charsRead);
464            }
465         }
466         catch (Throwable e)
467         {
468            throw new ExecutorException(e);
469         }
470         finally
471         {
472            mDone = true;
473         }
474
475      }
476
477      //------------------------------------------------------------------------
478      public boolean isDone()
479      {
480         return mDone;
481      }
482
483      //------------------------------------------------------------------------
484      public String getContent()
485      {
486         return mContent.toString();
487      }
488
489
490
491   }
492
493
494}