fix(startup): build asyncio socketpair over AF_UNIX - unblocks .bat console freeze

The run_bot.bat console path froze for minutes after two log lines with 0 CPU:
asyncio.run() -> ProactorEventLoop.__init__ -> socket.socketpair() -> Python
3.12 _fallback_socketpair() blocks forever in accept() on the loopback TCP
handshake (security software filters 127.0.0.1 in this interactive console +
xau-trader.exe session). Git-bash redirect runs worked 4/4, proving it is
environment, not interpreter.

Windows 10 17063+ supports AF_UNIX sockets, which need no loopback TCP at all.
Monkey-patch socket.socketpair on win32 to self-pair over AF_UNIX via ctypes
(ws2_32; python.org builds don't expose the AF_UNIX constant), falling back to
the stock socketpair if AF_UNIX is unavailable. socket.socket(fileno=N) treats
N as a raw SOCKET handle on Windows, so the raw handles wrap cleanly.

Verified: smoke test raw pair family=1 + full asyncio.run Proactor loop; the
exe boot went from hang to 'Main loop started' in ~10s.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:58:08 +08:00
co-authored by Claude Code
parent f38fa944ea
commit 1a7b0111ad
+102
View File
@@ -19,6 +19,108 @@ if hasattr(sys.stderr, "reconfigure"):
from loguru import logger
# ── Windows startup-freeze fix ─────────────────────────────────────────────
# asyncio.run() → ProactorEventLoop.__init__ calls socket.socketpair(). On
# Windows Python 3.12 that goes through _fallback_socketpair(), which does a
# loopback (127.0.0.1) TCP connect/accept dance. Under the run_bot.bat console
# path, security software filters that loopback handshake → accept() blocks
# FOREVER → the bot hangs before start() ever runs (2 init lines on console,
# log file stays empty, process idle at 0 CPU). Windows 10+ supports AF_UNIX
# sockets, which need no loopback TCP at all, so build the self-pipe pair over
# AF_UNIX and fall back to the stock socketpair if AF_UNIX is unavailable.
if sys.platform == "win32":
import ctypes as _ctypes
import os as _os
import socket as _socket_mod
import tempfile as _tempfile
# Python builds (incl. python.org 3.12) don't expose socket.AF_UNIX on
# Windows even though Winsock has supported it since Win10 17063, so we
# call ws2_32 directly. socket.socket(fileno=N) on Windows interprets N as
# a raw SOCKET handle (proven empirically), so the pair wraps cleanly.
_ws2 = _ctypes.WinDLL("ws2_32", use_last_error=True)
_ws2.socket.argtypes = [_ctypes.c_int, _ctypes.c_int, _ctypes.c_int]
_ws2.socket.restype = _ctypes.c_size_t
_ws2.bind.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.c_int]
_ws2.bind.restype = _ctypes.c_int
_ws2.listen.argtypes = [_ctypes.c_size_t, _ctypes.c_int]
_ws2.listen.restype = _ctypes.c_int
_ws2.connect.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.c_int]
_ws2.connect.restype = _ctypes.c_int
_ws2.accept.argtypes = [_ctypes.c_size_t, _ctypes.c_void_p, _ctypes.POINTER(_ctypes.c_int)]
_ws2.accept.restype = _ctypes.c_size_t
_ws2.closesocket.argtypes = [_ctypes.c_size_t]
_ws2.closesocket.restype = _ctypes.c_int
class _SockAddrUn(_ctypes.Structure):
_fields_ = [
("sun_family", _ctypes.c_ushort),
("sun_path", _ctypes.c_char * 260), # Windows uses MAX_PATH
]
_AF_UNIX = 1 # Winsock2.h: AF_UNIX == 1
_SOCK_STREAM = 1
_INVALID_SOCKET = _ctypes.c_size_t(-1).value
def _unix_socketpair():
path = _os.path.join(
_tempfile.gettempdir(),
f"xau_sp_{_os.getpid()}_{_os.urandom(4).hex()}.sock",
)
server = _ws2.socket(_AF_UNIX, _SOCK_STREAM, 0)
if server == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "socket(AF_UNIX)")
client = _INVALID_SOCKET
accepted = _INVALID_SOCKET
ok = False
try:
addr = _SockAddrUn()
addr.sun_family = _AF_UNIX
path_b = path.encode("utf-8")
if len(path_b) >= 260:
raise ValueError("AF_UNIX path too long")
addr.sun_path = path_b + b"\x00"
addr_len = _ctypes.sizeof(_ctypes.c_ushort) + len(path_b) + 1
if _ws2.bind(server, _ctypes.byref(addr), addr_len) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "bind AF_UNIX")
if _ws2.listen(server, 1) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "listen AF_UNIX")
client = _ws2.socket(_AF_UNIX, _SOCK_STREAM, 0)
if client == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "socket(AF_UNIX) client")
if _ws2.connect(client, _ctypes.byref(addr), addr_len) != 0:
raise _ctypes.OSError(_ctypes.get_last_error(), "connect AF_UNIX")
accepted = _ws2.accept(server, None, None)
if accepted == _INVALID_SOCKET:
raise _ctypes.OSError(_ctypes.get_last_error(), "accept AF_UNIX")
# Ownership of both raw handles transfers to the Python sockets.
result = (_socket_mod.socket(fileno=accepted), _socket_mod.socket(fileno=client))
ok = True
return result
finally:
if server != _INVALID_SOCKET:
_ws2.closesocket(server)
if not ok:
for h in (accepted, client):
if h != _INVALID_SOCKET:
_ws2.closesocket(h)
try:
_os.unlink(path)
except OSError:
pass # unique temp name → leftovers never collide
_orig_socketpair = _socket_mod.socketpair
def _safe_socketpair(*args, **kwargs):
try:
return _unix_socketpair()
except Exception:
# Rare fallback (pre-17063 Windows without AF_UNIX) — same
# behavior as before the patch.
return _orig_socketpair(*args, **kwargs)
_socket_mod.socketpair = _safe_socketpair
def parse_args():
parser = argparse.ArgumentParser(