「容器退出码是 0。」这是整件事最刺眼的地方。数据库根本没起来,wait-for-it.sh 明明打印了超时警告,然后照样把我的命令跑了起来。我检查到的那个 0,是 python app.py 启动成功的退出码——它确实成功了,成功了三秒。

我花了一下午怀疑数据库镜像。真正的问题不在数据库,在那行看起来天经地义的 entrypoint 脚本:./wait-for-it.sh db:5432 -- python app.py

打开网易新闻 查看精彩图片

这是设计决定,不是 bug

先把话说清楚:这不是一个漏洞,是一个被文档写明的设计决定。它之所以还是咬到了我,是因为这个被写明的行为,和工具名字承诺的东西正好相反。

这个仓库是 vishnubob 原版的复活副本,MIT 许可,上游历史保留,整个工具就是一个 182 行的 bash 文件。它的最后一段逻辑是这样的:只有当等待失败并且带了 -s 时,脚本才会拒绝执行子进程;否则,等待失败会直接落到 exec 上。

而因为是 exec,脚本自身的退出状态被子进程替换掉了。超时这件事,在返回码里一点痕迹都不留。

两次运行,两种命运

为了不靠记忆说话,我在自己机器上跑了一遍。第一次,只等 5 秒,超时后退出码是 124。第二次,等 2 秒并带上要执行的命令,端口 1 上什么都没开,脚本也明说了超时,但命令照样跑了,退出码是 0

第二次运行就是整个故事。124 是 coreutils 的 timeout 状态漏出来的——脚本会在 timeout 下重新调用自己,好让等待期间 Ctrl-C 能生效。这是个相当聪明的 bash 技巧,也是你看到 124 而不是 1 的原因。

所以规则是:不带 -s 的 wait-for-it.sh 不是一次检查,是一个有上限的延迟。如果你想要一道闸门,那个 flag 不是可选项。

探针本身才是最有意思的部分

重写会丢掉这段逻辑,所以直接看它怎么探测端口:如果处于 busy 状态,就用 nc -z 去连主机和端口,把结果码存下来;否则,用 bash 的 /dev/tcp 伪设备发一个空字节,同样把结果码存下来。

两种方式,一个目的:确认那个端口到底有没有人应答。问题从来不在探针准不准,而在探针说了「没有」之后,脚本接下来做什么。

名字叫 wait-for-it,读起来像「等到它好了再走」。实际行为是「等到它好了,或者等到我耐心用完,然后都继续走」。这两件事的差别,就是那个下午。