如果要检查recv_ready(),是否必须检查exit_status_ready?

时间:2014-05-06 20:16:45

标签: python paramiko

我正在使用以下命令运行远程命令:

ssh = paramiko.SSHClient()
ssh.connect(host)
stdin, stdout, stderr = ssh.exec_command(cmd)

现在我想得到输出。我见过这样的事情:

# Wait for the command to finish
while not stdout.channel.exit_status_ready():
    if stdout.channel.recv_ready():
        stdoutLines = stdout.readlines()

但是,有时似乎永远不会运行readlines()(即使应该有关于stdout的数据)。这对我来说意味着stdout.channel.recit_recat_ready()一旦stdout.channel.exit_status_ready()为True就不一定准备好(True)。

这样的事情是否合适?

# Wait until the data is available
while not stdout.channel.recv_ready():
    pass

stdoutLines = stdout.readlines()

也就是说,在等待recv_ready()说数据准备好之前,我是否真的首先要检查退出状态?

如果stdout.channel.recv_ready()在无限循环中等待成为True(如果不应该是任何stdout输出则不会),我怎么知道是否应该在stdout上有数据?

2 个答案:

答案 0 :(得分:20)

也就是说,在等待recv_ready()数据准备就绪之前,我是否真的首先要检查退出状态?

没有。从远程进程接收数据(例如stdout/stderr)是完全正常的,即使它还没有完成。此外,一些sshd实现甚至不提供远程proc的退出状态,在这种情况下你会遇到问题,请参阅paramiko doc: exit_status_ready

等待exit_status_code短生命远程命令的问题是,本地线程可能比检查循环条件更快地接收exit_code。在这种情况下,您将无法进入循环,并且永远不会调用readlines()。这是一个例子:

# spawns new thread to communicate with remote
# executes whoami which exits pretty fast
stdin, stdout, stderr = ssh.exec_command("whoami") 
time.sleep(5)  # main thread waits 5 seconds
# command already finished, exit code already received
#  and set by the exec_command thread.
# therefore the loop condition is not met 
#  as exit_status_ready() already returns True 
#  (remember, remote command already exited and was handled by a different thread)
while not stdout.channel.exit_status_ready():
    if stdout.channel.recv_ready():
        stdoutLines = stdout.readlines()

在等待stdout成为True的无限循环之前,我怎么知道stdout.channel.recv_ready()上是否应该有数据(如果不应该是{任何标准输出)?

channel.recv_ready()只表示缓冲区中有未读数据。

def recv_ready(self):
    """
    Returns true if data is buffered and ready to be read from this
    channel.  A ``False`` result does not mean that the channel has closed;
    it means you may need to wait before more data arrives.

这意味着可能由于网络(延迟数据包,重新传输......)或只是您的远程进程未定期写入stdout/stderr而导致recv_ready变为False。因此,将recv_ready()作为循环条件可能会导致代码过早返回,因为它有时会产生True(当远程进程写入stdout且本地通道线程收到该输出时)并且有时会产生在迭代中,False(例如,您的远程proc正在休眠而不是写入stdout)。

除此之外,人们偶尔会遇到可能与stdout/stderr buffers filling up有关的paramiko挂起(当你从未从stdout/stderr读取时,与popen和挂起过程有关的问题。内部缓冲区填满)。

下面的代码实现了一个分块解决方案,可以在通道打开时从stdout/stderr读取清空缓冲区。

def myexec(ssh, cmd, timeout, want_exitcode=False):
  # one channel per command
  stdin, stdout, stderr = ssh.exec_command(cmd) 
  # get the shared channel for stdout/stderr/stdin
  channel = stdout.channel

  # we do not need stdin.
  stdin.close()                 
  # indicate that we're not going to write to that channel anymore
  channel.shutdown_write()      

  # read stdout/stderr in order to prevent read block hangs
  stdout_chunks = []
  stdout_chunks.append(stdout.channel.recv(len(stdout.channel.in_buffer)))
  # chunked read to prevent stalls
  while not channel.closed or channel.recv_ready() or channel.recv_stderr_ready(): 
      # stop if channel was closed prematurely, and there is no data in the buffers.
      got_chunk = False
      readq, _, _ = select.select([stdout.channel], [], [], timeout)
      for c in readq:
          if c.recv_ready(): 
              stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
              got_chunk = True
          if c.recv_stderr_ready(): 
              # make sure to read stderr to prevent stall    
              stderr.channel.recv_stderr(len(c.in_stderr_buffer))  
              got_chunk = True  
      '''
      1) make sure that there are at least 2 cycles with no data in the input buffers in order to not exit too early (i.e. cat on a >200k file).
      2) if no data arrived in the last loop, check if we already received the exit code
      3) check if input buffers are empty
      4) exit the loop
      '''
      if not got_chunk \
          and stdout.channel.exit_status_ready() \
          and not stderr.channel.recv_stderr_ready() \
          and not stdout.channel.recv_ready(): 
          # indicate that we're not going to read from this channel anymore
          stdout.channel.shutdown_read()  
          # close the channel
          stdout.channel.close()
          break    # exit as remote side is finished and our bufferes are empty

  # close all the pseudofiles
  stdout.close()
  stderr.close()

  if want_exitcode:
      # exit code is always ready at this point
      return (''.join(stdout_chunks), stdout.channel.recv_exit_status())
  return ''.join(stdout_chunks)

如果频道提前关闭,channel.closed只是最终退出条件。在读取块之后,代码检查是否已经接收到exit_status并且在此期间没有缓冲新数据。如果新数据到达或没有收到exit_status,代码将继续尝试读取块。一旦远程过程退出并且缓冲区中没有新数据,我们就假设我们已经读完所有内容并开始关闭频道。请注意,如果您想要收到退出状态,您应该一直等到收到退出状态,否则paramiko可能永远阻止。

这样可以保证缓冲区不会填满并使proc挂起。 exec_command仅在远程命令退出且本地缓冲区中没有数据时才返回。通过在繁忙的循环中使用select()而不是轮询,代码对cpu更友好,但对于短生命命令可能会慢一些。

仅供参考,为了防止某些无限循环,可以设置在没有数据到达一段时间时触发的通道超时

 chan.settimeout(timeout)
 chan.exec_command(command)

答案 1 :(得分:0)

ssh.exec_command(cmd)之后添加以下几行。只要shell脚本正在运行,循环就将继续,并在完成后立即退出。

while int(stdout.channel.recv_exit_status()) != 0:
    time.sleep(1)