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