/* * Created on Jul 16, 2003 by mschilli * */ package alma.acs.commandcenter.engine; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.Enumeration; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import java.util.concurrent.ThreadFactory; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import alma.acs.commandcenter.util.MiscUtils; /** * Describes and encapsulates a native command. A native command * triggers a process, and provides various bells and whistles * around the pure process object as provided by the Java Runtime. * * The native command can be run * a) within its own thread (as it implements Runnable), or * b) directly via a call to its run() method. * * @author mschilli */ public class NativeCommand implements Runnable { // // ============= Constants =============== // // status constants public final static String NEW = "new"; public final static String RUNNING = "running"; public final static String TERMINATED = "terminated"; public final static String CANNOTRUN = "unable to run"; public final static String TIMEOUT = "timed out"; // maxExecutionTime constant public final static long NO_TIMEOUT = -1; // // ============= Members =============== // // time watcher sleeps between its iterations static public long DEFAULT_WATCHER_INTERVAL = 1000; //msecs // time before watcher threads start static public long DEFAULT_WATCHER_DELAY = 500; // msecs // there is 1 watcher for each command protected static Timer watchers; // keep a reference to the stdin of the process protected OutputStreamWriter stdin; // there are N listeners for each command protected Vector<NativeCommand.Listener> listeners = new Vector<NativeCommand.Listener>(); // the actual description of a native command protected String command; protected Process process; protected long interval; protected long delay; protected long maxExecutionTime; protected String status; protected boolean foreground; protected String endMark; // results of execution protected Throwable latestException; protected Integer exitValue = null; // assigned if process terminates // logger protected Logger log; // // ================= Constructors ==================== // public NativeCommand(String command, boolean foreground) { this(command, foreground, NO_TIMEOUT); } public NativeCommand(String command, boolean foreground, long maxExecutionTime) { this(command, foreground, maxExecutionTime, null); } public NativeCommand(String command, boolean foreground, long maxExecutionTime, String endMark) { this(command, foreground, maxExecutionTime, endMark, DEFAULT_WATCHER_INTERVAL, DEFAULT_WATCHER_DELAY); } public NativeCommand(String command, boolean foreground, long maxExecutionTime, String endMark, long interval, long delay) { this.command = command; this.foreground = foreground; this.maxExecutionTime = maxExecutionTime; this.endMark = endMark; this.interval = interval; this.delay = delay; this.status = NEW; this.log = MiscUtils.getPackageLogger(this); } // // ================= API ==================== // /** * The default thread factory for background actions. */ protected ThreadFactory threadFactoryDefault = new ThreadFactory(){ public Thread newThread (Runnable r) { return new Thread(r); } }; protected ThreadFactory threadFactory = threadFactoryDefault; /** * This class executes various actions concurrently. * With this setter, clients can control the threads to be used. * @param threads - null for default */ public void setThreadFactory (ThreadFactory threads) { threadFactory = (threads == null)? threadFactoryDefault : threads; } public void addListener(NativeCommand.Listener po) { listeners.add(po); } public void removeListener(NativeCommand.Listener po) { listeners.remove(po); } public String getStatus() { return status; } /** * @return null if no exit value due to ungraceful process death */ public Integer getExitValue() { return exitValue; } /** * Returns the most recent occured error. Note that * the internal exception cache is reset by this method, * thus it can only be called once for each error. * @return */ public Throwable getLatestException() { Throwable ret = latestException; latestException = null; return ret; } /** * Writes the given text to the <i>STDIN<i> of the process. * @param text the input to send to the process */ public void send(String text) { try { // TODO(msc): don't log passwords !!! log.finer("writing command '"+text+"' to stdin of "+process); stdin.write(text+"\n"); stdin.flush(); } catch (IOException exc) { log.log(Level.FINE, "failed to write command '"+command+"' to stdin of "+process, exc); } } /** * We use four delegates: <ol> * <li> One to start a process * <li> One to watch its progress * <li> Two to read its output (out and err) * </ol> * * The delegates give feedback to the main thread by provoking InterruptedExceptions on it. */ public void run() { Spawner deleg1 = new Spawner(); deleg1.run(); // nothing more to do in this case if (status.equals(CANNOTRUN)) { return; } // start a watcher for the process Watcher deleg2 = new Watcher(); if (watchers == null) watchers = new Timer(); watchers.schedule(deleg2, delay, interval); // start a delegate to read the process's output stream Reader deleg3 = new Reader(process.getInputStream()); threadFactory.newThread(deleg3).start(); // start a delegate to read the process's error stream Reader deleg4 = new Reader(process.getErrorStream()); threadFactory.newThread(deleg4).start(); // store a reference to the process's input stream stdin = new OutputStreamWriter(process.getOutputStream()); /*stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));*/ // See, if we should suspend the current thread till the process terminates if (foreground) { // now send the current thread to sleep until something interesting happens // but order a wake up call before try { // Besides process termination, an expected piece of output can wake this thread. if (endMark != null) { deleg3.interruptThreadOnExpectedOutput(Thread.currentThread(), endMark); deleg4.interruptThreadOnExpectedOutput(Thread.currentThread(), endMark); } // Variant A // obviously, the process output is not yet completely flushed // at the time this call returns - so it's of limited use for us. // process.waitFor(); // Variant B // thread.suspend() is deprecated, so we use this construct while (true) { deleg2.interruptThreadOnTaskTermination(Thread.currentThread()); Thread.sleep(Long.MAX_VALUE); } } catch (InterruptedException e) { log.finer("Native command interrupted by InterruptedException"); } } } // // ================= Internal ==================== // /** */ protected void changeStatus(String newStatus) { // close the IO streams and thereby all three OS pipes // ---------------------------------------------------- // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4523660 if (TERMINATED==newStatus) { try {process.getOutputStream().close();} catch (Exception exc) {} try {process.getErrorStream().close();} catch (Exception exc) {} try {process.getInputStream().close();} catch (Exception exc) {} } String oldStatus = this.status; this.status = newStatus; fireStatusChanged (oldStatus, newStatus); } /** */ protected void fireStatusChanged (String oldStatus, String newStatus) { for (Enumeration<Listener> en = listeners.elements(); en.hasMoreElements();) { NativeCommand.Listener po = (NativeCommand.Listener) en.nextElement(); po.statusChanged(this, oldStatus); } } /** */ protected void fireOutputWritten (InputStream sourceStream, String additionalOutput) { if (additionalOutput == null || "".equals(additionalOutput)) { return; // ignore } boolean isOutputOnStdout = (sourceStream == process.getInputStream()); for (Enumeration<Listener> en = listeners.elements(); en.hasMoreElements();) { NativeCommand.Listener po = (NativeCommand.Listener) en.nextElement(); if (isOutputOnStdout) po.stdoutWritten(this, additionalOutput); else po.stderrWritten(this, additionalOutput); } } // // ================= Inner Types ==================== // /** * Interested in Processes? Be a NativeCommand Listener today! */ static public interface Listener { /** */ public void statusChanged(NativeCommand command, String oldStatus); /** @param additionalOutput one or more lines of new output (last line will not contain a line terminator) */ public void stdoutWritten(NativeCommand command, String additionalOutput); /** @param additionalOutput one or more lines of new output (last line will not contain a line terminator) */ public void stderrWritten(NativeCommand command, String additionalOutput); } /** * An empty implementation of the Listener interface. */ static public class ListenerAdapter implements Listener { public void statusChanged(NativeCommand command, String oldStatus) {} public void stdoutWritten(NativeCommand command, String additionalOutput) {} public void stderrWritten(NativeCommand command, String additionalOutput) {} } /** * Runs a process. Very little code, but it's * prettier to have a dedicated class for it. */ protected class Spawner { public void run() { try { process = Runtime.getRuntime().exec(command); changeStatus(RUNNING); } catch (IOException e) { latestException = e; changeStatus(CANNOTRUN); } } } /** * Reads a process's streams. * Can notify a suspended thread if a specified piece of output * (as regex) occurs. */ protected class Reader implements Runnable { protected Thread interruptableThread; protected Pattern expectedOutput; protected InputStream sourceStream; protected Reader(InputStream sourceStream) { this.sourceStream = sourceStream; } /** @param thread this parameter is actually redundant but makes things clearer */ public void interruptThreadOnExpectedOutput(Thread thread, String expectedRegex) { this.interruptableThread = thread; this.expectedOutput = Pattern.compile(expectedRegex); } public void run() { try { BufferedReader sourceReader = new BufferedReader(new InputStreamReader(sourceStream)); String line; // this will sleep until data becomes available while ((line = sourceReader.readLine()) != null) { // readLine() is smart: it detects any of "\n", "\r", "\r\n". // It will also strip off any of them. Thus we add // the unix line terminator again. fireOutputWritten(sourceStream, line+"\n"); if (expectedOutput != null && expectedOutput.matcher(line).matches()) interruptableThread.interrupt(); } // TODO(msc) read out standard error } catch (IOException exc) { // seems to happen when the process is willingly destroy()-ed log.fine("will stop reading from process output stream, an I/O error occurred: "+exc); } } } /** * Polls the process behavior and sends events * to Listeners if something interesting happens. */ protected class Watcher extends TimerTask { protected long startTime; protected Thread interruptableThread; /** @param thread actually redundant but makes things clearer */ public void interruptThreadOnTaskTermination(Thread thread) { this.interruptableThread = thread; } @Override public void run() { // nothing more to observe in these cases if (status.equals(TERMINATED) || status.equals(CANNOTRUN)) { this.cancel(); } // save time of very first run if (startTime == 0) startTime = System.currentTimeMillis(); // check if process return value is readable... try { int x = process.exitValue(); // yes, now spread the news changeStatus(TERMINATED); exitValue = new Integer(x); if (interruptableThread != null) interruptableThread.interrupt(); } catch (IllegalThreadStateException exc) { // ... if not, the process has not ended yet // if maxExecutionTime exists and // the process runs too long already, send event if (maxExecutionTime > NO_TIMEOUT && System.currentTimeMillis() - startTime > maxExecutionTime) { changeStatus(TIMEOUT); } } } } }