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