Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion qoder_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,24 @@ def port_owner_hint(port):
return cmds


class _ExclusiveHTTPServer(ThreadingHTTPServer):
"""端口独占绑定(Windows 双开防护)。

POSIX 的 SO_REUSEADDR 只允许复用 TIME_WAIT,重启瞬间不会误伤,保持开启;
Windows 的 SO_REUSEADDR 连「正在被监听的端口」也允许抢绑 —— 升级重启时
新旧两个实例都能 bind 成功,连接被静默分流,旧实例继续应答 /health,新版本
永远不生效(1.9.7 实测:看板一直提示「有新版本」)。Windows 改用
SO_EXCLUSIVEADDRUSE,抢绑直接 WSAEADDRINUSE。
"""
allow_reuse_address = not IS_WINDOWS

if IS_WINDOWS: # pragma: no cover - 平台分支
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_EXCLUSIVEADDRUSE, 1)
super().server_bind()


CHAT_PATH = ("/algo/api/v2/service/pro/sse/agent_chat_generation"
"?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1")
MODELS_PATH = "/algo/api/v2/model/list?Encode=1"
Expand Down Expand Up @@ -14723,7 +14741,7 @@ def _probe_health(timeout=2.0):
server = None
for _attempt in range(3):
try:
server = ThreadingHTTPServer((args.host, args.port), Handler)
server = _ExclusiveHTTPServer((args.host, args.port), Handler)
break
except OSError as exc:
_kind2 = classify_existing_instance(_probe_health(1.0))
Expand All @@ -14733,6 +14751,9 @@ def _probe_health(timeout=2.0):
print(" 看板: http://127.0.0.1:%d/" % args.port)
print()
return
# A failed TCP probe cannot prove TIME_WAIT: a listener may be
# bound but not accepting, filtered, or start after the probe.
# Keep exclusive binding on every retry; never fall back to reuse.
if _attempt < 2:
print(" [WARN] bind %s:%d 失败(%s)——端口可能仍在释放,"
"重试 %d/3 ..." % (args.host, args.port, exc, _attempt + 1))
Expand Down
66 changes: 66 additions & 0 deletions tests/_test_windows_bind_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Windows listener exclusion: atomic kernel bind, no probe/reuse fallback."""
import os
import socket
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _suite_head import bootstrap, check, declare_suite, finish
P, A = bootstrap()
declare_suite(__file__, ["qoder_proxy.py"])
with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "qoder_proxy.py"), encoding="utf-8") as fh:
src = fh.read()
check("reuse policy matches platform", P._ExclusiveHTTPServer.allow_reuse_address == (not P.IS_WINDOWS))
check("normal startup uses exclusive server", "server = _ExclusiveHTTPServer((args.host, args.port), Handler)" in src)
check("no unsafe compatibility server fallback", "_CompatHTTPServer" not in src)

def rejected(server):
try:
extra = P._ExclusiveHTTPServer(("127.0.0.1", server.getsockname()[1]), P.Handler)
except OSError:
return True
else:
extra.server_close()
return False

for reuse in (0, 1):
for listening in (False, True):
owner = socket.socket()
owner.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, reuse)
owner.bind(("127.0.0.1", 0))
if listening:
owner.listen(1)
try:
if P.IS_WINDOWS or listening:
check("reject occupied socket reuse=%s listening=%s" % (reuse, listening), rejected(owner))
else:
# POSIX reusable sockets may share a bound-but-not-listening
# address. Listener exclusion, not pre-listen reservation, is
# the cross-platform contract.
check("POSIX owner remains bound before listen reuse=%s" % reuse, owner.getsockname()[1] > 0)
finally:
owner.close()

server = P._ExclusiveHTTPServer(("127.0.0.1", 0), P.Handler)
port = server.server_port
check("server is actually listening", server.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ACCEPTCONN) == 1)
if P.IS_WINDOWS:
check("kernel exclusive option enabled", server.socket.getsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE) == 1)
intruder = socket.socket()
intruder.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
try:
intruder.bind(("127.0.0.1", port))
intruder.listen(1)
except OSError:
blocked = True
else:
blocked = False
check("exclusive listener cannot be stolen by reusable socket", blocked)
finally:
intruder.close()
else:
check("POSIX keeps TIME_WAIT reuse enabled", server.socket.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1)
server.server_close()
reopened = P._ExclusiveHTTPServer(("127.0.0.1", port), P.Handler)
check("closed listener can reopen without fallback", reopened.server_port == port)
reopened.server_close()
finish()