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}