• 使用JSCH执行命令并读取终端输出的一些使用心得


    使用Jsch执行命令,并读取终端输出

    jsch

    http://www.jcraft.com/jsch/

    Jsch是java实现的一个SSH客户端。开发JSCH的公司是 jcraft:

    JCraft成立于1998年3月,是一家致力于Java应用程序和Internet / Intranet服务的应用程序开发公司。

    Jcraft的总裁兼首席执行官是Atsuhiko Yamanaka博士

    在Yamanaka博士于1998年3月创立JCraft之前,他已经加入NEC公司两年了,从事软件的研究和开发。

    Yamanaka博士拥有日本东北大学的信息科学硕士学位和博士学位。他还获得了东北大学信息科学学士学位。他的主题与计算机科学的数学基础有关,尤其是构造逻辑和功能编程语言的设计。

    执行命令

    public static String executeCommandWithAuth(String command, 
                                                SubmitMachineInfo submitMachineInfo,
                                                ExecuteCommandACallable<String> buffer) {
        Session session = null;
        Channel channel = null;
        InputStream in = null;
        InputStream er = null;
        Watchdog watchdog = new Watchdog(120000);//2分钟超时
        try {
          String user = submitMachineInfo.getUser();
          String host = submitMachineInfo.getHost();
          int remotePort = submitMachineInfo.getPort();
    
          JSch jsch = new JSch();
          session = jsch.getSession(user, host, remotePort);
    
          Properties prop = new Properties();
          //File file = new File(SystemUtils.getUserHome() + "/.ssh/id_rsa");
          //String knownHosts = SystemUtils.getUserHome() + "/.ssh/known_hosts".replace('/', File.separatorChar);
          //jsch.setKnownHosts(knownHosts)
          //jsch.addIdentity(file.getPath())
          //prop.put("PreferredAuthentications", "publickey");
          //prop.put("PreferredAuthentications", "password");
          //
    
          prop.put("StrictHostKeyChecking", "no");
          session.setConfig(prop);
          session.setPort(remotePort);
          session.connect();
    
          channel = session.openChannel("exec");
          ((ChannelExec) channel).setPty(false);
          ((ChannelExec) channel).setCommand(command);
    
          // get I/O streams
    
          in = channel.getInputStream();
          er = ((ChannelExec) channel).getErrStream();
          BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
          BufferedReader errorReader = new BufferedReader(new InputStreamReader(er, StandardCharsets.UTF_8));
    
          Thread thread = Thread.currentThread();
          watchdog.addTimeoutObserver(w -> thread.interrupt());
    
          channel.connect();
          watchdog.start();
          String buf;
          while ((buf = reader.readLine()) != null) {
            buffer.appendBuffer(buf);
            if (buffer.IamDone()) {
              break;
            }
          }
          String errbuf;
          while ((errbuf = errorReader.readLine()) != null) {
            buffer.appendBuffer(errbuf);
            if (buffer.IamDone()) {
              break;
            }
          }
    
          //两分钟超时,无论什么代码,永久运行下去并不是我们期望的结果,
          //加超时好处多多,至少能防止内存泄漏,也能符合我们的预期,程序结束,相关的命令也结束。
          //如果程序是前台进程,不能break掉,那么可以使用nohup去启动,或者使用子shell,但外层我们的程序一定要能结束。
          watchdog.stop();
          channel.disconnect();
          session.disconnect();
        } catch (Exception e) {
          e.printStackTrace();
        } finally {
          try {
            if (in != null) {
              in.close();
            }
            if (er != null) {
              er.close();
            }
          } catch (Exception e) {
            //
          }
    
          if (channel != null) {
            channel.disconnect();
          }
          if (session != null) {
            session.disconnect();
          }
          watchdog.stop();
        }
    
        return buffer.endBuffer();
    }
    
      public interface ExecuteCommandACallable<T> {
    
        boolean IamDone();//提前结束执行,如果终端是无限输出,则可以在达到一定条件的时候,通过IamDone通知上述程序结束读取。
    
        //for buffer
        ExecuteCommandACallable<T> appendBuffer(String content);//异步追加输出到自定义的Buffer
    
        String endBuffer();//正常结束Buffer,
      }
    

    上述两段代码已经用于生产环境,如果通过异步的方式启动,可以在Buffer中通过appendBuffer方法接收每一行的输出。可以打印到终端,也可以写如文件,甚至写到websocket,Kafka等。

    实际遇到的问题

    就是执行一些命令,例如启动 spark,spark-submit,启动 flink, flink run,都无法读取终端输出,且都阻塞到readLine。

    思路,既然我们读的是标准终端输出,以及错误终端输出,那么我们是见过 2>&1这种重定向,是不是可以利用他重定向到我们的流呢?

    经过实践,解决方案就是,无论执行什么命令,在后面都可以增加 2>&1,即便是 ls 2>&1, date 2>&1.

    至于为什么不加 2>&1就不行,或许是因为以这种方式启动的命令输出,是到了 /dev/tty了,或者某个非ssh进程的pipe。

    非充分验证

    1. 执行 executeCommandWithAuth("date;sleep 1m;date", submitMachineInfo, buffer);, 并查看Linux中通过ssh协议产生bash进程的fd
    2. 执行 executeCommandWithAuth("date;sleep 1m;date 2>&1", submitMachineInfo, buffer);, 并查看Linux中通过ssh协议产生bash进程的fd

    这两条命令都是可以在Java程序中打印出结果的。

    例如:

    2019年 12月 05日 星期四 11:57:16 CST
    2019年 12月 05日 星期四 11:58:16 CST
    

    Linux中的执行结果如下

    [root@hm arvin]# ps aux | grep bash
    arvin      7886  0.0  0.0 113248  1592 ?        Ss   11:55   0:00 bash -c date;sleep 1m;date
    root       7910  0.0  0.0 112668   972 pts/1    S+   11:56   0:00 grep --color=auto bash
    [root@hm arvin]# ll /proc/7886/fd
    总用量 0
    lr-x------ 1 arvin arvin 64 12月  5 11:55 0 -> pipe:[64748]
    l-wx------ 1 arvin arvin 64 12月  5 11:55 1 -> pipe:[64749]
    l-wx------ 1 arvin arvin 64 12月  5 11:55 2 -> pipe:[64750]
    [root@hm arvin]# ps aux | grep bash
    root        617  0.0  0.0 115248   944 ?        S    09:40   0:00 /bin/bash /usr/sbin/ksmtuned
    arvin      7968  0.0  0.0 113248  1588 ?        Ss   11:57   0:00 bash -c date;sleep 1m;date 2>&1
    root       8000  0.0  0.0 112672   972 pts/1    S+   11:57   0:00 grep --color=auto bash
    [root@hm arvin]# ll /proc/7968/fd
    总用量 0
    lr-x------ 1 arvin arvin 64 12月  5 11:57 0 -> pipe:[64278]
    l-wx------ 1 arvin arvin 64 12月  5 11:57 1 -> pipe:[64279]
    l-wx------ 1 arvin arvin 64 12月  5 11:57 2 -> pipe:[64280]
    [root@hm arvin]# 
    
    

    可以看到这两种执行方式,之所以能在Java中打印出来输出结果,是因为打印到了某些pipe,根据推测是打印到了ssh进程的pipe,所以才能通过SSH协议送回我们本地机器Java应用程序中的jsch的线程内的。
    这里我就不再制造不加重定向无法打印的例子,但可以用此方法验证,推测Linux进程输出到别的位置了。

  • 相关阅读:
    L2 L3 L4
    C 语言assert使用
    VIM 命令收藏
    C++继承实例
    关于 WinScp 的一点使用经验
    Boa服务器移植
    Android 去掉标题全屏显示
    sys下gpio操作
    linux下 XGCOM串口助手的安装
    linux中inittab文件详解
  • 原文地址:https://www.cnblogs.com/slankka/p/11988477.html
Copyright © 2020-2023  润新知