Repository navigation
fix(server): Windows 上改用独占绑定,堵住升级后的静默双开 - #155
Merged
shuishuipingan merged 2 commits intoNov 12, 2026
Merged
Conversation
Windows 的 SO_REUSEADDR 允许第二个 socket 绑一个正在被监听的端口,而 HTTPServer 默认 allow_reuse_address = 1。升级时 schedule_restart() 先起新进程、 旧进程随后才退出,新进程的启动守卫把应答者判成 stale 后继续 bind,在 Windows 上 这个 bind 不会失败:两个实例同时 listen,连接静默分流到旧实例,/health 一直回 旧版本,看板永远提示「有新版本」。 Windows 改用 SO_EXCLUSIVEADDRUSE 并关掉 allow_reuse_address,抢绑直接 WSAEADDRINUSE。若独占绑定失败而端口上没人 accept(只剩 TIME_WAIT 残留, Windows 关监听后两分钟内常见),才降级到兼容 socket——只要有人 accept 就绝 不降级,否则等于把双开的大门重新打开。 测试:tests/_test_windows_bind_guard.py(7 项)。
StephenBoyMi
force-pushed
the
fix/windows-exclusive-bind
branch
from
November 12, 2026 02:54
8ab43ac to
e7f0f66
Compare
Owner
|
方向有价值,但原兼容回退存在阻塞:Windows 本机可复现先由 _CompatHTTPServer 建监听,再由 SO_REUSEADDR socket 成功抢绑同端口;负向 _port_accepts 之后插入旧式 SO_REUSEADDR 监听者,fallback 同样可成功 bind。TCP 探测失败也不能证明仅剩 TIME_WAIT(可能 bind 未 listen、过滤或时序竞争)。维护者将保留 SO_EXCLUSIVEADDRUSE + 禁止 reuse 的正确部分,移除兼容回退,全程有限重试独占绑定,并补充 reuse=0/1 × bound/listening 与反向抢绑行为测试。修订通过后才合并。 |
Owner
|
维护者修订 e868e9b 已推送此分支:移除 _CompatHTTPServer/_port_accepts 回退,所有重试保持内核独占绑定;保留 POSIX reuse。Windows 本机行为测试11/11(含未listen占位、旧reuse监听者、反向抢绑、关闭后重绑),完整145套件/4034 checks通过。TCP探测失败不再被当作TIME_WAIT证据。即将合并修订版。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
Windows 上「一键更新」后文件已经换成新版本,看板却一直提示「有新版本」,
/health回的 version 也一直是旧的。实测现场:同一台机器上两个
qoder_proxy.py进程,命令行完全相同;hub.pid指向新进程,但 18096 上的连接全部落在旧进程。新进程日志里已经打印了listening: http://127.0.0.1:18096/v1,实际一个请求都收不到。根因
http.server.HTTPServer.allow_reuse_address = 1→ Windows 上 SO_REUSEADDR 允许第二个 socket 绑一个正在被监听的端口。升级走
schedule_restart():起新进程后旧进程才退出。新进程的启动守卫探测/health,把仍在应答的旧实例判成stale(_pid_alive为假),重试 3 次后继续 bind——而 Windows 上这个 bind 不会失败。结果两个实例同时 listen,连接静默分流到旧实例,新版本永远不生效。代码里「bind 失败会保护我」的假设只在 POSIX 成立:那里第二个 bind 会直接
EADDRINUSE,进程退出。修法
allow_reuse_address = False+SO_EXCLUSIVEADDRUSE,抢绑直接WSAEADDRINUSE;测试
tests/_test_windows_bind_guard.py:7 项。windows-latest上真实复现「端口被占 → 第二个实例 bind 失败」;POSIX 上断言仍保留 SO_REUSEADDR(复用 TIME_WAIT 不受影响)。复现
Windows,两个实例交替存活时点「一键更新」:
related: #89