/*
* Copyright (c) 2014 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.services.util;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.StringBuilder;
import java.lang.Thread;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.services.util.Strings;
public class Exec {
private static final Logger _log = LoggerFactory.getLogger(Exec.class);
public static final int SIGNAL_OFFSET = 128;
public static final long DEFAULT_CMD_TIMEOUT = 10 * 1000;
private static final int _SLEEP_MS = 100;
private static final int _EXCEPTION_EXIT_VALUE = 0xffff;
private enum Termination {
_NORMAL, _SIGNAL, _TIMEOUT, _EXCEPTION
};
/***
* An immutable result object for Exec.exec(...)
*
*/
public final static class Result {
private final String[] _cmd;
private final long _timeout;
private final int _exitValue;
private final String _stdOutput;
private final String _stdError;
private final Termination _termination;
private final Pattern _maskFilter;
Result(final String[] cmd, final long timeout,
final int exitValue, final String stdOutput, final String stdError,
final Termination termination, final Pattern maskFilter) {
if(cmd == null){
_cmd = new String[0];
}else{
_cmd = Arrays.copyOf(cmd, cmd.length);
}
_timeout = timeout;
_exitValue = exitValue;
_stdOutput = stdOutput;
_stdError = stdError;
_termination = termination;
_maskFilter = maskFilter;
}
public String[] getCmd() {
return _cmd.clone();
}
/**
*
* @return exit status or signal number + SIGNAL_OFFSET
*
* This returns java.;ang.Process.exitValue(), which appears to be either
* the exit code or the number of the signal number that killed the process
* plus SIGNAL_OFFSET (128).
*
* Thus, exitVakues above 128 are ambiguous.
*
*/
public int getExitValue() {
return _exitValue;
}
/**
* @return String with the process stdout
*/
public String getStdOutput() {
return _stdOutput;
}
/***
* @return String with the process stderr
*/
public String getStdError() {
return _stdError;
}
public boolean exitedNormally() {
return _termination == Termination._NORMAL;
}
public boolean timedOut() {
return _termination == Termination._TIMEOUT;
}
public boolean signalled() {
return _termination == Termination._SIGNAL;
}
public boolean execFailed() {
return _termination == Termination._EXCEPTION;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("cmd=");
if (_cmd != null) {
for (int i = 0; i < _cmd.length; i++) {
builder.append(_cmd[i]).append(" ");
}
} else {
builder.append("null");
}
// apply maskFilter to stand output
String maskedOutput = null;
if (_maskFilter != null) {
Matcher m = _maskFilter.matcher(_stdOutput.toString());
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, "***masked***");
}
m.appendTail(sb);
maskedOutput = sb.toString();
} else {
maskedOutput = _stdOutput.toString();
}
builder.append(" timeout=").append(_timeout).append(" ms");
builder.append(" terminated=").append(_termination);
builder.append(" status=").append(_exitValue);
builder.append(" stdout=").append(Strings.repr(maskedOutput));
builder.append(" stderr=").append(Strings.repr(_stdError));
return builder.toString();
}
}
/***
* Sudo an external command providing exitStatus, stdOutput and stdError with root privilege.
*
* @param args - A command and command arguments
* @param timeout - Maximum execution time in ms
*
* @return Exec.Result object
*/
public static Result sudo(long timeout, String... args) {
return sudo(timeout, null, args);
}
/**
* maskFilter is for masking any matched string when printing stdOutput in log file.
*/
public static Result sudo(long timeout, Pattern maskFilter, String... args) {
String userName = System.getProperty("user.name");
if (userName.equals("root")) {
// Root user does not need SUDO.
return exec(timeout, maskFilter, args);
}
List<String> tmpList = new ArrayList(Arrays.asList(args));
tmpList.add(0, "sudo");
String[] newArray = tmpList.toArray(new String[tmpList.size()]);
return exec(timeout, maskFilter, newArray);
}
/***
* Exec an external command providing exitStatus, stdOutput and stdError.
*
* @param args - A command and command arguments
* @param timeout - Maximum execution time in ms
*
* @return Exec.Result object
*/
public static Result exec(long timeout, String... args) {
return exec(timeout, null, args);
}
public static Result exec(long timeout, Pattern maskFilter, String... args) {
List<String> cmdList = new ArrayList(Arrays.asList(args));
final String[] cmd = cmdList.toArray(new String[cmdList.size()]);
_log.debug("exec(): timeout=" + timeout + " cmd=" + Strings.repr(cmd));
final long endTime = (timeout <= 0) ? 0 : System.currentTimeMillis() + timeout;
InputStreamReader stdOutputStream = null;
InputStreamReader stdErrorStream = null;
StringBuilder stdOutput = new StringBuilder();
StringBuilder stdError = new StringBuilder();
try {
boolean destroyed = false;
Process p = new ProcessBuilder(cmd).start();
stdOutputStream = new InputStreamReader(p.getInputStream());
stdErrorStream = new InputStreamReader(p.getErrorStream());
while (true) {
if (!destroyed && System.currentTimeMillis() > endTime) {
_log.error("exec(): Timeout. Destroying the process.");
destroyed = true;
p.destroy();
}
boolean done = exited(p);
int n1 = tryRead(stdErrorStream, stdError);
int n2 = tryRead(stdOutputStream, stdOutput);
if (n1 == 0 && n2 == 0 && (done || destroyed)) {
break;
} else {
_log.debug("Done=" + done);
_log.debug("Destroyed=" + destroyed);
_log.debug("n1=" + n1);
_log.debug("n2=" + n2);
_log.debug("exec(): Sleep.");
sleep(_SLEEP_MS);
}
}
final int exitValue = p.exitValue();
Result result = new Result(cmd, timeout,
p.exitValue(), stdOutput.toString(), stdError.toString(),
(destroyed && exitValue != 0) ? Termination._TIMEOUT : Termination._NORMAL, maskFilter);
_log.debug("exec(): " + result);
return result;
} catch (Exception e) {
Result result = new Result(cmd, timeout,
_EXCEPTION_EXIT_VALUE, stdOutput.toString(), stdError.toString(),
Termination._EXCEPTION, maskFilter);
_log.error("exec(): " + result + " (" + e + ")");
return result;
} finally {
tryClose(stdOutputStream);
tryClose(stdErrorStream);
}
}
private static int tryRead(InputStreamReader in, StringBuilder s) throws IOException {
final char[] charBuf = new char[0x1000];
if (in.ready()) {
final int charsRead = in.read(charBuf, 0, charBuf.length);
if (charsRead > 0) {
s.append(charBuf, 0, charsRead);
}
return charsRead;
} else {
return 0;
}
}
private static boolean exited(Process p) {
try {
int exitValue = p.exitValue();
_log.debug("exit=", exitValue);
return true;
} catch (IllegalThreadStateException e) {
return false;
}
}
private static void sleep(int milliseconds) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
;
}
}
private static void tryClose(InputStreamReader in) {
try {
if (in != null) {
in.close();
}
} catch (Exception e) {
; // Ignore
}
}
}