【问题标题】:Paramiko inside Python Daemon causes IOErrorPython Daemon 中的 Paramiko 导致 IOError
【发布时间】:2025-01-07 21:25:02
【问题描述】:

我正在尝试从 python 守护进程内部使用paramiko 执行ssh 命令。 我正在为守护进程使用以下实现:https://pypi.python.org/pypi/python-daemon/

当程序启动时,pycrypto 在 paramiko 尝试连接时引发 IOErrorBad file descriptor。 如果我删除守护程序代码(只需取消注释最后一行并注释上面的两行),ssh 连接就会按预期建立。

一个简短的测试程序的代码如下所示:

#!/usr/bin/env python2
from daemon import runner
import paramiko

class App():

    def __init__(self):
        self.stdin_path = '/dev/null'
        self.stdout_path = '/dev/tty'
        self.stderr_path = '/dev/tty'
        self.pidfile_path =  '/tmp/testdaemon.pid'
        self.pidfile_timeout = 5

    def run(self):
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.load_system_host_keys()
        ssh.connect("hostname", username="username")
        ssh.close()

app = App()
daemon_runner = runner.DaemonRunner(app)
daemon_runner.do_action()
#app.run()

轨迹如下所示:

Traceback (most recent call last):
  File "./daemon-test.py", line 31, in <module>
    daemon_runner.do_action()
  File "/usr/lib/python2.7/site-packages/daemon/runner.py", line 189, in do_action
    func(self)
  File "/usr/lib/python2.7/site-packages/daemon/runner.py", line 134, in _start
    self.app.run()
  File "./daemon-test.py", line 22, in run
    ssh.connect("hostname", username="username")
  File "/usr/lib/python2.7/site-packages/paramiko/client.py", line 311, in connect
    t.start_client()
  File "/usr/lib/python2.7/site-packages/paramiko/transport.py", line 460, in start_client
    Random.atfork()
  File "/usr/lib/python2.7/site-packages/Crypto/Random/__init__.py", line 37, in atfork
_UserFriendlyRNG.reinit()
  File "/usr/lib/python2.7/site-packages/Crypto/Random/_UserFriendlyRNG.py", line 224, in reinit
_get_singleton().reinit()
  File "/usr/lib/python2.7/site-packages/Crypto/Random/_UserFriendlyRNG.py", line 171, in reinit
    return _UserFriendlyRNG.reinit(self)
  File "/usr/lib/python2.7/site-packages/Crypto/Random/_UserFriendlyRNG.py", line 99, in reinit
    self._ec.reinit()
  File "/usr/lib/python2.7/site-packages/Crypto/Random/_UserFriendlyRNG.py", line 62, in reinit
    block = self._osrng.read(32*32)
  File "/usr/lib/python2.7/site-packages/Crypto/Random/OSRNG/rng_base.py", line 76, in read
data = self._read(N)
  File "/usr/lib/python2.7/site-packages/Crypto/Random/OSRNG/posix.py", line 65, in _read
    d = self.__file.read(N - len(data))
IOError: [Errno 9] Bad file descriptor

我猜这与守护进程生成时的流重定向有关。我尝试将它们全部设置为/dev/tty,甚至设置为普通文件,但没有任何效果。

当我使用strace 运行程序时,我可以看到某些东西试图关闭一个文件两次,这就是我收到错误的时候。但我找不到描述符实际指向的文件(strace 显示的内存位置似乎没有在任何地方设置)。

【问题讨论】:

    标签: python linux ssh paramiko python-daemon


    【解决方案1】:

    这是我自己实际遇到的一个已知问题(这就是导致我提出这个问题的原因)。基本上,它与 UNIX 守护进程的定义以及 paramiko 实现其随机数生成器 (RNG) 的方式有关。

    如果您引用PEP 3143 - Standard daemon process library,成为正确守护进程的第一步是“关闭所有打开的文件描述符”。不幸的是,这会将文件描述符关闭为/dev/urandom,该描述符用于 Crypto 模块的 RNG,而后者又被 paramiko 使用。

    目前有一些解决方法,但作者表示他目前没有时间研究这个错误(尽管第一个链接中的最后一篇文章是作者的,并且在撰写本文时已经 8 天了)。

    总而言之,如果您在进程成为守护进程后import paramiko,那么它应该可以正常工作,因为在守护进程关闭所有文件描述符后文件描述符将被打开。

    用户 @xraj 也有一个 hackish 但聪明的解决方法,用于在守护进程时查找文件描述符并将其保存到 /dev/urandom(上面的第一个链接):

    import os
    from resource import getrlimit, RLIMIT_NOFILE
    
    def files_preserve_by_path(*paths):
        wanted=[]
        for path in paths:
            fd = os.open(path, os.O_RDONLY)
            try:
                wanted.append(os.fstat(fd)[1:3])
            finally:
                os.close(fd)
    
        def fd_wanted(fd):
            try:
                return os.fstat(fd)[1:3] in wanted
            except OSError:
                return False
    
        fd_max = getrlimit(RLIMIT_NOFILE)[1]
        return [ fd for fd in xrange(fd_max) if fd_wanted(fd) ]
    
    daemon_context.files_preserve = files_preserve_by_path('/dev/urandom')
    

    【讨论】:

      【解决方案2】:

      这最近发生在守护进程和多线程应用程序中,这使得大量 close() 在分离线程的循环中。我在类 pipe.PosixPipe 中发现了问题。 set() 和 close() 方法之间没有同步。 PosixPipe 的方法可以同时读/写和关闭套接字描述符。 问题已创建:https://github.com/paramiko/paramiko/issues/692 请求拉取:https://github.com/paramiko/paramiko/pull/691/files

      【讨论】: