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

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

【问题讨论】:

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

标签: java ssh jsch


【解决方案1】:

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

【讨论】:

【解决方案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 虽然这是一种非常原始的方法,但允许快速测试示例以确保它确实有效。并非每个好的项目都需要比编写的代码量多三倍的文档,即使存在这种情况,这也并不总是有助于更快地编写概念的工作原型。

      【讨论】:

      【解决方案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();
        }
        

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-11-21
          • 1970-01-01
          • 2017-08-17
          相关资源
          最近更新 更多