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