Ganymed SSH-2 for Java

Ganymed SSH-2 for Java是一个纯Java实现的SHH2库,官网为http://www.ganymed.ethz.ch/ssh2/,最新的更新时间为2006年10月,在用之前,请仔细看一下FAQ,真的能避免很多很多问题,下面列出几条重要的:

一,如果使用Session.execCommand()方法,则每个session中只能执行一条命令

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;
public class SSHTest {
        public static void main(String[] args) {
                String hostname = "192.168.192.130";
                String username = "hadoop";
                String password = "hadoop";

                try {
                        Connection conn = new Connection(hostname);
                        conn.connect();
                        boolean isAuthenticated = conn.authenticateWithPassword(
                                        username,password);
                        if (isAuthenticated == false)
                                throw new IOException("Authentication failed.");
                        Session sess = conn.openSession();
                        
                        sess.execCommand("cd test");//这么写是不行的
                        sess.execCommand("cat 1.txt");

                        InputStream stdout = new StreamGobbler(sess.getStdout());
                        InputStream stderr = new StreamGobbler(sess.getStderr());
                        BufferedReader stdoutReader = new BufferedReader(
                                        new InputStreamReader(stdout));
                        BufferedReader stderrReader = new BufferedReader(
                                        new InputStreamReader(stderr));
                        
                        System.out.println("Here is the output from stdout:");
                        while (true) {
                                String line = stdoutReader.readLine();
                                if (line == null)
                                        break;
                                System.out.println(line);
                        }

                        System.out.println("Here is the output from stderr:");
                        while (true) {
                                String line = stderrReader.readLine();
                                if (line == null)
                                        break;
                                System.out.println(line);
                        }
                        sess.close();
                        conn.close();
                } catch (IOException e) {
                        e.printStackTrace(System.err);
                        System.exit(2);
                }
        }
}

执行结果为:

java.io.IOException: A remote execution has already started.
        at ch.ethz.ssh2.Session.execCommand(Session.java:244)
        at SSHTest.main(SSHTest.java:28)

If you use Session.execCommand(), then you indeed can only execute only one command per session. This is not a restriction of the library, but rather an enforcement by the underlying SSH-2 protocol (a Session object models the underlying SSH-2 session).

There are several solutions:

1,Simple: Execute several commands in one batch, e.g., something like Session.execCommand("echo Hello && echo again")一批次执行多条命令


2,Simple: The intended way: simply open a new session for each command - once you have opened a connection, you can ask for as many sessions as you want, they are only a "virtual" construct为每一个命令打开一个新的会话
Session sess = conn.openSession();
sess.execCommand("cd test");//进入~/test路径
sess.close();
sess = conn.openSession();//由于是新打开的会话,此时在~路径下
sess.execCommand("cat 1.txt");

由于每个命令执行完成后会话会关闭,所以在执行"cat 1.txt"时会报找不到该文件,因为第二次登陆成功后在~路径下,而1.txt文件在~/test路径下
3,Advanced: Don't use Session.execCommand(), but rather aquire a shell with Session.startShell()使用Session.startShell()方法代替Session.execCommand()方法
二,如果使用Sess.execCommand()得到的结果和预期不一样或根本不能执行,那么要注意你的环境变量
The most often source of problems when executing a command with Session.execCommand() are missing/wrong set environment variables on the remote machine. Make sure that the minimum needed environment for XYZ is the same, independentely on how the shell is being invoked.
Example quickfix for bash users:
1,Define all your settings in the file ~/.bashrc
2,Make sure that the file ~/.bash_profile only contains the linesource ~/.bashrc
3,Before executing Session.execCommand(), do NOT aquire any type of pseudo terminal in the session. Be prepared to consume stdout and stderr data.
在~/test/路径下准备一个test.sh脚本:
#!/bin/sh
opt=$1
if [ ${opt}x = startx ]
        then
        echo "opt:start"
        status=`lsmod | grep ip_tables | wc -l`
        if [ ${status} -gt 0 ]
        then
                sudo service iptables start
        else
                echo "service iptables already started"
        fi
fi

然后尝试调用这个脚本:

运行结果如下:
Here is the output from stdout:
opt:start
service iptables already started
Here is the output from stderr:
+ opt=start
+ '[' startx = startx ']'
+ echo opt:start
++ lsmod
/home/hadoop/test/test.sh:行6: lsmod: 未找到命令
++ grep ip_tables
++ wc -l
+ status=0
+ '[' 0 -gt 0 ']'
+ echo 'service iptables already started'

可以看到脚本打印信息由stdout输出,脚本-x的调试信息由stderr输出,脚本在虚拟机中运行正常,为什么这里却提示"lsmd:未找到命令"呢
虚拟机中的环境变量如下:
[hadoop@localhost test]$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/root/bin:/sbin

再来看看代码中建立的会话的环境变量:
Here is the output from stdout:
/usr/local/bin:/usr/bin
Here is the output from stderr:

相差十万八千里,反正我是懒得整了,咱就直接全路径吧,将脚本中的lsmod修改为/usr/sbin/lsmod
三,当我只从stdout中读取数据时,进程有时候会挂起
In the SSH-2 low level protocol, each channel (e.g., session) has a receive window. When the remote SSH daemon has filled up our receive window, it must wait until we have consumed the input and are ready to accept new data.
在底层SSH2协议中,每个channel(写过socket的应该都知道这个是什么)都有一个接收窗,当远程的SSH进程填充满我们的接收窗,在我们消耗掉接收窗中的数据并可以接收新的数据之前它都会处于等待状态
Unfortunately, the SSH-2 protocol defines a shared window for stderr and stdout. As a consequence, if, for example, the remote process produces a lot of stderr data and you never consume it, then after some time the local receive window will be full and the sender is blocked. If you then try to read() from stdout, your call will be blocked: there is no stdout data (locally) available and the SSH daemon cannot send you any, since the receive window is full (you would have to read some stderr data first to "free" up space in the receive window).
不幸的是,底层SSH2协议为stdout和stderr定义了一个共享的接收窗,如果远程的SSH进程产生了很多stderr信息但是你从来都没有消费过它们,一段时间后,本地的接收窗将被填满,此时数据发送端将被挂起,如果这时你试图读取stdout,你的请求也将被挂起。由于接收窗已满,因此接收窗中没有任何可用的stdout信息,并且远程SSH进程也不会发送任何stdout信息(除非先读取一些stderr信息以释放部分接收窗的空间)
Fortunately, Ganymed SSH-2 uses a 30KB window - the above described scenario should be very rare.
幸运的是,Ganymed SSH-2使用30KB大小的接收窗,所以上面描述的情景很少发生
Many other SSH-2 client implementations just blindly consume any remotely produced data into a buffer which gets automatically extended - however, this can lead to another problem: in the extreme case the remote side can overflow you with data (e.g., leading to out of memory errors).
What can you do about this?
1,Bad: Do nothing - just work with stderr and stdout Inputstreams and hope that the 30KB window is enough for your application. 啥也不做,期盼30KB足够程序使用
2,Better, recommended for most users: use two worker threads that consume remote stdout and stderr in parallel. Since you probably are not in the mood to program such a thing, you can use the StreamGobbler class supplied with Ganymed SSH-2. The Streamgobbler is a special InputStream that uses an internal worker thread to read and buffer internally all data produced by another InputStream. It is very simple to use:
只需使用Ganymed SSH-2自带的StreamGobbler类即可使用2个工作线程并行消费远程的stdout和stderr,StreamGobbler是一个特殊的输入流,它能使用内部工作线程读取、缓存所有另一个输入流输入的信息,示例如下:
InputStream stdout = new StreamGobbler(mysession.getStdout());
InputStream stderr = new StreamGobbler(mysession.getStderr());

You then can access stdout and stderr in any order, in the background the StreamGobblers will automatically consume all data from the remote side and store in an internal buffer.
然后你就可以以任何顺序访问stdout和stderr,StreamGobblers将会在后台自动消费所有远程端口传递过来的数据并存放在一个内部的buffer中
3,Advanced: you are paranoid and don't like programs that automatically extend buffers without asking you. You then have to implement a state machine. The condition wait facility offered by Session.waitForCondition() is exactly what you need: you can use it to wait until either stdout or stderr data has arrived and can be consumed with the two InputStreams. You can either use the return value of Session.waitForCondition() or check with InputStream.available() (for stdout and stderr) which InputStream has data available (i.e., a read() call will not block). Be careful when wrapping the InputStreams, also do not concurrently call read() on the InputStreams while calling Session.waitForCondition() (unless you know what you are doing).
Sess.waitForCondition方法可以获取流的状态信息,通过得到的状态信息来判断下一步做什么
接下来遇到的问题相当的棘手,反正我是没查到问题的原因,当我在项目中使用Ganymed SSH-2调用服务器脚本的时候,进程经常会卡主(不是所有的脚本,少部分脚本,但是我没发现产生异常的脚本和可以正常调用的脚本之间有什么不同,并且可以确定,绝对不是接收窗满引起的,因为只返回了很少的运行结果,并且debug时也可以看到,BufferedReader中创建的长度为8000多的字符数组只被使用了几百的空间),奇怪的是如果我把脚本调用部分单独提取出来放到一个Java项目中执行,运行正常
脚本最后部分如下:
......
                if [ -n "$pid" ]
                then
                        echo "success"
                        exit 0
                else
                        echo "faild."
                        exit -1
                fi
        fi
fi

代码如下(程序第一次进入while循环时打出success,第二次进入循环后,在String line = stdoutReader.readLine();这一步卡主):
......
BufferedReader stderrReader = new BufferedReader(
                new InputStreamReader(stderr));
                        
System.out.println("Here is the output from stdout:");
while (true) {
        String line = stdoutReader.readLine();
        if (line == null)
                break;
        System.out.println(line);
}
......

日志如下:
......
Here is the output from stdout:
success

由于Java源代码调试起来很费劲,最终也没调试问题出在哪里,介绍下解决方式吧,虽然程序进程卡住了,但是后台的脚本其实已经成功运行完成,所以要做的就是为连接返回结果的读取设置超时时间
InputStream stdout = sess.getStdout();
InputStream stderr = sess.getStderr();
                    
byte[] buffer = new byte[100];
String result = "";
while (true) {
        if ((stdout.available() == 0)) {
          int conditions = sess.waitForCondition(ChannelCondition.STDOUT_DATA | 
                        ChannelCondition.STDERR_DATA | ChannelCondition.EOF, 1000*5);
                if ((conditions & ChannelCondition.TIMEOUT) != 0) {
                        logger.info("time out break");
                        break;//超时后退出循环,要保证超时时间内,脚本可以运行完成
                }
          if ((conditions & ChannelCondition.EOF) != 0) {
                if ((conditions & (ChannelCondition.STDOUT_DATA | 
                                ChannelCondition.STDERR_DATA)) == 0) {
                        logger.info("break");
                                break;
                        }
                }
        }
  while (stdout.available() > 0) {
        int len = stdout.read(buffer);
    if (len > 0){
      System.err.write(buffer, 0, len);
                        result += new String(buffer, 0, len);
                }
  }                                  
  while (stderr.available() > 0) {
                int len = stderr.read(buffer);
                if (len > 0){
                        System.err.write(buffer, 0, len);
                        result += new String(buffer, 0, len);
                }
        }
}

反正我以后是不准备用这个东西了