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.InputStream; 024import java.io.InputStreamReader; 025import java.util.Map; 026import java.util.Timer; 027import java.util.TimerTask; 028import java.util.concurrent.atomic.AtomicBoolean; 029 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032import org.apache.hadoop.classification.InterfaceAudience; 033import org.apache.hadoop.classification.InterfaceStability; 034import org.apache.hadoop.conf.Configuration; 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 045abstract public class Shell { 046 047 public static final Log LOG = LogFactory.getLog(Shell.class); 048 049 /** a Unix command to get the current user's name */ 050 public final static String USER_NAME_COMMAND = "whoami"; 051 /** a Unix command to get the current user's groups list */ 052 public static String[] getGroupsCommand() { 053 return new String[]{"bash", "-c", "groups"}; 054 } 055 /** a Unix command to get a given user's groups list */ 056 public static String[] getGroupsForUserCommand(final String user) { 057 //'groups username' command return is non-consistent across different unixes 058 return new String [] {"bash", "-c", "id -Gn " + user}; 059 } 060 /** a Unix command to get a given netgroup's user list */ 061 public static String[] getUsersForNetgroupCommand(final String netgroup) { 062 //'groups username' command return is non-consistent across different unixes 063 return new String [] {"bash", "-c", "getent netgroup " + netgroup}; 064 } 065 /** a Unix command to set permission */ 066 public static final String SET_PERMISSION_COMMAND = "chmod"; 067 /** a Unix command to set owner */ 068 public static final String SET_OWNER_COMMAND = "chown"; 069 public static final String SET_GROUP_COMMAND = "chgrp"; 070 /** a Unix command to create a link */ 071 public static final String LINK_COMMAND = "ln"; 072 /** a Unix command to get a link target */ 073 public static final String READ_LINK_COMMAND = "readlink"; 074 /** Return a Unix command to get permission information. */ 075 public static String[] getGET_PERMISSION_COMMAND() { 076 //force /bin/ls, except on windows. 077 return new String[] {(WINDOWS ? "ls" : "/bin/ls"), "-ld"}; 078 } 079 080 /**Time after which the executing script would be timedout*/ 081 protected long timeOutInterval = 0L; 082 /** If or not script timed out*/ 083 private AtomicBoolean timedOut; 084 085 /** Set to true on Windows platforms */ 086 public static final boolean WINDOWS /* borrowed from Path.WINDOWS */ 087 = System.getProperty("os.name").startsWith("Windows"); 088 089 private long interval; // refresh interval in msec 090 private long lastTime; // last time the command was performed 091 private Map<String, String> environment; // env for the command execution 092 private File dir; 093 private Process process; // sub process used to execute the command 094 private int exitCode; 095 096 /**If or not script finished executing*/ 097 private volatile AtomicBoolean completed; 098 099 public Shell() { 100 this(0L); 101 } 102 103 /** 104 * @param interval the minimum duration to wait before re-executing the 105 * command. 106 */ 107 public Shell( long interval ) { 108 this.interval = interval; 109 this.lastTime = (interval<0) ? 0 : -interval; 110 } 111 112 /** set the environment for the command 113 * @param env Mapping of environment variables 114 */ 115 protected void setEnvironment(Map<String, String> env) { 116 this.environment = env; 117 } 118 119 /** set the working directory 120 * @param dir The directory where the command would be executed 121 */ 122 protected void setWorkingDirectory(File dir) { 123 this.dir = dir; 124 } 125 126 /** check to see if a command needs to be executed and execute if needed */ 127 protected void run() throws IOException { 128 if (lastTime + interval > System.currentTimeMillis()) 129 return; 130 exitCode = 0; // reset for next run 131 runCommand(); 132 } 133 134 /** Run a command */ 135 private void runCommand() throws IOException { 136 ProcessBuilder builder = new ProcessBuilder(getExecString()); 137 Timer timeOutTimer = null; 138 ShellTimeoutTimerTask timeoutTimerTask = null; 139 timedOut = new AtomicBoolean(false); 140 completed = new AtomicBoolean(false); 141 142 if (environment != null) { 143 builder.environment().putAll(this.environment); 144 } 145 if (dir != null) { 146 builder.directory(this.dir); 147 } 148 149 process = builder.start(); 150 if (timeOutInterval > 0) { 151 timeOutTimer = new Timer("Shell command timeout"); 152 timeoutTimerTask = new ShellTimeoutTimerTask( 153 this); 154 //One time scheduling. 155 timeOutTimer.schedule(timeoutTimerTask, timeOutInterval); 156 } 157 final BufferedReader errReader = 158 new BufferedReader(new InputStreamReader(process 159 .getErrorStream())); 160 BufferedReader inReader = 161 new BufferedReader(new InputStreamReader(process 162 .getInputStream())); 163 final StringBuffer errMsg = new StringBuffer(); 164 165 // read error and input streams as this would free up the buffers 166 // free the error stream buffer 167 Thread errThread = new Thread() { 168 @Override 169 public void run() { 170 try { 171 String line = errReader.readLine(); 172 while((line != null) && !isInterrupted()) { 173 errMsg.append(line); 174 errMsg.append(System.getProperty("line.separator")); 175 line = errReader.readLine(); 176 } 177 } catch(IOException ioe) { 178 LOG.warn("Error reading the error stream", ioe); 179 } 180 } 181 }; 182 try { 183 errThread.start(); 184 } catch (IllegalStateException ise) { } 185 try { 186 parseExecResult(inReader); // parse the output 187 // clear the input stream buffer 188 String line = inReader.readLine(); 189 while(line != null) { 190 line = inReader.readLine(); 191 } 192 // wait for the process to finish and check the exit code 193 exitCode = process.waitFor(); 194 try { 195 // make sure that the error thread exits 196 errThread.join(); 197 } catch (InterruptedException ie) { 198 LOG.warn("Interrupted while reading the error stream", ie); 199 } 200 completed.set(true); 201 //the timeout thread handling 202 //taken care in finally block 203 if (exitCode != 0) { 204 throw new ExitCodeException(exitCode, errMsg.toString()); 205 } 206 } catch (InterruptedException ie) { 207 throw new IOException(ie.toString()); 208 } finally { 209 if (timeOutTimer != null) { 210 timeOutTimer.cancel(); 211 } 212 // close the input stream 213 try { 214 // JDK 7 tries to automatically drain the input streams for us 215 // when the process exits, but since close is not synchronized, 216 // it creates a race if we close the stream first and the same 217 // fd is recycled. the stream draining thread will attempt to 218 // drain that fd!! it may block, OOM, or cause bizarre behavior 219 // see: https://bugs.openjdk.java.net/browse/JDK-8024521 220 // issue is fixed in build 7u60 221 InputStream stdout = process.getInputStream(); 222 synchronized (stdout) { 223 inReader.close(); 224 } 225 } catch (IOException ioe) { 226 LOG.warn("Error while closing the input stream", ioe); 227 } 228 if (!completed.get()) { 229 errThread.interrupt(); 230 } 231 try { 232 InputStream stderr = process.getErrorStream(); 233 synchronized (stderr) { 234 errReader.close(); 235 } 236 } catch (IOException ioe) { 237 LOG.warn("Error while closing the error stream", ioe); 238 } 239 process.destroy(); 240 lastTime = System.currentTimeMillis(); 241 } 242 } 243 244 /** return an array containing the command name & its parameters */ 245 protected abstract String[] getExecString(); 246 247 /** Parse the execution result */ 248 protected abstract void parseExecResult(BufferedReader lines) 249 throws IOException; 250 251 /** get the current sub-process executing the given command 252 * @return process executing the command 253 */ 254 public Process getProcess() { 255 return process; 256 } 257 258 /** get the exit code 259 * @return the exit code of the process 260 */ 261 public int getExitCode() { 262 return exitCode; 263 } 264 265 /** 266 * This is an IOException with exit code added. 267 */ 268 public static class ExitCodeException extends IOException { 269 int exitCode; 270 271 public ExitCodeException(int exitCode, String message) { 272 super(message); 273 this.exitCode = exitCode; 274 } 275 276 public int getExitCode() { 277 return exitCode; 278 } 279 } 280 281 /** 282 * A simple shell command executor. 283 * 284 * <code>ShellCommandExecutor</code>should be used in cases where the output 285 * of the command needs no explicit parsing and where the command, working 286 * directory and the environment remains unchanged. The output of the command 287 * is stored as-is and is expected to be small. 288 */ 289 public static class ShellCommandExecutor extends Shell { 290 291 private String[] command; 292 private StringBuffer output; 293 294 295 public ShellCommandExecutor(String[] execString) { 296 this(execString, null); 297 } 298 299 public ShellCommandExecutor(String[] execString, File dir) { 300 this(execString, dir, null); 301 } 302 303 public ShellCommandExecutor(String[] execString, File dir, 304 Map<String, String> env) { 305 this(execString, dir, env , 0L); 306 } 307 308 /** 309 * Create a new instance of the ShellCommandExecutor to execute a command. 310 * 311 * @param execString The command to execute with arguments 312 * @param dir If not-null, specifies the directory which should be set 313 * as the current working directory for the command. 314 * If null, the current working directory is not modified. 315 * @param env If not-null, environment of the command will include the 316 * key-value pairs specified in the map. If null, the current 317 * environment is not modified. 318 * @param timeout Specifies the time in milliseconds, after which the 319 * command will be killed and the status marked as timedout. 320 * If 0, the command will not be timed out. 321 */ 322 public ShellCommandExecutor(String[] execString, File dir, 323 Map<String, String> env, long timeout) { 324 command = execString.clone(); 325 if (dir != null) { 326 setWorkingDirectory(dir); 327 } 328 if (env != null) { 329 setEnvironment(env); 330 } 331 timeOutInterval = timeout; 332 } 333 334 335 /** Execute the shell command. */ 336 public void execute() throws IOException { 337 this.run(); 338 } 339 340 public String[] getExecString() { 341 return command; 342 } 343 344 protected void parseExecResult(BufferedReader lines) throws IOException { 345 output = new StringBuffer(); 346 char[] buf = new char[512]; 347 int nRead; 348 while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) { 349 output.append(buf, 0, nRead); 350 } 351 } 352 353 /** Get the output of the shell command.*/ 354 public String getOutput() { 355 return (output == null) ? "" : output.toString(); 356 } 357 358 /** 359 * Returns the commands of this instance. 360 * Arguments with spaces in are presented with quotes round; other 361 * arguments are presented raw 362 * 363 * @return a string representation of the object. 364 */ 365 public String toString() { 366 StringBuilder builder = new StringBuilder(); 367 String[] args = getExecString(); 368 for (String s : args) { 369 if (s.indexOf(' ') >= 0) { 370 builder.append('"').append(s).append('"'); 371 } else { 372 builder.append(s); 373 } 374 builder.append(' '); 375 } 376 return builder.toString(); 377 } 378 } 379 380 /** 381 * To check if the passed script to shell command executor timed out or 382 * not. 383 * 384 * @return if the script timed out. 385 */ 386 public boolean isTimedOut() { 387 return timedOut.get(); 388 } 389 390 /** 391 * Set if the command has timed out. 392 * 393 */ 394 private void setTimedOut() { 395 this.timedOut.set(true); 396 } 397 398 /** 399 * Static method to execute a shell command. 400 * Covers most of the simple cases without requiring the user to implement 401 * the <code>Shell</code> interface. 402 * @param cmd shell command to execute. 403 * @return the output of the executed command. 404 */ 405 public static String execCommand(String ... cmd) throws IOException { 406 return execCommand(null, cmd, 0L); 407 } 408 409 /** 410 * Static method to execute a shell command. 411 * Covers most of the simple cases without requiring the user to implement 412 * the <code>Shell</code> interface. 413 * @param env the map of environment key=value 414 * @param cmd shell command to execute. 415 * @param timeout time in milliseconds after which script should be marked timeout 416 * @return the output of the executed command.o 417 */ 418 419 public static String execCommand(Map<String, String> env, String[] cmd, 420 long timeout) throws IOException { 421 ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, 422 timeout); 423 exec.execute(); 424 return exec.getOutput(); 425 } 426 427 /** 428 * Static method to execute a shell command. 429 * Covers most of the simple cases without requiring the user to implement 430 * the <code>Shell</code> interface. 431 * @param env the map of environment key=value 432 * @param cmd shell command to execute. 433 * @return the output of the executed command. 434 */ 435 public static String execCommand(Map<String,String> env, String ... cmd) 436 throws IOException { 437 return execCommand(env, cmd, 0L); 438 } 439 440 /** 441 * Timer which is used to timeout scripts spawned off by shell. 442 */ 443 private static class ShellTimeoutTimerTask extends TimerTask { 444 445 private Shell shell; 446 447 public ShellTimeoutTimerTask(Shell shell) { 448 this.shell = shell; 449 } 450 451 @Override 452 public void run() { 453 Process p = shell.getProcess(); 454 try { 455 p.exitValue(); 456 } catch (Exception e) { 457 //Process has not terminated. 458 //So check if it has completed 459 //if not just destroy it. 460 if (p != null && !shell.completed.get()) { 461 shell.setTimedOut(); 462 p.destroy(); 463 } 464 } 465 } 466 } 467}