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