diff --git a/src/main/groovy/org/hidetake/groovy/ssh/extension/Shell.groovy b/src/main/groovy/org/hidetake/groovy/ssh/extension/Shell.groovy
index def3782f..fa9edf87 100644
--- a/src/main/groovy/org/hidetake/groovy/ssh/extension/Shell.groovy
+++ b/src/main/groovy/org/hidetake/groovy/ssh/extension/Shell.groovy
@@ -20,4 +20,12 @@ trait Shell implements SessionExtension {
assert settings != null, 'settings must not be null'
operations.shell(operationSettings + new OperationSettings(settings))
}
+
+ /**
+ * Performs an expect-like session
+ * @param interaction sequence of 'send' and 'expect' instructions
+ */
+ void shellExpect(Closure interaction){
+ operations.shellExpect(interaction)
+ }
}
\ No newline at end of file
diff --git a/src/main/groovy/org/hidetake/groovy/ssh/operation/DefaultOperations.groovy b/src/main/groovy/org/hidetake/groovy/ssh/operation/DefaultOperations.groovy
index 30c06375..ce8e3a06 100644
--- a/src/main/groovy/org/hidetake/groovy/ssh/operation/DefaultOperations.groovy
+++ b/src/main/groovy/org/hidetake/groovy/ssh/operation/DefaultOperations.groovy
@@ -10,6 +10,8 @@ import org.hidetake.groovy.ssh.extension.settings.LocalPortForwardSettings
import org.hidetake.groovy.ssh.extension.settings.RemotePortForwardSettings
import org.hidetake.groovy.ssh.interaction.Interaction
import org.hidetake.groovy.ssh.session.BadExitStatusException
+import org.hidetake.groovy.ssh.operation.expect.Expect
+import com.jcraft.jsch.ChannelShell
import static org.hidetake.groovy.ssh.util.Utility.callWithDelegate
@@ -208,4 +210,20 @@ class DefaultOperations implements Operations {
break
}
}
+
+
+ @Override
+ void shellExpect(Closure interaction) {
+ ChannelShell channel = connection.createShellChannel(null)
+ Expect expectObj = new Expect(channel.getInputStream(), channel.getOutputStream())
+ channel.connect()
+ interaction.delegate = expectObj
+ try {
+ interaction.call()
+ }
+ finally {
+ expectObj.close()
+ channel.disconnect()
+ }
+ }
}
diff --git a/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperations.groovy b/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperations.groovy
index 4e13ab8e..f50d4da5 100644
--- a/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperations.groovy
+++ b/src/main/groovy/org/hidetake/groovy/ssh/operation/DryRunOperations.groovy
@@ -5,6 +5,7 @@ import org.hidetake.groovy.ssh.extension.settings.LocalPortForwardSettings
import org.hidetake.groovy.ssh.core.settings.OperationSettings
import org.hidetake.groovy.ssh.core.Remote
import org.hidetake.groovy.ssh.extension.settings.RemotePortForwardSettings
+import org.hidetake.groovy.ssh.operation.expect.DryRunExpect
/**
* Dry-run implementation of {@link Operations}.
@@ -25,6 +26,13 @@ class DryRunOperations implements Operations {
log.info("[dry-run] Executing a shell")
}
+ @Override
+ void shellExpect(Closure interaction) {
+ DryRunExpect expectObj = new DryRunExpect()
+ interaction.delegate = expectObj
+ interaction.call()
+ }
+
@Override
String execute(OperationSettings settings, String command, Closure callback) {
log.info("[dry-run] Executing the command ($command)")
diff --git a/src/main/groovy/org/hidetake/groovy/ssh/operation/Operations.groovy b/src/main/groovy/org/hidetake/groovy/ssh/operation/Operations.groovy
index 817efe0a..e23a939d 100644
--- a/src/main/groovy/org/hidetake/groovy/ssh/operation/Operations.groovy
+++ b/src/main/groovy/org/hidetake/groovy/ssh/operation/Operations.groovy
@@ -30,4 +30,6 @@ interface Operations {
* @return result of the closure
*/
def sftp(@DelegatesTo(SftpOperations) Closure closure)
+
+ void shellExpect(Closure interaction)
}
diff --git a/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/DryRunExpect.groovy b/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/DryRunExpect.groovy
new file mode 100644
index 00000000..fa5097fb
--- /dev/null
+++ b/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/DryRunExpect.groovy
@@ -0,0 +1,20 @@
+package org.hidetake.groovy.ssh.operation.expect
+
+import groovy.util.logging.Slf4j
+
+/**
+ * Dry-run implementation of {@link Expect}.
+ *
+ * @author Romano Zabini
+ */
+@Slf4j
+class DryRunExpect {
+
+ def expectOrThrow(int timeout, Object... patterns){
+ log.info("waiting {} seconds for: {}", timeout, patterns)
+ }
+
+ def send(String command){
+ log.info("sending command: {}", command)
+ }
+}
diff --git a/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/Expect.groovy b/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/Expect.groovy
new file mode 100644
index 00000000..c8e5054f
--- /dev/null
+++ b/src/main/groovy/org/hidetake/groovy/ssh/operation/expect/Expect.groovy
@@ -0,0 +1,454 @@
+package org.hidetake.groovy.ssh.operation.expect
+
+import groovy.util.logging.Slf4j
+
+import java.nio.ByteBuffer
+import java.nio.channels.Channels
+import java.nio.channels.Pipe
+import java.nio.channels.SelectionKey
+import java.nio.channels.Selector
+import java.util.logging.Level
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * Provides similar functions as the Unix Expect tool.
+ * 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.
+ *
+ * The API is loosely based on Perl Expect library:
+ *
+ * http://search.cpan.org/~rgiersig/Expect-1.15/Expect.pod
+ *
+ *
+ * If you are not familiar with the Tcl version of Expect, take a look at:
+ *
+ * http://oreilly.com/catalog/expect/chapter/ch03.html
+ *
+ * Expect uses a thread to convert InputStream to a SelectableChannel; other
+ * than this, no multi-threading is used.
+ * 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.
+ *
+ * @author Ronnie Dong
+ * @version 1.1
+ * @author Romano Zabini
+ * @version 1.1.1 (groovy porting)
+ */
+@Slf4j
+public class Expect {
+
+ private OutputStream output;
+ protected Pipe.SourceChannel inputChannel;
+
+ protected Selector selector;
+
+ public Expect(InputStream input, OutputStream output) {
+ try {
+ this.inputChannel = inputStreamToSelectableChannel(input);
+ selector = Selector.open();
+ inputChannel.register(selector, SelectionKey.OP_READ);
+ } catch (IOException e) {
+ log.error(e.getMessage());
+ }
+ this.output = output;
+ }
+
+
+
+ /**
+ * Essentially, this method converts an {@link java.io.InputStream} to a
+ * . 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
+ * @return a non-blocking Channel you can read from
+ * @throws java.io.IOException
+ * most unlikely
+ *
+ */
+ private static Pipe.SourceChannel inputStreamToSelectableChannel(
+ final InputStream input) throws IOException {
+ Pipe pipe = Pipe.open();
+ pipe.source().configureBlocking(false);
+ final OutputStream out = Channels.newOutputStream(pipe.sink());
+ Thread piping = new Thread(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);
+ duplicatedTo.append(toWrite); // no Exception will be thrown
+ }
+ }
+ log.info("EOF from InputStream");
+ input.close(); // now that input has EOF, close it.
+ // other than this, do not close input
+ } catch (IOException e) {
+ log.warn( e.getMessage());
+ } finally {
+ try {
+ log.info("closing sink of the pipe");
+ out.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ });
+ piping.setName("Piping InputStream to SelectableChannel Thread");
+ piping.setDaemon(true);
+ piping.start();
+ return pipe.source();
+ }
+
+ public void send(String str){
+ sendInternal(String.format("%s\n", str));
+ }
+
+ /**
+ * @param str
+ * Convenience method to send a string to output handle
+ */
+ private void sendInternal(String str) {
+ this.send(str.getBytes());
+ }
+
+ /**
+ * @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( e.getMessage());
+ }
+ }
+
+ protected int defaultTimeout = 60;
+ protected boolean restartTimeoutUponReceive = false;
+ protected StringBuffer buffer = new StringBuffer();
+ protected boolean notransfer = false;
+
+ /**String before the last match(if there was a match),
+ * updated after each expect() call*/
+ public String before;
+ /**String representing the last match(if there was a match),
+ * updated after each expect() call*/
+ public String match;
+ public String[] groups;
+ /**Whether the last match was successful,
+ * updated after each expect() call*/
+ public boolean isSuccess = false;
+
+ public static final int RETV_TIMEOUT = -1, RETV_EOF = -2,
+ RETV_IOEXCEPTION = -9;
+
+ /**
+ * Convenience method, same as calling {@link #expect(int, java.lang.Object...)
+ * expect(defaultTimeout, patterns)}
+ *
+ * @param patterns
+ * @return
+ */
+ public int expect(Object... patterns) {
+ return expect(defaultTimeout, patterns);
+ }
+
+ /**
+ * Convenience method, internally it constructs a List{@literal }
+ * using the object array, and call {@link #expect(int, java.util.List) } using the
+ * List. The {@link java.lang.String}s in the object array will be treated as
+ * literals; meanwhile {@link java.util.regex.Pattern}s will be directly added to the List.
+ * If the array contains other objects, they will be converted by
+ * {@link #toString()} and then used as literal strings.
+ *
+ * @param patterns
+ * @return
+ */
+ public int expect(int timeout, Object... patterns) {
+ ArrayList list = new ArrayList();
+ for (Object o : patterns) {
+ if (o instanceof String || o instanceof GString)
+ list.add(Pattern.compile(Pattern.quote((String) o))); // requires 1.5 and up
+ 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 java.util.regex.Pattern}s used for match the internal
+ * buffer obtained by reading the InputStream
+ * @return position of the matched pattern within the list (starting from
+ * 0); or a negative number if there is an IOException, EOF or
+ * timeout
+ */
+ public int expect(int timeout, List list) {
+ log.debug("Expecting " + list);
+
+ clearGlobalVariables();
+ long endTime = System.currentTimeMillis() + (long)timeout * 1000;
+
+ try {
+ ByteBuffer bytes = ByteBuffer.allocate(1024);
+ int n;
+ while (true) {
+ for (int i = 0; i < list.size(); i++) {
+ log.info( "trying to match " + list.get(i)
+ + " against buffer \"" + buffer + "\"");
+ Matcher m = list.get(i).matcher(buffer);
+ if (m.find()) {
+ log.debug("success!");
+ storeMatchingOutput(m)
+ this.isSuccess = true;
+ if(!notransfer)buffer.delete(0, m.end());
+ return i;
+ }
+ }
+
+ long waitTime = endTime - System.currentTimeMillis();
+ if (restartTimeoutUponReceive)
+ waitTime = timeout * 1000;
+ if (waitTime <= 0) {
+ log.log(Level.FINE,"Timeout when expecting " + list);
+ return RETV_TIMEOUT;
+ }
+
+ selector.select(waitTime);
+ //System.out.println(selector.selectedKeys().size());
+ if (selector.selectedKeys().size() == 0) {
+ //break; //we can directly "break" here
+ log.debug("Timeout when expecting " + list);
+ return RETV_TIMEOUT;
+ }
+ selector.selectedKeys().clear();
+ if ((n = inputChannel.read(bytes)) == -1) {
+ //break;
+ 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));
+ tmp.append(byteToPrintableString(bytes.get(i)));
+ }
+ log.info("$tmp".replaceAll("\\\\r\\\\n","\r\n"));
+
+
+ bytes.clear();
+ }
+ } catch (IOException e) {
+ log.error( e.getMessage());
+ thrownIOE = e;
+ return RETV_IOEXCEPTION;
+ }
+
+ }
+
+ private void storeMatchingOutput(Matcher m) {
+ this.before = buffer.substring(0, m.start());
+ this.match = m.group();
+ this.groups = new String[m.groupCount() + 1];
+ for (int k = 0; k <= m.groupCount(); ++k)
+ this.groups[k] = m.group(k);
+ }
+
+ /**
+ * Convenience method, internally it calls {@link #expect(int, java.util.List)
+ * expect(timeout, new ArrayList<Pattern>())}. Given an empty list,
+ * {@link #expect(int, java.util.List)} will not perform any regex matching, therefore
+ * the only conditions for it to return is EOF or timeout (or IOException).
+ * If EOF is detected, {@link #isSuccess} and {@link #before} are properly
+ * set.
+ *
+ * @param timeout
+ * @return same as return value of {@link #expect(int, java.util.List)}
+ */
+ public int expectEOF(int timeout) {
+ int retv = expect(timeout, new ArrayList());
+ if (retv == RETV_EOF) {
+ this.isSuccess = true;
+ this.before = this.buffer.toString();
+ this.buffer.delete(0, buffer.length());
+ }
+ return retv;
+ }
+ /**Convenience method, same as calling {@link #expectEOF(int)
+ * expectEOF(defaultTimeout)}*/
+ public int expectEOF() {
+ return expectEOF(defaultTimeout);
+ }
+
+ /**
+ * Throws checked exceptions when expectEOF was not successful.
+ */
+ public int expectEOFOrThrow(int timeout) throws TimeoutException,
+ IOException {
+ int retv = expectEOF(timeout);
+ if (retv == RETV_TIMEOUT)
+ throw new TimeoutException("timeout in waiting for EOF");
+ if (retv == RETV_IOEXCEPTION)
+ throw thrownIOE;
+ return retv;
+ }
+ /**Convenience method, same as calling {@link #expectEOF(int)
+ * expectEOF(defaultTimeout)}*/
+ public int expectEOFOrThrow() throws TimeoutException, IOException {
+ return expectEOFOrThrow(defaultTimeout);
+ }
+
+ /**useful when calling {@link #expectOrThrow(int, java.lang.Object...)}*/
+ protected IOException thrownIOE;
+
+ /**
+ * This method calls {@link #expect(int, java.lang.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
+ * @param patterns
+ * @throws TimeoutException
+ * when expect times out
+ * @throws EOFException
+ * when EOF is encountered
+ * @throws java.io.IOException
+ * when there is a problem reading from the InputStream
+ * @return same as {@link #expect(int, java.lang.Object...) expect(timeout, patterns)}
+ */
+ public int expectOrThrow(int timeout, Object... patterns)
+ throws TimeoutException, EOFException, IOException {
+ int retv = expect(timeout, patterns);
+ switch (retv) {
+ case RETV_TIMEOUT:
+ throw new TimeoutException("could not match patterns:${patterns} in $timeout seconds".toString());
+ case RETV_EOF:
+ throw new EOFException();
+ case RETV_IOEXCEPTION:
+ throw thrownIOE;
+ default:
+ return retv;
+ }
+ }
+ /**Convenience method, same as calling {@link #expectOrThrow(int, java.lang.Object...)
+ * expectOrThrow(defaultTimeout, patterns)}*/
+ public int expectOrThrow(Object... patterns) throws TimeoutException,
+ EOFException, IOException {
+ return expectOrThrow(defaultTimeout, patterns);
+ }
+
+ protected void clearGlobalVariables() {
+ isSuccess = 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).
+ * 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.
+ *
+ *
+ * When this method is called, the thread which write to the sink of the
+ * pipe will end.
+ */
+ public void close() {
+ try {
+ this.output.close();
+ } catch (IOException e) {
+ log.warning(e.getMessage());
+ }
+ try {
+ this.inputChannel.close();
+ } catch (IOException e) {
+ log.warning(e.getMessage());
+ }
+ }
+
+ public int getDefault_timeout() {
+ return defaultTimeout;
+ }
+ public void setDefault_timeout(int default_timeout) {
+ this.defaultTimeout = default_timeout;
+ }
+ public boolean isRestart_timeout_upon_receive() {
+ return restartTimeoutUponReceive;
+ }
+ public void setRestart_timeout_upon_receive(boolean restart_timeout_upon_receive) {
+ this.restartTimeoutUponReceive = restart_timeout_upon_receive;
+ }
+ public void setNotransfer(boolean notransfer) {
+ this.notransfer = notransfer;
+ }
+ public boolean isNotransfer() {
+ return notransfer;
+ }
+
+ public static String byteToPrintableString(byte b) {
+ //String s = new String(new byte[1] { b });
+ String s = new String(b);
+ // control characters
+ if (b >= 0 && b < 32) s = "^" + (char) (b + 64);
+ else if (b == 127) s = "^?";
+ // some escape characters
+ if (b == 9) s = "\\t";
+ if (b == 10) s = "\\n";
+ if (b == 13) s = "\\r";
+ return s;
+ }
+
+ @SuppressWarnings("serial")
+ public static class TimeoutException extends Exception{
+ TimeoutException(String ex){
+ super(ex)
+ }
+ }
+ @SuppressWarnings("serial")
+ public static class EOFException extends Exception{
+ }
+
+
+
+ private static PrintStream duplicatedTo = null;
+
+
+
+ public String toString(){
+ return "last match: $match\ngroups: $groups"
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/test/groovy/org/hidetake/groovy/ssh/expect/CommandExecutor.groovy b/src/test/groovy/org/hidetake/groovy/ssh/expect/CommandExecutor.groovy
new file mode 100644
index 00000000..ebf86e45
--- /dev/null
+++ b/src/test/groovy/org/hidetake/groovy/ssh/expect/CommandExecutor.groovy
@@ -0,0 +1,5 @@
+package org.hidetake.groovy.ssh.expect
+
+interface CommandExecutor {
+ String processCommand(String command)
+}
\ No newline at end of file
diff --git a/src/test/groovy/org/hidetake/groovy/ssh/expect/ExpectSpec.groovy b/src/test/groovy/org/hidetake/groovy/ssh/expect/ExpectSpec.groovy
new file mode 100644
index 00000000..d6b49d40
--- /dev/null
+++ b/src/test/groovy/org/hidetake/groovy/ssh/expect/ExpectSpec.groovy
@@ -0,0 +1,143 @@
+package org.hidetake.groovy.ssh.expect
+
+import org.apache.sshd.SshServer
+import org.apache.sshd.common.Factory
+import org.apache.sshd.server.CommandFactory
+import org.apache.sshd.server.PasswordAuthenticator
+import org.hidetake.groovy.ssh.Ssh
+import org.hidetake.groovy.ssh.core.Service
+import org.hidetake.groovy.ssh.operation.expect.Expect
+import org.hidetake.groovy.ssh.server.ServerIntegrationTest
+import org.hidetake.groovy.ssh.server.SshServerMock
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import spock.lang.Specification
+
+@org.junit.experimental.categories.Category(ServerIntegrationTest)
+class ExpectSpec extends Specification {
+
+ SshServer server
+ CommandExecutor commandExecutor=Mock(CommandExecutor)
+
+ Service ssh
+ def PROMPT='$'
+
+ @Rule
+ TemporaryFolder temporaryFolder
+
+
+ def setup() {
+ startServer()
+
+ ssh = Ssh.newService()
+ ssh.settings {
+ knownHosts = allowAnyHosts
+ }
+ ssh.remotes {
+ testServer {
+ host = server.host
+ port = server.port
+ user = 'someuser'
+ password = 'somepassword'
+ }
+
+ }
+ }
+
+ private startServer() {
+ server = SshServerMock.setUpLocalhostServer()
+ server.with {
+ commandFactory = Mock(CommandFactory)
+ shellFactory = Mock(Factory) {
+ create() >> new StubShell(commandExecutor, PROMPT)
+ }
+ passwordAuthenticator = Mock(PasswordAuthenticator) {
+ (0.._) * authenticate('someuser', 'somepassword', _) >> true
+ }
+ start()
+ }
+ }
+
+ def cleanup() {
+ server.stop(true)
+ }
+
+ def "can expect for prompt"() {
+ when:
+ ssh.run {
+ session(ssh.remotes.testServer) {
+ shellExpect {
+ expectOrThrow 1, PROMPT
+ }
+ }
+ }
+
+ then:
+ notThrown(Exception)
+
+ }
+
+ Closure sayHello = { String param ->
+ shellExpect {
+ send "hello $param"
+ expectOrThrow 3,'please enter password:'
+ }
+ }
+
+ def "can execute a simple extension"() {
+ when:
+ ssh.settings {
+ extensions.add hello: sayHello
+ }
+ ssh.run {
+ session(ssh.remotes.testServer) {
+ hello "server"
+ }
+ }
+ then:
+
+ 1 * commandExecutor.processCommand("hello server") >> 'please enter password:'
+ notThrown Exception
+ }
+
+ def "can send a command and expect a result"() {
+ when:
+ ssh.run {
+ session(ssh.remotes.testServer) {
+ shellExpect {
+ send 'hello server'
+ expectOrThrow 1,'please enter password:'
+ send 'Welcome1'
+ expectOrThrow 1, 'password OK'
+ }
+ }
+ }
+
+ then:
+ 1 * commandExecutor.processCommand("hello server") >> 'please enter password:'
+ 1 * commandExecutor.processCommand("Welcome1") >> 'password OK'
+ notThrown Exception
+ }
+
+ def "throws exception when expected result is not found"() {
+ when:
+ ssh.run {
+ session(ssh.remotes.testServer) {
+ shellExpect {
+ send 'hello server'
+ expectOrThrow 1,'please enter password:'
+ send 'Welcome2'
+ expectOrThrow 1, 'password OK'
+ }
+ }
+ }
+
+ then:
+ 1 * commandExecutor.processCommand("hello server") >> 'please enter password:'
+ commandExecutor.processCommand("Welcome1") >> 'password OK'
+ thrown Expect.TimeoutException
+ }
+
+
+
+}
diff --git a/src/test/groovy/org/hidetake/groovy/ssh/expect/StubShell.groovy b/src/test/groovy/org/hidetake/groovy/ssh/expect/StubShell.groovy
new file mode 100644
index 00000000..37fd08a0
--- /dev/null
+++ b/src/test/groovy/org/hidetake/groovy/ssh/expect/StubShell.groovy
@@ -0,0 +1,93 @@
+package org.hidetake.groovy.ssh.expect
+
+import org.apache.sshd.server.Command
+import org.apache.sshd.server.Environment
+import org.apache.sshd.server.ExitCallback
+
+public class StubShell implements Command, Runnable {
+
+ private InputStream inp;
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback callback;
+ private Environment environment;
+ private Thread thread;
+
+ CommandExecutor commandExecutor
+ String prompt
+
+ StubShell(CommandExecutor commandExecutor, String prompt){
+ this.commandExecutor=commandExecutor
+ this.prompt=prompt
+ }
+
+ public InputStream getIn() {
+ return inp;
+ }
+
+ public OutputStream getOut() {
+ return out;
+ }
+
+ public OutputStream getErr() {
+ return err;
+ }
+
+ public Environment getEnvironment() {
+ return environment;
+ }
+
+ public void setInputStream(InputStream inp) {
+ this.inp = inp;
+ }
+
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ public void setExitCallback(ExitCallback callback) {
+ this.callback = callback;
+ }
+
+ public void start(Environment env) throws IOException {
+ environment = env;
+ thread = new Thread(this, "StubShell");
+ thread.start();
+ }
+
+ public void destroy() {
+ thread.interrupt();
+ }
+
+ public void run() {
+ BufferedReader r = new BufferedReader(new InputStreamReader(inp));
+ out.write(prompt.bytes)
+ out.flush()
+ try {
+ for (;;) {
+ String s = r.readLine();
+ if (s == null) {
+ return;
+ }
+ if ("exit".equals(s)) {
+ return;
+ }
+ out.write((commandExecutor.processCommand(s) + "\n").getBytes());
+ out.flush();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ callback.onExit(0);
+ }
+ }
+
+
+
+
+}
+