最近用vscode远程开发功能调试一台centos虚拟机上的python的时候遇到connection refused问题,而之前在windows本地环境一直是好的,非常奇怪。研究了2天的时间,才把问题找到,通过定位问题,把vscode的调试器的架构也有了更深入的了解,下面记录下问题定位过程。
环境
- VS Code version: 1.44.2
- Python Extension version: 2020.4.74986
- OS and version: centos7
- Python version: 2.7.5
vscode调试器架构
官方参考:
vscode Debugger Extension:https://code.visualstudio.com/api/extension-guides/debugger-extension
Debug Adapter Protocol:https://microsoft.github.io/debug-adapter-protocol/overview
为了解耦vscode和具体语言的调试器,中间增加了一个Debug Adapter,它是在独立进程运行的,vscode和Debug Adapter之间通过Debug Adapter Protocol通信。
Python的Debug Adapter就是在Microsoft Python Extension中实现的。
深入调试交互过程
通过一个Windows正常环境来探索调式器交互过程。
1 | { |
上面的launch.json指定了具体端口,便于我们分析进程之间的关系,打上断点,启动一个调试,然后通过端口分析进程之间的关系。
上述9088进程就是Debug Adapter,运行代码如下:
14272进程如下:
正是vscode终端打印的:
ptvsd_launcher.py是调用的ptvsd,上述命令就是ptvsd --client --host localhost --port 50624 D:/test/pytest/main.py。
然后再结合阅读python-extension和ptvsd的源代码,终于理清上述进程关系,下面是launch的过程,attach的过程有所区别。
- vscode Debugger UI启动一个调试,首先会创建Debug Adapter进程,就是上述9088进程,它们之间是通过stdin和stdout通信的。Python Debug Adapter的实现是通过派生node debug adapter实现的。
- Debug Adapter进程会根据launch.json的host和port进行TCP监听,如果不设置host就是localhost,如果不设置port,会自动随机选择一个。
- Debug Adapter启动调试程序,实际是通过ptvsd完成的,运行在单独的进程,就是上述14272进程,launch的情况下ptvsd以client mode运行,也就是ptvsd主动连接Debug Adapter,因为这时Debug Adapter先于调试程序运行,调试程序可以第一时间知道Debug Adapter的端口。如果是attach的情况,就需要IDE连接调试程序,告诉调试程序Debug Adapter的地址,然后调试程序再连接Debug Adapter。
- 调式程序连接到Debug Adapter,然后按照DAP进行调试交互。
问题定位
弄清楚调式的交互过程后,就容易定位问题了,connection refused问题就出在ptvsd连接Debug Adapter的时候,一开始以为是不是Debug Adapter退出了,但是ps查看进程是正常的。通过阅读源代码发现ptvsd连接Debug Adapter的超时时间可以在launch.json中设置,默认是20秒(20000),我将timeout改大了一点,便于分析问题。
1 | { |
然后通过netstat查看,发现Debug Adapter并不是绑定在127.0.0.1。
进一步查看/etc/hosts文件,发现localhost正是10.0.2.15,是eth0的ip。
而Debug Adapter启动调试程序时的–host参数总是为localhost,不管launch.json中如何设置。
自此问题基本定位清楚:
-
Debug Adapter中监听是通过node net模块实现,net模块对于localhost会通过getaddrinfo获取ip,而getaddrinfo会通过/etc/hosts查询ip;
-
ptvsd中是通过Python socket模块实现tcp通信,socke模块connect时,如果地址是localhost,它总是会使用127.0.0.1,bind也是一样。下述代码除了返回/etc/hosts中的ip,总是会加上127.0.0.1。
1
socket.getaddrinfo("localhost", None, socket.AF_INET, socket.SOCK_STREAM)
问题解决
- 方案1:launch.json中指定host为127.0.0.1且必须为127.0.0.1
- 方案2:Debug Adapter启动调试程序时的–host参数从launch.json中获取,同时launch.json中指定host为任意合法ip
- Debug Adapter总是绑定到0.0.0.0,也就是任意地址
- ptvsd连接Debug Adapter时也根据getaddrinfo将localhost转换为ip,并选择非127.0.0.1的ip
第一种方案是最简单的,其他方案需要修改python-extension或ptvsd的源代码。
以上都是仍然使用ptvsd的解决方法,还有一个方法是切换到最新的debugpy也可以解决问题,参照microsoft/ptvsd#2104。