三个同名 jar 和一条走不通的 SFTP 通道
项目里三个子目录各放一个 jar,包名完全相同。我写了个 ssh134.py,用 paramiko 做 SFTP 助手,负责探查服务器和上传部署脚本。
第一版跑起来,mkdirs()成功,紧接着put()抛ENOENT。重试五次,三次失败。把同一逻辑拆成file.open()手动分块写入,反而稳定通过。两条路径走的是同一个 SFTP 连接,行为却完全不同。
两个中间坑:通道复用和 setpipeline
翻 ssh134.py 源码发现:mkdirs和upload各自调了一次self.ssh.open_sftp(),等于对同一台服务器开了两条 SFTP 通道。第二通道在 paramiko 里生命周期管理比较脆弱,没做连接池复用时直接崩。
改成复用同一条通道后,逐步复现全部成功。但再跑一遍 ssh134.py,又稳定失败;用python -c内联执行同一段代码,又稳定成功。差异不在代码逻辑,在执行环境。
排查过程中还发现一个 API 兼容问题:SFTPFile.setpipeline()在 paramiko 5.0 里已经不存在了。旧脚本里留着这个调用,抛的是AttributeError而不是预期的ENOENT,一度把排查方向带偏。grep 了一遍项目里所有SFTPFile方法调用,把 5.0 已砍掉的旧 API 全清掉。
另一个小坑:脚本里部分路径写的是相对路径opt/apps/...,Git Bash 下会被拼到当前工作目录。改成强制绝对路径/opt/apps/...后,路径解析这一环不再引入额外变量。
真凶:MSYS 把 /opt 改写成了 C 盘
ssh134.py 稳定失败、inline 稳定成功——这个对比指向 shell 环境。我在put()前加了一行print,把实际传给 SFTP 的远程路径打出来,输出是:
C:/Program Files/Git/opt/apps/xxx-a/app.jar不是/opt/apps/...,是 Windows 路径。Git Bash 底层的 MSYS2 默认开启参数转换:看到以/开头的命令行参数,就认为它是本地 Unix 路径,自动前缀C:/Program Files/Git/。服务器端收到这个路径,自然返回ENOENT。
解法一行:
exportMSYS2_ARG_CONV_EXCL="*"我在 ssh134.py 里加了一层防御性路径还原——检测传入路径是否被注入了C:/Program Files/Git前缀,是就 strip 掉。这样不管在 Git Bash 还是 cmd 里跑都不会再踩。
可带走的三条经验
这次从"偶发 ENOENT"到定位 MSYS 路径转换,关键转折点是那组对比实验:
- 脚本执行 → 稳定失败;同逻辑 inline → 稳定成功;把远程路径 print 出来 → 看到 Windows 前缀。三个事实一拼,根因锁死。
- SFTP 操作如果"偶发"失败,优先检查是否每次操作都开了新通道,改成复用单通道能消掉一类问题。
- 升级 paramiko 大版本前,grep 项目里所有 SFTP 相关方法调用,5.0 砍掉了一批旧 API(
setpipeline就是典型)。 - 在 Git Bash 里跑涉及
/xxx路径参数的脚本前,先echo "$1"看看参数有没有被 MSYS 改写;生产脚本里加MSYS2_ARG_CONV_EXCL="*"或做路径防御还原。
排查顺序比工具更重要。如果一开始就钻进 paramiko 源码翻,大概率绕更久;把"什么条件下成功、什么条件下失败"这个变量隔离出来,根因就跳出来了。
做自动化这几年,最耗时间的从来不是写代码,是摸清每个平台的脾气。
这篇里提到的坑,都是真金白银踩出来的。
如果你手上也有重复度很高的活儿——批量发布、数据搬运、有固定规则的机械操作
——可以在评论区说说你的场景,我看看能不能自动化掉。