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}