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.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStreamReader;
025import java.io.InputStream;
026import java.io.InterruptedIOException;
027import java.nio.charset.Charset;
028import java.util.Arrays;
029import java.util.Map;
030import java.util.Timer;
031import java.util.TimerTask;
032import java.util.concurrent.atomic.AtomicBoolean;
033
034import com.google.common.annotations.VisibleForTesting;
035import org.apache.hadoop.classification.InterfaceAudience;
036import org.apache.hadoop.classification.InterfaceStability;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * A base class for running a Shell command.
042 *
043 * <code>Shell</code> can be used to run shell commands like <code>du</code> or
044 * <code>df</code>. It also offers facilities to gate commands by 
045 * time-intervals.
046 */
047@InterfaceAudience.Public
048@InterfaceStability.Evolving
049public abstract class Shell {
050  public static final Logger LOG = LoggerFactory.getLogger(Shell.class);
051
052  /**
053   * Text to include when there are windows-specific problems.
054   * {@value}
055   */
056  private static final String WINDOWS_PROBLEMS =
057      "https://wiki.apache.org/hadoop/WindowsProblems";
058
059  /**
060   * Name of the windows utils binary: {@value}.
061   */
062  static final String WINUTILS_EXE = "winutils.exe";
063
064  /**
065   * System property for the Hadoop home directory: {@value}.
066   */
067  public static final String SYSPROP_HADOOP_HOME_DIR = "hadoop.home.dir";
068
069  /**
070   * Environment variable for Hadoop's home dir: {@value}.
071   */
072  public static final String ENV_HADOOP_HOME = "HADOOP_HOME";
073
074  /**
075   * query to see if system is Java 7 or later.
076   * Now that Hadoop requires Java 7 or later, this always returns true.
077   * @deprecated This call isn't needed any more: please remove uses of it.
078   * @return true, always.
079   */
080  @Deprecated
081  public static boolean isJava7OrAbove() {
082    return true;
083  }
084
085  /**
086   * Maximum command line length in Windows
087   * KB830473 documents this as 8191
088   */
089  public static final int WINDOWS_MAX_SHELL_LENGTH = 8191;
090
091  /**
092   * mis-spelling of {@link #WINDOWS_MAX_SHELL_LENGTH}.
093   * @deprecated use the correctly spelled constant.
094   */
095  @Deprecated
096  public static final int WINDOWS_MAX_SHELL_LENGHT = WINDOWS_MAX_SHELL_LENGTH;
097
098  /**
099   * Checks if a given command (String[]) fits in the Windows maximum command
100   * line length Note that the input is expected to already include space
101   * delimiters, no extra count will be added for delimiters.
102   *
103   * @param commands command parts, including any space delimiters
104   */
105  public static void checkWindowsCommandLineLength(String...commands)
106      throws IOException {
107    int len = 0;
108    for (String s: commands) {
109      len += s.length();
110    }
111    if (len > WINDOWS_MAX_SHELL_LENGTH) {
112      throw new IOException(String.format(
113        "The command line has a length of %d exceeds maximum allowed length" +
114            " of %d. Command starts with: %s",
115        len, WINDOWS_MAX_SHELL_LENGTH,
116        StringUtils.join("", commands).substring(0, 100)));
117    }
118  }
119
120  /** a Unix command to get the current user's name: {@value}. */
121  public static final String USER_NAME_COMMAND = "whoami";
122
123  /** Windows <code>CreateProcess</code> synchronization object. */
124  public static final Object WindowsProcessLaunchLock = new Object();
125
126  // OSType detection
127
128  public enum OSType {
129    OS_TYPE_LINUX,
130    OS_TYPE_WIN,
131    OS_TYPE_SOLARIS,
132    OS_TYPE_MAC,
133    OS_TYPE_FREEBSD,
134    OS_TYPE_OTHER
135  }
136
137  /**
138   * Get the type of the operating system, as determined from parsing
139   * the <code>os.name</code> property.
140   */
141  public static final OSType osType = getOSType();
142
143  private static OSType getOSType() {
144    String osName = System.getProperty("os.name");
145    if (osName.startsWith("Windows")) {
146      return OSType.OS_TYPE_WIN;
147    } else if (osName.contains("SunOS") || osName.contains("Solaris")) {
148      return OSType.OS_TYPE_SOLARIS;
149    } else if (osName.contains("Mac")) {
150      return OSType.OS_TYPE_MAC;
151    } else if (osName.contains("FreeBSD")) {
152      return OSType.OS_TYPE_FREEBSD;
153    } else if (osName.startsWith("Linux")) {
154      return OSType.OS_TYPE_LINUX;
155    } else {
156      // Some other form of Unix
157      return OSType.OS_TYPE_OTHER;
158    }
159  }
160
161  // Helper static vars for each platform
162  public static final boolean WINDOWS = (osType == OSType.OS_TYPE_WIN);
163  public static final boolean SOLARIS = (osType == OSType.OS_TYPE_SOLARIS);
164  public static final boolean MAC     = (osType == OSType.OS_TYPE_MAC);
165  public static final boolean FREEBSD = (osType == OSType.OS_TYPE_FREEBSD);
166  public static final boolean LINUX   = (osType == OSType.OS_TYPE_LINUX);
167  public static final boolean OTHER   = (osType == OSType.OS_TYPE_OTHER);
168
169  public static final boolean PPC_64
170                = System.getProperties().getProperty("os.arch").contains("ppc64");
171
172  /** a Unix command to get the current user's groups list. */
173  public static String[] getGroupsCommand() {
174    return (WINDOWS)? new String[]{"cmd", "/c", "groups"}
175                    : new String[]{"bash", "-c", "groups"};
176  }
177
178  /**
179   * A command to get a given user's groups list.
180   * If the OS is not WINDOWS, the command will get the user's primary group
181   * first and finally get the groups list which includes the primary group.
182   * i.e. the user's primary group will be included twice.
183   */
184  public static String[] getGroupsForUserCommand(final String user) {
185    //'groups username' command return is inconsistent across different unixes
186    return WINDOWS ?
187      new String[]
188          {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""}
189      : new String[] {"bash", "-c", "id -gn " + user + "; id -Gn " + user};
190  }
191
192  /**
193   * A command to get a given user's group id list.
194   * The command will get the user's primary group
195   * first and finally get the groups list which includes the primary group.
196   * i.e. the user's primary group will be included twice.
197   * This command does not support Windows and will only return group names.
198   */
199  public static String[] getGroupsIDForUserCommand(final String user) {
200    //'groups username' command return is inconsistent across different unixes
201    return WINDOWS ?
202        new String[]
203            {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""}
204        : new String[] {"bash", "-c", "id -g " + user + "; id -G " + user};
205  }
206
207  /** A command to get a given netgroup's user list. */
208  public static String[] getUsersForNetgroupCommand(final String netgroup) {
209    //'groups username' command return is non-consistent across different unixes
210    return WINDOWS ? new String [] {"cmd", "/c", "getent netgroup " + netgroup}
211                    : new String [] {"bash", "-c", "getent netgroup " + netgroup};
212  }
213
214  /** Return a command to get permission information. */
215  public static String[] getGetPermissionCommand() {
216    return (WINDOWS) ? new String[] { getWinUtilsPath(), "ls", "-F" }
217                     : new String[] { "/bin/ls", "-ld" };
218  }
219
220  /** Return a command to set permission. */
221  public static String[] getSetPermissionCommand(String perm, boolean recursive) {
222    if (recursive) {
223      return (WINDOWS) ?
224          new String[] { getWinUtilsPath(), "chmod", "-R", perm }
225          : new String[] { "chmod", "-R", perm };
226    } else {
227      return (WINDOWS) ?
228          new String[] { getWinUtilsPath(), "chmod", perm }
229          : new String[] { "chmod", perm };
230    }
231  }
232
233  /**
234   * Return a command to set permission for specific file.
235   * 
236   * @param perm String permission to set
237   * @param recursive boolean true to apply to all sub-directories recursively
238   * @param file String file to set
239   * @return String[] containing command and arguments
240   */
241  public static String[] getSetPermissionCommand(String perm,
242      boolean recursive, String file) {
243    String[] baseCmd = getSetPermissionCommand(perm, recursive);
244    String[] cmdWithFile = Arrays.copyOf(baseCmd, baseCmd.length + 1);
245    cmdWithFile[cmdWithFile.length - 1] = file;
246    return cmdWithFile;
247  }
248
249  /** Return a command to set owner. */
250  public static String[] getSetOwnerCommand(String owner) {
251    return (WINDOWS) ?
252        new String[] { getWinUtilsPath(), "chown", "\"" + owner + "\"" }
253        : new String[] { "chown", owner };
254  }
255
256  /** Return a command to create symbolic links. */
257  public static String[] getSymlinkCommand(String target, String link) {
258    return WINDOWS ?
259       new String[] { getWinUtilsPath(), "symlink", link, target }
260       : new String[] { "ln", "-s", target, link };
261  }
262
263  /** Return a command to read the target of the a symbolic link. */
264  public static String[] getReadlinkCommand(String link) {
265    return WINDOWS ?
266        new String[] { getWinUtilsPath(), "readlink", link }
267        : new String[] { "readlink", link };
268  }
269
270  /**
271   * Return a command for determining if process with specified pid is alive.
272   * @param pid process ID
273   * @return a <code>kill -0</code> command or equivalent
274   */
275  public static String[] getCheckProcessIsAliveCommand(String pid) {
276    return getSignalKillCommand(0, pid);
277  }
278
279  /** Return a command to send a signal to a given pid. */
280  public static String[] getSignalKillCommand(int code, String pid) {
281    // Code == 0 means check alive
282    if (Shell.WINDOWS) {
283      if (0 == code) {
284        return new String[] {Shell.getWinUtilsPath(), "task", "isAlive", pid };
285      } else {
286        return new String[] {Shell.getWinUtilsPath(), "task", "kill", pid };
287      }
288    }
289
290    if (isSetsidAvailable) {
291      // Use the shell-builtin as it support "--" in all Hadoop supported OSes
292      return new String[] { "bash", "-c", "kill -" + code + " -- -" + pid };
293    } else {
294      return new String[] { "bash", "-c", "kill -" + code + " " + pid };
295    }
296  }
297
298  /** Regular expression for environment variables: {@value}. */
299  public static final String ENV_NAME_REGEX = "[A-Za-z_][A-Za-z0-9_]*";
300
301  /** Return a regular expression string that match environment variables. */
302  public static String getEnvironmentVariableRegex() {
303    return (WINDOWS)
304        ? "%(" + ENV_NAME_REGEX + "?)%"
305        : "\\$(" + ENV_NAME_REGEX + ")";
306  }
307
308  /**
309   * Returns a File referencing a script with the given basename, inside the
310   * given parent directory.  The file extension is inferred by platform:
311   * <code>".cmd"</code> on Windows, or <code>".sh"</code> otherwise.
312   * 
313   * @param parent File parent directory
314   * @param basename String script file basename
315   * @return File referencing the script in the directory
316   */
317  public static File appendScriptExtension(File parent, String basename) {
318    return new File(parent, appendScriptExtension(basename));
319  }
320
321  /**
322   * Returns a script file name with the given basename.
323   *
324   * The file extension is inferred by platform:
325   * <code>".cmd"</code> on Windows, or <code>".sh"</code> otherwise.
326   *
327   * @param basename String script file basename
328   * @return String script file name
329   */
330  public static String appendScriptExtension(String basename) {
331    return basename + (WINDOWS ? ".cmd" : ".sh");
332  }
333
334  /**
335   * Returns a command to run the given script.  The script interpreter is
336   * inferred by platform: cmd on Windows or bash otherwise.
337   *
338   * @param script File script to run
339   * @return String[] command to run the script
340   */
341  public static String[] getRunScriptCommand(File script) {
342    String absolutePath = script.getAbsolutePath();
343    return WINDOWS ?
344      new String[] { "cmd", "/c", absolutePath }
345      : new String[] { "/bin/bash", absolutePath };
346  }
347
348  /** a Unix command to set permission: {@value}. */
349  public static final String SET_PERMISSION_COMMAND = "chmod";
350  /** a Unix command to set owner: {@value}. */
351  public static final String SET_OWNER_COMMAND = "chown";
352
353  /** a Unix command to set the change user's groups list: {@value}. */
354  public static final String SET_GROUP_COMMAND = "chgrp";
355  /** a Unix command to create a link: {@value}. */
356  public static final String LINK_COMMAND = "ln";
357  /** a Unix command to get a link target: {@value}. */
358  public static final String READ_LINK_COMMAND = "readlink";
359
360  /**Time after which the executing script would be timedout. */
361  protected long timeOutInterval = 0L;
362  /** If or not script timed out*/
363  private final AtomicBoolean timedOut = new AtomicBoolean(false);
364
365  /**
366   *  Centralized logic to discover and validate the sanity of the Hadoop
367   *  home directory.
368   *
369   *  This does a lot of work so it should only be called
370   *  privately for initialization once per process.
371   *
372   * @return A directory that exists and via was specified on the command line
373   * via <code>-Dhadoop.home.dir</code> or the <code>HADOOP_HOME</code>
374   * environment variable.
375   * @throws FileNotFoundException if the properties are absent or the specified
376   * path is not a reference to a valid directory.
377   */
378  private static File checkHadoopHome() throws FileNotFoundException {
379
380    // first check the Dflag hadoop.home.dir with JVM scope
381    String home = System.getProperty(SYSPROP_HADOOP_HOME_DIR);
382
383    // fall back to the system/user-global env variable
384    if (home == null) {
385      home = System.getenv(ENV_HADOOP_HOME);
386    }
387    return checkHadoopHomeInner(home);
388  }
389
390  /*
391  A set of exception strings used to construct error messages;
392  these are referred to in tests
393  */
394  static final String E_DOES_NOT_EXIST = "does not exist";
395  static final String E_IS_RELATIVE = "is not an absolute path.";
396  static final String E_NOT_DIRECTORY = "is not a directory.";
397  static final String E_NO_EXECUTABLE = "Could not locate Hadoop executable";
398  static final String E_NOT_EXECUTABLE_FILE = "Not an executable file";
399  static final String E_HADOOP_PROPS_UNSET = ENV_HADOOP_HOME + " and "
400      + SYSPROP_HADOOP_HOME_DIR + " are unset.";
401  static final String E_HADOOP_PROPS_EMPTY = ENV_HADOOP_HOME + " or "
402      + SYSPROP_HADOOP_HOME_DIR + " set to an empty string";
403  static final String E_NOT_A_WINDOWS_SYSTEM = "Not a Windows system";
404
405  /**
406   *  Validate the accessibility of the Hadoop home directory.
407   *
408   * @return A directory that is expected to be the hadoop home directory
409   * @throws FileNotFoundException if the specified
410   * path is not a reference to a valid directory.
411   */
412  @VisibleForTesting
413  static File checkHadoopHomeInner(String home) throws FileNotFoundException {
414    // couldn't find either setting for hadoop's home directory
415    if (home == null) {
416      throw new FileNotFoundException(E_HADOOP_PROPS_UNSET);
417    }
418    // strip off leading and trailing double quotes
419    while (home.startsWith("\"")) {
420      home = home.substring(1);
421    }
422    while (home.endsWith("\"")) {
423      home = home.substring(0, home.length() - 1);
424    }
425
426    // after stripping any quotes, check for home dir being non-empty
427    if (home.isEmpty()) {
428      throw new FileNotFoundException(E_HADOOP_PROPS_EMPTY);
429    }
430
431    // check that the hadoop home dir value
432    // is an absolute reference to a directory
433    File homedir = new File(home);
434    if (!homedir.isAbsolute()) {
435      throw new FileNotFoundException("Hadoop home directory " + homedir
436          + " " + E_IS_RELATIVE);
437    }
438    if (!homedir.exists()) {
439      throw new FileNotFoundException("Hadoop home directory " + homedir
440          + " " + E_DOES_NOT_EXIST);
441    }
442    if (!homedir.isDirectory()) {
443      throw new FileNotFoundException("Hadoop home directory " + homedir
444          + " "+ E_NOT_DIRECTORY);
445    }
446    return homedir;
447  }
448
449  /**
450   * The Hadoop home directory.
451   */
452  private static final File HADOOP_HOME_FILE;
453
454  /**
455   * Rethrowable cause for the failure to determine the hadoop
456   * home directory
457   */
458  private static final IOException HADOOP_HOME_DIR_FAILURE_CAUSE;
459
460  static {
461    File home;
462    IOException ex;
463    try {
464      home = checkHadoopHome();
465      ex = null;
466    } catch (IOException ioe) {
467      if (LOG.isDebugEnabled()) {
468        LOG.debug("Failed to detect a valid hadoop home directory", ioe);
469      }
470      ex = ioe;
471      home = null;
472    }
473    HADOOP_HOME_FILE = home;
474    HADOOP_HOME_DIR_FAILURE_CAUSE = ex;
475  }
476
477  /**
478   * Optionally extend an error message with some OS-specific text.
479   * @param message core error message
480   * @return error message, possibly with some extra text
481   */
482  private static String addOsText(String message) {
483    return WINDOWS ? (message + " -see " + WINDOWS_PROBLEMS) : message;
484  }
485
486  /**
487   * Create a {@code FileNotFoundException} with the inner nested cause set
488   * to the given exception. Compensates for the fact that FNFE doesn't
489   * have an initializer that takes an exception.
490   * @param text error text
491   * @param ex inner exception
492   * @return a new exception to throw.
493   */
494  private static FileNotFoundException fileNotFoundException(String text,
495      Exception ex) {
496    return (FileNotFoundException) new FileNotFoundException(text)
497        .initCause(ex);
498  }
499
500  /**
501   * Get the Hadoop home directory. Raises an exception if not found
502   * @return the home dir
503   * @throws IOException if the home directory cannot be located.
504   */
505  public static String getHadoopHome() throws IOException {
506    return getHadoopHomeDir().getCanonicalPath();
507  }
508
509  /**
510   * Get the Hadoop home directory. If it is invalid,
511   * throw an exception.
512   * @return a path referring to hadoop home.
513   * @throws FileNotFoundException if the directory doesn't exist.
514   */
515  private static File getHadoopHomeDir() throws FileNotFoundException {
516    if (HADOOP_HOME_DIR_FAILURE_CAUSE != null) {
517      throw fileNotFoundException(
518          addOsText(HADOOP_HOME_DIR_FAILURE_CAUSE.toString()),
519          HADOOP_HOME_DIR_FAILURE_CAUSE);
520    }
521    return HADOOP_HOME_FILE;
522  }
523
524  /**
525   *  Fully qualify the path to a binary that should be in a known hadoop
526   *  bin location. This is primarily useful for disambiguating call-outs 
527   *  to executable sub-components of Hadoop to avoid clashes with other 
528   *  executables that may be in the path.  Caveat:  this call doesn't 
529   *  just format the path to the bin directory.  It also checks for file 
530   *  existence of the composed path. The output of this call should be 
531   *  cached by callers.
532   *
533   * @param executable executable
534   * @return executable file reference
535   * @throws FileNotFoundException if the path does not exist
536   */
537  public static File getQualifiedBin(String executable)
538      throws FileNotFoundException {
539    // construct hadoop bin path to the specified executable
540    return getQualifiedBinInner(getHadoopHomeDir(), executable);
541  }
542
543  /**
544   * Inner logic of {@link #getQualifiedBin(String)}, accessible
545   * for tests.
546   * @param hadoopHomeDir home directory (assumed to be valid)
547   * @param executable executable
548   * @return path to the binary
549   * @throws FileNotFoundException if the executable was not found/valid
550   */
551  static File getQualifiedBinInner(File hadoopHomeDir, String executable)
552      throws FileNotFoundException {
553    String binDirText = "Hadoop bin directory ";
554    File bin = new File(hadoopHomeDir, "bin");
555    if (!bin.exists()) {
556      throw new FileNotFoundException(addOsText(binDirText + E_DOES_NOT_EXIST
557          + ": " + bin));
558    }
559    if (!bin.isDirectory()) {
560      throw new FileNotFoundException(addOsText(binDirText + E_NOT_DIRECTORY
561          + ": " + bin));
562    }
563
564    File exeFile = new File(bin, executable);
565    if (!exeFile.exists()) {
566      throw new FileNotFoundException(
567          addOsText(E_NO_EXECUTABLE + ": " + exeFile));
568    }
569    if (!exeFile.isFile()) {
570      throw new FileNotFoundException(
571          addOsText(E_NOT_EXECUTABLE_FILE + ": " + exeFile));
572    }
573    try {
574      return exeFile.getCanonicalFile();
575    } catch (IOException e) {
576      // this isn't going to happen, because of all the upfront checks.
577      // so if it does, it gets converted to a FNFE and rethrown
578      throw fileNotFoundException(e.toString(), e);
579    }
580  }
581
582  /**
583   *  Fully qualify the path to a binary that should be in a known hadoop
584   *  bin location. This is primarily useful for disambiguating call-outs
585   *  to executable sub-components of Hadoop to avoid clashes with other
586   *  executables that may be in the path.  Caveat:  this call doesn't
587   *  just format the path to the bin directory.  It also checks for file
588   *  existence of the composed path. The output of this call should be
589   *  cached by callers.
590   *
591   * @param executable executable
592   * @return executable file reference
593   * @throws FileNotFoundException if the path does not exist
594   * @throws IOException on path canonicalization failures
595   */
596  public static String getQualifiedBinPath(String executable)
597      throws IOException {
598    return getQualifiedBin(executable).getCanonicalPath();
599  }
600
601  /**
602   * Location of winutils as a string; null if not found.
603   * <p>
604   * <i>Important: caller must check for this value being null</i>.
605   * The lack of such checks has led to many support issues being raised.
606   * <p>
607   * @deprecated use one of the exception-raising getter methods,
608   * specifically {@link #getWinUtilsPath()} or {@link #getWinUtilsFile()}
609   */
610  @Deprecated
611  public static final String WINUTILS;
612
613  /** Canonical path to winutils, private to Shell. */
614  private static final String WINUTILS_PATH;
615
616  /** file reference to winutils. */
617  private static final File WINUTILS_FILE;
618
619  /** the exception raised on a failure to init the WINUTILS fields. */
620  private static final IOException WINUTILS_FAILURE;
621
622  /*
623   * Static WINUTILS_* field initializer.
624   * On non-Windows systems sets the paths to null, and
625   * adds a specific exception to the failure cause, so
626   * that on any attempt to resolve the paths will raise
627   * a meaningful exception.
628   */
629  static {
630    IOException ioe = null;
631    String path = null;
632    File file = null;
633    // invariant: either there's a valid file and path,
634    // or there is a cached IO exception.
635    if (WINDOWS) {
636      try {
637        file = getQualifiedBin(WINUTILS_EXE);
638        path = file.getCanonicalPath();
639        ioe = null;
640      } catch (IOException e) {
641        LOG.warn("Did not find {}: {}", WINUTILS_EXE, e);
642        // stack trace comes at debug level
643        LOG.debug("Failed to find " + WINUTILS_EXE, e);
644        file = null;
645        path = null;
646        ioe = e;
647      }
648    } else {
649      // on a non-windows system, the invariant is kept
650      // by adding an explicit exception.
651      ioe = new FileNotFoundException(E_NOT_A_WINDOWS_SYSTEM);
652    }
653    WINUTILS_PATH = path;
654    WINUTILS_FILE = file;
655
656    WINUTILS = path;
657    WINUTILS_FAILURE = ioe;
658  }
659
660  /**
661   * Predicate to indicate whether or not the path to winutils is known.
662   *
663   * If true, then {@link #WINUTILS} is non-null, and both
664   * {@link #getWinUtilsPath()} and {@link #getWinUtilsFile()}
665   * will successfully return this value. Always false on non-windows systems.
666   * @return true if there is a valid path to the binary
667   */
668  public static boolean hasWinutilsPath() {
669    return WINUTILS_PATH != null;
670  }
671
672  /**
673   * Locate the winutils binary, or fail with a meaningful
674   * exception and stack trace as an RTE.
675   * This method is for use in methods which don't explicitly throw
676   * an <code>IOException</code>.
677   * @return the path to {@link #WINUTILS_EXE}
678   * @throws RuntimeException if the path is not resolvable
679   */
680  public static String getWinUtilsPath() {
681    if (WINUTILS_FAILURE == null) {
682      return WINUTILS_PATH;
683    } else {
684      throw new RuntimeException(WINUTILS_FAILURE.toString(),
685          WINUTILS_FAILURE);
686    }
687  }
688
689  /**
690   * Get a file reference to winutils.
691   * Always raises an exception if there isn't one
692   * @return the file instance referring to the winutils bin.
693   * @throws FileNotFoundException on any failure to locate that file.
694   */
695  public static File getWinUtilsFile() throws FileNotFoundException {
696    if (WINUTILS_FAILURE == null) {
697      return WINUTILS_FILE;
698    } else {
699      // raise a new exception to generate a new stack trace
700      throw fileNotFoundException(WINUTILS_FAILURE.toString(),
701          WINUTILS_FAILURE);
702    }
703  }
704
705  public static final boolean isBashSupported = checkIsBashSupported();
706  private static boolean checkIsBashSupported() {
707    if (Shell.WINDOWS) {
708      return false;
709    }
710
711    ShellCommandExecutor shexec;
712    boolean supported = true;
713    try {
714      String[] args = {"bash", "-c", "echo 1000"};
715      shexec = new ShellCommandExecutor(args);
716      shexec.execute();
717    } catch (IOException ioe) {
718      LOG.warn("Bash is not supported by the OS", ioe);
719      supported = false;
720    }
721
722    return supported;
723  }
724
725  /**
726   * Flag which is true if setsid exists.
727   */
728  public static final boolean isSetsidAvailable = isSetsidSupported();
729
730  /**
731   * Look for <code>setsid</code>.
732   * @return true if <code>setsid</code> was present
733   */
734  private static boolean isSetsidSupported() {
735    if (Shell.WINDOWS) {
736      return false;
737    }
738    ShellCommandExecutor shexec = null;
739    boolean setsidSupported = true;
740    try {
741      String[] args = {"setsid", "bash", "-c", "echo $$"};
742      shexec = new ShellCommandExecutor(args);
743      shexec.execute();
744    } catch (IOException ioe) {
745      LOG.debug("setsid is not available on this machine. So not using it.");
746      setsidSupported = false;
747    }  catch (Error err) {
748      if (err.getMessage() != null
749          && err.getMessage().contains("posix_spawn is not " +
750          "a supported process launch mechanism")
751          && (Shell.FREEBSD || Shell.MAC)) {
752        // HADOOP-11924: This is a workaround to avoid failure of class init
753        // by JDK issue on TR locale(JDK-8047340).
754        LOG.info("Avoiding JDK-8047340 on BSD-based systems.", err);
755        setsidSupported = false;
756      }
757    }  finally { // handle the exit code
758      if (LOG.isDebugEnabled()) {
759        LOG.debug("setsid exited with exit code "
760                 + (shexec != null ? shexec.getExitCode() : "(null executor)"));
761      }
762    }
763    return setsidSupported;
764  }
765
766  /** Token separator regex used to parse Shell tool outputs. */
767  public static final String TOKEN_SEPARATOR_REGEX
768                = WINDOWS ? "[|\n\r]" : "[ \t\n\r\f]";
769
770  private long interval;   // refresh interval in msec
771  private long lastTime;   // last time the command was performed
772  private final boolean redirectErrorStream; // merge stdout and stderr
773  private Map<String, String> environment; // env for the command execution
774  private File dir;
775  private Process process; // sub process used to execute the command
776  private int exitCode;
777
778  /** Flag to indicate whether or not the script has finished executing. */
779  private final AtomicBoolean completed = new AtomicBoolean(false);
780
781  /**
782   * Create an instance with no minimum interval between runs; stderr is
783   * not merged with stdout.
784   */
785  protected Shell() {
786    this(0L);
787  }
788
789  /**
790   * Create an instance with a minimum interval between executions; stderr is
791   * not merged with stdout.
792   * @param interval interval in milliseconds between command executions.
793   */
794  protected Shell(long interval) {
795    this(interval, false);
796  }
797
798  /**
799   * Create a shell instance which can be re-executed when the {@link #run()}
800   * method is invoked with a given elapsed time between calls.
801   *
802   * @param interval the minimum duration in milliseconds to wait before
803   *        re-executing the command. If set to 0, there is no minimum.
804   * @param redirectErrorStream should the error stream be merged with
805   *        the normal output stream?
806   */
807  protected Shell(long interval, boolean redirectErrorStream) {
808    this.interval = interval;
809    this.lastTime = (interval < 0) ? 0 : -interval;
810    this.redirectErrorStream = redirectErrorStream;
811  }
812
813  /**
814   * Set the environment for the command.
815   * @param env Mapping of environment variables
816   */
817  protected void setEnvironment(Map<String, String> env) {
818    this.environment = env;
819  }
820
821  /**
822   * Set the working directory.
823   * @param dir The directory where the command will be executed
824   */
825  protected void setWorkingDirectory(File dir) {
826    this.dir = dir;
827  }
828
829  /** Check to see if a command needs to be executed and execute if needed. */
830  protected void run() throws IOException {
831    if (lastTime + interval > Time.monotonicNow()) {
832      return;
833    }
834    exitCode = 0; // reset for next run
835    if (Shell.MAC) {
836      System.setProperty("jdk.lang.Process.launchMechanism", "POSIX_SPAWN");
837    }
838    runCommand();
839  }
840
841  /** Run the command. */
842  private void runCommand() throws IOException { 
843    ProcessBuilder builder = new ProcessBuilder(getExecString());
844    Timer timeOutTimer = null;
845    ShellTimeoutTimerTask timeoutTimerTask = null;
846    timedOut.set(false);
847    completed.set(false);
848
849    if (environment != null) {
850      builder.environment().putAll(this.environment);
851    }
852    if (dir != null) {
853      builder.directory(this.dir);
854    }
855
856    builder.redirectErrorStream(redirectErrorStream);
857    
858    if (Shell.WINDOWS) {
859      synchronized (WindowsProcessLaunchLock) {
860        // To workaround the race condition issue with child processes
861        // inheriting unintended handles during process launch that can
862        // lead to hangs on reading output and error streams, we
863        // serialize process creation. More info available at:
864        // http://support.microsoft.com/kb/315939
865        process = builder.start();
866      }
867    } else {
868      process = builder.start();
869    }
870
871    if (timeOutInterval > 0) {
872      timeOutTimer = new Timer("Shell command timeout");
873      timeoutTimerTask = new ShellTimeoutTimerTask(
874          this);
875      //One time scheduling.
876      timeOutTimer.schedule(timeoutTimerTask, timeOutInterval);
877    }
878    final BufferedReader errReader = 
879            new BufferedReader(new InputStreamReader(
880                process.getErrorStream(), Charset.defaultCharset()));
881    BufferedReader inReader =
882            new BufferedReader(new InputStreamReader(
883                process.getInputStream(), Charset.defaultCharset()));
884    final StringBuffer errMsg = new StringBuffer();
885
886    // read error and input streams as this would free up the buffers
887    // free the error stream buffer
888    Thread errThread = new Thread() {
889      @Override
890      public void run() {
891        try {
892          String line = errReader.readLine();
893          while((line != null) && !isInterrupted()) {
894            errMsg.append(line);
895            errMsg.append(System.getProperty("line.separator"));
896            line = errReader.readLine();
897          }
898        } catch(IOException ioe) {
899          LOG.warn("Error reading the error stream", ioe);
900        }
901      }
902    };
903    try {
904      errThread.start();
905    } catch (IllegalStateException ise) {
906    } catch (OutOfMemoryError oe) {
907      LOG.error("Caught " + oe + ". One possible reason is that ulimit"
908          + " setting of 'max user processes' is too low. If so, do"
909          + " 'ulimit -u <largerNum>' and try again.");
910      throw oe;
911    }
912    try {
913      parseExecResult(inReader); // parse the output
914      // clear the input stream buffer
915      String line = inReader.readLine();
916      while(line != null) { 
917        line = inReader.readLine();
918      }
919      // wait for the process to finish and check the exit code
920      exitCode  = process.waitFor();
921      // make sure that the error thread exits
922      joinThread(errThread);
923      completed.set(true);
924      //the timeout thread handling
925      //taken care in finally block
926      if (exitCode != 0) {
927        throw new ExitCodeException(exitCode, errMsg.toString());
928      }
929    } catch (InterruptedException ie) {
930      InterruptedIOException iie = new InterruptedIOException(ie.toString());
931      iie.initCause(ie);
932      throw iie;
933    } finally {
934      if (timeOutTimer != null) {
935        timeOutTimer.cancel();
936      }
937      // close the input stream
938      try {
939        // JDK 7 tries to automatically drain the input streams for us
940        // when the process exits, but since close is not synchronized,
941        // it creates a race if we close the stream first and the same
942        // fd is recycled.  the stream draining thread will attempt to
943        // drain that fd!!  it may block, OOM, or cause bizarre behavior
944        // see: https://bugs.openjdk.java.net/browse/JDK-8024521
945        //      issue is fixed in build 7u60
946        InputStream stdout = process.getInputStream();
947        synchronized (stdout) {
948          inReader.close();
949        }
950      } catch (IOException ioe) {
951        LOG.warn("Error while closing the input stream", ioe);
952      }
953      if (!completed.get()) {
954        errThread.interrupt();
955        joinThread(errThread);
956      }
957      try {
958        InputStream stderr = process.getErrorStream();
959        synchronized (stderr) {
960          errReader.close();
961        }
962      } catch (IOException ioe) {
963        LOG.warn("Error while closing the error stream", ioe);
964      }
965      process.destroy();
966      lastTime = Time.monotonicNow();
967    }
968  }
969
970  private static void joinThread(Thread t) {
971    while (t.isAlive()) {
972      try {
973        t.join();
974      } catch (InterruptedException ie) {
975        if (LOG.isWarnEnabled()) {
976          LOG.warn("Interrupted while joining on: " + t, ie);
977        }
978        t.interrupt(); // propagate interrupt
979      }
980    }
981  }
982
983  /** return an array containing the command name and its parameters. */
984  protected abstract String[] getExecString();
985
986  /** Parse the execution result */
987  protected abstract void parseExecResult(BufferedReader lines)
988  throws IOException;
989
990  /**
991   * Get an environment variable.
992   * @param env the environment var
993   * @return the value or null if it was unset.
994   */
995  public String getEnvironment(String env) {
996    return environment.get(env);
997  }
998
999  /** get the current sub-process executing the given command.
1000   * @return process executing the command
1001   */
1002  public Process getProcess() {
1003    return process;
1004  }
1005
1006  /** get the exit code.
1007   * @return the exit code of the process
1008   */
1009  public int getExitCode() {
1010    return exitCode;
1011  }
1012
1013  /**
1014   * This is an IOException with exit code added.
1015   */
1016  public static class ExitCodeException extends IOException {
1017    private final int exitCode;
1018
1019    public ExitCodeException(int exitCode, String message) {
1020      super(message);
1021      this.exitCode = exitCode;
1022    }
1023
1024    public int getExitCode() {
1025      return exitCode;
1026    }
1027
1028    @Override
1029    public String toString() {
1030      final StringBuilder sb =
1031          new StringBuilder("ExitCodeException ");
1032      sb.append("exitCode=").append(exitCode)
1033        .append(": ");
1034      sb.append(super.getMessage());
1035      return sb.toString();
1036    }
1037  }
1038
1039  public interface CommandExecutor {
1040
1041    void execute() throws IOException;
1042
1043    int getExitCode() throws IOException;
1044
1045    String getOutput() throws IOException;
1046
1047    void close();
1048
1049  }
1050
1051  /**
1052   * A simple shell command executor.
1053   * 
1054   * <code>ShellCommandExecutor</code>should be used in cases where the output
1055   * of the command needs no explicit parsing and where the command, working
1056   * directory and the environment remains unchanged. The output of the command
1057   * is stored as-is and is expected to be small.
1058   */
1059  public static class ShellCommandExecutor extends Shell 
1060      implements CommandExecutor {
1061
1062    private String[] command;
1063    private StringBuffer output;
1064
1065
1066    public ShellCommandExecutor(String[] execString) {
1067      this(execString, null);
1068    }
1069
1070    public ShellCommandExecutor(String[] execString, File dir) {
1071      this(execString, dir, null);
1072    }
1073
1074    public ShellCommandExecutor(String[] execString, File dir,
1075                                 Map<String, String> env) {
1076      this(execString, dir, env , 0L);
1077    }
1078
1079    /**
1080     * Create a new instance of the ShellCommandExecutor to execute a command.
1081     * 
1082     * @param execString The command to execute with arguments
1083     * @param dir If not-null, specifies the directory which should be set
1084     *            as the current working directory for the command.
1085     *            If null, the current working directory is not modified.
1086     * @param env If not-null, environment of the command will include the
1087     *            key-value pairs specified in the map. If null, the current
1088     *            environment is not modified.
1089     * @param timeout Specifies the time in milliseconds, after which the
1090     *                command will be killed and the status marked as timed-out.
1091     *                If 0, the command will not be timed out. 
1092     */
1093    public ShellCommandExecutor(String[] execString, File dir, 
1094        Map<String, String> env, long timeout) {
1095      command = execString.clone();
1096      if (dir != null) {
1097        setWorkingDirectory(dir);
1098      }
1099      if (env != null) {
1100        setEnvironment(env);
1101      }
1102      timeOutInterval = timeout;
1103    }
1104
1105    /**
1106     * Execute the shell command.
1107     * @throws IOException if the command fails, or if the command is
1108     * not well constructed.
1109     */
1110    public void execute() throws IOException {
1111      for (String s : command) {
1112        if (s == null) {
1113          throw new IOException("(null) entry in command string: "
1114              + StringUtils.join(" ", command));
1115        }
1116      }
1117      this.run();    
1118    }
1119
1120    @Override
1121    public String[] getExecString() {
1122      return command;
1123    }
1124
1125    @Override
1126    protected void parseExecResult(BufferedReader lines) throws IOException {
1127      output = new StringBuffer();
1128      char[] buf = new char[512];
1129      int nRead;
1130      while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) {
1131        output.append(buf, 0, nRead);
1132      }
1133    }
1134
1135    /** Get the output of the shell command. */
1136    public String getOutput() {
1137      return (output == null) ? "" : output.toString();
1138    }
1139
1140    /**
1141     * Returns the commands of this instance.
1142     * Arguments with spaces in are presented with quotes round; other
1143     * arguments are presented raw
1144     *
1145     * @return a string representation of the object.
1146     */
1147    @Override
1148    public String toString() {
1149      StringBuilder builder = new StringBuilder();
1150      String[] args = getExecString();
1151      for (String s : args) {
1152        if (s.indexOf(' ') >= 0) {
1153          builder.append('"').append(s).append('"');
1154        } else {
1155          builder.append(s);
1156        }
1157        builder.append(' ');
1158      }
1159      return builder.toString();
1160    }
1161
1162    @Override
1163    public void close() {
1164    }
1165  }
1166
1167  /**
1168   * To check if the passed script to shell command executor timed out or
1169   * not.
1170   * 
1171   * @return if the script timed out.
1172   */
1173  public boolean isTimedOut() {
1174    return timedOut.get();
1175  }
1176
1177  /**
1178   * Declare that the command has timed out.
1179   * 
1180   */
1181  private void setTimedOut() {
1182    this.timedOut.set(true);
1183  }
1184
1185  /** 
1186   * Static method to execute a shell command. 
1187   * Covers most of the simple cases without requiring the user to implement  
1188   * the <code>Shell</code> interface.
1189   * @param cmd shell command to execute.
1190   * @return the output of the executed command.
1191   */
1192  public static String execCommand(String ... cmd) throws IOException {
1193    return execCommand(null, cmd, 0L);
1194  }
1195
1196  /**
1197   * Static method to execute a shell command.
1198   * Covers most of the simple cases without requiring the user to implement
1199   * the <code>Shell</code> interface.
1200   * @param env the map of environment key=value
1201   * @param cmd shell command to execute.
1202   * @param timeout time in milliseconds after which script should be marked timeout
1203   * @return the output of the executed command.
1204   * @throws IOException on any problem.
1205   */
1206
1207  public static String execCommand(Map<String, String> env, String[] cmd,
1208      long timeout) throws IOException {
1209    ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, 
1210                                                          timeout);
1211    exec.execute();
1212    return exec.getOutput();
1213  }
1214
1215  /**
1216   * Static method to execute a shell command.
1217   * Covers most of the simple cases without requiring the user to implement
1218   * the <code>Shell</code> interface.
1219   * @param env the map of environment key=value
1220   * @param cmd shell command to execute.
1221   * @return the output of the executed command.
1222   * @throws IOException on any problem.
1223   */
1224  public static String execCommand(Map<String,String> env, String ... cmd)
1225  throws IOException {
1226    return execCommand(env, cmd, 0L);
1227  }
1228
1229  /**
1230   * Timer which is used to timeout scripts spawned off by shell.
1231   */
1232  private static class ShellTimeoutTimerTask extends TimerTask {
1233
1234    private final Shell shell;
1235
1236    public ShellTimeoutTimerTask(Shell shell) {
1237      this.shell = shell;
1238    }
1239
1240    @Override
1241    public void run() {
1242      Process p = shell.getProcess();
1243      try {
1244        p.exitValue();
1245      } catch (Exception e) {
1246        //Process has not terminated.
1247        //So check if it has completed 
1248        //if not just destroy it.
1249        if (p != null && !shell.completed.get()) {
1250          shell.setTimedOut();
1251          p.destroy();
1252        }
1253      }
1254    }
1255  }
1256}