关于python:如果你在加载模块时使用Paramiko,为什么挂起?

Why does Paramiko hang if you use it while loading a module?

将以下内容放入一个文件hello.py(如果您还没有找到,还可以将easy_install paramiko):

1
2
3
4
5
6
7
8
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()

适当地填写第一行。

现在类型

1
python hello.py

您将看到一些ls输出。

现在改为键入

1
python

然后从解释器类型中

1
import hello

瞧!它挂起来了!如果您将代码包装在一个函数foo中并改为执行import hello; hello.foo(),那么它将取消运行。

为什么在模块初始化中使用Paramiko时挂起?Paramiko是如何知道在模块初始化过程中使用它的?


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

一般来说,模块在导入时不应该有任何副作用,否则会得到不可预知的结果。只要用__name__ == '__main__'把戏拖延执行,你就没事了。

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

这是一个很好的例子,为什么你总是想要最小化副作用,为什么函数式编程技术变得越来越流行。


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

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

  • 如前所述,解决您的问题的简单而明智的方法是将您的代码放在if __name__ == '__main__':主体中,该主体仅在您执行此特定模块时执行,在其他模块导入此mmodule时不会执行。

  • (不推荐)另一个解决方法是在调用SSHClient.connect()之前,在代码中执行一个伪str.decode(‘utf-8’)——见下面的分析。

  • 那么这个问题的根本原因是什么呢?

    分析(简单密码验证)

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

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

  • 这基本上是作为SSHClient.connect()的一部分来完成的。当调用EDCOX1×9调用时,创建EDOCX1×10的锁,并且线程开始EDOCX1×11。注意,EDCOX1的12个方法将执行类的EDCOX1×13的方法。

  • EDOCX1 14打印我们的日志消息"开始线程",然后继续到EDCOX1 15。

  • check_banner做了一件事。它检索ssh横幅(服务器的第一个响应)transport.py:1707::self.packetizer.readline(timeout)(请注意,超时只是一个套接字读取超时),并在末尾检查换行。否则就会超时。

  • 如果收到服务器横幅,它会尝试UTF-8解码响应字符串packet.py:287::return u(buf),这就是死锁发生的地方。u(s, encoding='utf-8')执行str.decode("utf-i"),并通过encodings.search_function隐式导入encodings.utf8encodings:99中,最终导致导入死锁。

  • 所以一个不好的解决方法就是只导入一次UTF-8解码器,这样就不会因为模块导入副作用而阻塞特定的导入。(''.decode('utf-8')号)

    固定

    脏修复-不推荐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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()

    好固定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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()

    参考参数问题跟踪:第104期


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

    1
    2
    3
    from paramiko import py3compat
    # dirty hack to fix threading import lock (issue 104) by preloading module
    py3compat.u("dirty hack")

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