/** * Provides services to run shell processes with input/output and sudo password * authentication. * * Created : Feb 18, 2012 * * Note : The responses will not work if the process spawns a child process that * expects input. * */ package javaforce; import java.lang.reflect.*; import java.util.*; import java.io.*; public class ShellProcess { private Process p; private InputStream is, es; private OutputStream os; private StringBuffer output; private boolean active; private ArrayList<Response> script = new ArrayList<Response>(); private ArrayList<Variable> envAdd = new ArrayList<Variable>(); private ArrayList<Variable> envRemove = new ArrayList<Variable>(); private ShellProcessListener listener = null; private File path; private int errorLevel; private boolean keepOutput = true; public static boolean log = false; public static boolean logPrompt = false; public String command; private static class Response { public String prompt; public String reply; public boolean repeat, used, regex, term; }; private static class Variable { public Variable(String name, String value) { this.name = name; this.value = value; } public String name, value; } /** * Registers a response to a prompt. If repeat is true the response can be * used many times. */ public void addResponse(String prompt, String reply, boolean repeat) { Response res = new Response(); res.prompt = prompt.trim(); res.reply = reply; res.repeat = repeat; res.regex = false; res.term = false; script.add(res); } /** * Terminates the program if prompt is detected. */ public void addTerminate(String prompt) { Response res = new Response(); res.prompt = prompt.trim(); res.reply = ""; res.repeat = false; res.regex = false; res.term = true; script.add(res); } /** * Registers a response to a prompt in the form of a regex. If repeat is true * the response can be used many times. */ public void addRegexResponse(String prompt_regex, String reply, boolean repeat) { Response res = new Response(); res.prompt = prompt_regex.trim(); res.reply = reply; res.repeat = repeat; res.regex = true; res.term = false; script.add(res); } /** * Terminates the program if prompt_regex is detected. */ public void addRegexTerminate(String prompt_regex) { Response res = new Response(); res.prompt = prompt_regex.trim(); res.reply = ""; res.repeat = false; res.regex = true; res.term = true; script.add(res); } /** * The listener will receive all output. */ public void addListener(ShellProcessListener listener) { this.listener = listener; } public ShellProcessListener getListener() { return listener; } public void addEnvironmentVariable(String name, String value) { envAdd.add(new Variable(name, value)); } public void removeEnvironmentVariable(String name) { envRemove.add(new Variable(name, null)); } /** * Sets init working folder. */ public void setFolder(File path) { this.path = path; } /** * Discards all output. Note that responses are disabled when this is enabled. * This is intended for long processes that are monitored with a * ShellProcessListener */ public void keepOutput(boolean state) { keepOutput = state; } /** * See run(String cmd[], boolean redirStderr) */ public String run(List<String> cmd, boolean redirStderr) { return run(cmd.toArray(new String[0]), redirStderr); } /** * Runs a process, sending responses to stdin and returning all stdout. The * responses should cause the process to terminate. * If cmd[0] is 'sudo' then jsudo-ask is used if a password is required to run the command. * If redirStderr is true then stderr will be redir to stdout. */ public String run(String cmd[], boolean redirStderr) { if (cmd[0].equals("sudo")) { //if running sudo add -A option to use jsudo-ask to request password String newcmd[] = new String[cmd.length + 1]; newcmd[0] = "sudo"; newcmd[1] = "-A"; System.arraycopy(cmd, 1, newcmd, 2, cmd.length - 1); cmd = newcmd; } ProcessBuilder pb = new ProcessBuilder(cmd); if (redirStderr) { pb.redirectErrorStream(true); } if (path != null) { pb.directory(path); } Map<String, String> env = pb.environment(); if (cmd[0].equals("sudo")) { env.put("SUDO_ASKPASS", "/usr/bin/jsudo-ask"); } for (int a = 0; a < envAdd.size(); a++) { Variable v = envAdd.get(a); env.put(v.name, v.value); } for (int a = 0; a < envRemove.size(); a++) { Variable v = envRemove.get(a); env.remove(v.name); } command = ""; for (int a = 0; a < cmd.length; a++) { command += cmd[a] + " "; //test } output = new StringBuffer(); active = true; if (log) { String msg = "Exec :"; for (int a = 0; a < cmd.length; a++) { msg += " "; msg += cmd[a]; } JFLog.log(msg); } try { //start the process p = pb.start(); is = p.getInputStream(); os = p.getOutputStream(); es = p.getErrorStream(); //start worker thread Worker wi = new Worker(is, true); wi.start(); Worker we = null; if (!redirStderr) { we = new Worker(es, false); we.start(); } //wait for process to terminate p.waitFor(); active = false; //wait for worker threads to exit wi.join(); if (!redirStderr) { we.join(); } } catch (Exception e) { JFLog.log(e); errorLevel = -1; return null; } if (log) { JFLog.log("Exec:Complete"); } errorLevel = p.exitValue(); return output.toString(); } /** * Terminates the process. */ public void destroy() { if (p != null) { p.destroy(); } } /** * Forcibly Terminates the process. */ public void destroyForcibly() { if (p != null) { p.destroyForcibly(); } } private void _destroy() { destroy(); } /** * Returns error level from last process run. */ public int getErrorLevel() { return errorLevel; } private class Worker extends Thread { private InputStream wis; private boolean processScript; public Worker(InputStream wis, boolean processScript) { this.processScript = processScript; this.wis = wis; } public void run() { byte buf[] = new byte[1024]; int read; String str; String prompt; int in, ir; Response res; int pidx = 0; try { while ((active) || (wis.available() > 0)) { //InputStream may have more data after process is inactive!!! read = wis.read(buf); if (read == -1) { break; } if (read == 0) { continue; } str = new String(buf, 0, read); if (log) { JFLog.log(str); } if (listener != null) { try { listener.shellProcessOutput(str); } catch (Exception e1) { } } if (!keepOutput) { continue; } if (!processScript) { continue; } output.append(str); //check for possible response prompt = output.substring(pidx).trim(); if (prompt.length() == 0) { continue; } in = prompt.lastIndexOf("\n"); ir = prompt.lastIndexOf("\r"); if ((in != -1) && (in > ir)) { prompt = prompt.substring(in + 1).trim(); } if ((ir != -1) && (ir > in)) { prompt = prompt.substring(ir + 1).trim(); } if (logPrompt) { JFLog.log("prompt=" + prompt); } for (int a = 0; a < script.size(); a++) { res = script.get(a); if (res.used) { continue; } boolean matches = false; if (res.regex) { matches = prompt.matches(res.prompt); } else { matches = prompt.equals(res.prompt); } if (matches) { pidx = output.length(); //avoid reusing a part of this prompt if (res.term) { _destroy(); return; } if (log) { JFLog.log(res.reply); } os.write(res.reply.getBytes()); os.flush(); if (!res.repeat) { res.used = true; } break; } } } } catch (Exception e) { // if (log) JFLog.log(e); } if (log) { JFLog.log("ShellProcess:Worker thread exiting"); } } } public OutputStream getOutputStream() { return os; } public boolean isAlive() { if (p == null) return false; return p.isAlive(); } public Process getProcess() { return p; } }