如果在加载模块时使用Paramiko,为什么挂起?

时间:2009-01-14 15:35:26

标签: python multithreading ssh module paramiko

将以下内容放入 hello.py 文件中(如果还没有,请加easy_install paramiko):

hostname,username,password='fill','these','in'
import paramiko
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect(hostname=hostname, username=username, password=password)
i,o,e = c.exec_command('ls /')
print(o.read())
c.close()

适当填写第一行。

现在输入

python hello.py

你会看到一些ls输出。

现在改为输入

python

然后从解释器类型

import hello
瞧,瞧!它挂了!如果您将代码包装在函数foo中并执行import hello; hello.foo(),它将会解除支持。

为什么Paramiko在模块初始化时使用时会挂起? Paramiko如何知道它首先在模块初始化期间被使用?

3 个答案:

答案 0 :(得分:18)

Paramiko为底层传输使用单独的线程。 你应该永远不会拥有一个产生线程的模块作为导入的副作用。据我所知,有一个可用的导入锁,所以当你的模块中的子线程尝试另一次导入时,它可以无限期地阻塞,因为你的主线程仍然持有锁。 (可能还有其他我不了解的问题)

通常,模块在导入时不应具有任何类型的副作用,否则您将获得不可预测的结果。用__name__ == '__main__'技巧暂缓执行,你会没事的。

[编辑] 我似乎无法创建一个简单的测试用例来重现这种死锁。我仍然认为这是导入的线程问题,因为auth代码正在等待永远不会触发的事件。这可能是paramiko或python中的一个错误,但好消息是,如果你做得对,你就不应该看到它;)

这是一个很好的例子,为什么你总是希望尽量减少副作用,以及为什么函数式编程技术变得越来越普遍。

答案 1 :(得分:3)

正如JimB指出,当python尝试在ssh连接尝试期间首次使用时隐式导入str.decode('utf-8')解码器时,它是导入问题。有关详细信息,请参阅分析部分。

一般来说,人们不能强调你应该避免让模块在导入时自动生成新线程。如果可以的话,尽量避免使用魔术模块代码,因为它几乎总会导致不必要的副作用。

  1. 对于您的问题,简单明了的解决方法 - 如前所述 - 是将您的代码放在if __name__ == '__main__':主体中,只有在执行此特定模块时才执行,并且在执行时不执行这个mmodule是由其他模块导入的。

  2. (不推荐)另一种解决方法是在调用SSHClient.connect()之前在代码中执行虚拟str.decode(' utf-8') - 请参阅下面的分析。 / p>

  3. 那么这个问题的根本原因是什么?

    分析(简单密码验证)

    提示:如果你想在python导入中调试线程并设置threading._VERBOSE = True

    1. paramiko.SSHClient().connect(.., look_for_keys=False, ..)隐式为您的连接生成一个新线程。如果打开paramiko.transport的调试输出,也可以看到此信息。
    2. [Thread-5 ] [paramiko.transport ] DEBUG : starting thread (client mode): 0x317f1d0L

      1. 这基本上是SSHClient.connect()的一部分。调用client.py:324::start_client()时,会创建一个锁transport.py:399::event=threading.Event()并启动线程transport.py:400::self.start()。请注意,start()方法将执行类transport.py:1565::run()方法。

      2. transport.py:1580::self._log(..)打印我们的日志消息"启动线程"然后进入transport.py:1584::self._check_banner()

      3. check_banner做了一件事。它检索ssh标志(来自服务器的第一个响应)transport.py:1707::self.packetizer.readline(timeout)(注意超时只是套接字读取超时),最后检查换行符 否则超时

      4. 如果收到服务器标题,它会尝试utf-8解码响应字符串packet.py:287::return u(buf)以及发生死锁的地方。 u(s, encoding='utf-8')执行str.decode(' utf-i')并通过encodings.utf8隐式导入encodings:99中的encodings.search_function,导致导入死锁。

      5. 所以一个肮脏的修复就是只导入一次utf-8解码器,以便不会因为模块导入副作用而阻止该指定导入。 (''.decode('utf-8')

        <强>修正

        脏修复 - 不推荐

        import paramiko
        hostname,username,password='fill','these','in'
        ''.decode('utf-8')  # dirty fix
        c = paramiko.SSHClient()
        c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        c.connect(hostname=hostname, username=username, password=password)
        i,o,e = c.exec_command('ls /')
        print(o.read())
        c.close()
        

        良好的修复

        import paramiko
        if __name__ == '__main__':
            hostname,username,password='fill','these','in'
            c = paramiko.SSHClient()
            c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            c.connect(hostname=hostname, username=username, password=password)
            i,o,e = c.exec_command('ls /')
            print(o.read())
            c.close()
        

        ref paramiko issue tracker: issue 104

答案 2 :(得分:0)

“”。decode(“utf-8”)对我不起作用,我最终做到了这一点。

$dialogScope.user = {}; 

我有一个paramiko包装器实现了。 https://github.com/bucknerns/sshaolin