用JSch通过SSH运行命令

我试图用JSch通过SSH运行一个命令,但是JSch几乎没有文档,我发现的例子很糟糕。 例如, 这个不显示处理输出stream的代码。 而且, 这个使用一个丑陋的黑客知道什么时候停止从输出stream中读取。

以下用Java编写的代码示例将允许您在Java程序中通过SSH在外部计算机上执行任何命令。 您将需要包含com.jcraft.jsch jar文件。

/* * SSHManager * * @author cabbott * @version 1.0 */ package cabbott.net; import com.jcraft.jsch.*; import java.io.IOException; import java.io.InputStream; import java.util.logging.Level; import java.util.logging.Logger; public class SSHManager { private static final Logger LOGGER = Logger.getLogger(SSHManager.class.getName()); private JSch jschSSHChannel; private String strUserName; private String strConnectionIP; private int intConnectionPort; private String strPassword; private Session sesConnection; private int intTimeOut; private void doCommonConstructorActions(String userName, String password, String connectionIP, String knownHostsFileName) { jschSSHChannel = new JSch(); try { jschSSHChannel.setKnownHosts(knownHostsFileName); } catch(JSchException jschX) { logError(jschX.getMessage()); } strUserName = userName; strPassword = password; strConnectionIP = connectionIP; } public SSHManager(String userName, String password, String connectionIP, String knownHostsFileName) { doCommonConstructorActions(userName, password, connectionIP, knownHostsFileName); intConnectionPort = 22; intTimeOut = 60000; } public SSHManager(String userName, String password, String connectionIP, String knownHostsFileName, int connectionPort) { doCommonConstructorActions(userName, password, connectionIP, knownHostsFileName); intConnectionPort = connectionPort; intTimeOut = 60000; } public SSHManager(String userName, String password, String connectionIP, String knownHostsFileName, int connectionPort, int timeOutMilliseconds) { doCommonConstructorActions(userName, password, connectionIP, knownHostsFileName); intConnectionPort = connectionPort; intTimeOut = timeOutMilliseconds; } public String connect() { String errorMessage = null; try { sesConnection = jschSSHChannel.getSession(strUserName, strConnectionIP, intConnectionPort); sesConnection.setPassword(strPassword); // UNCOMMENT THIS FOR TESTING PURPOSES, BUT DO NOT USE IN PRODUCTION // sesConnection.setConfig("StrictHostKeyChecking", "no"); sesConnection.connect(intTimeOut); } catch(JSchException jschX) { errorMessage = jschX.getMessage(); } return errorMessage; } private String logError(String errorMessage) { if(errorMessage != null) { LOGGER.log(Level.SEVERE, "{0}:{1} - {2}", new Object[]{strConnectionIP, intConnectionPort, errorMessage}); } return errorMessage; } private String logWarning(String warnMessage) { if(warnMessage != null) { LOGGER.log(Level.WARNING, "{0}:{1} - {2}", new Object[]{strConnectionIP, intConnectionPort, warnMessage}); } return warnMessage; } public String sendCommand(String command) { StringBuilder outputBuffer = new StringBuilder(); try { Channel channel = sesConnection.openChannel("exec"); ((ChannelExec)channel).setCommand(command); InputStream commandOutput = channel.getInputStream(); channel.connect(); int readByte = commandOutput.read(); while(readByte != 0xffffffff) { outputBuffer.append((char)readByte); readByte = commandOutput.read(); } channel.disconnect(); } catch(IOException ioX) { logWarning(ioX.getMessage()); return null; } catch(JSchException jschX) { logWarning(jschX.getMessage()); return null; } return outputBuffer.toString(); } public void close() { sesConnection.disconnect(); } } 

进行testing。

  /** * Test of sendCommand method, of class SSHManager. */ @Test public void testSendCommand() { System.out.println("sendCommand"); /** * YOU MUST CHANGE THE FOLLOWING * FILE_NAME: A FILE IN THE DIRECTORY * USER: LOGIN USER NAME * PASSWORD: PASSWORD FOR THAT USER * HOST: IP ADDRESS OF THE SSH SERVER **/ String command = "ls FILE_NAME"; String userName = "USER"; String password = "PASSWORD"; String connectionIP = "HOST"; SSHManager instance = new SSHManager(userName, password, connectionIP, ""); String errorMessage = instance.connect(); if(errorMessage != null) { System.out.println(errorMessage); fail(); } String expResult = "FILE_NAME\n"; // call sendCommand for each command and the output //(without prompts) is returned String result = instance.sendCommand(command); // close only after all commands are sent instance.close(); assertEquals(expResult, result); } 

这是一个无耻的插件,但我现在正在为JSch 写一些广泛的Javadoc 。

另外,现在在JSch Wiki中有一本手册 (主要由我撰写)。


关于原来的问题,处理这些stream是没有真正的例子。 读/写stream是一如既往的。

但是,仅仅通过读取shell的输出(这与SSH协议无关),就不可能知道shell中的一个命令何时完成。

如果shell是交互式的,即它有一个terminal连接,它通常会打印一个提示,你可以尝试识别。 但至less在理论上这个提示string也可能发生在命令的正常输出中。 如果您想确定,请为每个命令打开单独的exec通道,而不是使用shell通道。 shell的频道主要是供人类用户交互使用,我想。

我挣扎了半天,让JSCH无法使用System.in作为inputstream,无济于事。 我尝试了Ganymed http://www.ganymed.ethz.ch/ssh2/ ,并在5分钟内完成。 所有的例子似乎是针对应用程序的一个用法,没有任何例子显示我需要什么。 Ganymed的例子Basic.java Baaaboof有我需要的一切。

用法:

 String remoteCommandOutput = exec("ssh://user:pass@host/work/dir/path", "ls -t | head -n1"); String remoteShellOutput = shell("ssh://user:pass@host/work/dir/path", "ls"); shell("ssh://user:pass@host/work/dir/path", "ls", System.out); shell("ssh://user:pass@host", System.in, System.out); sftp("file:/C:/home/file.txt", "ssh://user:pass@host/home"); sftp("ssh://user:pass@host/home/file.txt", "file:/C:/home"); 

执行:

 import static com.google.common.base.Preconditions.checkState; import static java.lang.Thread.sleep; import static org.apache.commons.io.FilenameUtils.getFullPath; import static org.apache.commons.io.FilenameUtils.getName; import static org.apache.commons.lang3.StringUtils.trim; import com.google.common.collect.ImmutableMap; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.ChannelShell; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.UIKeyboardInteractive; import com.jcraft.jsch.UserInfo; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.io.PrintWriter; import java.net.URI; import java.util.Map; import java.util.Properties; public final class SshUtils { private static final Logger LOG = LoggerFactory.getLogger(SshUtils.class); private static final String SSH = "ssh"; private static final String FILE = "file"; private SshUtils() { } /** * <pre> * <code> * sftp("file:/C:/home/file.txt", "ssh://user:pass@host/home"); * sftp("ssh://user:pass@host/home/file.txt", "file:/C:/home"); * </code> * * <pre> * * @param fromUri * file * @param toUri * directory */ public static void sftp(String fromUri, String toUri) { URI from = URI.create(fromUri); URI to = URI.create(toUri); if (SSH.equals(to.getScheme()) && FILE.equals(from.getScheme())) upload(from, to); else if (SSH.equals(from.getScheme()) && FILE.equals(to.getScheme())) download(from, to); else throw new IllegalArgumentException(); } private static void upload(URI from, URI to) { try (SessionHolder<ChannelSftp> session = new SessionHolder<>("sftp", to); FileInputStream fis = new FileInputStream(new File(from))) { LOG.info("Uploading {} --> {}", from, session.getMaskedUri()); ChannelSftp channel = session.getChannel(); channel.connect(); channel.cd(to.getPath()); channel.put(fis, getName(from.getPath())); } catch (Exception e) { throw new RuntimeException("Cannot upload file", e); } } private static void download(URI from, URI to) { File out = new File(new File(to), getName(from.getPath())); try (SessionHolder<ChannelSftp> session = new SessionHolder<>("sftp", from); OutputStream os = new FileOutputStream(out); BufferedOutputStream bos = new BufferedOutputStream(os)) { LOG.info("Downloading {} --> {}", session.getMaskedUri(), to); ChannelSftp channel = session.getChannel(); channel.connect(); channel.cd(getFullPath(from.getPath())); channel.get(getName(from.getPath()), bos); } catch (Exception e) { throw new RuntimeException("Cannot download file", e); } } /** * <pre> * <code> * shell("ssh://user:pass@host", System.in, System.out); * </code> * </pre> */ public static void shell(String connectUri, InputStream is, OutputStream os) { try (SessionHolder<ChannelShell> session = new SessionHolder<>("shell", URI.create(connectUri))) { shell(session, is, os); } } /** * <pre> * <code> * String remoteOutput = shell("ssh://user:pass@host/work/dir/path", "ls") * </code> * </pre> */ public static String shell(String connectUri, String command) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { shell(connectUri, command, baos); return baos.toString(); } catch (RuntimeException e) { LOG.warn(baos.toString()); throw e; } } /** * <pre> * <code> * shell("ssh://user:pass@host/work/dir/path", "ls", System.out) * </code> * </pre> */ public static void shell(String connectUri, String script, OutputStream out) { try (SessionHolder<ChannelShell> session = new SessionHolder<>("shell", URI.create(connectUri)); PipedOutputStream pipe = new PipedOutputStream(); PipedInputStream in = new PipedInputStream(pipe); PrintWriter pw = new PrintWriter(pipe)) { if (session.getWorkDir() != null) pw.println("cd " + session.getWorkDir()); pw.println(script); pw.println("exit"); pw.flush(); shell(session, in, out); } catch (IOException e) { throw new RuntimeException(e); } } private static void shell(SessionHolder<ChannelShell> session, InputStream is, OutputStream os) { try { ChannelShell channel = session.getChannel(); channel.setInputStream(is, true); channel.setOutputStream(os, true); LOG.info("Starting shell for " + session.getMaskedUri()); session.execute(); session.assertExitStatus("Check shell output for error details."); } catch (InterruptedException | JSchException e) { throw new RuntimeException("Cannot execute script", e); } } /** * <pre> * <code> * System.out.println(exec("ssh://user:pass@host/work/dir/path", "ls -t | head -n1")); * </code> * * <pre> * * @param connectUri * @param command * @return */ public static String exec(String connectUri, String command) { try (SessionHolder<ChannelExec> session = new SessionHolder<>("exec", URI.create(connectUri))) { String scriptToExecute = session.getWorkDir() == null ? command : "cd " + session.getWorkDir() + "\n" + command; return exec(session, scriptToExecute); } } private static String exec(SessionHolder<ChannelExec> session, String command) { try (PipedOutputStream errPipe = new PipedOutputStream(); PipedInputStream errIs = new PipedInputStream(errPipe); InputStream is = session.getChannel().getInputStream()) { ChannelExec channel = session.getChannel(); channel.setInputStream(null); channel.setErrStream(errPipe); channel.setCommand(command); LOG.info("Starting exec for " + session.getMaskedUri()); session.execute(); String output = IOUtils.toString(is); session.assertExitStatus(IOUtils.toString(errIs)); return trim(output); } catch (InterruptedException | JSchException | IOException e) { throw new RuntimeException("Cannot execute command", e); } } public static class SessionHolder<C extends Channel> implements Closeable { private static final int DEFAULT_CONNECT_TIMEOUT = 5000; private static final int DEFAULT_PORT = 22; private static final int TERMINAL_HEIGHT = 1000; private static final int TERMINAL_WIDTH = 1000; private static final int TERMINAL_WIDTH_IN_PIXELS = 1000; private static final int TERMINAL_HEIGHT_IN_PIXELS = 1000; private static final int DEFAULT_WAIT_TIMEOUT = 100; private String channelType; private URI uri; private Session session; private C channel; public SessionHolder(String channelType, URI uri) { this(channelType, uri, ImmutableMap.of("StrictHostKeyChecking", "no")); } public SessionHolder(String channelType, URI uri, Map<String, String> props) { this.channelType = channelType; this.uri = uri; this.session = newSession(props); this.channel = newChannel(session); } private Session newSession(Map<String, String> props) { try { Properties config = new Properties(); config.putAll(props); JSch jsch = new JSch(); Session newSession = jsch.getSession(getUser(), uri.getHost(), getPort()); newSession.setPassword(getPass()); newSession.setUserInfo(new User(getUser(), getPass())); newSession.setDaemonThread(true); newSession.setConfig(config); newSession.connect(DEFAULT_CONNECT_TIMEOUT); return newSession; } catch (JSchException e) { throw new RuntimeException("Cannot create session for " + getMaskedUri(), e); } } @SuppressWarnings("unchecked") private C newChannel(Session session) { try { Channel newChannel = session.openChannel(channelType); if (newChannel instanceof ChannelShell) { ChannelShell channelShell = (ChannelShell) newChannel; channelShell.setPtyType("ANSI", TERMINAL_WIDTH, TERMINAL_HEIGHT, TERMINAL_WIDTH_IN_PIXELS, TERMINAL_HEIGHT_IN_PIXELS); } return (C) newChannel; } catch (JSchException e) { throw new RuntimeException("Cannot create " + channelType + " channel for " + getMaskedUri(), e); } } public void assertExitStatus(String failMessage) { checkState(channel.getExitStatus() == 0, "Exit status %s for %s\n%s", channel.getExitStatus(), getMaskedUri(), failMessage); } public void execute() throws JSchException, InterruptedException { channel.connect(); channel.start(); while (!channel.isEOF()) sleep(DEFAULT_WAIT_TIMEOUT); } public Session getSession() { return session; } public C getChannel() { return channel; } @Override public void close() { if (channel != null) channel.disconnect(); if (session != null) session.disconnect(); } public String getMaskedUri() { return uri.toString().replaceFirst(":[^:]*?@", "@"); } public int getPort() { return uri.getPort() < 0 ? DEFAULT_PORT : uri.getPort(); } public String getUser() { return uri.getUserInfo().split(":")[0]; } public String getPass() { return uri.getUserInfo().split(":")[1]; } public String getWorkDir() { return uri.getPath(); } } private static class User implements UserInfo, UIKeyboardInteractive { private String user; private String pass; public User(String user, String pass) { this.user = user; this.pass = pass; } @Override public String getPassword() { return pass; } @Override public boolean promptYesNo(String str) { return false; } @Override public String getPassphrase() { return user; } @Override public boolean promptPassphrase(String message) { return true; } @Override public boolean promptPassword(String message) { return true; } @Override public void showMessage(String message) { // do nothing } @Override public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) { return null; } } } 

使用来自java的ssh应该不会像jsch那样困难。 用sshj可能会更好。

这个坚韧不拔的terminal被写成使用Jsch,但更好的处理和vt102模拟。 你可以看看那里的代码。 我们使用它,它工作得很好。

我从2000年左右开始使用JSCH,但仍然觉得它是一个很好的库。 我同意它没有足够好的logging,但是提供的例子看起来足够好,足以在几分钟内得到理解,并且用户友好的Swing虽然是相当原始的方法,但它允许快速testing这个例子,以确保它实际工作。 并不总是如此,每一个好的项目都需要比写入的代码多三倍的文档,即使存在这样的代码,这也不一定能帮助你更快地写出你的概念的原型。

请注意,慈善Leschinski的答案可能有一些问题,当响应有一些延迟。 例如:
lparstat 1 5返回一个响应行并工作,
lparstat 5 1应该返回5行,但只返回第一个

我已经把命令输出在另一个里面了…我确定有更好的方法,我必须做这个快速修复

  while (commandOutput.available() > 0) { while (readByte != 0xffffffff) { outputBuffer.append((char) readByte); readByte = commandOutput.read(); } try {Thread.sleep(1000);} catch (Exception ee) {} }