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