package interactivespaces.util.command.expect; import interactivespaces.InteractiveSpacesException; import interactivespaces.util.resource.ManagedResource; import com.google.common.collect.Lists; import org.apache.commons.logging.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.Pipe; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Provides similar functions as the Unix Expect tool.<br> * There are two ways to create an Expect object: a constructor that takes an * {@link InputStream} handle and {@link OutputStream} handle; or spawning a * process by providing a comamnd String. <br> * <br> * The API is loosely based on Perl Expect library:<br> * <a href="http://search.cpan.org/~rgiersig/Expect-1.15/Expect.pod"> * http://search.cpan.org/~rgiersig/Expect-1.15/Expect.pod</a> * * <p> * If you are not familiar with the Tcl version of Expect, take a look at:<br> * <a href="http://oreilly.com/catalog/expect/chapter/ch03.html"> * http://oreilly.com/catalog/expect/chapter/ch03.html</a> <br> * <br> * Expect uses a thread to convert InputStream to a SelectableChannel; other * than this, no multi-threading is used.<br> * A call to expect() will block for at most timeout seconds. Expect is not * designed to be thread-safe, in other words, do not call methods of the same * Expect object in different threads. * * <p> * Expect return values either give success or say what error condition * happened. * * @author Ronnie Dong * @author Trevor Pering -- modified for use with Interactive Spaces * @author Keith M. Hughes even more IS changes */ public class Expect implements ManagedResource { /** * Creates an Expect object by spawning a command.<br> * To Linux users, perhaps you need to use "bash -i" if you want to spawn * Bash. * * <p> * Note: error stream of the process is redirected to output stream. * * @param command * the command to run in the process * @param executorService * the executor service to use * @param log * the log to use * * @return expect object created using the input and output handles from the * spawned process * * @throws InteractiveSpacesException * the process didn't start up properly */ public static Expect spawn(String command, ScheduledExecutorService executorService, Log log) throws InteractiveSpacesException { try { ProcessBuilder pb = new ProcessBuilder(command.split(" ")); pb.redirectErrorStream(true); Process p; p = pb.start(); Expect expect = new Expect(p.getInputStream(), p.getOutputStream(), executorService, log); expect.setProcess(p); return expect; } catch (Exception e) { throw new InteractiveSpacesException("Could not spawn expect process", e); } } /** * Successful return. */ public static final int RETV_OK = 0; /** * Timeout error. */ public static final int RETV_TIMEOUT = -1; /** * EOF error. */ public static final int RETV_EOF = -2; /** * IOException error. */ public static final int RETV_IOEXCEPTION = -9; /** * A stream to duplicate output to. */ private static PrintStream duplicatedTo = null; /** * The default timeout, in milliseconds. */ private long defaultTimeout = 60 * 1000; /** * {@code true} if calls should throw an exception on non-successful return * values. */ private boolean throwOnError = false; /** * {@code true} if the timeout should be reset every time new input is * received. */ private boolean restartTimeoutUponReceive = false; /** * Buffer for storing responses. */ private final StringBuffer buffer = new StringBuffer(); /** * {@true} if should not transfer previous content. */ private boolean noTransfer = false; /** * String before the last match (if there was a match), updated after each * expect() call. */ private String before; /** * String representing the last match (if there was a match), updated after * each expect() call. */ private String match; /** * {@code true} if the last match was successful, updated after each expect() * call. */ private boolean success = false; /** * The input stream for detecting what we expect. */ private final InputStream input; /** * The input stream for detecting what we expect. */ private final OutputStream output; /** * A selectable channel, created around the given input stream. */ private Pipe.SourceChannel inputChannel; /** * A selector used for detecting input on the input channel. */ private Selector selector; /** * The potential process being read to get the expect responses. */ private Process process = null; /** * The executor service to use. */ private final ScheduledExecutorService executorService; /** * Logging class for managing log output. */ private final Log log; /** * If an IOException is thrown during an expt call this will be that * exception. */ private IOException thrownIOException; /** * The future for controlling the thread which is reading information from the * input. */ private Future<?> pipingFuture; /** * Construct an Expect object that can be used to manage communication to/from * the given streams. * * @param input * input stream for what to expect * @param output * output stream for sending commands * @param executorService * the executor service to use * @param log * logger used for logging */ public Expect(InputStream input, OutputStream output, ScheduledExecutorService executorService, Log log) { this.executorService = executorService; this.input = input; this.output = output; this.log = log; } @Override public void startup() { Pipe.SourceChannel inputChannel = null; Selector selector = null; try { inputChannel = inputStreamToSelectableChannel(input); selector = Selector.open(); inputChannel.register(selector, SelectionKey.OP_READ); } catch (IOException e) { close(); throw new InteractiveSpacesException("Fatal error when initializing pipe or selector", e); } this.inputChannel = inputChannel; this.selector = selector; } @Override public void shutdown() { if (pipingFuture != null) { pipingFuture.cancel(true); pipingFuture = null; close(); } } /** * Get the string before the current match. * * @return the string before a match */ public String getBefore() { return before; } /** * Get the current match string. * * @return the current match string */ public String getMatch() { return match; } /** * Is the current match a success? * * @return {@code true} if a success */ public boolean isSuccess() { return success; } /** * Converts an {@link InputStream} to a {@link SelectableChannel}. A thread is * created to read from the InputStream, and write to a pipe. The source of * the pipe is returned as an input handle from which you can perform * unblocking read. The thread will terminate when reading EOF from * InputStream, or when InputStream is closed, or when the returned Channel is * closed(pipe broken). * * @param input * the input stream to be read from * * @return a non-blocking channel to be read from * * @throws IOException * most unlikely */ private Pipe.SourceChannel inputStreamToSelectableChannel(final InputStream input) throws IOException { Pipe pipe = Pipe.open(); pipe.source().configureBlocking(false); final OutputStream out = Channels.newOutputStream(pipe.sink()); Runnable piping = new Runnable() { @Override public void run() { byte[] buffer = new byte[1024]; try { for (int n = 0; n != -1; n = input.read(buffer)) { out.write(buffer, 0, n); if (duplicatedTo != null) { String toWrite = new String(buffer, 0, n); // no exception will be thrown duplicatedTo.append(toWrite); } } log.debug("EOF from InputStream"); // now that input has EOF, close it. other than this, do not close // input input.close(); } catch (IOException e) { log.warn("IOException when piping from InputStream, " + "now the piping thread will end", e); } finally { log.debug("closing sink of the pipe"); try { output.close(); } catch (IOException e) { log.warn("Trouble closing Expect output", e); } } } }; pipingFuture = executorService.submit(piping); return pipe.source(); } /** * @return the spawned process, if this {@link Expect} object is created by * spawning a process */ public Process getProcess() { return process; } /** * Set the process that is being read. * * @param process * the spawned process, if this {@link Expect} object is created by * spawning a process */ void setProcess(Process process) { this.process = process; } /** * Send the string. * * @param str * the string to send */ public void send(String str) { this.send(str.getBytes()); } /** * Send the string with a new line on the end. * * @param str * the string to send */ public void sendLn(String str) { String strLn = str + "\r"; this.send(strLn.getBytes()); } /** * Send the byte array on the expect output. * * @param toWrite * Write a byte array to the output handle, notice flush() */ public void send(byte[] toWrite) { log.info("sending: " + bytesToPrintableString(toWrite)); try { output.write(toWrite); output.flush(); } catch (IOException e) { log.error("Error when sending bytes to output", e); } } /** * match a sequence of patterns. * * <p> * Use the default timeout. * * @param patterns * the patterns in the order to match, can be strings of regex * patterns or actual {@code java.util.regex.Pattern} instances. * * @return the expect return value * * @throws Exception * exception if return values should throw exceptions */ public int expect(Object... patterns) throws Exception { return expect(defaultTimeout, patterns); } /** * Match a series of patterns. * * @param timeout * timeout for input in milliseconds * @param patterns * the patterns in the order to match, can be strings of regex * patterns or actual {@code java.util.regex.Pattern} instances. * * @return the expect return value * * @throws Exception * exception if return values should throw exceptions */ public int expect(long timeout, Object... patterns) throws Exception { List<Pattern> list = Lists.newArrayList(); for (Object o : patterns) { if (o instanceof String) { list.add(Pattern.compile(Pattern.quote((String) o))); } else if (o instanceof Pattern) { list.add((Pattern) o); } else { log.warn("Object " + o.toString() + " (class: " + o.getClass().getName() + ") is neither a String nor " + "a java.util.regex.Pattern, using as a literal String"); list.add(Pattern.compile(Pattern.quote(o.toString()))); } } return expect(timeout, list); } /** * Expect will wait for the input handle to produce one of the patterns in the * list. If a match is found, this method returns immediately; otherwise, the * methods waits for up to timeout seconds, then returns. If timeout is less * than or equal to 0 Expect will check one time to see if the internal buffer * contains the pattern. * * @param timeout * timeout in seconds * @param list * List of Java {@link Pattern}s used for matching the input stream * * @return position of the matched pattern within the list (starting from 0); * or a negative number if there is an IOException, EOF or timeout * * @throws Exception * exception if return values should throw exceptions */ public int expect(long timeout, List<Pattern> list) throws Exception { return maybeThrow(expectInternal(timeout, list)); } /** * Match a collection of regex patterns. * * @param timeout * the timeout to wait for responses * @param list * the commands * * @return the return value for the result */ private int expectInternal(long timeout, List<Pattern> list) { log.info("Expecting " + list); clearGlobalVariables(); long endTime = System.currentTimeMillis() + timeout; try { ByteBuffer bytes = ByteBuffer.allocate(1024); int n; while (true) { for (int i = 0; i < list.size(); i++) { log.trace("trying to match " + list.get(i) + " against buffer \"" + buffer + "\""); Matcher m = list.get(i).matcher(buffer); if (m.find()) { log.trace("success!"); int matchStart = m.start(), matchEnd = m.end(); this.before = buffer.substring(0, matchStart); this.match = m.group(); this.success = true; if (!noTransfer) { buffer.delete(0, matchEnd); } return i; } } long waitTime = endTime - System.currentTimeMillis(); if (restartTimeoutUponReceive) { waitTime = timeout; } if (waitTime <= 0) { log.debug("Timeout when expecting " + list); return RETV_TIMEOUT; } selector.select(waitTime); if (selector.selectedKeys().size() == 0) { log.debug("Timeout when expecting " + list); return RETV_TIMEOUT; } selector.selectedKeys().clear(); if ((n = inputChannel.read(bytes)) == -1) { log.debug("EOF when expecting " + list); return RETV_EOF; } StringBuilder tmp = new StringBuilder(); for (int i = 0; i < n; i++) { buffer.append((char) bytes.get(i)); byteToPrintableString(tmp, bytes.get(i)); } log.debug("Obtained following from InputStream: " + tmp); bytes.clear(); } } catch (IOException e) { log.error("IOException when selecting or reading", e); thrownIOException = e; return RETV_IOEXCEPTION; } } /** * Check for an expected EOF. * * @param timeout * the timeout to wait for the EOF, in milliseconds * * @return the expect return value * * @throws Exception * exception if return values should throw exceptions */ public int expectEOF(long timeout) throws Exception { int retv = RETV_OK; try { retv = expect(timeout, new ArrayList<Pattern>()); } catch (EOFException e) { // This is actually what we expect! } if (retv == RETV_EOF) { success = true; before = this.buffer.toString(); buffer.delete(0, buffer.length()); } try { return maybeThrow(retv); } catch (EOFException e) { throw new RuntimeException("Shouldn't throw EOF here", e); } } /** * Check for an expected EOF within the default timeout. * * @return the expect return value * * @throws Exception * exception if return values should throw exceptions */ public int expectEOF() throws Exception { return expectEOF(defaultTimeout); } /** * Throws checked exceptions when expectEOF was not successful. * * @param timeout * the timeout to wait for the EOF * * @return a successful return value * * @throws Exception * exception if something bad happened */ public int expectEOFOrThrow(long timeout) throws Exception { int retv = expectEOF(timeout); if (retv == RETV_TIMEOUT) { throw new TimeoutException(); } else if (retv == RETV_IOEXCEPTION) { throw thrownIOException; } return retv; } /** * Expect an EOF. The default timeout is used. * * @return the expect return value * * @throws Exception * exception if return values should throw exceptions */ public int expectEOFOrThrow() throws Exception { return expectEOFOrThrow(defaultTimeout); } /** * Throw an error based on the return value if throw on error was requested. * * @param retValue * the return value * * @return the return value if a throw was not requested * * @throws Exception * the exception thrown */ private int maybeThrow(int retValue) throws Exception { if (!success && throwOnError) { switch (retValue) { case RETV_TIMEOUT: throw new TimeoutException(); case RETV_EOF: throw new EOFException(); case RETV_IOEXCEPTION: throw thrownIOException; default: throw new RuntimeException("Unknown error code " + retValue); } } return retValue; } /** * This method calls {@link #expect(int, Object...) expect(timeout, patterns)} * , and throws checked exceptions when expect was not successful. Useful when * you want to simplify error handling: for example, when you send a series of * commands to an SSH server, you expect a prompt after each send, however the * server may die or the prompt may take forever to appear, you would want to * skip the following commands if those occurred. In such a case this method * will be handy. * * @param timeout * the time, in milliseconds, to wait for responses * @param patterns * the patterns in the order to match, can be strings of regex * patterns or actual {@code java.util.regex.Pattern} instances. * * @return the return code for the match * * @throws Exception * exception if return values should throw exceptions */ public int expectOrThrow(long timeout, Object... patterns) throws Exception { int retv = expect(timeout, patterns); switch (retv) { case RETV_TIMEOUT: throw new TimeoutException(); case RETV_EOF: throw new EOFException(); case RETV_IOEXCEPTION: throw thrownIOException; default: return retv; } } /** * Expect a series of patterns. Use the default timeout. * * @param patterns * the patterns in the order to match, can be strings of regex * patterns or actual {@code java.util.regex.Pattern} instances. * * @return the return code * * @throws Exception * exception if return values should throw exceptions */ public int expectOrThrow(Object... patterns) throws Exception { return expectOrThrow(defaultTimeout, patterns); } /** * Clear all variables that the expect keeps. */ private void clearGlobalVariables() { success = false; match = null; before = null; } /** * The OutputStream passed to Expect constructor is closed; the InputStream is * not closed (there is no need to close the InputStream).<br> * It is suggested that this method be called after the InputStream has come * to EOF. For example, when you connect through SSH, send an "exit" command * first, and then call this method.<br> * <br> * * When this method is called, the thread which write to the sink of the pipe * will end. */ public void close() { try { output.close(); } catch (IOException e) { log.warn("Exception when closing OutputStream", e); } try { inputChannel.close(); } catch (IOException e) { log.warn("Exception when closing input Channel", e); } } /** * Get the default timeout for responses. * * @return the timeout, in milliseconds */ public long getDefaultTimeout() { return defaultTimeout; } /** * Set the default timeout for responses. * * @param defaultTimeout * the timeout, in milliseconds */ public void setDefaultTimeout(long defaultTimeout) { this.defaultTimeout = defaultTimeout; } /** * Set if the expect should throw exceptions on errors. * * @param throwOnError * {@code true} if exceptions will be thrown */ public void setThrowOnError(boolean throwOnError) { this.throwOnError = throwOnError; } /** * Is the expect throwing exceptions on errors? * * @return {@code true} if exceptions will be thrown */ public boolean isThrowOnError() { return throwOnError; } /** * Should the timeout be reset when new input is received? * * @return {@code true} if the timeout should be reset */ public boolean isRestartTimeoutUponReceive() { return restartTimeoutUponReceive; } /** * Set whether the timeout should be reset when new input is received? * * @param restartTimeoutUponReceive * {@code true} if the timeout should be reset */ public void setRestartTimeoutUponReceive(boolean restartTimeoutUponReceive) { this.restartTimeoutUponReceive = restartTimeoutUponReceive; } /** * Set whether previous content should not be transfered. * * @param noTransfer * {@true} if should not transfer previous content */ public void setNoTransfer(boolean noTransfer) { this.noTransfer = noTransfer; } /** * Should previous content not be transfered? * * @return {@true} if should not transfer previous content */ public boolean isNoTransfer() { return noTransfer; } /** * Convert a byte array to a string, each byte is converted to an ASCII * character, if the byte represents a control character, it is replaced by a * printable caret notation <a href="http://en.wikipedia.org/wiki/ASCII"> * http://en.wikipedia.org/wiki/ASCII </a>, or an escape code if possible. * * @param bytes * bytes to be printed * * @return string representation of the byte array */ private String bytesToPrintableString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { byteToPrintableString(sb, b); } return sb.toString(); } /** * Add the byte to the builder as part of a string. * * <p> * Special characters are escaped. * * @param buffer * the builder being added to * @param b * the byte to change */ private void byteToPrintableString(StringBuilder buffer, byte b) { String s = new String(new byte[] { b }); if (b >= 0 && b < 32) { buffer.append("^").append((char) (b + 64)); } else if (b == 127) { buffer.append("^?"); } else if (b == 9) { buffer.append("\\t"); } else if (b == 10) { buffer.append("\\n"); } else if (b == 13) { buffer.append("\\r"); } else { buffer.append((char) b); } } /** * The exception if a timeout occurred while waiting for input. */ @SuppressWarnings("serial") public static class TimeoutException extends Exception { } /** * The exception if EOF was reached before the match. */ @SuppressWarnings("serial") public static class EOFException extends Exception { } }