"""测试 app._start_sync_loop 后台线程能否自动救活死 SOCKS5 实例。 只用一次 create_app(), 保证只有一个 InstanceManager + 一个 sync loop。 让同一个 loop 既负责初次拉起实例, 也负责在实例死后自动救活。 流程: 1. create_app() 启动 sync loop (SM_SYNC_INTERVAL=2s), 此时 DB 无实例 2. 往 DB 插入一条 enabled 实例 3. 等 loop 周期把它拉起来 (端口 listen) 4. 模拟死: 关闭 server socket 5. 等 loop 探测到并自动重启 (端口重新 listen, 且是新 Socks5Server 对象) """ import asyncio import logging import os import socket import sys import tempfile import time sys.path.insert(0, os.getcwd()) _tmp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False, dir="/tmp") _tmp_db.close() os.environ["SM_DB_URI"] = f"sqlite:///{_tmp_db.name}" os.environ["SM_ADMIN_PASSWORD"] = "test123" os.environ["SM_SECRET_KEY"] = "test-secret-key" os.environ["SM_LOG_LEVEL"] = "WARNING" # 短周期, 加快测试 os.environ["SM_SYNC_INTERVAL"] = "2" os.environ["SM_SYNC_LOOP_ENABLE"] = "true" from app import create_app from database import db as _db from models import Instance def find_free_port(): s = socket.socket() s.bind(("127.0.0.1", 0)) p = s.getsockname()[1] s.close() return p def is_listening(port): try: s = socket.socket(); s.settimeout(0.5) s.connect(("127.0.0.1", port)); s.close() return True except (ConnectionRefusedError, socket.timeout, OSError): return False def wait_listening(port, timeout_s=8.0, step=0.3): t0 = time.time() while time.time() - t0 < timeout_s: if is_listening(port): return True, time.time() - t0 time.sleep(step) return False, time.time() - t0 def main(): logging.basicConfig(level=logging.WARNING, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") port = find_free_port() # 唯一一次 create_app: 启动 sync loop + 初始 sync_instances (此时 DB 无实例) flask_app = create_app() mgr = flask_app.instance_manager # type: ignore[attr-defined] # 往 DB 插入 enabled 实例, 让 sync loop 在下一周期拉起它 with flask_app.app_context(): inst = Instance( name="looper", listen_host="127.0.0.1", listen_port=port, timeout=5, enabled=True, auth_method="none", bandwidth_down=0, bandwidth_up=0, max_concurrent=10, ) _db.session.add(inst) _db.session.commit() print(f"[setup] inserted instance port={port}") # 等 sync loop 初次拉起实例 ok, elapsed = wait_listening(port, timeout_s=8.0) assert ok, f"sync loop 没在 8s 内拉起 port {port}" print(f"[1] sync loop 初次拉起 OK, port {port} listening (耗时 {elapsed:.1f}s)") # 模拟死: 关闭 server socket srv = mgr._instances["looper"] srv.loop.call_soon_threadsafe(srv.server.close) time.sleep(0.6) assert not is_listening(port), f"关闭后 port {port} 仍在 listen" print(f"[2] killed port {port}, 等 sync loop 救活...") # 等 sync loop 探测并重启 ok, elapsed = wait_listening(port, timeout_s=8.0) assert ok, f"sync loop 没在 8s 内救活 port {port}" print(f"[3] port {port} 被 sync loop 救活 (耗时 {elapsed:.1f}s)") # 验证是新的 Socks5Server 对象 (不是复用死掉的那个) new_srv = mgr._instances["looper"] assert new_srv is not srv, "sync loop 复用了死掉的 Socks5Server, 没换对象" print(f"[4] 确认是新 Socks5Server 对象 (旧 id={id(srv)}, 新 id={id(new_srv)})") # 清理 new_srv.stop() try: os.unlink(_tmp_db.name) except Exception: pass print("\nPASS: 后台 sync loop 自动救活死 SOCKS5 实例 (含对象替换)") return True if __name__ == "__main__": rc = main() sys.exit(0 if rc else 1)