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