关于python:读取大输出时,Paramiko通道卡住

Paramiko channel stucks when reading large ouput

我在远程Linux机器上执行命令并使用Paramiko读取输出的代码。 代码def看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IPAddress, username=user['username'], password=user['password'])


chan = self.ssh.get_transport().open_session()

chan.settimeout(10800)

try:
    # Execute thecommand
    chan.exec_command(cmd)

    contents = StringIO.StringIO()

    data = chan.recv(1024)

    # Capturing data from chan buffer.
    while data:
        contents.write(data)
        data = chan.recv(1024)

except socket.timeout:
    raise socket.timeout


output = contents.getvalue()

return output,chan.recv_stderr(600),chan.recv_exit_status()

上面的代码适用于小输出,但是卡在大输出上。

这里有任何与缓冲区相关的问题吗?


我要发布与Bruce Wayne(:)的输入一起使用的最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IPAddress, username=user['username'], password=user['password'])

chan = self.ssh.get_transport().open_session()
chan.settimeout(10800)

try:
    # Execute the given command
    chan.exec_command(cmd)

    # To capture Data. Need to read the entire buffer to capture output
    contents = StringIO.StringIO()
    error = StringIO.StringIO()

    while not chan.exit_status_ready():
        if chan.recv_ready():
            data = chan.recv(1024)
            #print"Indside stdout"
            while data:
                contents.write(data)
                data = chan.recv(1024)

        if chan.recv_stderr_ready():            
            error_buff = chan.recv_stderr(1024)
            while error_buff:
                error.write(error_buff)
                error_buff = chan.recv_stderr(1024)

    exit_status = chan.recv_exit_status()

except socket.timeout:
    raise socket.timeout

output = contents.getvalue()
error_value = error.getvalue()

return output, error_value, exit_status


我没有看到与stdout通道相关的问题,但是我不确定您处理stderr的方式。您能确认,不是stderr捕获那引起问题的吗?
我会尝试您的代码,并告诉您。

更新:
当您执行的命令在STDERR中给出大量消息时,代码将冻结。我不确定为什么,但是recv_stderr(600)可能是原因。
因此,捕获错误流的方式与捕获标准输出的方式相同。
就像是,

1
2
3
4
5
6
contents_err = StringIO.StringIO()

data_err = chan.recv_stderr(1024)
while data_err:
    contents_err.write(data_err)
    data_err = chan.recv_stderr(1024)

您甚至可以先尝试将recv_stderr(600)更改为recv_stderr(1024)或更高版本。


实际上,我认为以上所有答案都无法解决真正的问题:

如果远程程序首先产生大量stderr输出,则

1
2
stdout.readlines()
stderr.readlines()

将永远挂。虽然

1
2
stderr.readlines()
stdout.readlines()

可以解决这种情况,但是如果远程程序首先产生大量stdout输出,它将失败。

我还没有解决方案...


如果使用打开的ssh会话的高级表示,则会更容易。由于您已经使用ssh-client打开频道,因此您可以从那里运行命令,而无需执行其他工作。

1
2
3
4
5
6
7
8
9
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IPAddress, username=user['username'], password=user['password'])

stdin, stdout, stderr = ssh.exec_command(cmd)
for line in stdout.readlines():
    print line
for line in stderr.readlines():
    print line

如果您随后收到其他数据,则需要再次从这些文件句柄中读取。


TL; DR:如果使用ssh.exec_command(),则在stderr.readlines()之前调用stdout.readlines()

如果使用@Spencer Rathbun的答案:

1
2
3
4
5
sh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IPAddress, username=user['username'], password=user['password'])

stdin, stdout, stderr = ssh.exec_command(cmd)

您可能想知道大输出可能带来的限制。

实验上,stdin, stdout, stderr = ssh.exec_command(cmd)将无法立即将完整输出写入stdoutstderr。更具体地说,在填充之前,缓冲区似乎包含2^21(2,097,152)个字符。如果任何缓冲区已满,exec_command在写入该缓冲区时将阻塞,并且将保持阻塞状态,直到该缓冲区被清空为止。这意味着如果您的stdout太大,您将无法读取stderr,因为在任何一个缓冲区中,直到它可以写入完整的输出,您都不会收到EOF。

Spencer使用的一种简单方法就是-在尝试读取stderr之前通过stdout.readlines()获取所有正常输出。仅当stderr中的字符数超过2^21个时,这才会失败,这在我的用例中是可以接受的限制。

我之所以发布这个帖子,是因为我很笨,花了很长时间,试图弄清楚我是如何破坏代码的,答案是我从stderr之前读到stdout,而我的stdout是太大而无法容纳缓冲区。


为了使paramiko命令表现得像subprocess.call一样,您可以使用以下代码(经过python-3.5和paramiko-2.1.1测试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env /usr/bin/python3                                                

import os                                                                  
import sys                                                                                                                    
from paramiko import SSHClient, AutoAddPolicy              
from socket import getfqdn                                      

class SecureSHell(object):                                                
    reuser = os.environ['USER']                                            
    remote = ''                                                            
    def __init__(self, *args, **kwargs):                                  
        for arg in args:                                                  
            if hasattr(self, arg):                                        
                setattr(self, arg, True)                                  
        for (key, val) in kwargs.items():                                  
            if hasattr(self, key):                                        
                setattr(self, key, val)

    @staticmethod                                                          
    def _ssh_(remote, reuser, port=22):                                    
        if '@' in remote:                                                  
            _reuser, remote = remote.split('@')                            
        _fqdn = getfqdn(remote)                                            
        remote = _fqdn if _fqdn else remote                                
        ssh = SSHClient()                                                  
        ssh.set_missing_host_key_policy(AutoAddPolicy())
        ssh.connect(remote, int(port), username=reuser)                                                                    
        return ssh                                                        

    def call(self, cmd, remote=None, reuser=None):                        
        remote = remote if remote else self.remote                        
        reuser = reuser if reuser else self.reuser              
        ssh = self._ssh_(remote, reuser)                                  
        chn = ssh.get_transport().open_session()                          
        chn.settimeout(10800)                                              
        chn.exec_command(cmd)                                              
        while not chn.exit_status_ready():                                
            if chn.recv_ready():                                          
                och = chn.recv(1024)                                      
                while och:                                                
                    sys.stdout.write(och.decode())                        
                    och = chn.recv(1024)                                  
            if chn.recv_stderr_ready():                                    
                ech = chn.recv_stderr(1024)                                
                while ech:                                                
                    sys.stderr.write(och.decode())                        
                    ech = chn.recv_stderr(1024)                            
        return int(chn.recv_exit_status())                                

ssh = SecureSHell(remote='example.com', user='d0n')                      
ssh.call('find')