1. 程式人生 > >使用JSCH執行命令並讀取終端輸出的一些使用心得

使用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程序輸出到別的位置了