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