""" FastAPI application factory. Entry point: uvicorn main:app --reload """ from __future__ import annotations import asyncio import logging import queue from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.util import get_remote_address from config import settings logging.basicConfig( level=getattr(logging, settings.log_level.upper(), logging.INFO), format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger(__name__) limiter = Limiter(key_func=get_remote_address) @asynccontextmanager async def lifespan(app: FastAPI): """Startup + shutdown logic.""" # ── Startup ── logger.info("Starting Languard...") # 1. Init DB and run migrations from database import get_engine, run_migrations engine = get_engine() run_migrations(engine) # 2. Register adapters from adapters import initialize_adapters initialize_adapters() # 3. Create WebSocket manager (asyncio-only) from core.websocket.manager import WebSocketManager ws_manager = WebSocketManager() app.state.ws_manager = ws_manager # 4. Create global broadcast queue and BroadcastThread broadcast_queue = queue.Queue(maxsize=1000) app.state.broadcast_queue = broadcast_queue from core.websocket.broadcast_thread import BroadcastThread loop = asyncio.get_event_loop() broadcast_thread = BroadcastThread( event_queue=broadcast_queue, ws_manager=ws_manager, loop=loop, ) broadcast_thread.start() app.state.broadcast_thread = broadcast_thread # 5. Create ThreadRegistry from core.threads.thread_registry import ThreadRegistry from core.servers.process_manager import ProcessManager from adapters.registry import GameAdapterRegistry process_manager = ProcessManager.get() thread_registry = ThreadRegistry( process_manager=process_manager, adapter_registry=GameAdapterRegistry, global_broadcast_queue=broadcast_queue, ) ThreadRegistry.set_instance(thread_registry) app.state.thread_registry = thread_registry # 6. Recover processes that survived a restart process_manager.recover_on_startup(engine.connect()) # 7. Reattach threads for running servers from core.dal.server_repository import ServerRepository with engine.connect() as db: server_repo = ServerRepository(db) running_servers = server_repo.get_running() for server in running_servers: try: thread_registry.reattach_server_threads(server["id"], db) logger.info("Reattached threads for server %d", server["id"]) except Exception as exc: logger.error("Failed to reattach threads for server %d: %s", server["id"], exc) # 8. Seed default admin if no users exist from core.auth.service import AuthService with engine.connect() as db: svc = AuthService(db) generated_password = svc.seed_admin_if_empty() db.commit() if generated_password: logger.warning("=" * 60) logger.warning(" FIRST RUN — default admin created") logger.warning(" Username: admin") logger.warning(" Password: %s", generated_password) logger.warning(" Change this password immediately!") logger.warning("=" * 60) # 9. Register and start APScheduler cleanup jobs from core.jobs.scheduler import start_scheduler, stop_scheduler from core.jobs.cleanup_jobs import register_cleanup_jobs register_cleanup_jobs() start_scheduler() yield # ── Shutdown ── logger.info("Shutting down Languard...") try: ThreadRegistry.stop_all() except Exception as e: logger.error("Thread shutdown error: %s", e) broadcast_thread.stop() broadcast_thread.join(timeout=5.0) from core.jobs.scheduler import stop_scheduler stop_scheduler() def create_app() -> FastAPI: app = FastAPI( title="Languard Server Manager", version="1.0.0", lifespan=lifespan, docs_url="/docs", redoc_url="/redoc", ) # ── Middleware ── app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── Global exception handler ── @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): logger.error("Unhandled exception: %s", exc, exc_info=True) return JSONResponse( status_code=500, content={ "success": False, "data": None, "error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}, }, ) # ── Routers ── from core.auth.router import router as auth_router from core.games.router import router as games_router from core.system.router import router as system_router from core.servers.router import router as servers_router from core.servers.players_router import router as players_router from core.servers.bans_router import router as bans_router from core.servers.missions_router import router as missions_router from core.servers.mods_router import router as mods_router from core.websocket.router import router as ws_router app.include_router(auth_router, prefix="/api") app.include_router(games_router, prefix="/api") app.include_router(system_router, prefix="/api") app.include_router(servers_router, prefix="/api") app.include_router(players_router, prefix="/api") app.include_router(bans_router, prefix="/api") app.include_router(missions_router, prefix="/api") app.include_router(mods_router, prefix="/api") app.include_router(ws_router) return app app = create_app()