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 */ 018 package org.apache.hadoop.util; 019 020 import java.io.BufferedReader; 021 import java.io.File; 022 import java.io.IOException; 023 import java.io.InputStreamReader; 024 import java.util.Arrays; 025 import java.util.Map; 026 import java.util.Timer; 027 import java.util.TimerTask; 028 import java.util.concurrent.atomic.AtomicBoolean; 029 030 import org.apache.commons.logging.Log; 031 import org.apache.commons.logging.LogFactory; 032 import org.apache.hadoop.classification.InterfaceAudience; 033 import org.apache.hadoop.classification.InterfaceStability; 034 035 /** 036 * A base class for running a Unix command. 037 * 038 * <code>Shell</code> can be used to run unix commands like <code>du</code> or 039 * <code>df</code>. It also offers facilities to gate commands by 040 * time-intervals. 041 */ 042 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) 043 @InterfaceStability.Unstable 044 abstract public class Shell { 045 046 public static final Log LOG = LogFactory.getLog(Shell.class); 047 048 private static boolean IS_JAVA7_OR_ABOVE = 049 System.getProperty("java.version").substring(0, 3).compareTo("1.7") >= 0; 050 051 public static boolean isJava7OrAbove() { 052 return IS_JAVA7_OR_ABOVE; 053 } 054 055 /** a Unix command to get the current user's name */ 056 public final static String USER_NAME_COMMAND = "whoami"; 057 058 /** Windows CreateProcess synchronization object */ 059 public static final Object WindowsProcessLaunchLock = new Object(); 060 061 /** a Unix command to get the current user's groups list */ 062 public static String[] getGroupsCommand() { 063 return (WINDOWS)? new String[]{"cmd", "/c", "groups"} 064 : new String[]{"bash", "-c", "groups"}; 065 } 066 067 /** a Unix command to get a given user's groups list */ 068 public static String[] getGroupsForUserCommand(final String user) { 069 //'groups username' command return is non-consistent across different unixes 070 return (WINDOWS)? new String[] { WINUTILS, "groups", "-F", "\"" + user + "\""} 071 : new String [] {"bash", "-c", "id -Gn " + user}; 072 } 073 074 /** a Unix command to get a given netgroup's user list */ 075 public static String[] getUsersForNetgroupCommand(final String netgroup) { 076 //'groups username' command return is non-consistent across different unixes 077 return (WINDOWS)? new String [] {"cmd", "/c", "getent netgroup " + netgroup} 078 : new String [] {"bash", "-c", "getent netgroup " + netgroup}; 079 } 080 081 /** Return a command to get permission information. */ 082 public static String[] getGetPermissionCommand() { 083 return (WINDOWS) ? new String[] { WINUTILS, "ls", "-F" } 084 : new String[] { "/bin/ls", "-ld" }; 085 } 086 087 /** Return a command to set permission */ 088 public static String[] getSetPermissionCommand(String perm, boolean recursive) { 089 if (recursive) { 090 return (WINDOWS) ? new String[] { WINUTILS, "chmod", "-R", perm } 091 : new String[] { "chmod", "-R", perm }; 092 } else { 093 return (WINDOWS) ? new String[] { WINUTILS, "chmod", perm } 094 : new String[] { "chmod", perm }; 095 } 096 } 097 098 /** 099 * Return a command to set permission for specific file. 100 * 101 * @param perm String permission to set 102 * @param recursive boolean true to apply to all sub-directories recursively 103 * @param file String file to set 104 * @return String[] containing command and arguments 105 */ 106 public static String[] getSetPermissionCommand(String perm, boolean recursive, 107 String file) { 108 String[] baseCmd = getSetPermissionCommand(perm, recursive); 109 String[] cmdWithFile = Arrays.copyOf(baseCmd, baseCmd.length + 1); 110 cmdWithFile[cmdWithFile.length - 1] = file; 111 return cmdWithFile; 112 } 113 114 /** Return a command to set owner */ 115 public static String[] getSetOwnerCommand(String owner) { 116 return (WINDOWS) ? new String[] { WINUTILS, "chown", "\"" + owner + "\"" } 117 : new String[] { "chown", owner }; 118 } 119 120 /** Return a command to create symbolic links */ 121 public static String[] getSymlinkCommand(String target, String link) { 122 return WINDOWS ? new String[] { WINUTILS, "symlink", link, target } 123 : new String[] { "ln", "-s", target, link }; 124 } 125 126 /** Return a command for determining if process with specified pid is alive. */ 127 public static String[] getCheckProcessIsAliveCommand(String pid) { 128 return Shell.WINDOWS ? 129 new String[] { Shell.WINUTILS, "task", "isAlive", pid } : 130 new String[] { "kill", "-0", isSetsidAvailable ? "-" + pid : pid }; 131 } 132 133 /** Return a command to send a signal to a given pid */ 134 public static String[] getSignalKillCommand(int code, String pid) { 135 return Shell.WINDOWS ? new String[] { Shell.WINUTILS, "task", "kill", pid } : 136 new String[] { "kill", "-" + code, isSetsidAvailable ? "-" + pid : pid }; 137 } 138 139 /** Return a regular expression string that match environment variables */ 140 public static String getEnvironmentVariableRegex() { 141 return (WINDOWS) ? "%([A-Za-z_][A-Za-z0-9_]*?)%" : 142 "\\$([A-Za-z_][A-Za-z0-9_]*)"; 143 } 144 145 /** 146 * Returns a File referencing a script with the given basename, inside the 147 * given parent directory. The file extension is inferred by platform: ".cmd" 148 * on Windows, or ".sh" otherwise. 149 * 150 * @param parent File parent directory 151 * @param basename String script file basename 152 * @return File referencing the script in the directory 153 */ 154 public static File appendScriptExtension(File parent, String basename) { 155 return new File(parent, appendScriptExtension(basename)); 156 } 157 158 /** 159 * Returns a script file name with the given basename. The file extension is 160 * inferred by platform: ".cmd" on Windows, or ".sh" otherwise. 161 * 162 * @param basename String script file basename 163 * @return String script file name 164 */ 165 public static String appendScriptExtension(String basename) { 166 return basename + (WINDOWS ? ".cmd" : ".sh"); 167 } 168 169 /** 170 * Returns a command to run the given script. The script interpreter is 171 * inferred by platform: cmd on Windows or bash otherwise. 172 * 173 * @param script File script to run 174 * @return String[] command to run the script 175 */ 176 public static String[] getRunScriptCommand(File script) { 177 String absolutePath = script.getAbsolutePath(); 178 return WINDOWS ? new String[] { "cmd", "/c", absolutePath } : 179 new String[] { "/bin/bash", absolutePath }; 180 } 181 182 /** a Unix command to set permission */ 183 public static final String SET_PERMISSION_COMMAND = "chmod"; 184 /** a Unix command to set owner */ 185 public static final String SET_OWNER_COMMAND = "chown"; 186 187 /** a Unix command to set the change user's groups list */ 188 public static final String SET_GROUP_COMMAND = "chgrp"; 189 /** a Unix command to create a link */ 190 public static final String LINK_COMMAND = "ln"; 191 /** a Unix command to get a link target */ 192 public static final String READ_LINK_COMMAND = "readlink"; 193 194 /**Time after which the executing script would be timedout*/ 195 protected long timeOutInterval = 0L; 196 /** If or not script timed out*/ 197 private AtomicBoolean timedOut; 198 199 200 /** Centralized logic to discover and validate the sanity of the Hadoop 201 * home directory. Returns either NULL or a directory that exists and 202 * was specified via either -Dhadoop.home.dir or the HADOOP_HOME ENV 203 * variable. This does a lot of work so it should only be called 204 * privately for initialization once per process. 205 **/ 206 private static String checkHadoopHome() { 207 208 // first check the Dflag hadoop.home.dir with JVM scope 209 String home = System.getProperty("hadoop.home.dir"); 210 211 // fall back to the system/user-global env variable 212 if (home == null) { 213 home = System.getenv("HADOOP_HOME"); 214 } 215 216 try { 217 // couldn't find either setting for hadoop's home directory 218 if (home == null) { 219 throw new IOException("HADOOP_HOME or hadoop.home.dir are not set."); 220 } 221 222 if (home.startsWith("\"") && home.endsWith("\"")) { 223 home = home.substring(1, home.length()-1); 224 } 225 226 // check that the home setting is actually a directory that exists 227 File homedir = new File(home); 228 if (!homedir.isAbsolute() || !homedir.exists() || !homedir.isDirectory()) { 229 throw new IOException("Hadoop home directory " + homedir 230 + " does not exist, is not a directory, or is not an absolute path."); 231 } 232 233 home = homedir.getCanonicalPath(); 234 235 } catch (IOException ioe) { 236 if (LOG.isDebugEnabled()) { 237 LOG.debug("Failed to detect a valid hadoop home directory", ioe); 238 } 239 home = null; 240 } 241 242 return home; 243 } 244 private static String HADOOP_HOME_DIR = checkHadoopHome(); 245 246 // Public getter, throws an exception if HADOOP_HOME failed validation 247 // checks and is being referenced downstream. 248 public static final String getHadoopHome() throws IOException { 249 if (HADOOP_HOME_DIR == null) { 250 throw new IOException("Misconfigured HADOOP_HOME cannot be referenced."); 251 } 252 253 return HADOOP_HOME_DIR; 254 } 255 256 /** fully qualify the path to a binary that should be in a known hadoop 257 * bin location. This is primarily useful for disambiguating call-outs 258 * to executable sub-components of Hadoop to avoid clashes with other 259 * executables that may be in the path. Caveat: this call doesn't 260 * just format the path to the bin directory. It also checks for file 261 * existence of the composed path. The output of this call should be 262 * cached by callers. 263 * */ 264 public static final String getQualifiedBinPath(String executable) 265 throws IOException { 266 // construct hadoop bin path to the specified executable 267 String fullExeName = HADOOP_HOME_DIR + File.separator + "bin" 268 + File.separator + executable; 269 270 File exeFile = new File(fullExeName); 271 if (!exeFile.exists()) { 272 throw new IOException("Could not locate executable " + fullExeName 273 + " in the Hadoop binaries."); 274 } 275 276 return exeFile.getCanonicalPath(); 277 } 278 279 /** Set to true on Windows platforms */ 280 public static final boolean WINDOWS /* borrowed from Path.WINDOWS */ 281 = System.getProperty("os.name").startsWith("Windows"); 282 283 public static final boolean LINUX 284 = System.getProperty("os.name").startsWith("Linux"); 285 286 /** a Windows utility to emulate Unix commands */ 287 public static final String WINUTILS = getWinUtilsPath(); 288 289 public static final String getWinUtilsPath() { 290 String winUtilsPath = null; 291 292 try { 293 if (WINDOWS) { 294 winUtilsPath = getQualifiedBinPath("winutils.exe"); 295 } 296 } catch (IOException ioe) { 297 LOG.error("Failed to locate the winutils binary in the hadoop binary path", 298 ioe); 299 } 300 301 return winUtilsPath; 302 } 303 304 public static final boolean isSetsidAvailable = isSetsidSupported(); 305 private static boolean isSetsidSupported() { 306 if (Shell.WINDOWS) { 307 return false; 308 } 309 ShellCommandExecutor shexec = null; 310 boolean setsidSupported = true; 311 try { 312 String[] args = {"setsid", "bash", "-c", "echo $$"}; 313 shexec = new ShellCommandExecutor(args); 314 shexec.execute(); 315 } catch (IOException ioe) { 316 LOG.debug("setsid is not available on this machine. So not using it."); 317 setsidSupported = false; 318 } finally { // handle the exit code 319 if (LOG.isDebugEnabled()) { 320 LOG.debug("setsid exited with exit code " 321 + (shexec != null ? shexec.getExitCode() : "(null executor)")); 322 } 323 } 324 return setsidSupported; 325 } 326 327 /** Token separator regex used to parse Shell tool outputs */ 328 public static final String TOKEN_SEPARATOR_REGEX 329 = WINDOWS ? "[|\n\r]" : "[ \t\n\r\f]"; 330 331 private long interval; // refresh interval in msec 332 private long lastTime; // last time the command was performed 333 private Map<String, String> environment; // env for the command execution 334 private File dir; 335 private Process process; // sub process used to execute the command 336 private int exitCode; 337 338 /**If or not script finished executing*/ 339 private volatile AtomicBoolean completed; 340 341 public Shell() { 342 this(0L); 343 } 344 345 /** 346 * @param interval the minimum duration to wait before re-executing the 347 * command. 348 */ 349 public Shell( long interval ) { 350 this.interval = interval; 351 this.lastTime = (interval<0) ? 0 : -interval; 352 } 353 354 /** set the environment for the command 355 * @param env Mapping of environment variables 356 */ 357 protected void setEnvironment(Map<String, String> env) { 358 this.environment = env; 359 } 360 361 /** set the working directory 362 * @param dir The directory where the command would be executed 363 */ 364 protected void setWorkingDirectory(File dir) { 365 this.dir = dir; 366 } 367 368 /** check to see if a command needs to be executed and execute if needed */ 369 protected void run() throws IOException { 370 if (lastTime + interval > Time.now()) 371 return; 372 exitCode = 0; // reset for next run 373 runCommand(); 374 } 375 376 /** Run a command */ 377 private void runCommand() throws IOException { 378 ProcessBuilder builder = new ProcessBuilder(getExecString()); 379 Timer timeOutTimer = null; 380 ShellTimeoutTimerTask timeoutTimerTask = null; 381 timedOut = new AtomicBoolean(false); 382 completed = new AtomicBoolean(false); 383 384 if (environment != null) { 385 builder.environment().putAll(this.environment); 386 } 387 if (dir != null) { 388 builder.directory(this.dir); 389 } 390 391 if (Shell.WINDOWS) { 392 synchronized (WindowsProcessLaunchLock) { 393 // To workaround the race condition issue with child processes 394 // inheriting unintended handles during process launch that can 395 // lead to hangs on reading output and error streams, we 396 // serialize process creation. More info available at: 397 // http://support.microsoft.com/kb/315939 398 process = builder.start(); 399 } 400 } else { 401 process = builder.start(); 402 } 403 404 if (timeOutInterval > 0) { 405 timeOutTimer = new Timer("Shell command timeout"); 406 timeoutTimerTask = new ShellTimeoutTimerTask( 407 this); 408 //One time scheduling. 409 timeOutTimer.schedule(timeoutTimerTask, timeOutInterval); 410 } 411 final BufferedReader errReader = 412 new BufferedReader(new InputStreamReader(process 413 .getErrorStream())); 414 BufferedReader inReader = 415 new BufferedReader(new InputStreamReader(process 416 .getInputStream())); 417 final StringBuffer errMsg = new StringBuffer(); 418 419 // read error and input streams as this would free up the buffers 420 // free the error stream buffer 421 Thread errThread = new Thread() { 422 @Override 423 public void run() { 424 try { 425 String line = errReader.readLine(); 426 while((line != null) && !isInterrupted()) { 427 errMsg.append(line); 428 errMsg.append(System.getProperty("line.separator")); 429 line = errReader.readLine(); 430 } 431 } catch(IOException ioe) { 432 LOG.warn("Error reading the error stream", ioe); 433 } 434 } 435 }; 436 try { 437 errThread.start(); 438 } catch (IllegalStateException ise) { } 439 try { 440 parseExecResult(inReader); // parse the output 441 // clear the input stream buffer 442 String line = inReader.readLine(); 443 while(line != null) { 444 line = inReader.readLine(); 445 } 446 // wait for the process to finish and check the exit code 447 exitCode = process.waitFor(); 448 try { 449 // make sure that the error thread exits 450 errThread.join(); 451 } catch (InterruptedException ie) { 452 LOG.warn("Interrupted while reading the error stream", ie); 453 } 454 completed.set(true); 455 //the timeout thread handling 456 //taken care in finally block 457 if (exitCode != 0) { 458 throw new ExitCodeException(exitCode, errMsg.toString()); 459 } 460 } catch (InterruptedException ie) { 461 throw new IOException(ie.toString()); 462 } finally { 463 if (timeOutTimer != null) { 464 timeOutTimer.cancel(); 465 } 466 // close the input stream 467 try { 468 inReader.close(); 469 } catch (IOException ioe) { 470 LOG.warn("Error while closing the input stream", ioe); 471 } 472 if (!completed.get()) { 473 errThread.interrupt(); 474 } 475 try { 476 errReader.close(); 477 } catch (IOException ioe) { 478 LOG.warn("Error while closing the error stream", ioe); 479 } 480 process.destroy(); 481 lastTime = Time.now(); 482 } 483 } 484 485 /** return an array containing the command name & its parameters */ 486 protected abstract String[] getExecString(); 487 488 /** Parse the execution result */ 489 protected abstract void parseExecResult(BufferedReader lines) 490 throws IOException; 491 492 /** get the current sub-process executing the given command 493 * @return process executing the command 494 */ 495 public Process getProcess() { 496 return process; 497 } 498 499 /** get the exit code 500 * @return the exit code of the process 501 */ 502 public int getExitCode() { 503 return exitCode; 504 } 505 506 /** 507 * This is an IOException with exit code added. 508 */ 509 public static class ExitCodeException extends IOException { 510 int exitCode; 511 512 public ExitCodeException(int exitCode, String message) { 513 super(message); 514 this.exitCode = exitCode; 515 } 516 517 public int getExitCode() { 518 return exitCode; 519 } 520 } 521 522 /** 523 * A simple shell command executor. 524 * 525 * <code>ShellCommandExecutor</code>should be used in cases where the output 526 * of the command needs no explicit parsing and where the command, working 527 * directory and the environment remains unchanged. The output of the command 528 * is stored as-is and is expected to be small. 529 */ 530 public static class ShellCommandExecutor extends Shell { 531 532 private String[] command; 533 private StringBuffer output; 534 535 536 public ShellCommandExecutor(String[] execString) { 537 this(execString, null); 538 } 539 540 public ShellCommandExecutor(String[] execString, File dir) { 541 this(execString, dir, null); 542 } 543 544 public ShellCommandExecutor(String[] execString, File dir, 545 Map<String, String> env) { 546 this(execString, dir, env , 0L); 547 } 548 549 /** 550 * Create a new instance of the ShellCommandExecutor to execute a command. 551 * 552 * @param execString The command to execute with arguments 553 * @param dir If not-null, specifies the directory which should be set 554 * as the current working directory for the command. 555 * If null, the current working directory is not modified. 556 * @param env If not-null, environment of the command will include the 557 * key-value pairs specified in the map. If null, the current 558 * environment is not modified. 559 * @param timeout Specifies the time in milliseconds, after which the 560 * command will be killed and the status marked as timedout. 561 * If 0, the command will not be timed out. 562 */ 563 public ShellCommandExecutor(String[] execString, File dir, 564 Map<String, String> env, long timeout) { 565 command = execString.clone(); 566 if (dir != null) { 567 setWorkingDirectory(dir); 568 } 569 if (env != null) { 570 setEnvironment(env); 571 } 572 timeOutInterval = timeout; 573 } 574 575 576 /** Execute the shell command. */ 577 public void execute() throws IOException { 578 this.run(); 579 } 580 581 @Override 582 public String[] getExecString() { 583 return command; 584 } 585 586 @Override 587 protected void parseExecResult(BufferedReader lines) throws IOException { 588 output = new StringBuffer(); 589 char[] buf = new char[512]; 590 int nRead; 591 while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) { 592 output.append(buf, 0, nRead); 593 } 594 } 595 596 /** Get the output of the shell command.*/ 597 public String getOutput() { 598 return (output == null) ? "" : output.toString(); 599 } 600 601 /** 602 * Returns the commands of this instance. 603 * Arguments with spaces in are presented with quotes round; other 604 * arguments are presented raw 605 * 606 * @return a string representation of the object. 607 */ 608 @Override 609 public String toString() { 610 StringBuilder builder = new StringBuilder(); 611 String[] args = getExecString(); 612 for (String s : args) { 613 if (s.indexOf(' ') >= 0) { 614 builder.append('"').append(s).append('"'); 615 } else { 616 builder.append(s); 617 } 618 builder.append(' '); 619 } 620 return builder.toString(); 621 } 622 } 623 624 /** 625 * To check if the passed script to shell command executor timed out or 626 * not. 627 * 628 * @return if the script timed out. 629 */ 630 public boolean isTimedOut() { 631 return timedOut.get(); 632 } 633 634 /** 635 * Set if the command has timed out. 636 * 637 */ 638 private void setTimedOut() { 639 this.timedOut.set(true); 640 } 641 642 /** 643 * Static method to execute a shell command. 644 * Covers most of the simple cases without requiring the user to implement 645 * the <code>Shell</code> interface. 646 * @param cmd shell command to execute. 647 * @return the output of the executed command. 648 */ 649 public static String execCommand(String ... cmd) throws IOException { 650 return execCommand(null, cmd, 0L); 651 } 652 653 /** 654 * Static method to execute a shell command. 655 * Covers most of the simple cases without requiring the user to implement 656 * the <code>Shell</code> interface. 657 * @param env the map of environment key=value 658 * @param cmd shell command to execute. 659 * @param timeout time in milliseconds after which script should be marked timeout 660 * @return the output of the executed command.o 661 */ 662 663 public static String execCommand(Map<String, String> env, String[] cmd, 664 long timeout) throws IOException { 665 ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, 666 timeout); 667 exec.execute(); 668 return exec.getOutput(); 669 } 670 671 /** 672 * Static method to execute a shell command. 673 * Covers most of the simple cases without requiring the user to implement 674 * the <code>Shell</code> interface. 675 * @param env the map of environment key=value 676 * @param cmd shell command to execute. 677 * @return the output of the executed command. 678 */ 679 public static String execCommand(Map<String,String> env, String ... cmd) 680 throws IOException { 681 return execCommand(env, cmd, 0L); 682 } 683 684 /** 685 * Timer which is used to timeout scripts spawned off by shell. 686 */ 687 private static class ShellTimeoutTimerTask extends TimerTask { 688 689 private Shell shell; 690 691 public ShellTimeoutTimerTask(Shell shell) { 692 this.shell = shell; 693 } 694 695 @Override 696 public void run() { 697 Process p = shell.getProcess(); 698 try { 699 p.exitValue(); 700 } catch (Exception e) { 701 //Process has not terminated. 702 //So check if it has completed 703 //if not just destroy it. 704 if (p != null && !shell.completed.get()) { 705 shell.setTimedOut(); 706 p.destroy(); 707 } 708 } 709 } 710 } 711 }