/** * Original work provided by defunct 'autoandroid-1.0-rc5': http://code.google.com/p/autoandroid/ * New Derivative work required to repackage for wider distribution and continued development. * Copyright (C) SAS Institute * General Public License: http://www.opensource.org/licenses/gpl-license.php **/ package org.safs.android.auto.lib; import java.io.BufferedReader; import java.io.Flushable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.CharBuffer; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; /** Extends Process by wrapping one. */ public class Process2 { /** * Appendable sink for Appendable streams. * @see #discardStdout() * @see #discardStderr() */ private final static Appendable DEV_NULL = new Appendable() { public Appendable append(CharSequence csq) throws IOException { return this; } public Appendable append(char c) throws IOException { return this; } public Appendable append(CharSequence csq, int start, int end) throws IOException { return this; } }; /** * List of all Process2 instances created. * Used to potentially destroy (cleanup) all processes launched by a JVM instance. */ private final static Set<Process2> processes = new HashSet<Process2>(); private static boolean _jvmShutdown = false; /** * Set to true to stop the contained process from being destroyed as part of a JVM shutdown. * Default is false--allow the process to be destroyed at JVM shutdown.*/ private boolean persist = false; static { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { _jvmShutdown = true; for (Process2 process : processes) { try { if(! process.persist){ System.out.println("JVM Shutdown..."); process.destroy(); } } catch (Exception ignored) {} } } }); } private final Process process; /** * Default constructor allowing process cleanup on JVM Shutdown. * This is the same as instancing Process2(process, false) * @param process object to wrap */ public Process2(Process process) { this(process, false); } /** * Alternate constructor to allow the prevention of cleanup on JVM Shutdown. * Use this if you need to allow the underlying process to persist after shutdown. * @param process object to wrap * @param no_shutdown_hook -- true to persist the process beyond JVM shutdown. * @see #persist */ public Process2(Process process, boolean no_shutdown_hook){ persist = no_shutdown_hook; if(! persist) processes.add(this); this.process = process; } /** * Change the persist status of this process. * If persist is being set "true", then we will attempt to remove this process * from the cache of processes that will be destroyed upon JVM Shutdown. * If persist is being set "false", then we will attempt to add this process to * the cache of processes that will be destroyed upon JVM Shutdown. * @param doPersist */ public void setPersist(boolean doPersist){ persist = doPersist; if(persist) { if(!_jvmShutdown) removeJVMShutdownReference(); }else{ if(! processes.contains(this)){ if(!_jvmShutdown) processes.add(this); } } } /** * destroy() the underlying process IF the flag to persist beyond shutdown is not set. * If the flag is not set we will remove this process from the JVM shutdown hook thread * and destroy it immediately. * @see #persist */ public void destroy() { if(!persist) { removeJVMShutdownReference(); process.destroy(); } } /** * Remove this process from the JVM Shutdown Hook thread. * This will only succeed if we are not already in JVM shutdown mode. */ public void removeJVMShutdownReference(){ if(!_jvmShutdown){ try{processes.remove(this);}catch(Exception x){} } } /** * Returns the exit value for the subprocess. * @return the exit value of the subprocess represented by this Process object. * By convention, the value 0 indicates normal termination. * @throws IllegalThreadStateException - if the subprocess represented by this Process object has not yet terminated. */ public int exitValue() { return process.exitValue(); } /** * Wait/Block for the exitValue/completion from the wrapped Process. * @return this * @throws InterruptedException if thrown by the underlying process. * @see java.lang.Process#waitFor() */ public Process2 waitFor() throws InterruptedException { process.waitFor(); return this; } /** * Wait/Block for the exitValue/completion from the wrapped Process up to * the provided secsTimeout period. * @param secsTimeout -- number of seconds to wait before issuing IllegalThreadStateException. * @return this * @throws InterruptedException if thrown by the underlying process. * @throws IllegalThreadStateException if the timeout has been reached without the * process having exited. */ public Process2 waitFor(int secsTimeout) throws InterruptedException { long timeout = System.currentTimeMillis() + (1000 * secsTimeout); boolean done = false; while(!done){ try{ exitValue(); return this; }catch(IllegalThreadStateException x){} done = timeout < System.currentTimeMillis(); if(!done) try{ Thread.sleep(1000);}catch(Exception x){} } throw new IllegalThreadStateException("Process has not completed within specified timeout period."); } /** * Wait for exitValue (0) from the wrapped Process. If the exitValue is not 0 * then the routine will throw a RuntimeException. * @return this * @throws InterruptedException if thrown by the underlying process. * @throws RuntimeException if the exitValue is NOT 0 (success). * @see #waitFor() * @see java.lang.Process#waitFor() */ public Process2 waitForSuccess() throws InterruptedException { int exitValue = waitFor().exitValue(); if (exitValue != 0) throw new RuntimeException("Tool return " + exitValue); return this; } /** * Wait for exitValue (0) from the wrapped Process within the specified timeout period. * If the exitValue is not 0 then the routine will throw a RuntimeException. * @param secsTimeout -- number of seconds to wait before issuing an IllegalThreadStateException. * @return this * @throws InterruptedException if thrown by the underlying process. * @throws RuntimeException if the exitValue is NOT 0 (success). * @throws IllegalThreadStateException if the timeout is reached without process termination. * @see #waitFor(int) */ public Process2 waitForSuccess(int secsTimeout) throws InterruptedException { int exitValue = waitFor(secsTimeout).exitValue(); if (exitValue != 0) throw new RuntimeException("Tool return " + exitValue); return this; } private Writer wrap(OutputStream sink) { return new OutputStreamWriter(sink); } private Reader wrap(InputStream sink) { return new InputStreamReader(sink); } public Process getProcess(){ return process; } public OutputStream getStdin() { return process.getOutputStream(); } public Writer getStdinWriter() { return wrap(process.getOutputStream()); } public InputStream getStdout() { return process.getInputStream(); } public BufferedReader getStdoutReader() { return new BufferedReader(wrap(process.getInputStream())); } public InputStream getStderr() { return process.getErrorStream(); } public BufferedReader getStderrReader() { return new BufferedReader(wrap(process.getErrorStream())); } /** * Connect an Appendable sink to a Readable source and immediately begin * processing the source contents into the sink. * <p> * Important: A separate thread is launched to read the source and write to the * sink to prevent blocking. However, the thread only lasts for as long as there * are characters to be read. A long-running process that does not consistently * send characters can allow this thread to run to completion and exit before the * source is truly done sending output. * <p> * Set persist to true to prevent the IO reading from stopping when it otherwise * would stop. Reset persist to false to allow the thread to terminate. * @param source * @param sink * @see #persist */ private void connect(final Readable source, final Appendable sink) { Thread thread = new Thread(new Runnable() { public void run() { CharBuffer cb = CharBuffer.wrap(new char [256]); try { int numchars = source.read(cb); while(numchars != -1 || persist){ if(numchars != -1){ cb.flip(); sink.append(cb); cb.clear(); if (sink instanceof Flushable) { ((Flushable)sink).flush(); } }else{ // don't hog the cpu if there is nothing to read yet try{Thread.sleep(200);}catch(Exception x){} } numchars = source.read(cb); } } catch (IOException e) { /* prolly broken pipe, just die */ setPersist(false); } } }); thread.setDaemon(!persist); thread.start(); } public Process2 connectStdin(Readable source) { connect(source, wrap(getStdin())); return this; } public Process2 connectStdin(InputStream source) { return connectStdin(wrap(source)); } public Process2 connectStdout(Appendable sink) { connect(wrap(getStdout()), sink); return this; } public Process2 connectStderr(Appendable sink) { connect(wrap(getStderr()), sink); return this; } /** * Connects Stdout to the internal DEV_NULL sink. * @return this (from connectStdout) * @see #connectStdout(Appendable) */ public Process2 discardStdout() { return connectStdout(DEV_NULL); } /** * Connects Stderr to the internal DEV_NULL sink. * @return this (from connectStdout) * @see #connectStderr(Appendable) */ public Process2 discardStderr() { return connectStderr(DEV_NULL); } /** * Forward Process Stdin to System.in * Also invokes forwardOutput * * @return this (from forwardOutput()) * @see #forwardOutput() */ public Process2 forwardIO() { connectStdin(System.in); return forwardOutput(); } /** * Forward Process StdOut to System.out and forward StdErr to System.err. * Uses connectStdout(System.out); connectStderr(System.err); * * @return this */ public Process2 forwardOutput() { connectStdout(System.out); connectStderr(System.err); return this; } }