package rhogenwizard;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SysCommandExecutor
{
// use it to run executables with "standard" command line parser
public static Decorator CRT = new Decorator(1)
{
public String decorate(String arg)
{
return decorateCrt(arg);
}
};
// Ruby on Windows uses special command line parser
public static Decorator RUBY = new Decorator(1)
{
public String decorate(String arg)
{
return decorateRuby(arg);
}
};
// Some Ruby commands on Windows are batches. This decorator is for rake,
// gem and so on.
public static Decorator RUBY_BAT = new Decorator(2)
{
public String decorate(String arg)
{
return decorateRuby(arg);
}
};
private ILogDevice m_ouputLogDevice = null;
private ILogDevice m_errorLogDevice = null;
private String m_workingDirectory = null;
private List<EnvironmentVar> m_environmentVarList = null;
private StringBuffer m_cmdOutput = null;
private StringBuffer m_cmdError = null;
private AsyncStreamReader m_cmdOutputThread = null;
private AsyncStreamReader m_cmdErrorThread = null;
public void setOutputLogDevice(ILogDevice logDevice)
{
m_ouputLogDevice = logDevice;
}
public void setErrorLogDevice(ILogDevice logDevice)
{
m_errorLogDevice = logDevice;
}
public void setWorkingDirectory(String workingDirectory)
{
m_workingDirectory = workingDirectory;
}
public void setEnvironmentVar(String name, String value)
{
if (m_environmentVarList == null)
m_environmentVarList = new ArrayList<EnvironmentVar>();
m_environmentVarList.add(new EnvironmentVar(name, value));
}
public String getCommandOutput()
{
return m_cmdOutput.toString();
}
public String getCommandError()
{
return m_cmdError.toString();
}
public Process startCommand(Decorator decorator, List<String> commandLine, String input) throws IOException
{
if (m_cmdOutput != null)
{
m_cmdOutput.delete(0, m_cmdOutput.length());
}
/* run command */
Process process = runCommandHelper(Arrays.asList(decorateCommandLine(decorator, commandLine.toArray(new String[0]))));
/* close process input stream (required for WMIC on Win32) */
new AsyncStreamWriter(process.getOutputStream(), input).start();
/* start output and error read threads */
startOutputAndErrorReadThreads(process.getInputStream(), process.getErrorStream());
return process;
}
public int runCommand(Decorator decorator, List<String> commandLine)
throws IOException, InterruptedException
{
return runCommand(decorator, commandLine, null);
}
public int runCommand(Decorator decorator, List<String> commandLine, String input)
throws IOException, InterruptedException
{
Process process = startCommand(decorator, commandLine, input);
try
{
return process.waitFor();
}
finally
{
/* notify output and error read threads to stop reading */
notifyOutputAndErrorReadThreadsToStopReading();
}
}
private Process runCommandHelper(List<String> commandLine) throws IOException
{
ProcessBuilder pb = new ProcessBuilder(commandLine);
if (m_workingDirectory != null)
{
pb.directory(new File(m_workingDirectory));
}
if (m_environmentVarList != null && !m_environmentVarList.isEmpty())
{
for (EnvironmentVar envVar : m_environmentVarList)
{
pb.environment().put(envVar.m_envName, envVar.m_envValue);
}
}
return pb.start();
}
private void startOutputAndErrorReadThreads(InputStream processOut, InputStream processErr)
{
m_cmdOutput = new StringBuffer();
m_cmdOutputThread = new AsyncStreamReader(false, processOut, m_cmdOutput, m_ouputLogDevice, "OUTPUT");
m_cmdOutputThread.start();
m_cmdError = new StringBuffer();
m_cmdErrorThread = new AsyncStreamReader(false, processErr, m_cmdError, m_errorLogDevice, "ERROR");
m_cmdErrorThread.start();
}
private void notifyOutputAndErrorReadThreadsToStopReading() throws InterruptedException
{
m_cmdOutputThread.join(1000);
if (m_cmdOutputThread.isAlive())
{
m_cmdOutputThread.stopReading();
}
m_cmdErrorThread.join(1000);
if (m_cmdErrorThread.isAlive())
{
m_cmdErrorThread.stopReading();
}
}
private static String[] decorateCommandLine(Decorator decorator, String... args)
{
if (!OSHelper.isWindows())
{
return args;
}
// Read this to understand command line decoration in Win32:
// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
// In general we have to escape command line for command line parser (1)
// and
// then we have to escape command line for command interpreter (2).
// 1
// Merge arguments into command line.
// Unfortunately there is no single standard way to escape command line
// arguments in Win32.
// Ruby parser is different from Win32 CommandLineToArgvW. We use
// decorator to implement details.
String cl = mergeArgs(decorator, args);
// 2
// Some characters (metacharacters) have special meaning for command
// interpreter.
// We have to escape them with '^' character.
for (int i = 0; i < decorator.metaCount; i++)
{
cl = cl.replaceAll("[()%!^\"<>&|]", "^$0");
}
// If argument contains space or tab then ProcessBuilder surrounds it
// with quotes.
// Otherwise we have to add quotes yourself.
// Quotes will be removed by command interpreter. See help for cmd /s
// option.
if (!containsAny(cl, " \t"))
{
cl = '"' + cl + '"';
}
return new String[] { "cmd", "/s", "/c", cl };
}
private static String mergeArgs(Decorator decorator, String[] args)
{
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String arg : args)
{
if (first)
{
first = false;
}
else
{
sb.append(' ');
}
sb.append(decorator.decorate(arg));
}
return sb.toString();
}
public static abstract class Decorator
{
// See MSDN "Parsing C++ Command-Line Arguments" article.
protected static String decorateCrt(String arg)
{
String decor = (containsAny(arg, " \t")) ? "\"" : "";
return decorate(decor, "\"", arg);
}
// See Ruby source code: rb_w32_cmdvector function in win32.c file.
protected static String decorateRuby(String arg)
{
String shortest = decorate("\"", "'\"", arg);
if (!containsAny(arg, " \t\n"))
{
String candidate = decorate("", "'\"", arg);
if (candidate.length() <= shortest.length())
{
shortest = candidate;
}
}
if (!arg.contains("'"))
{
String candidate = decorate("'", "", arg);
if (candidate.length() < shortest.length())
{
shortest = candidate;
}
}
return shortest;
}
public final int metaCount;
private Decorator(int metaCount)
{
this.metaCount = metaCount;
}
public abstract String decorate(String arg);
private static String decorate(String decor, String quotes, String text)
{
assert decor.length() <= 1;
StringBuilder sb = new StringBuilder();
sb.append(decor);
Escaper e = new Escaper(sb, quotes);
for (int i = 0; i < text.length(); i++)
{
e.append(text.charAt(i));
}
e.flush(decor.length() == 1 && quotes.contains(decor));
sb.append(decor);
return sb.toString();
}
}
private static class Escaper
{
private final StringBuilder m_sb;
private final String m_quotes;
private int m_nBackSlashes = 0;
public Escaper(StringBuilder sb, String quotes)
{
m_sb = sb;
m_quotes = quotes;
}
public void append(char c)
{
if (c == '\n')
{
throw new IllegalArgumentException();
}
else if (c == '\\')
{
++m_nBackSlashes;
}
else if (m_quotes.indexOf(c) != -1)
{
flush(true);
m_sb.append("\\");
m_sb.append(c);
}
else
{
flush(false);
m_sb.append(c);
}
}
public void flush(boolean withQuote)
{
int n = (withQuote) ? 2 * m_nBackSlashes : m_nBackSlashes;
for (int i = 0; i < n; i++)
{
m_sb.append('\\');
}
m_nBackSlashes = 0;
}
}
private static boolean containsAny(String s, CharSequence chars)
{
for (int i = 0; i < chars.length(); i++)
{
if (s.indexOf(chars.charAt(i)) != -1)
{
return true;
}
}
return false;
}
}
class EnvironmentVar
{
public String m_envName = null;
public String m_envValue = null;
public EnvironmentVar(String name, String value)
{
m_envName = name;
m_envValue = value;
}
}