使用Jsch執行命令,並讀取終端輸出
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。
非充分驗證
- 執行
executeCommandWithAuth("date;sleep 1m;date", submitMachineInfo, buffer);, 並查看Linux中通過ssh協議產生bash進程的fd - 執行
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進程輸出到別的位置了。
