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.IOException; 023import java.io.InputStreamReader; 024import java.io.InputStream; 025import java.nio.charset.Charset; 026import java.util.Arrays; 027import java.util.Map; 028import java.util.Timer; 029import java.util.TimerTask; 030import java.util.concurrent.atomic.AtomicBoolean; 031 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.apache.hadoop.classification.InterfaceAudience; 035import org.apache.hadoop.classification.InterfaceStability; 036import org.apache.hadoop.security.alias.AbstractJavaKeyStoreProvider; 037 038/** 039 * A base class for running a Unix command. 040 * 041 * <code>Shell</code> can be used to run unix commands like <code>du</code> or 042 * <code>df</code>. It also offers facilities to gate commands by 043 * time-intervals. 044 */ 045@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) 046@InterfaceStability.Unstable 047abstract public class Shell { 048 049 public static final Log LOG = LogFactory.getLog(Shell.class); 050 051 /** 052 * java.version is enforced via maven-enforcer-plugin. 053 * Starting from Hadoop 2.7.0 Java 1.7 or higher is required. 054 */ 055 private static boolean IS_JAVA7_OR_ABOVE = true; 056 057 public static boolean isJava7OrAbove() { 058 return IS_JAVA7_OR_ABOVE; 059 } 060 061 /** 062 * Maximum command line length in Windows 063 * KB830473 documents this as 8191 064 */ 065 public static final int WINDOWS_MAX_SHELL_LENGHT = 8191; 066 067 /** 068 * Checks if a given command (String[]) fits in the Windows maximum command line length 069 * Note that the input is expected to already include space delimiters, no extra count 070 * will be added for delimiters. 071 * 072 * @param commands command parts, including any space delimiters 073 */ 074 public static void checkWindowsCommandLineLength(String...commands) 075 throws IOException { 076 int len = 0; 077 for (String s: commands) { 078 len += s.length(); 079 } 080 if (len > WINDOWS_MAX_SHELL_LENGHT) { 081 throw new IOException(String.format( 082 "The command line has a length of %d exceeds maximum allowed length of %d. " + 083 "Command starts with: %s", 084 len, WINDOWS_MAX_SHELL_LENGHT, 085 StringUtils.join("", commands).substring(0, 100))); 086 } 087 } 088 089 /** 090 * Quote the given arg so that bash will interpret it as a single value. 091 * Note that this quotes it for one level of bash, if you are passing it 092 * into a badly written shell script, you need to fix your shell script. 093 * @param arg the argument to quote 094 * @return the quoted string 095 */ 096 static String bashQuote(String arg) { 097 StringBuilder buffer = new StringBuilder(arg.length() + 2); 098 buffer.append('\''); 099 buffer.append(arg.replace("'", "'\\''")); 100 buffer.append('\''); 101 return buffer.toString(); 102 } 103 104 /** a Unix command to get the current user's name: {@value}. */ 105 public static final String USER_NAME_COMMAND = "whoami"; 106 107 /** Windows CreateProcess synchronization object */ 108 public static final Object WindowsProcessLaunchLock = new Object(); 109 110 // OSType detection 111 112 public enum OSType { 113 OS_TYPE_LINUX, 114 OS_TYPE_WIN, 115 OS_TYPE_SOLARIS, 116 OS_TYPE_MAC, 117 OS_TYPE_FREEBSD, 118 OS_TYPE_OTHER 119 } 120 121 public static final OSType osType = getOSType(); 122 123 static private OSType getOSType() { 124 String osName = System.getProperty("os.name"); 125 if (osName.startsWith("Windows")) { 126 return OSType.OS_TYPE_WIN; 127 } else if (osName.contains("SunOS") || osName.contains("Solaris")) { 128 return OSType.OS_TYPE_SOLARIS; 129 } else if (osName.contains("Mac")) { 130 return OSType.OS_TYPE_MAC; 131 } else if (osName.contains("FreeBSD")) { 132 return OSType.OS_TYPE_FREEBSD; 133 } else if (osName.startsWith("Linux")) { 134 return OSType.OS_TYPE_LINUX; 135 } else { 136 // Some other form of Unix 137 return OSType.OS_TYPE_OTHER; 138 } 139 } 140 141 // Helper static vars for each platform 142 public static final boolean WINDOWS = (osType == OSType.OS_TYPE_WIN); 143 public static final boolean SOLARIS = (osType == OSType.OS_TYPE_SOLARIS); 144 public static final boolean MAC = (osType == OSType.OS_TYPE_MAC); 145 public static final boolean FREEBSD = (osType == OSType.OS_TYPE_FREEBSD); 146 public static final boolean LINUX = (osType == OSType.OS_TYPE_LINUX); 147 public static final boolean OTHER = (osType == OSType.OS_TYPE_OTHER); 148 149 public static final boolean PPC_64 150 = System.getProperties().getProperty("os.arch").contains("ppc64"); 151 152 /** a Unix command to get the current user's groups list */ 153 public static String[] getGroupsCommand() { 154 return (WINDOWS)? new String[]{"cmd", "/c", "groups"} 155 : new String[]{"groups"}; 156 } 157 158 /** 159 * a Unix command to get a given user's groups list. 160 * If the OS is not WINDOWS, the command will get the user's primary group 161 * first and finally get the groups list which includes the primary group. 162 * i.e. the user's primary group will be included twice. 163 */ 164 public static String[] getGroupsForUserCommand(final String user) { 165 //'groups username' command return is inconsistent across different unixes 166 if (WINDOWS) { 167 return new String[] 168 {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""}; 169 } else { 170 String quotedUser = bashQuote(user); 171 return new String[] {"bash", "-c", "id -gn " + quotedUser + 172 "; id -Gn " + quotedUser}; 173 } 174 } 175 176 /** a Unix command to get a given netgroup's user list */ 177 public static String[] getUsersForNetgroupCommand(final String netgroup) { 178 //'groups username' command return is non-consistent across different unixes 179 return new String[] {"getent", "netgroup", netgroup}; 180 } 181 182 /** Return a command to get permission information. */ 183 public static String[] getGetPermissionCommand() { 184 return (WINDOWS) ? new String[] { WINUTILS, "ls", "-F" } 185 : new String[] { "/bin/ls", "-ld" }; 186 } 187 188 /** Return a command to set permission */ 189 public static String[] getSetPermissionCommand(String perm, boolean recursive) { 190 if (recursive) { 191 return (WINDOWS) ? new String[] { WINUTILS, "chmod", "-R", perm } 192 : new String[] { "chmod", "-R", perm }; 193 } else { 194 return (WINDOWS) ? new String[] { WINUTILS, "chmod", perm } 195 : new String[] { "chmod", perm }; 196 } 197 } 198 199 /** 200 * Return a command to set permission for specific file. 201 * 202 * @param perm String permission to set 203 * @param recursive boolean true to apply to all sub-directories recursively 204 * @param file String file to set 205 * @return String[] containing command and arguments 206 */ 207 public static String[] getSetPermissionCommand(String perm, boolean recursive, 208 String file) { 209 String[] baseCmd = getSetPermissionCommand(perm, recursive); 210 String[] cmdWithFile = Arrays.copyOf(baseCmd, baseCmd.length + 1); 211 cmdWithFile[cmdWithFile.length - 1] = file; 212 return cmdWithFile; 213 } 214 215 /** Return a command to set owner */ 216 public static String[] getSetOwnerCommand(String owner) { 217 return (WINDOWS) ? new String[] { WINUTILS, "chown", "\"" + owner + "\"" } 218 : new String[] { "chown", owner }; 219 } 220 221 /** Return a command to create symbolic links */ 222 public static String[] getSymlinkCommand(String target, String link) { 223 return WINDOWS ? new String[] { WINUTILS, "symlink", link, target } 224 : new String[] { "ln", "-s", target, link }; 225 } 226 227 /** Return a command to read the target of the a symbolic link*/ 228 public static String[] getReadlinkCommand(String link) { 229 return WINDOWS ? new String[] { WINUTILS, "readlink", link } 230 : new String[] { "readlink", link }; 231 } 232 233 /** Return a command for determining if process with specified pid is alive. */ 234 public static String[] getCheckProcessIsAliveCommand(String pid) { 235 return Shell.WINDOWS ? 236 new String[] { Shell.WINUTILS, "task", "isAlive", pid } : 237 new String[] { "kill", "-0", isSetsidAvailable ? "-" + pid : pid }; 238 } 239 240 /** Return a command to send a signal to a given pid */ 241 public static String[] getSignalKillCommand(int code, String pid) { 242 return Shell.WINDOWS ? new String[] { Shell.WINUTILS, "task", "kill", pid } : 243 new String[] { "kill", "-" + code, isSetsidAvailable ? "-" + pid : pid }; 244 } 245 246 /** Return a regular expression string that match environment variables */ 247 public static String getEnvironmentVariableRegex() { 248 return (WINDOWS) ? "%([A-Za-z_][A-Za-z0-9_]*?)%" : 249 "\\$([A-Za-z_][A-Za-z0-9_]*)"; 250 } 251 252 /** 253 * Returns a File referencing a script with the given basename, inside the 254 * given parent directory. The file extension is inferred by platform: ".cmd" 255 * on Windows, or ".sh" otherwise. 256 * 257 * @param parent File parent directory 258 * @param basename String script file basename 259 * @return File referencing the script in the directory 260 */ 261 public static File appendScriptExtension(File parent, String basename) { 262 return new File(parent, appendScriptExtension(basename)); 263 } 264 265 /** 266 * Returns a script file name with the given basename. The file extension is 267 * inferred by platform: ".cmd" on Windows, or ".sh" otherwise. 268 * 269 * @param basename String script file basename 270 * @return String script file name 271 */ 272 public static String appendScriptExtension(String basename) { 273 return basename + (WINDOWS ? ".cmd" : ".sh"); 274 } 275 276 /** 277 * Returns a command to run the given script. The script interpreter is 278 * inferred by platform: cmd on Windows or bash otherwise. 279 * 280 * @param script File script to run 281 * @return String[] command to run the script 282 */ 283 public static String[] getRunScriptCommand(File script) { 284 String absolutePath = script.getAbsolutePath(); 285 return WINDOWS ? 286 new String[] {"cmd", "/c", absolutePath } 287 : new String[] {"/bin/bash", bashQuote(absolutePath) }; 288 } 289 290 /** a Unix command to set permission */ 291 public static final String SET_PERMISSION_COMMAND = "chmod"; 292 /** a Unix command to set owner */ 293 public static final String SET_OWNER_COMMAND = "chown"; 294 295 /** a Unix command to set the change user's groups list */ 296 public static final String SET_GROUP_COMMAND = "chgrp"; 297 /** a Unix command to create a link */ 298 public static final String LINK_COMMAND = "ln"; 299 /** a Unix command to get a link target */ 300 public static final String READ_LINK_COMMAND = "readlink"; 301 302 /**Time after which the executing script would be timedout*/ 303 protected long timeOutInterval = 0L; 304 /** If or not script timed out*/ 305 private AtomicBoolean timedOut; 306 307 /** Indicates if the parent env vars should be inherited or not*/ 308 protected boolean inheritParentEnv = true; 309 310 /** Centralized logic to discover and validate the sanity of the Hadoop 311 * home directory. Returns either NULL or a directory that exists and 312 * was specified via either -Dhadoop.home.dir or the HADOOP_HOME ENV 313 * variable. This does a lot of work so it should only be called 314 * privately for initialization once per process. 315 **/ 316 private static String checkHadoopHome() { 317 318 // first check the Dflag hadoop.home.dir with JVM scope 319 String home = System.getProperty("hadoop.home.dir"); 320 321 // fall back to the system/user-global env variable 322 if (home == null) { 323 home = System.getenv("HADOOP_HOME"); 324 } 325 326 try { 327 // couldn't find either setting for hadoop's home directory 328 if (home == null) { 329 throw new IOException("HADOOP_HOME or hadoop.home.dir are not set."); 330 } 331 332 if (home.startsWith("\"") && home.endsWith("\"")) { 333 home = home.substring(1, home.length()-1); 334 } 335 336 // check that the home setting is actually a directory that exists 337 File homedir = new File(home); 338 if (!homedir.isAbsolute() || !homedir.exists() || !homedir.isDirectory()) { 339 throw new IOException("Hadoop home directory " + homedir 340 + " does not exist, is not a directory, or is not an absolute path."); 341 } 342 343 home = homedir.getCanonicalPath(); 344 345 } catch (IOException ioe) { 346 if (LOG.isDebugEnabled()) { 347 LOG.debug("Failed to detect a valid hadoop home directory", ioe); 348 } 349 home = null; 350 } 351 352 return home; 353 } 354 private static String HADOOP_HOME_DIR = checkHadoopHome(); 355 356 // Public getter, throws an exception if HADOOP_HOME failed validation 357 // checks and is being referenced downstream. 358 public static final String getHadoopHome() throws IOException { 359 if (HADOOP_HOME_DIR == null) { 360 throw new IOException("Misconfigured HADOOP_HOME cannot be referenced."); 361 } 362 363 return HADOOP_HOME_DIR; 364 } 365 366 /** fully qualify the path to a binary that should be in a known hadoop 367 * bin location. This is primarily useful for disambiguating call-outs 368 * to executable sub-components of Hadoop to avoid clashes with other 369 * executables that may be in the path. Caveat: this call doesn't 370 * just format the path to the bin directory. It also checks for file 371 * existence of the composed path. The output of this call should be 372 * cached by callers. 373 * */ 374 public static final String getQualifiedBinPath(String executable) 375 throws IOException { 376 // construct hadoop bin path to the specified executable 377 String fullExeName = HADOOP_HOME_DIR + File.separator + "bin" 378 + File.separator + executable; 379 380 File exeFile = new File(fullExeName); 381 if (!exeFile.exists()) { 382 throw new IOException("Could not locate executable " + fullExeName 383 + " in the Hadoop binaries."); 384 } 385 386 return exeFile.getCanonicalPath(); 387 } 388 389 /** a Windows utility to emulate Unix commands */ 390 public static final String WINUTILS = getWinUtilsPath(); 391 392 public static final String getWinUtilsPath() { 393 String winUtilsPath = null; 394 395 try { 396 if (WINDOWS) { 397 winUtilsPath = getQualifiedBinPath("winutils.exe"); 398 } 399 } catch (IOException ioe) { 400 LOG.error("Failed to locate the winutils binary in the hadoop binary path", 401 ioe); 402 } 403 404 return winUtilsPath; 405 } 406 407 public static final boolean isSetsidAvailable = isSetsidSupported(); 408 private static boolean isSetsidSupported() { 409 if (Shell.WINDOWS) { 410 return false; 411 } 412 ShellCommandExecutor shexec = null; 413 boolean setsidSupported = true; 414 try { 415 String[] args = {"setsid", "bash", "-c", "echo $$"}; 416 shexec = new ShellCommandExecutor(args); 417 shexec.execute(); 418 } catch (IOException ioe) { 419 LOG.debug("setsid is not available on this machine. So not using it."); 420 setsidSupported = false; 421 } finally { // handle the exit code 422 if (LOG.isDebugEnabled()) { 423 LOG.debug("setsid exited with exit code " 424 + (shexec != null ? shexec.getExitCode() : "(null executor)")); 425 } 426 } 427 return setsidSupported; 428 } 429 430 /** Token separator regex used to parse Shell tool outputs */ 431 public static final String TOKEN_SEPARATOR_REGEX 432 = WINDOWS ? "[|\n\r]" : "[ \t\n\r\f]"; 433 434 private long interval; // refresh interval in msec 435 private long lastTime; // last time the command was performed 436 final private boolean redirectErrorStream; // merge stdout and stderr 437 private Map<String, String> environment; // env for the command execution 438 private File dir; 439 private Process process; // sub process used to execute the command 440 private int exitCode; 441 442 /**If or not script finished executing*/ 443 private volatile AtomicBoolean completed; 444 445 public Shell() { 446 this(0L); 447 } 448 449 public Shell(long interval) { 450 this(interval, false); 451 } 452 453 /** 454 * @param interval the minimum duration to wait before re-executing the 455 * command. 456 */ 457 public Shell(long interval, boolean redirectErrorStream) { 458 this.interval = interval; 459 this.lastTime = (interval<0) ? 0 : -interval; 460 this.redirectErrorStream = redirectErrorStream; 461 } 462 463 /** set the environment for the command 464 * @param env Mapping of environment variables 465 */ 466 protected void setEnvironment(Map<String, String> env) { 467 this.environment = env; 468 } 469 470 /** set the working directory 471 * @param dir The directory where the command would be executed 472 */ 473 protected void setWorkingDirectory(File dir) { 474 this.dir = dir; 475 } 476 477 /** check to see if a command needs to be executed and execute if needed */ 478 protected void run() throws IOException { 479 if (lastTime + interval > Time.monotonicNow()) 480 return; 481 exitCode = 0; // reset for next run 482 runCommand(); 483 } 484 485 /** Run a command */ 486 private void runCommand() throws IOException { 487 ProcessBuilder builder = new ProcessBuilder(getExecString()); 488 Timer timeOutTimer = null; 489 ShellTimeoutTimerTask timeoutTimerTask = null; 490 timedOut = new AtomicBoolean(false); 491 completed = new AtomicBoolean(false); 492 493 if (environment != null) { 494 builder.environment().putAll(this.environment); 495 } 496 497 // Remove all env vars from the Builder to prevent leaking of env vars from 498 // the parent process. 499 if (!inheritParentEnv) { 500 // branch-2: Only do this for HADOOP_CREDSTORE_PASSWORD 501 // Sometimes daemons are configured to use the CredentialProvider feature 502 // and given their jceks password via an environment variable. We need to 503 // make sure to remove it so it doesn't leak to child processes, which 504 // might be owned by a different user. For example, the NodeManager 505 // running a User's container. 506 builder.environment().remove( 507 AbstractJavaKeyStoreProvider.CREDENTIAL_PASSWORD_NAME); 508 } 509 510 if (dir != null) { 511 builder.directory(this.dir); 512 } 513 514 builder.redirectErrorStream(redirectErrorStream); 515 516 if (Shell.WINDOWS) { 517 synchronized (WindowsProcessLaunchLock) { 518 // To workaround the race condition issue with child processes 519 // inheriting unintended handles during process launch that can 520 // lead to hangs on reading output and error streams, we 521 // serialize process creation. More info available at: 522 // http://support.microsoft.com/kb/315939 523 process = builder.start(); 524 } 525 } else { 526 process = builder.start(); 527 } 528 529 if (timeOutInterval > 0) { 530 timeOutTimer = new Timer("Shell command timeout"); 531 timeoutTimerTask = new ShellTimeoutTimerTask( 532 this); 533 //One time scheduling. 534 timeOutTimer.schedule(timeoutTimerTask, timeOutInterval); 535 } 536 final BufferedReader errReader = 537 new BufferedReader(new InputStreamReader( 538 process.getErrorStream(), Charset.defaultCharset())); 539 BufferedReader inReader = 540 new BufferedReader(new InputStreamReader( 541 process.getInputStream(), Charset.defaultCharset())); 542 final StringBuffer errMsg = new StringBuffer(); 543 544 // read error and input streams as this would free up the buffers 545 // free the error stream buffer 546 Thread errThread = new Thread() { 547 @Override 548 public void run() { 549 try { 550 String line = errReader.readLine(); 551 while((line != null) && !isInterrupted()) { 552 errMsg.append(line); 553 errMsg.append(System.getProperty("line.separator")); 554 line = errReader.readLine(); 555 } 556 } catch(IOException ioe) { 557 LOG.warn("Error reading the error stream", ioe); 558 } 559 } 560 }; 561 try { 562 errThread.start(); 563 } catch (IllegalStateException ise) { 564 } catch (OutOfMemoryError oe) { 565 LOG.error("Caught " + oe + ". One possible reason is that ulimit" 566 + " setting of 'max user processes' is too low. If so, do" 567 + " 'ulimit -u <largerNum>' and try again."); 568 throw oe; 569 } 570 try { 571 parseExecResult(inReader); // parse the output 572 // clear the input stream buffer 573 String line = inReader.readLine(); 574 while(line != null) { 575 line = inReader.readLine(); 576 } 577 // wait for the process to finish and check the exit code 578 exitCode = process.waitFor(); 579 // make sure that the error thread exits 580 joinThread(errThread); 581 completed.set(true); 582 //the timeout thread handling 583 //taken care in finally block 584 if (exitCode != 0) { 585 throw new ExitCodeException(exitCode, errMsg.toString()); 586 } 587 } catch (InterruptedException ie) { 588 throw new IOException(ie.toString()); 589 } finally { 590 if (timeOutTimer != null) { 591 timeOutTimer.cancel(); 592 } 593 // close the input stream 594 try { 595 // JDK 7 tries to automatically drain the input streams for us 596 // when the process exits, but since close is not synchronized, 597 // it creates a race if we close the stream first and the same 598 // fd is recycled. the stream draining thread will attempt to 599 // drain that fd!! it may block, OOM, or cause bizarre behavior 600 // see: https://bugs.openjdk.java.net/browse/JDK-8024521 601 // issue is fixed in build 7u60 602 InputStream stdout = process.getInputStream(); 603 synchronized (stdout) { 604 inReader.close(); 605 } 606 } catch (IOException ioe) { 607 LOG.warn("Error while closing the input stream", ioe); 608 } 609 if (!completed.get()) { 610 errThread.interrupt(); 611 joinThread(errThread); 612 } 613 try { 614 InputStream stderr = process.getErrorStream(); 615 synchronized (stderr) { 616 errReader.close(); 617 } 618 } catch (IOException ioe) { 619 LOG.warn("Error while closing the error stream", ioe); 620 } 621 process.destroy(); 622 lastTime = Time.monotonicNow(); 623 } 624 } 625 626 private static void joinThread(Thread t) { 627 while (t.isAlive()) { 628 try { 629 t.join(); 630 } catch (InterruptedException ie) { 631 if (LOG.isWarnEnabled()) { 632 LOG.warn("Interrupted while joining on: " + t, ie); 633 } 634 t.interrupt(); // propagate interrupt 635 } 636 } 637 } 638 639 /** return an array containing the command name & its parameters */ 640 protected abstract String[] getExecString(); 641 642 /** Parse the execution result */ 643 protected abstract void parseExecResult(BufferedReader lines) 644 throws IOException; 645 646 /** 647 * Get the environment variable 648 */ 649 public String getEnvironment(String env) { 650 return environment.get(env); 651 } 652 653 /** get the current sub-process executing the given command 654 * @return process executing the command 655 */ 656 public Process getProcess() { 657 return process; 658 } 659 660 /** get the exit code 661 * @return the exit code of the process 662 */ 663 public int getExitCode() { 664 return exitCode; 665 } 666 667 /** 668 * This is an IOException with exit code added. 669 */ 670 public static class ExitCodeException extends IOException { 671 private final int exitCode; 672 673 public ExitCodeException(int exitCode, String message) { 674 super(message); 675 this.exitCode = exitCode; 676 } 677 678 public int getExitCode() { 679 return exitCode; 680 } 681 682 @Override 683 public String toString() { 684 final StringBuilder sb = 685 new StringBuilder("ExitCodeException "); 686 sb.append("exitCode=").append(exitCode) 687 .append(": "); 688 sb.append(super.getMessage()); 689 return sb.toString(); 690 } 691 } 692 693 public interface CommandExecutor { 694 695 void execute() throws IOException; 696 697 int getExitCode() throws IOException; 698 699 String getOutput() throws IOException; 700 701 void close(); 702 703 } 704 705 /** 706 * A simple shell command executor. 707 * 708 * <code>ShellCommandExecutor</code>should be used in cases where the output 709 * of the command needs no explicit parsing and where the command, working 710 * directory and the environment remains unchanged. The output of the command 711 * is stored as-is and is expected to be small. 712 */ 713 public static class ShellCommandExecutor extends Shell 714 implements CommandExecutor { 715 716 private String[] command; 717 private StringBuffer output; 718 719 720 public ShellCommandExecutor(String[] execString) { 721 this(execString, null); 722 } 723 724 public ShellCommandExecutor(String[] execString, File dir) { 725 this(execString, dir, null); 726 } 727 728 public ShellCommandExecutor(String[] execString, File dir, 729 Map<String, String> env) { 730 this(execString, dir, env , 0L); 731 } 732 733 public ShellCommandExecutor(String[] execString, File dir, 734 Map<String, String> env, long timeout) { 735 this(execString, dir, env , timeout, true); 736 } 737 738 /** 739 * Create a new instance of the ShellCommandExecutor to execute a command. 740 * 741 * @param execString The command to execute with arguments 742 * @param dir If not-null, specifies the directory which should be set 743 * as the current working directory for the command. 744 * If null, the current working directory is not modified. 745 * @param env If not-null, environment of the command will include the 746 * key-value pairs specified in the map. If null, the current 747 * environment is not modified. 748 * @param timeout Specifies the time in milliseconds, after which the 749 * command will be killed and the status marked as timedout. 750 * If 0, the command will not be timed out. 751 * @param inheritParentEnv Indicates if the process should inherit the env 752 * vars from the parent process or not. 753 */ 754 public ShellCommandExecutor(String[] execString, File dir, 755 Map<String, String> env, long timeout, boolean inheritParentEnv) { 756 command = execString.clone(); 757 if (dir != null) { 758 setWorkingDirectory(dir); 759 } 760 if (env != null) { 761 setEnvironment(env); 762 } 763 timeOutInterval = timeout; 764 this.inheritParentEnv = inheritParentEnv; 765 } 766 767 768 /** Execute the shell command. */ 769 public void execute() throws IOException { 770 for (String s : command) { 771 if (s == null) { 772 throw new IOException("(null) entry in command string: " 773 + StringUtils.join(" ", command)); 774 } 775 } 776 this.run(); 777 } 778 779 @Override 780 public String[] getExecString() { 781 return command; 782 } 783 784 @Override 785 protected void parseExecResult(BufferedReader lines) throws IOException { 786 output = new StringBuffer(); 787 char[] buf = new char[512]; 788 int nRead; 789 while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) { 790 output.append(buf, 0, nRead); 791 } 792 } 793 794 /** Get the output of the shell command.*/ 795 public String getOutput() { 796 return (output == null) ? "" : output.toString(); 797 } 798 799 /** 800 * Returns the commands of this instance. 801 * Arguments with spaces in are presented with quotes round; other 802 * arguments are presented raw 803 * 804 * @return a string representation of the object. 805 */ 806 @Override 807 public String toString() { 808 StringBuilder builder = new StringBuilder(); 809 String[] args = getExecString(); 810 for (String s : args) { 811 if (s.indexOf(' ') >= 0) { 812 builder.append('"').append(s).append('"'); 813 } else { 814 builder.append(s); 815 } 816 builder.append(' '); 817 } 818 return builder.toString(); 819 } 820 821 @Override 822 public void close() { 823 } 824 } 825 826 /** 827 * To check if the passed script to shell command executor timed out or 828 * not. 829 * 830 * @return if the script timed out. 831 */ 832 public boolean isTimedOut() { 833 return timedOut.get(); 834 } 835 836 /** 837 * Set if the command has timed out. 838 * 839 */ 840 private void setTimedOut() { 841 this.timedOut.set(true); 842 } 843 844 /** 845 * Static method to execute a shell command. 846 * Covers most of the simple cases without requiring the user to implement 847 * the <code>Shell</code> interface. 848 * @param cmd shell command to execute. 849 * @return the output of the executed command. 850 */ 851 public static String execCommand(String ... cmd) throws IOException { 852 return execCommand(null, cmd, 0L); 853 } 854 855 /** 856 * Static method to execute a shell command. 857 * Covers most of the simple cases without requiring the user to implement 858 * the <code>Shell</code> interface. 859 * @param env the map of environment key=value 860 * @param cmd shell command to execute. 861 * @param timeout time in milliseconds after which script should be marked timeout 862 * @return the output of the executed command.o 863 */ 864 865 public static String execCommand(Map<String, String> env, String[] cmd, 866 long timeout) throws IOException { 867 ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, 868 timeout); 869 exec.execute(); 870 return exec.getOutput(); 871 } 872 873 /** 874 * Static method to execute a shell command. 875 * Covers most of the simple cases without requiring the user to implement 876 * the <code>Shell</code> interface. 877 * @param env the map of environment key=value 878 * @param cmd shell command to execute. 879 * @return the output of the executed command. 880 */ 881 public static String execCommand(Map<String,String> env, String ... cmd) 882 throws IOException { 883 return execCommand(env, cmd, 0L); 884 } 885 886 /** 887 * Timer which is used to timeout scripts spawned off by shell. 888 */ 889 private static class ShellTimeoutTimerTask extends TimerTask { 890 891 private Shell shell; 892 893 public ShellTimeoutTimerTask(Shell shell) { 894 this.shell = shell; 895 } 896 897 @Override 898 public void run() { 899 Process p = shell.getProcess(); 900 try { 901 p.exitValue(); 902 } catch (Exception e) { 903 //Process has not terminated. 904 //So check if it has completed 905 //if not just destroy it. 906 if (p != null && !shell.completed.get()) { 907 shell.setTimedOut(); 908 p.destroy(); 909 } 910 } 911 } 912 } 913}