Files
pruchase_jhw/src/main.py
T

113 lines
4.6 KiB
Python

import asyncio
import signal
import sys
from pathlib import Path
# src 为代码 root:项目不打包安装(tool.uv package=false),
# 此处显式将 src 加入 sys.path,使 `uv run python -m src.main` 与 IDE source-root 行为一致
_SRC_DIR = str(Path(__file__).resolve().parent)
if _SRC_DIR not in sys.path:
sys.path.insert(0, _SRC_DIR)
from loguru import logger
from src.browser.manager import get_browser_manager
from src.config.settings import get_settings
from src.config.accounts import load_accounts_config
from src.notifications.service import NotificationService
from src.utils.logger import setup_logger
from src.task_queue.dispatcher import TaskDispatcher
from src.task_queue.delay_zset_listener import RedisZSetDelayListener
from src.task_queue.listener import RedisStreamListener
from src.task_queue.trade_fetch_listener import TradeFetchStreamListener
from src.task_queue.redis_manager import get_redis_manager
from src.api.server import start_api_server
async def main() -> None:
"""
应用程序主入口函数。
- 初始化定制的 Loguru 结构化日志系统。
- 校验 Pydantic 配置及多站点账号 YAML 配置文件是否正确。
- 针对不同操作系统平台注册优雅退出信号处理器。
- 启动 Redis Stream 消息队列持续消费监听循环。
"""
# 1. 初始化按天分割日志系统
setup_logger()
logger.info("正在初始化日本采购自动化守护进程...")
# 2. 启动前快速校验:验证全局环境变量设置与账号配置文件
try:
settings = get_settings()
logger.info(f"应用配置加载成功: APP_ENV={settings.app_env.upper()}")
NotificationService(settings).validate_configuration()
# 解析并校验 accounts.yaml
load_accounts_config()
await get_redis_manager().initialize()
except Exception as e:
logger.critical(f"由于配置错误,守护进程启动失败: {e}")
sys.exit(1)
# 3. 预热浏览器:为所有已配置账号提前启动常驻浏览器上下文,任务到达时即开即用
await get_browser_manager().prewarm()
# 4. 实例化核心任务分发器与 Stream 监听器
dispatcher = TaskDispatcher()
stream_listener = RedisStreamListener(dispatcher)
delay_listener = RedisZSetDelayListener(dispatcher)
trade_fetch_listener = TradeFetchStreamListener(dispatcher)
# 4.1 启动截图查看 API 服务
api_runner = await start_api_server()
# 5. 优雅停机信号处理 (跨平台支持)
loop = asyncio.get_running_loop()
def shutdown_handler(sig_name: str) -> None:
logger.warning(f"接收到系统退出信号 '{sig_name}'。正在启动优雅停机流程...")
# 触发监听器停止的协程任务
asyncio.create_task(stream_listener.stop())
asyncio.create_task(delay_listener.stop())
asyncio.create_task(trade_fetch_listener.stop())
# Windows 默认的 Proactor 事件循环不支持 add_signal_handler。
if sys.platform != "win32":
logger.debug("正在注册 Unix 系统信号监听器 (SIGINT / SIGTERM)。")
for sig in (signal.SIGINT, signal.SIGTERM):
try:
loop.add_signal_handler(sig, lambda s=sig: shutdown_handler(s.name))
except NotImplementedError:
pass
else:
logger.debug("运行于 Windows 平台。跳过系统信号注册,将直接捕获 KeyboardInterrupt。")
# 6. 启动消息监听消费主循环
try:
stream_task = asyncio.create_task(stream_listener.listen())
delay_task = asyncio.create_task(delay_listener.listen())
trade_fetch_task = asyncio.create_task(trade_fetch_listener.listen())
await asyncio.gather(stream_task, delay_task, trade_fetch_task)
except asyncio.CancelledError:
logger.warning("监听器轮询执行被事件循环取消。")
except Exception as e:
logger.exception(f"主守护进程轮询中发生致命崩溃: {e}")
finally:
# 保证安全回收与连接断开
await asyncio.gather(
stream_listener.stop(),
delay_listener.stop(),
trade_fetch_listener.stop(),
get_browser_manager().close_all(),
get_redis_manager().close(),
api_runner.cleanup(),
return_exceptions=True,
)
logger.success("日本采购自动化守护进程已安全优雅退出。")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.warning("捕获到键盘手动中断。立即退出程序...")
sys.exit(0)