001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.util;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.io.InputStream;
025import java.nio.charset.Charset;
026import java.util.Arrays;
027import java.util.Map;
028import java.util.Timer;
029import java.util.TimerTask;
030import java.util.concurrent.atomic.AtomicBoolean;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.hadoop.classification.InterfaceAudience;
035import org.apache.hadoop.classification.InterfaceStability;
036import org.apache.hadoop.security.alias.AbstractJavaKeyStoreProvider;
037
038/** 
039 * A base class for running a Unix command.
040 * 
041 * <code>Shell</code> can be used to run unix commands like <code>du</code> or
042 * <code>df</code>. It also offers facilities to gate commands by 
043 * time-intervals.
044 */
045@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
046@InterfaceStability.Unstable
047abstract public class Shell {
048  
049  public static final Log LOG = LogFactory.getLog(Shell.class);
050  
051  private static boolean IS_JAVA7_OR_ABOVE =
052      System.getProperty("java.version").substring(0, 3).compareTo("1.7") >= 0;
053
054  public static boolean isJava7OrAbove() {
055    return IS_JAVA7_OR_ABOVE;
056  }
057
058  /**
059   * Maximum command line length in Windows
060   * KB830473 documents this as 8191
061   */
062  public static final int WINDOWS_MAX_SHELL_LENGHT = 8191;
063
064  /**
065   * Checks if a given command (String[]) fits in the Windows maximum command line length
066   * Note that the input is expected to already include space delimiters, no extra count
067   * will be added for delimiters.
068   *
069   * @param commands command parts, including any space delimiters
070   */
071  public static void checkWindowsCommandLineLength(String...commands)
072      throws IOException {
073    int len = 0;
074    for (String s: commands) {
075      len += s.length();
076    }
077    if (len > WINDOWS_MAX_SHELL_LENGHT) {
078      throw new IOException(String.format(
079          "The command line has a length of %d exceeds maximum allowed length of %d. " +
080          "Command starts with: %s",
081          len, WINDOWS_MAX_SHELL_LENGHT,
082          StringUtils.join("", commands).substring(0, 100)));
083    }
084  }
085
086  /**
087   * Quote the given arg so that bash will interpret it as a single value.
088   * Note that this quotes it for one level of bash, if you are passing it
089   * into a badly written shell script, you need to fix your shell script.
090   * @param arg the argument to quote
091   * @return the quoted string
092   */
093  static String bashQuote(String arg) {
094    StringBuilder buffer = new StringBuilder(arg.length() + 2);
095    buffer.append('\'');
096    buffer.append(arg.replace("'", "'\\''"));
097    buffer.append('\'');
098    return buffer.toString();
099  }
100
101  /** a Unix command to get the current user's name: {@value}. */
102  public static final String USER_NAME_COMMAND = "whoami";
103
104  /** Windows CreateProcess synchronization object */
105  public static final Object WindowsProcessLaunchLock = new Object();
106
107  // OSType detection
108
109  public enum OSType {
110    OS_TYPE_LINUX,
111    OS_TYPE_WIN,
112    OS_TYPE_SOLARIS,
113    OS_TYPE_MAC,
114    OS_TYPE_FREEBSD,
115    OS_TYPE_OTHER
116  }
117
118  public static final OSType osType = getOSType();
119
120  static private OSType getOSType() {
121    String osName = System.getProperty("os.name");
122    if (osName.startsWith("Windows")) {
123      return OSType.OS_TYPE_WIN;
124    } else if (osName.contains("SunOS") || osName.contains("Solaris")) {
125      return OSType.OS_TYPE_SOLARIS;
126    } else if (osName.contains("Mac")) {
127      return OSType.OS_TYPE_MAC;
128    } else if (osName.contains("FreeBSD")) {
129      return OSType.OS_TYPE_FREEBSD;
130    } else if (osName.startsWith("Linux")) {
131      return OSType.OS_TYPE_LINUX;
132    } else {
133      // Some other form of Unix
134      return OSType.OS_TYPE_OTHER;
135    }
136  }
137
138  // Helper static vars for each platform
139  public static final boolean WINDOWS = (osType == OSType.OS_TYPE_WIN);
140  public static final boolean SOLARIS = (osType == OSType.OS_TYPE_SOLARIS);
141  public static final boolean MAC     = (osType == OSType.OS_TYPE_MAC);
142  public static final boolean FREEBSD = (osType == OSType.OS_TYPE_FREEBSD);
143  public static final boolean LINUX   = (osType == OSType.OS_TYPE_LINUX);
144  public static final boolean OTHER   = (osType == OSType.OS_TYPE_OTHER);
145
146  public static final boolean PPC_64
147                = System.getProperties().getProperty("os.arch").contains("ppc64");
148
149  /** a Unix command to get the current user's groups list */
150  public static String[] getGroupsCommand() {
151    return (WINDOWS)? new String[]{"cmd", "/c", "groups"}
152                    : new String[]{"groups"};
153  }
154
155  /**
156   * a Unix command to get a given user's groups list.
157   * If the OS is not WINDOWS, the command will get the user's primary group
158   * first and finally get the groups list which includes the primary group.
159   * i.e. the user's primary group will be included twice.
160   */
161  public static String[] getGroupsForUserCommand(final String user) {
162    //'groups username' command return is inconsistent across different unixes
163    if (WINDOWS) {
164      return new String[]
165          {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""};
166    } else {
167      String quotedUser = bashQuote(user);
168      return new String[] {"bash", "-c", "id -gn " + quotedUser +
169                            "; id -Gn " + quotedUser};
170    }
171  }
172
173  /** a Unix command to get a given netgroup's user list */
174  public static String[] getUsersForNetgroupCommand(final String netgroup) {
175    //'groups username' command return is non-consistent across different unixes
176    return new String[] {"getent", "netgroup", netgroup};
177  }
178
179  /** Return a command to get permission information. */
180  public static String[] getGetPermissionCommand() {
181    return (WINDOWS) ? new String[] { WINUTILS, "ls", "-F" }
182                     : new String[] { "/bin/ls", "-ld" };
183  }
184
185  /** Return a command to set permission */
186  public static String[] getSetPermissionCommand(String perm, boolean recursive) {
187    if (recursive) {
188      return (WINDOWS) ? new String[] { WINUTILS, "chmod", "-R", perm }
189                         : new String[] { "chmod", "-R", perm };
190    } else {
191      return (WINDOWS) ? new String[] { WINUTILS, "chmod", perm }
192                       : new String[] { "chmod", perm };
193    }
194  }
195
196  /**
197   * Return a command to set permission for specific file.
198   * 
199   * @param perm String permission to set
200   * @param recursive boolean true to apply to all sub-directories recursively
201   * @param file String file to set
202   * @return String[] containing command and arguments
203   */
204  public static String[] getSetPermissionCommand(String perm, boolean recursive,
205                                                 String file) {
206    String[] baseCmd = getSetPermissionCommand(perm, recursive);
207    String[] cmdWithFile = Arrays.copyOf(baseCmd, baseCmd.length + 1);
208    cmdWithFile[cmdWithFile.length - 1] = file;
209    return cmdWithFile;
210  }
211
212  /** Return a command to set owner */
213  public static String[] getSetOwnerCommand(String owner) {
214    return (WINDOWS) ? new String[] { WINUTILS, "chown", "\"" + owner + "\"" }
215                     : new String[] { "chown", owner };
216  }
217  
218  /** Return a command to create symbolic links */
219  public static String[] getSymlinkCommand(String target, String link) {
220    return WINDOWS ? new String[] { WINUTILS, "symlink", link, target }
221                   : new String[] { "ln", "-s", target, link };
222  }
223
224  /** Return a command to read the target of the a symbolic link*/
225  public static String[] getReadlinkCommand(String link) {
226    return WINDOWS ? new String[] { WINUTILS, "readlink", link }
227        : new String[] { "readlink", link };
228  }
229
230  /** Return a command for determining if process with specified pid is alive. */
231  public static String[] getCheckProcessIsAliveCommand(String pid) {
232    return Shell.WINDOWS ?
233      new String[] { Shell.WINUTILS, "task", "isAlive", pid } :
234      new String[] { "kill", "-0", isSetsidAvailable ? "-" + pid : pid };
235  }
236
237  /** Return a command to send a signal to a given pid */
238  public static String[] getSignalKillCommand(int code, String pid) {
239    return Shell.WINDOWS ? new String[] { Shell.WINUTILS, "task", "kill", pid } :
240      new String[] { "kill", "-" + code, isSetsidAvailable ? "-" + pid : pid };
241  }
242
243  /** Return a regular expression string that match environment variables */
244  public static String getEnvironmentVariableRegex() {
245    return (WINDOWS) ? "%([A-Za-z_][A-Za-z0-9_]*?)%" :
246      "\\$([A-Za-z_][A-Za-z0-9_]*)";
247  }
248  
249  /**
250   * Returns a File referencing a script with the given basename, inside the
251   * given parent directory.  The file extension is inferred by platform: ".cmd"
252   * on Windows, or ".sh" otherwise.
253   * 
254   * @param parent File parent directory
255   * @param basename String script file basename
256   * @return File referencing the script in the directory
257   */
258  public static File appendScriptExtension(File parent, String basename) {
259    return new File(parent, appendScriptExtension(basename));
260  }
261
262  /**
263   * Returns a script file name with the given basename.  The file extension is
264   * inferred by platform: ".cmd" on Windows, or ".sh" otherwise.
265   * 
266   * @param basename String script file basename
267   * @return String script file name
268   */
269  public static String appendScriptExtension(String basename) {
270    return basename + (WINDOWS ? ".cmd" : ".sh");
271  }
272
273  /**
274   * Returns a command to run the given script.  The script interpreter is
275   * inferred by platform: cmd on Windows or bash otherwise.
276   * 
277   * @param script File script to run
278   * @return String[] command to run the script
279   */
280  public static String[] getRunScriptCommand(File script) {
281    String absolutePath = script.getAbsolutePath();
282    return WINDOWS ?
283      new String[] {"cmd", "/c", absolutePath }
284      : new String[] {"/bin/bash", bashQuote(absolutePath) };
285  }
286
287  /** a Unix command to set permission */
288  public static final String SET_PERMISSION_COMMAND = "chmod";
289  /** a Unix command to set owner */
290  public static final String SET_OWNER_COMMAND = "chown";
291
292  /** a Unix command to set the change user's groups list */
293  public static final String SET_GROUP_COMMAND = "chgrp";
294  /** a Unix command to create a link */
295  public static final String LINK_COMMAND = "ln";
296  /** a Unix command to get a link target */
297  public static final String READ_LINK_COMMAND = "readlink";
298
299  /**Time after which the executing script would be timedout*/
300  protected long timeOutInterval = 0L;
301  /** If or not script timed out*/
302  private AtomicBoolean timedOut;
303
304  /** Indicates if the parent env vars should be inherited or not*/
305  protected boolean inheritParentEnv = true;
306
307  /** Centralized logic to discover and validate the sanity of the Hadoop 
308   *  home directory. Returns either NULL or a directory that exists and 
309   *  was specified via either -Dhadoop.home.dir or the HADOOP_HOME ENV 
310   *  variable.  This does a lot of work so it should only be called 
311   *  privately for initialization once per process.
312   **/
313  private static String checkHadoopHome() {
314
315    // first check the Dflag hadoop.home.dir with JVM scope
316    String home = System.getProperty("hadoop.home.dir");
317
318    // fall back to the system/user-global env variable
319    if (home == null) {
320      home = System.getenv("HADOOP_HOME");
321    }
322
323    try {
324       // couldn't find either setting for hadoop's home directory
325       if (home == null) {
326         throw new IOException("HADOOP_HOME or hadoop.home.dir are not set.");
327       }
328
329       if (home.startsWith("\"") && home.endsWith("\"")) {
330         home = home.substring(1, home.length()-1);
331       }
332
333       // check that the home setting is actually a directory that exists
334       File homedir = new File(home);
335       if (!homedir.isAbsolute() || !homedir.exists() || !homedir.isDirectory()) {
336         throw new IOException("Hadoop home directory " + homedir
337           + " does not exist, is not a directory, or is not an absolute path.");
338       }
339
340       home = homedir.getCanonicalPath();
341
342    } catch (IOException ioe) {
343      if (LOG.isDebugEnabled()) {
344        LOG.debug("Failed to detect a valid hadoop home directory", ioe);
345      }
346      home = null;
347    }
348    
349    return home;
350  }
351  private static String HADOOP_HOME_DIR = checkHadoopHome();
352
353  // Public getter, throws an exception if HADOOP_HOME failed validation
354  // checks and is being referenced downstream.
355  public static final String getHadoopHome() throws IOException {
356    if (HADOOP_HOME_DIR == null) {
357      throw new IOException("Misconfigured HADOOP_HOME cannot be referenced.");
358    }
359
360    return HADOOP_HOME_DIR;
361  }
362
363  /** fully qualify the path to a binary that should be in a known hadoop 
364   *  bin location. This is primarily useful for disambiguating call-outs 
365   *  to executable sub-components of Hadoop to avoid clashes with other 
366   *  executables that may be in the path.  Caveat:  this call doesn't 
367   *  just format the path to the bin directory.  It also checks for file 
368   *  existence of the composed path. The output of this call should be 
369   *  cached by callers.
370   * */
371  public static final String getQualifiedBinPath(String executable) 
372  throws IOException {
373    // construct hadoop bin path to the specified executable
374    String fullExeName = HADOOP_HOME_DIR + File.separator + "bin" 
375      + File.separator + executable;
376
377    File exeFile = new File(fullExeName);
378    if (!exeFile.exists()) {
379      throw new IOException("Could not locate executable " + fullExeName
380        + " in the Hadoop binaries.");
381    }
382
383    return exeFile.getCanonicalPath();
384  }
385
386  /** a Windows utility to emulate Unix commands */
387  public static final String WINUTILS = getWinUtilsPath();
388
389  public static final String getWinUtilsPath() {
390    String winUtilsPath = null;
391
392    try {
393      if (WINDOWS) {
394        winUtilsPath = getQualifiedBinPath("winutils.exe");
395      }
396    } catch (IOException ioe) {
397       LOG.error("Failed to locate the winutils binary in the hadoop binary path",
398         ioe);
399    }
400
401    return winUtilsPath;
402  }
403
404  public static final boolean isSetsidAvailable = isSetsidSupported();
405  private static boolean isSetsidSupported() {
406    if (Shell.WINDOWS) {
407      return false;
408    }
409    ShellCommandExecutor shexec = null;
410    boolean setsidSupported = true;
411    try {
412      String[] args = {"setsid", "bash", "-c", "echo $$"};
413      shexec = new ShellCommandExecutor(args);
414      shexec.execute();
415    } catch (IOException ioe) {
416      LOG.debug("setsid is not available on this machine. So not using it.");
417      setsidSupported = false;
418    } finally { // handle the exit code
419      if (LOG.isDebugEnabled()) {
420        LOG.debug("setsid exited with exit code "
421                 + (shexec != null ? shexec.getExitCode() : "(null executor)"));
422      }
423    }
424    return setsidSupported;
425  }
426
427  /** Token separator regex used to parse Shell tool outputs */
428  public static final String TOKEN_SEPARATOR_REGEX
429                = WINDOWS ? "[|\n\r]" : "[ \t\n\r\f]";
430
431  private long    interval;   // refresh interval in msec
432  private long    lastTime;   // last time the command was performed
433  final private boolean redirectErrorStream; // merge stdout and stderr
434  private Map<String, String> environment; // env for the command execution
435  private File dir;
436  private Process process; // sub process used to execute the command
437  private int exitCode;
438
439  /**If or not script finished executing*/
440  private volatile AtomicBoolean completed;
441  
442  public Shell() {
443    this(0L);
444  }
445  
446  public Shell(long interval) {
447    this(interval, false);
448  }
449
450  /**
451   * @param interval the minimum duration to wait before re-executing the 
452   *        command.
453   */
454  public Shell(long interval, boolean redirectErrorStream) {
455    this.interval = interval;
456    this.lastTime = (interval<0) ? 0 : -interval;
457    this.redirectErrorStream = redirectErrorStream;
458  }
459  
460  /** set the environment for the command 
461   * @param env Mapping of environment variables
462   */
463  protected void setEnvironment(Map<String, String> env) {
464    this.environment = env;
465  }
466
467  /** set the working directory 
468   * @param dir The directory where the command would be executed
469   */
470  protected void setWorkingDirectory(File dir) {
471    this.dir = dir;
472  }
473
474  /** check to see if a command needs to be executed and execute if needed */
475  protected void run() throws IOException {
476    if (lastTime + interval > Time.monotonicNow())
477      return;
478    exitCode = 0; // reset for next run
479    runCommand();
480  }
481
482  /** Run a command */
483  private void runCommand() throws IOException { 
484    ProcessBuilder builder = new ProcessBuilder(getExecString());
485    Timer timeOutTimer = null;
486    ShellTimeoutTimerTask timeoutTimerTask = null;
487    timedOut = new AtomicBoolean(false);
488    completed = new AtomicBoolean(false);
489    
490    if (environment != null) {
491      builder.environment().putAll(this.environment);
492    }
493
494    // Remove all env vars from the Builder to prevent leaking of env vars from
495    // the parent process.
496    if (!inheritParentEnv) {
497      // branch-2: Only do this for HADOOP_CREDSTORE_PASSWORD
498      // Sometimes daemons are configured to use the CredentialProvider feature
499      // and given their jceks password via an environment variable.  We need to
500      // make sure to remove it so it doesn't leak to child processes, which
501      // might be owned by a different user.  For example, the NodeManager
502      // running a User's container.
503      builder.environment().remove(
504          AbstractJavaKeyStoreProvider.CREDENTIAL_PASSWORD_NAME);
505    }
506
507    if (dir != null) {
508      builder.directory(this.dir);
509    }
510
511    builder.redirectErrorStream(redirectErrorStream);
512    
513    if (Shell.WINDOWS) {
514      synchronized (WindowsProcessLaunchLock) {
515        // To workaround the race condition issue with child processes
516        // inheriting unintended handles during process launch that can
517        // lead to hangs on reading output and error streams, we
518        // serialize process creation. More info available at:
519        // http://support.microsoft.com/kb/315939
520        process = builder.start();
521      }
522    } else {
523      process = builder.start();
524    }
525
526    if (timeOutInterval > 0) {
527      timeOutTimer = new Timer("Shell command timeout");
528      timeoutTimerTask = new ShellTimeoutTimerTask(
529          this);
530      //One time scheduling.
531      timeOutTimer.schedule(timeoutTimerTask, timeOutInterval);
532    }
533    final BufferedReader errReader = 
534            new BufferedReader(new InputStreamReader(
535                process.getErrorStream(), Charset.defaultCharset()));
536    BufferedReader inReader = 
537            new BufferedReader(new InputStreamReader(
538                process.getInputStream(), Charset.defaultCharset()));
539    final StringBuffer errMsg = new StringBuffer();
540    
541    // read error and input streams as this would free up the buffers
542    // free the error stream buffer
543    Thread errThread = new Thread() {
544      @Override
545      public void run() {
546        try {
547          String line = errReader.readLine();
548          while((line != null) && !isInterrupted()) {
549            errMsg.append(line);
550            errMsg.append(System.getProperty("line.separator"));
551            line = errReader.readLine();
552          }
553        } catch(IOException ioe) {
554          LOG.warn("Error reading the error stream", ioe);
555        }
556      }
557    };
558    try {
559      errThread.start();
560    } catch (IllegalStateException ise) {
561    } catch (OutOfMemoryError oe) {
562      LOG.error("Caught " + oe + ". One possible reason is that ulimit"
563          + " setting of 'max user processes' is too low. If so, do"
564          + " 'ulimit -u <largerNum>' and try again.");
565      throw oe;
566    }
567    try {
568      parseExecResult(inReader); // parse the output
569      // clear the input stream buffer
570      String line = inReader.readLine();
571      while(line != null) { 
572        line = inReader.readLine();
573      }
574      // wait for the process to finish and check the exit code
575      exitCode  = process.waitFor();
576      // make sure that the error thread exits
577      joinThread(errThread);
578      completed.set(true);
579      //the timeout thread handling
580      //taken care in finally block
581      if (exitCode != 0) {
582        throw new ExitCodeException(exitCode, errMsg.toString());
583      }
584    } catch (InterruptedException ie) {
585      throw new IOException(ie.toString());
586    } finally {
587      if (timeOutTimer != null) {
588        timeOutTimer.cancel();
589      }
590      // close the input stream
591      try {
592        // JDK 7 tries to automatically drain the input streams for us
593        // when the process exits, but since close is not synchronized,
594        // it creates a race if we close the stream first and the same
595        // fd is recycled.  the stream draining thread will attempt to
596        // drain that fd!!  it may block, OOM, or cause bizarre behavior
597        // see: https://bugs.openjdk.java.net/browse/JDK-8024521
598        //      issue is fixed in build 7u60
599        InputStream stdout = process.getInputStream();
600        synchronized (stdout) {
601          inReader.close();
602        }
603      } catch (IOException ioe) {
604        LOG.warn("Error while closing the input stream", ioe);
605      }
606      if (!completed.get()) {
607        errThread.interrupt();
608        joinThread(errThread);
609      }
610      try {
611        InputStream stderr = process.getErrorStream();
612        synchronized (stderr) {
613          errReader.close();
614        }
615      } catch (IOException ioe) {
616        LOG.warn("Error while closing the error stream", ioe);
617      }
618      process.destroy();
619      lastTime = Time.monotonicNow();
620    }
621  }
622
623  private static void joinThread(Thread t) {
624    while (t.isAlive()) {
625      try {
626        t.join();
627      } catch (InterruptedException ie) {
628        if (LOG.isWarnEnabled()) {
629          LOG.warn("Interrupted while joining on: " + t, ie);
630        }
631        t.interrupt(); // propagate interrupt
632      }
633    }
634  }
635
636  /** return an array containing the command name & its parameters */ 
637  protected abstract String[] getExecString();
638  
639  /** Parse the execution result */
640  protected abstract void parseExecResult(BufferedReader lines)
641  throws IOException;
642
643  /** 
644   * Get the environment variable
645   */
646  public String getEnvironment(String env) {
647    return environment.get(env);
648  }
649  
650  /** get the current sub-process executing the given command 
651   * @return process executing the command
652   */
653  public Process getProcess() {
654    return process;
655  }
656
657  /** get the exit code 
658   * @return the exit code of the process
659   */
660  public int getExitCode() {
661    return exitCode;
662  }
663
664  /**
665   * This is an IOException with exit code added.
666   */
667  public static class ExitCodeException extends IOException {
668    private final int exitCode;
669    
670    public ExitCodeException(int exitCode, String message) {
671      super(message);
672      this.exitCode = exitCode;
673    }
674    
675    public int getExitCode() {
676      return exitCode;
677    }
678
679    @Override
680    public String toString() {
681      final StringBuilder sb =
682          new StringBuilder("ExitCodeException ");
683      sb.append("exitCode=").append(exitCode)
684        .append(": ");
685      sb.append(super.getMessage());
686      return sb.toString();
687    }
688  }
689  
690  public interface CommandExecutor {
691
692    void execute() throws IOException;
693
694    int getExitCode() throws IOException;
695
696    String getOutput() throws IOException;
697
698    void close();
699    
700  }
701  
702  /**
703   * A simple shell command executor.
704   * 
705   * <code>ShellCommandExecutor</code>should be used in cases where the output 
706   * of the command needs no explicit parsing and where the command, working 
707   * directory and the environment remains unchanged. The output of the command 
708   * is stored as-is and is expected to be small.
709   */
710  public static class ShellCommandExecutor extends Shell 
711      implements CommandExecutor {
712    
713    private String[] command;
714    private StringBuffer output;
715    
716    
717    public ShellCommandExecutor(String[] execString) {
718      this(execString, null);
719    }
720    
721    public ShellCommandExecutor(String[] execString, File dir) {
722      this(execString, dir, null);
723    }
724   
725    public ShellCommandExecutor(String[] execString, File dir, 
726                                 Map<String, String> env) {
727      this(execString, dir, env , 0L);
728    }
729
730    public ShellCommandExecutor(String[] execString, File dir,
731                                Map<String, String> env, long timeout) {
732      this(execString, dir, env , timeout, true);
733    }
734
735    /**
736     * Create a new instance of the ShellCommandExecutor to execute a command.
737     * 
738     * @param execString The command to execute with arguments
739     * @param dir If not-null, specifies the directory which should be set
740     *            as the current working directory for the command.
741     *            If null, the current working directory is not modified.
742     * @param env If not-null, environment of the command will include the
743     *            key-value pairs specified in the map. If null, the current
744     *            environment is not modified.
745     * @param timeout Specifies the time in milliseconds, after which the
746     *                command will be killed and the status marked as timedout.
747     *                If 0, the command will not be timed out.
748     * @param inheritParentEnv Indicates if the process should inherit the env
749     *                         vars from the parent process or not.
750     */
751    public ShellCommandExecutor(String[] execString, File dir, 
752        Map<String, String> env, long timeout, boolean inheritParentEnv) {
753      command = execString.clone();
754      if (dir != null) {
755        setWorkingDirectory(dir);
756      }
757      if (env != null) {
758        setEnvironment(env);
759      }
760      timeOutInterval = timeout;
761      this.inheritParentEnv = inheritParentEnv;
762    }
763        
764
765    /** Execute the shell command. */
766    public void execute() throws IOException {
767      for (String s : command) {
768        if (s == null) {
769          throw new IOException("(null) entry in command string: "
770              + StringUtils.join(" ", command));
771        }
772      }
773      this.run();
774    }
775
776    @Override
777    public String[] getExecString() {
778      return command;
779    }
780
781    @Override
782    protected void parseExecResult(BufferedReader lines) throws IOException {
783      output = new StringBuffer();
784      char[] buf = new char[512];
785      int nRead;
786      while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) {
787        output.append(buf, 0, nRead);
788      }
789    }
790    
791    /** Get the output of the shell command.*/
792    public String getOutput() {
793      return (output == null) ? "" : output.toString();
794    }
795
796    /**
797     * Returns the commands of this instance.
798     * Arguments with spaces in are presented with quotes round; other
799     * arguments are presented raw
800     *
801     * @return a string representation of the object.
802     */
803    @Override
804    public String toString() {
805      StringBuilder builder = new StringBuilder();
806      String[] args = getExecString();
807      for (String s : args) {
808        if (s.indexOf(' ') >= 0) {
809          builder.append('"').append(s).append('"');
810        } else {
811          builder.append(s);
812        }
813        builder.append(' ');
814      }
815      return builder.toString();
816    }
817
818    @Override
819    public void close() {
820    }
821  }
822  
823  /**
824   * To check if the passed script to shell command executor timed out or
825   * not.
826   * 
827   * @return if the script timed out.
828   */
829  public boolean isTimedOut() {
830    return timedOut.get();
831  }
832  
833  /**
834   * Set if the command has timed out.
835   * 
836   */
837  private void setTimedOut() {
838    this.timedOut.set(true);
839  }
840  
841  /** 
842   * Static method to execute a shell command. 
843   * Covers most of the simple cases without requiring the user to implement  
844   * the <code>Shell</code> interface.
845   * @param cmd shell command to execute.
846   * @return the output of the executed command.
847   */
848  public static String execCommand(String ... cmd) throws IOException {
849    return execCommand(null, cmd, 0L);
850  }
851  
852  /** 
853   * Static method to execute a shell command. 
854   * Covers most of the simple cases without requiring the user to implement  
855   * the <code>Shell</code> interface.
856   * @param env the map of environment key=value
857   * @param cmd shell command to execute.
858   * @param timeout time in milliseconds after which script should be marked timeout
859   * @return the output of the executed command.o
860   */
861  
862  public static String execCommand(Map<String, String> env, String[] cmd,
863      long timeout) throws IOException {
864    ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, 
865                                                          timeout);
866    exec.execute();
867    return exec.getOutput();
868  }
869
870  /** 
871   * Static method to execute a shell command. 
872   * Covers most of the simple cases without requiring the user to implement  
873   * the <code>Shell</code> interface.
874   * @param env the map of environment key=value
875   * @param cmd shell command to execute.
876   * @return the output of the executed command.
877   */
878  public static String execCommand(Map<String,String> env, String ... cmd) 
879  throws IOException {
880    return execCommand(env, cmd, 0L);
881  }
882  
883  /**
884   * Timer which is used to timeout scripts spawned off by shell.
885   */
886  private static class ShellTimeoutTimerTask extends TimerTask {
887
888    private Shell shell;
889
890    public ShellTimeoutTimerTask(Shell shell) {
891      this.shell = shell;
892    }
893
894    @Override
895    public void run() {
896      Process p = shell.getProcess();
897      try {
898        p.exitValue();
899      } catch (Exception e) {
900        //Process has not terminated.
901        //So check if it has completed 
902        //if not just destroy it.
903        if (p != null && !shell.completed.get()) {
904          shell.setTimedOut();
905          p.destroy();
906        }
907      }
908    }
909  }
910}