SmartEDT/backend/main.py

172 lines
6.3 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.

"""SmartEDT 后端服务入口。
主要职责:
- 加载配置与初始化日志
- 初始化数据库 schema/TimescaleDB
- 构造核心服务(仿真、监控、鉴权/RBAC
- 挂载 HTTP/WebSocket 路由并启动 uvicorn
"""
from __future__ import annotations
import argparse
import asyncio
import logging
import multiprocessing
import platform
import sys
from pathlib import Path
# 修复 Windows 下 psycopg 异步连接的 Event Loop 问题
if platform.system() == "Windows":
# 1. 强制设置 SelectorEventLoopPolicy
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# 2. Monkeypatch 阻止 uvicorn 覆盖策略 (uvicorn 默认在 Windows 上强制使用 Proactor)
# 这对 psycopg 3 (SQLAlchemy async) 是必须的,因为 Proactor 不支持 add_reader/add_writer
original_set_policy = asyncio.set_event_loop_policy
def patched_set_policy(policy):
# 如果尝试设置 Proactor则忽略并保持 Selector
if isinstance(policy, asyncio.WindowsProactorEventLoopPolicy):
return
original_set_policy(policy)
asyncio.set_event_loop_policy = patched_set_policy
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket
from contextlib import asynccontextmanager
from backend.config.settings import load_settings
from backend.database.engine import create_engine, create_session_factory
from backend.database.schema import init_schema, init_timescaledb
from backend.services.broadcaster import Broadcaster
from backend.services.simulation_manager import SimulationManager
from backend.services.server_monitor import ServerMonitorService
from backend.services.unity_socket_client import UnitySocketClient
from backend.device.mock_vehicle import MockVehicleDevice
from backend.api import auth_routes, rbac_routes, routes, unity_routes, user_routes, ws
from backend.utils import configure_logging
logger = logging.getLogger("backend")
def _default_backend_log_file() -> Path | None:
"""在打包运行态下返回默认日志文件路径;开发态返回 None。"""
if getattr(sys, "frozen", False):
exe_dir = Path(sys.executable).resolve().parent
return exe_dir / "logs" / "backend.log"
return None
def _force_windows_selector_event_loop_for_uvicorn() -> None:
"""避免 uvicorn 在 Windows 上切换到 ProactorEventLoop与 psycopg async 不兼容)。"""
if platform.system() != "Windows":
return
try:
import uvicorn.loops.asyncio as uvicorn_asyncio_loop
except Exception:
return
def _selector_loop_factory(use_subprocess: bool = False):
return asyncio.SelectorEventLoop
uvicorn_asyncio_loop.asyncio_loop_factory = _selector_loop_factory
# 全局单例容器(简单实现)
class Container:
"""简易全局容器集中创建与持有配置、DB 引擎、session 工厂与各服务单例。"""
def __init__(self):
load_dotenv()
self.settings = load_settings()
configure_logging(
"INFO" if not self.settings.server.debug else "DEBUG",
log_file=_default_backend_log_file(),
)
self.file_root = self.settings.files.root_path
self.file_root.mkdir(parents=True, exist_ok=True)
self.engine = create_engine(self.settings.database)
self.session_factory = create_session_factory(self.engine)
self.broadcaster = Broadcaster()
self.unity_client = UnitySocketClient(self.settings.unity.host, self.settings.unity.port)
# 实例化服务
self.simulation_manager = SimulationManager(
self.session_factory,
self.broadcaster,
unity_client=self.unity_client,
)
# 实例化监控服务
self.server_monitor = ServerMonitorService(
self.session_factory,
self.broadcaster
)
container = Container()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""FastAPI 生命周期:启动初始化与停机清理。"""
# 启动前初始化
await init_schema(container.engine)
if container.settings.database.timescaledb:
try:
await init_timescaledb(container.engine)
except Exception as e:
# 可能是已存在或权限问题,仅打印日志
logger.warning("TimescaleDB init warning: %s", e)
# 注册默认设备
mock_dev = MockVehicleDevice(device_id="mock_01")
await container.simulation_manager.register_device(mock_dev)
# 启动监控服务
await container.server_monitor.start()
yield
# 停机清理
await container.server_monitor.stop()
await container.simulation_manager.stop()
await container.engine.dispose()
app = FastAPI(title="SmartEDT Backend", version="0.1.0", lifespan=lifespan)
app.include_router(routes.get_router(simulation_manager=container.simulation_manager, file_root=container.file_root))
app.include_router(auth_routes.get_router(session_factory=container.session_factory))
app.include_router(rbac_routes.get_router(session_factory=container.session_factory))
app.include_router(user_routes.get_router(session_factory=container.session_factory))
app.include_router(unity_routes.get_router(simulation_manager=container.simulation_manager, session_factory=container.session_factory))
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
await ws.websocket_handler(websocket, broadcaster=container.broadcaster)
def main() -> None:
"""命令行入口:解析参数并启动 uvicorn。"""
parser = argparse.ArgumentParser()
parser.add_argument("--host", default=None)
parser.add_argument("--port", type=int, default=None)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
settings = load_settings()
host = args.host or settings.server.host
port = args.port or settings.server.port
debug = args.debug or settings.server.debug
_force_windows_selector_event_loop_for_uvicorn()
if getattr(sys, 'frozen', False):
uvicorn.run(app, host=host, port=port, log_level="debug" if debug else "info")
else:
uvicorn.run("backend.main:app", host=host, port=port, reload=debug, log_level="debug" if debug else "info")
if __name__ == "__main__":
multiprocessing.freeze_support()
main()