/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import javax.annotation.concurrent.ThreadSafe; /** * A base class for running a Unix command. */ @ThreadSafe public final class ShellUtils { private static final Logger LOG = LoggerFactory.getLogger(ShellUtils.class); /** a Unix command to set permission. */ public static final String SET_PERMISSION_COMMAND = "chmod"; /** * Gets a Unix command to get a given user's groups list. * * @param user the user name * @return the Unix command to get a given user's groups list */ public static String[] getGroupsForUserCommand(final String user) { return new String[] {"bash", "-c", "id -gn " + user + "; id -Gn " + user}; } /** * Returns a Unix command to set permission. * * @param perm the permission of file * @param filePath the file path * @return the Unix command to set permission */ public static String[] getSetPermissionCommand(String perm, String filePath) { return new String[] {SET_PERMISSION_COMMAND, perm, filePath}; } /** Token separator regex used to parse Shell tool outputs. */ public static final String TOKEN_SEPARATOR_REGEX = "[ \t\n\r\f]"; private Process mProcess; // sub process used to execute the command private int mExitCode; private String[] mCommand; private StringBuffer mOutput; private ShellUtils(String[] execString) { mCommand = execString.clone(); } /** Checks to see if a command needs to be executed and execute command. */ protected void run() throws IOException { mExitCode = 0; // reset for next run runCommand(); } /** * Runs a command. */ private void runCommand() throws IOException { ProcessBuilder builder = new ProcessBuilder(getExecString()); mProcess = builder.start(); BufferedReader inReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream(), Charset.defaultCharset())); final StringBuffer errMsg = new StringBuffer(); // read input streams as this would free up the buffers try { parseExecResult(inReader); // parse the output // clear the input stream buffer String line = inReader.readLine(); while (line != null) { line = inReader.readLine(); } // wait for the process to finish and check the exit code mExitCode = mProcess.waitFor(); if (mExitCode != 0) { throw new ExitCodeException(mExitCode, errMsg.toString()); } } catch (InterruptedException e) { throw new IOException(e); } finally { // close the input stream try { // JDK 7 tries to automatically drain the input streams for us // when the process exits, but since close is not synchronized, // it creates a race if we close the stream first and the same // fd is recycled. the stream draining thread will attempt to // drain that fd!! it may block, OOM, or cause bizarre behavior // see: https://bugs.openjdk.java.net/browse/JDK-8024521 // issue is fixed in build 7u60 InputStream stdout = mProcess.getInputStream(); synchronized (stdout) { inReader.close(); } } catch (IOException e) { LOG.warn("Error while closing the input stream", e); } mProcess.destroy(); } } /** * @return an array containing the command name & its parameters */ protected String[] getExecString() { return mCommand; } /** Parse the execution result. */ protected void parseExecResult(BufferedReader lines) throws IOException { mOutput = new StringBuffer(); char[] buf = new char[512]; int nRead; while ((nRead = lines.read(buf, 0, buf.length)) > 0) { mOutput.append(buf, 0, nRead); } } /** @return the output of the shell command. */ public String getOutput() { return (mOutput == null) ? "" : mOutput.toString(); } /** * Gets the current sub-process executing the given command. * * @return process executing the command */ public Process getProcess() { return mProcess; } /** * Gets the exit code. * * @return the exit code of the process */ public int getExitCode() { return mExitCode; } /** * This is an IOException with exit code added. */ public static class ExitCodeException extends IOException { private static final long serialVersionUID = -6520494427049734809L; private final int mExitCode; /** * Constructs an ExitCodeException. * * @param exitCode the exit code returns by shell * @param message the exception message */ public ExitCodeException(int exitCode, String message) { super(message); mExitCode = exitCode; } /** * Gets the exit code. * * @return the exit code */ public int getExitCode() { return mExitCode; } @Override public String toString() { final StringBuilder sb = new StringBuilder("ExitCodeException "); sb.append("exitCode=").append(mExitCode).append(": "); sb.append(super.getMessage()); return sb.toString(); } } /** * Static method to execute a shell command. * * @param cmd shell command to execute * @return the output of the executed command */ public static String execCommand(String... cmd) throws IOException { ShellUtils exec = new ShellUtils(cmd); exec.run(); return exec.getOutput(); } }