Files
socks-manager/engine/instances.py
T
Your Name 76dca88ee7 fix: 服务器停止时优雅等待连接关闭
问题: stop() 只调用 server.close() 后立即停止事件循环
导致活跃连接收到 GeneratorExit,产生大量 Task destroyed 警告

修复: 使用 run_coroutine_threadsafe 在子线程事件循环中执行
server.wait_closed(),最多等待5秒优雅关闭
2026-07-17 00:09:27 +08:00

306 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""多实例管理器:每个实例是一个独立的 asyncio 服务器。"""
import asyncio
import logging
import threading
import time
from dataclasses import dataclass, field
from engine.server import ConnectionHandler
log = logging.getLogger("socks.instances")
@dataclass
class InstanceConfig:
"""单个实例的配置快照(运行时使用)。"""
instance_id: int
name: str
listen_host: str
listen_port: int
timeout: int
auth_method: str
bandwidth_down: float
bandwidth_up: float
max_concurrent: int
def match(self, new_name, new_host, new_port):
return (self.name == new_name and
self.listen_host == new_host and
self.listen_port == new_port)
class Socks5Server:
"""单个 SOCKS5 服务器的 asyncio 事件循环。"""
def __init__(self, config: InstanceConfig, user_service, flask_app=None):
self.config = config
self.user_service = user_service
self.flask_app = flask_app
self.server = None
self.loop = None
self.thread = None
self.running = False
self._active_connections = 0
self._lock = threading.Lock()
def start(self):
"""启动服务器(在新线程中运行独立事件循环)。"""
if self.running:
return
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self._active_connections = 0
self.running = True
self.server = None
self._start_error = None # 用于在主线程透传子线程异常
def run_loop():
asyncio.set_event_loop(self.loop)
try:
self.server = self.loop.run_until_complete(
self._create_server()
)
self.loop.run_forever()
except Exception as e:
# 端口占用等异常:保存异常并停止循环,避免主线程死等
self._start_error = e
log.error("[%s] SOCKS5 服务器启动失败: %s",
self.config.name, e)
try:
self.loop.stop()
except Exception:
pass
self.thread = threading.Thread(target=run_loop, daemon=True,
name=f"socks5-{self.config.name}")
self.thread.start()
# 等 server 创建完成 (或失败) — 设了上限避免永久死等
deadline = time.time() + 5.0
while self.server is None and self.running and self._start_error is None:
time.sleep(0.05)
if time.time() > deadline:
log.error("[%s] SOCKS5 服务器启动超时", self.config.name)
self.running = False
break
# 如果子线程失败, 把异常抛回调用方
if self._start_error is not None:
self.running = False
raise self._start_error
if self.server:
log.info("[%s] SOCKS5 服务器已启动: %s:%d",
self.config.name, self.config.listen_host, self.config.listen_port)
def stop(self):
"""停止服务器。"""
if not self.running:
return
self.running = False
if self.server and self.loop:
# 在子线程事件循环中优雅关闭服务器
import concurrent.futures
async def _shutdown():
self.server.close()
await self.server.wait_closed()
try:
future = asyncio.run_coroutine_threadsafe(_shutdown(), self.loop)
future.result(timeout=5) # 最多等待5秒
except (concurrent.futures.TimeoutError, Exception):
pass
if self.loop:
try:
self.loop.call_soon_threadsafe(self.loop.stop)
except Exception:
pass
if self.thread:
self.thread.join(timeout=5)
log.info("[%s] SOCKS5 服务器已停止", self.config.name)
async def _create_server(self):
return await asyncio.start_server(
self._accept_connection,
self.config.listen_host, self.config.listen_port
)
async def _accept_connection(self, reader, writer):
try:
with self._lock:
self._active_connections += 1
handler = ConnectionHandler(
reader, writer, self.config, self.user_service,
flask_app=self.flask_app,
on_close_cb=self._on_connection_close
)
await handler.handle()
except (asyncio.CancelledError, GeneratorExit):
# 服务器关闭时正常取消,不记录错误
pass
finally:
try:
writer.close()
except Exception:
pass
def _on_connection_close(self, handler):
with self._lock:
self._active_connections = max(0, self._active_connections - 1)
@property
def active_connections(self):
with self._lock:
return self._active_connections
import time # noqa: E402
class InstanceManager:
"""管理所有 SOCKS5 实例。"""
def __init__(self, user_service, flask_app=None):
self.user_service = user_service
self.flask_app = flask_app
self._instances = {} # name -> Socks5Server
self._config_cache = {} # name -> InstanceConfig
self._lock = threading.Lock()
def reload_config(self):
"""从数据库重新加载实例配置。"""
from models import Instance
from database import db
new_configs = {}
with db.app.app_context():
for inst in Instance.query.filter_by(enabled=True).all():
new_configs[inst.name] = InstanceConfig(
instance_id=inst.id,
name=inst.name,
listen_host=inst.listen_host,
listen_port=inst.listen_port,
timeout=inst.timeout,
auth_method=inst.auth_method,
bandwidth_down=inst.bandwidth_down,
bandwidth_up=inst.bandwidth_up,
max_concurrent=inst.max_concurrent,
)
self._config_cache = new_configs
def sync_instances(self):
"""根据数据库配置同步实例运行状态。"""
from models import Instance
from database import db
self.reload_config()
current_names = set(self._config_cache.keys())
running_names = set(self._instances.keys())
# 启动缺失的实例
for name in current_names - running_names:
cfg = self._config_cache[name]
srv = Socks5Server(cfg, self.user_service, self.flask_app)
try:
srv.start()
except OSError as e:
log.error("[%s] 跳过启动(端口冲突?: %s", name, e)
continue
with self._lock:
self._instances[name] = srv
# 停止多余的实例
for name in running_names - current_names:
srv = self._instances.pop(name)
srv.stop()
# 更新实例运行状态到数据库
with db.app.app_context():
for inst in Instance.query.all():
srv = self._instances.get(inst.name)
inst.running = srv is not None and srv.running
if srv:
inst.active_connections = srv.active_connections
else:
inst.active_connections = 0
db.session.commit()
def start_instance(self, instance_id):
"""启动单个实例。"""
from models import Instance
from database import db
with db.app.app_context():
inst = Instance.query.get(instance_id)
if not inst:
return False
cfg = InstanceConfig(
instance_id=inst.id,
name=inst.name,
listen_host=inst.listen_host,
listen_port=inst.listen_port,
timeout=inst.timeout,
auth_method=inst.auth_method,
bandwidth_down=inst.bandwidth_down,
bandwidth_up=inst.bandwidth_up,
max_concurrent=inst.max_concurrent,
)
self._config_cache[inst.name] = cfg
srv = Socks5Server(cfg, self.user_service, self.flask_app)
try:
srv.start()
except OSError as e:
# 端口占用等启动失败: 回滚状态, 清理缓存, 返回 False
log.error("[%s] 启动实例失败: %s", inst.name, e)
self._config_cache.pop(inst.name, None)
inst.running = False
db.session.commit()
raise
with self._lock:
self._instances[inst.name] = srv
inst.running = True
db.session.commit()
return True
def stop_instance(self, instance_id):
"""停止单个实例。"""
from models import Instance
from database import db
with db.app.app_context():
inst = Instance.query.get(instance_id)
if not inst:
return False
srv = self._instances.pop(inst.name, None)
if srv:
srv.stop()
del self._config_cache[inst.name]
inst.running = False
inst.active_connections = 0
db.session.commit()
return True
def restart_instance(self, instance_id):
"""重启单个实例。"""
self.stop_instance(instance_id)
return self.start_instance(instance_id)
def get_status(self):
"""获取所有实例状态。"""
from models import Instance
from database import db
with db.app.app_context():
result = []
for inst in Instance.query.all():
srv = self._instances.get(inst.name)
result.append({
"id": inst.id,
"name": inst.name,
"host": inst.listen_host,
"port": inst.listen_port,
"enabled": inst.enabled,
"running": bool(srv and srv.running),
"active_connections": srv.active_connections if srv else 0,
"auth_method": inst.auth_method,
"timeout": inst.timeout,
"bandwidth_down": inst.bandwidth_down,
"bandwidth_up": inst.bandwidth_up,
"max_concurrent": inst.max_concurrent,
"notes": inst.notes,
})
return result