"""影子柜台 CLI 入口。 用法: python -m sanguo_trader.shadow # 读 env(见 runner.py docstring) python -m sanguo_trader.shadow --auto # 主管模式:轮询 paper 库自动拉起影子账户 """ from __future__ import annotations import logging import os import sys from pathlib import Path LOCK_FILE = Path( os.environ.get("SANGUO_SHADOW_LOCK") or Path.home() / ".sanguo_shadow_desk.lock" ) def _acquire_lock() -> "object | None": """单实例锁(Windows/msvcrt 与 POSIX/fcntl 双兼容)。""" LOCK_FILE.parent.mkdir(parents=True, exist_ok=True) try: import fcntl # POSIX fh = open(LOCK_FILE, "w") fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) return fh except ImportError: pass except OSError: return None # 已有实例在跑 try: import msvcrt # Windows fh = open(LOCK_FILE, "w") msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1) return fh except (ImportError, OSError): return None def main() -> int: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) if "--auto" in sys.argv: # supervisor 自身日志落文件(schtasks 控制台输出是黑洞,拉起/重启决策无处可查) try: _log_dir = Path(__file__).resolve().parents[2] / "logs" _log_dir.mkdir(parents=True, exist_ok=True) _fh = logging.FileHandler(_log_dir / "shadow_supervisor.log") _fh.setFormatter(logging.Formatter( "%(asctime)s %(levelname)s %(name)s: %(message)s")) logging.getLogger().addHandler(_fh) except Exception: pass # 日志失败不挡主管 from .supervisor import run_auto_supervisor run_auto_supervisor() return 0 if "--account" in sys.argv: # 多账户 worker 不抢全局 solo 锁:全局锁全机只放行一个 worker,supervisor # 多账户并存时其余全部"锁占用"优雅退出(VPS 2026-08-16 实录 40/41 反复 # rc=0,单账户时代从未暴露)。同账户防重复由 bullet_trade # runtime/shadow_{aid} 实例锁兜底,supervisor children 表保证一账户一进程。 from .runner import run_shadow run_shadow() return 0 lock = _acquire_lock() if lock is None: print("影子柜台已在运行(锁占用),本次启动退出。", flush=True) return 0 from .runner import run_shadow run_shadow() return 0 if __name__ == "__main__": sys.exit(main())