使用 JSch 通过 SSH 运行命令

Posted

技术标签:

【中文标题】使用 JSch 通过 SSH 运行命令【英文标题】:Run a command over SSH with JSch 【发布时间】:2011-01-25 05:47:01 【问题描述】:

我正在尝试,但 JSch 几乎没有文档,而且我发现的示例很糟糕。例如,this one 不显示用于处理输出流的代码。而且,this one 使用了一个丑陋的 hack 来知道何时停止从输出流中读取。

【问题讨论】:

另见How to read JSch command output? - 它展示了如何正确同时读取标准和错误输出,以允许命令完成并收集包括错误在内的所有输出。跨度> 【参考方案1】:

gritty 终端是为使用 Jsch 而编写的,但具有更好的处理和 vt102 仿真。你可以看看那里的代码。我们使用它,它工作得很好。

【讨论】:

但是,从 GitHub 访问它更容易(例如 - github.com/xliangwu/gritty)。【参考方案2】:

从 java 中使用 ssh 不应该像 jsch 那样难。 sshj 可能会更好。

【讨论】:

【参考方案3】:

这是一个无耻的插件,但我现在只是writing一些广泛的Javadoc for JSch。

另外,JSch Wiki 中现在有一个Manual(主要由我编写)。


关于原始问题,实际上并没有处理流的示例。 像往常一样读取/写入流。

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

如果 shell 是交互式的,即它连接了一个终端,它通常会打印一个提示,您可以尝试识别它。但至少从理论上讲,这个提示字符串也可能出现在命令的正常输出中。如果您想确定,请为每个命令打开单独的 exec 通道,而不是使用 shell 通道。我认为 shell 通道主要用于人类用户的交互使用。

【讨论】:

这非常需要,谢谢。 JSCH 是一个好主意,但似乎是一个零文档的废弃库,有时让我感到莫名其妙的是人们实际上在关键项目中使用它。【参考方案4】:

在不使用 System.in 作为输入流的情况下,我挣扎了半天让 JSCH 工作,但无济于事。我尝试了 Ganymed http://www.ganymed.ethz.ch/ssh2/ 并在 5 分钟内完成。 所有示例似乎都针对该应用程序的一种用法,并且没有一个示例显示我需要什么。 Ganymed 的示例 Basic.java Baaaboof 有我需要的一切。

【讨论】:

【参考方案5】:

我从大约 2000 年开始使用 JSCH,但仍然觉得它是一个很好的库。我同意它的文档记录不够好,但所提供的示例似乎足以在几分钟内理解这是必需的,并且用户友好的 Swing 虽然这是一种非常原始的方法,但允许快速测试示例以确保它确实有效。并非每个好的项目都需要比编写的代码量多三倍的文档,即使存在这种情况,这也并不总是有助于更快地编写概念的工作原型。

【讨论】:

如果您正在寻找 JSCH 的文档。这可能会对您有所帮助:epaul.github.io/jsch-documentation/javadoc/com/jcraft/jsch/…【参考方案6】:

以下用 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();
  

  

用于测试。

  /**
     * 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);
  

【讨论】:

InputStream commandOutput 似乎没有明确关闭。它会造成任何泄漏吗? @stanleyxu2005 docs.oracle.com/javase/7/docs/api/java/io/… InputStream 的 close 方法什么都不做。 根据 Java 文档,channel.getInputStream() 必须在 channel.connect() 之前:epaul.github.io/jsch-documentation/javadoc/com/jcraft/jsch/… 我不知道为什么... 我发现你的回答很有用,但是这会返回给我readByte=-1,我完全不明白为什么。 @richmondwang:如果readByte=-1 那么它就是流的结尾。如果您遇到问题,请提出新问题。【参考方案7】:

用法:

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;
        
    

【讨论】:

我很惊讶在这个答案下没有更多的 cmets。很棒的代码,但我在运行 sudo 引导的命令时遇到问题。 @Sedrick - 您可能需要指定一个“身份”,JSch 提供给远程系统以授权您的超级用户访问。我在此代码的扩展(Remote Session 库)中添加了对此功能的支持。 @ScottBabcock,谢谢,我早就想通了。【参考方案8】:

请注意,当响应出现延迟时,Charity 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) 
        

【讨论】:

【参考方案9】:

Mykhaylo Adamovych 提供的示例非常详尽,暴露了 JSch 的大部分主要功能。我将这段代码(当然要注明出处)打包到一个名为Remote Session 的开源库中。我添加了 JavaDoc 和自定义异常,还提供了一个工具来指定自定义会话参数 (RemoteConfig)。

Mykhaylo 的代码没有展示的一个功能是如何为远程系统交互提供“身份”。如果您要执行需要超级用户访问权限的命令(即 - sudo),这一点至关重要。 远程会话在其 SessionHolder.newSession() 实现中添加了此功能:

RemoteConfig remoteConfig = RemoteConfig.getConfig();
Path keyPath = remoteConfig.getKeyPath();

if (keyPath == null) 
    throw new RemoteCredentialsUnspecifiedException();


String keyPass = remoteConfig.getString(RemoteSettings.SSH_KEY_PASS.key());
if (keyPass != null) 
    Path pubPath = keyPath.resolveSibling(keyPath.getFileName() + ".pub");
    jsch.addIdentity(keyPath.toString(), pubPath.toString(), keyPass.getBytes());
 else 
    jsch.addIdentity(keyPath.toString());

请注意,如果远程系统 URL 包含凭据,则会绕过此行为。

远程会话演示的另一个功能是如何提供 known-hosts 文件:

if ( ! remoteConfig.getBoolean(RemoteSettings.IGNORE_KNOWN_HOSTS.key())) 
    Path knownHosts = keyPath.resolveSibling("known_hosts");
    if (knownHosts.toFile().exists()) 
        jsch.setKnownHosts(knownHosts.toString());
    

Remote Session 还添加了一个 ChannelStream 类,该类封装了附加到此会话的通道的输入/输出操作。这提供了从远程会话累积输出直到收到指定提示的能力:

private boolean appendAndCheckFor(String prompt, StringBuilder input, Logger logger) throws InterruptedException, IOException 
    String recv = readChannel(false);
    if ( ! ((recv == null) || recv.isEmpty())) 
        input.append(recv);
        if (logger != null) 
            logger.debug(recv);
        
        if (input.toString().contains(prompt)) 
            return false;
        
    
    return !channel.isClosed();

没什么太复杂的,但这可以大大简化交互式远程操作的实现。

【讨论】:

以上是关于使用 JSch 通过 SSH 运行命令的主要内容,如果未能解决你的问题,请参考以下文章

jsch通过SSH2执行linux命令

通过 JSch shell 执行多个命令

如何使用 Java 在远程系统上运行 SSH 命令? [关闭]

某些 Unix 命令在使用 JSch 通过 Java 执行时失败并显示“... not found”

使用 JSch 跳过 Kerberos 身份验证提示 [重复]

Jsch使用SSH协议连接到远程Shell执行脚本