/* * 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 com.google.common.base.Preconditions; 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 java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; /** * A utility class for running Unix commands. */ @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"; /** a Unix command for getting mount information. */ public static final String MOUNT_COMMAND = "mount"; /** * 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]"; /** * Gets system mount information. This method should only be attempted on Unix systems. * * @return system mount information */ public static List<UnixMountInfo> getUnixMountInfo() throws IOException { Preconditions.checkState(OSUtils.isLinux() || OSUtils.isMacOS()); String output = execCommand(MOUNT_COMMAND); List<UnixMountInfo> mountInfo = new ArrayList<>(); for (String line : output.split("\n")) { mountInfo.add(parseMountInfo(line)); } return mountInfo; } /** * @param line the line to parse * @return the parsed {@link UnixMountInfo} */ public static UnixMountInfo parseMountInfo(String line) { // Example mount lines: // ramfs on /mnt/ramdisk type ramfs (rw,relatime,size=1gb) // map -hosts on /net (autofs, nosuid, automounted, nobrowse) UnixMountInfo.Builder builder = new UnixMountInfo.Builder(); // First get and remove the mount type if it's provided. Matcher matcher = Pattern.compile(".* (type \\w+ ).*").matcher(line); String lineWithoutType; if (matcher.matches()) { String match = matcher.group(1); builder.setFsType(match.replace("type", "").trim()); lineWithoutType = line.replace(match, ""); } else { lineWithoutType = line; } // Now parse the rest matcher = Pattern.compile("(.*) on (.*) \\((.*)\\)").matcher(lineWithoutType); if (!matcher.matches()) { LOG.debug("Unable to parse output of 'mount': {}", line); return builder.build(); } builder.setDeviceSpec(matcher.group(1)); builder.setMountPoint(matcher.group(2)); builder.setOptions(parseUnixMountOptions(matcher.group(3))); return builder.build(); } private static UnixMountInfo.Options parseUnixMountOptions(String line) { UnixMountInfo.Options.Builder builder = new UnixMountInfo.Options.Builder(); for (String option : line.split(",")) { Matcher matcher = Pattern.compile("(.*)=(.*)").matcher(option.trim()); if (matcher.matches() && matcher.group(1).equalsIgnoreCase("size")) { try { builder.setSize(FormatUtils.parseSpaceSize(matcher.group(2))); } catch (IllegalArgumentException e) { LOG.debug("Failed to parse mount point size: {}", e); } } } return builder.build(); } @NotThreadSafe private static final class Command { private String[] mCommand; private Command(String[] execString) { mCommand = execString.clone(); } /** * Runs a command and returns its stdout on success. * * @return the output * @throws ExitCodeException if the command returns a non-zero exit code */ private String run() throws ExitCodeException, IOException { Process process = new ProcessBuilder(mCommand).redirectErrorStream(true).start(); BufferedReader inReader = new BufferedReader(new InputStreamReader(process.getInputStream(), Charset.defaultCharset())); try { // read the output of the command StringBuilder output = new StringBuilder(); String line = inReader.readLine(); while (line != null) { output.append(line); output.append("\n"); line = inReader.readLine(); } // wait for the process to finish and check the exit code int exitCode = process.waitFor(); if (exitCode != 0) { throw new ExitCodeException(exitCode, output.toString()); } return output.toString(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); 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 = process.getInputStream(); synchronized (stdout) { inReader.close(); } } catch (IOException e) { LOG.warn("Error while closing the input stream", e); } process.destroy(); } } } /** * 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 { return new Command(cmd).run(); } private ShellUtils() {} // prevent instantiation }