VPN 管理命令一启动就出现回溯信息时,第一反应往往是去安装另一个 Python,或者把系统默认指向别处。这种反应把两个毫无关系的运行时混为一谈。一个属于操作系统及其打包工具,另一个属于你正在构建的项目。为了满足后者而改动前者,正是一台正常工作的桌面失去包管理器的原因。

真正值得做比较的,是实际失败的组件与你被允许改动的运行时。它们几乎从不是同一个对象。

两个看起来是一个的 Python 问题

第一个问题是:是哪个解释器运行了打印出回溯信息的程序?第二个问题是:我的项目应该用哪个解释器?在终端里两者看起来完全一样,因为答案都是输入 python3,但它们的归属、升级路径和影响范围各不相同。

系统工具绑定的是发行版随包提供的解释器,以及针对该解释器 ABI 编译的模块。项目绑定的是你为它创建的任意环境。移动系统默认值会一次性改变机器上每个工具所用的第一个解释器;创建环境只改变一个目录所用的第二个解释器。

这些操作里只有一种能在一分钟内回退。不是第一种。

是哪个组件产生了错误

在阅读任何其他内容之前,先读回溯信息的第一行和产生它的可执行文件。命令行前端、特权服务和 API 绑定是不同的部件,而且其中只有一部分是 Python。

command -v openvpn3
head -n 1 "$(command -v openvpn3)"
dpkg -S "$(command -v openvpn3)" 2>/dev/null
systemctl status openvpn3-service-client --no-pager

脚本的第一行说明了它将使用哪个解释器,回溯信息正是来自那个解释器。包查询告诉你文件属于谁,这决定了谁有权修改它。服务状态回答的是完全另一个问题:真正建立会话的特权后端是否在运行。

模块导入错误、D-Bus 激活被拒绝和凭据被拒是三个不同阶段。导入错误发生在任何会话存在之前,所以再多的 VPN 配置也修不好它,正常工作的服务同样修不好它。把这条消息归入这三个阶段之一,才是真正的第一步。

项目环境能修什么、不能修什么

项目环境解决的是项目问题:某个应用所需的精确库版本,与其他一切隔离。这确实有用,当出错的程序是你自己的脚本时,它就是正确的工具。

uv run --python 3.11 app.py
python3 -c 'import openvpn3; print(openvpn3.__file__)'

第二条命令显示给定解释器能看到哪一份绑定,这是区分缺失包与被遮蔽包的最快方法。要用出错脚本所用的解释器运行它,而不是用另一个碰巧能跑的解释器。

项目环境做不到的是修复发行版打包的前端。那个程序的依赖是针对包管理器选定的解释器构建的,在项目环境里运行它要么失败,要么产生没人支持的混合体。用环境变量把路径注入进去,是同样错误的多余步骤:你改变了工具加载什么,却没有改变它被测试时所对照的东西。这种交换看起来像修复,直到下一次导入为止。

为什么改系统解释器是个陷阱

改默认解释器之所以有吸引力,是因为它看起来用一条命令就修好了回溯信息。它同时破坏了主机上每个打包 Python 工具所基于的前提,故障会在之后、在无关的地方出现。

备选方案切换机制是为在等价实现之间切换而设计的,不是为了在较旧的编译模块之下安装更新的语言。编译扩展针对特定 ABI 构建,所以昨天还能导入的模块今天可能加载失败,而一个包都没有被删除。

人们找到的建议里还有一个命名陷阱。无版本号的 python 命令在不同发行版和环境里没有唯一普适的含义,把它当作某个主版本的固定同义词,只会得到仅在写下它的那台机器上有效的修法。请读眼前这个系统的策略,而不是博客文章里的策略。

分开的验收测试

最后应当有两个测试,通过其中一个并不说明另一个。第一个是受支持的客户端命令能用它为打包的解释器工作。第二个是特权服务建立会话,隧道的流量能通过。

openvpn3 sessions-list
openvpn3-service-client --help 2>/dev/null | head -n 3
ip -4 addr show tun0

工作时保留一条独立的机器入口,尤其是当 VPN 承载着你唯一的管理访问通道时。改动系统解释器正是那种会在中途夺走该访问的编辑。

如果故障是系统工具缺少模块,修法是通过受支持流程执行包操作。如果故障是你自己项目里的版本冲突,修法是一个环境。在两者之间做选择就是全部决定,而它由哪个程序打印了回溯信息来决定。

相关文章