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