"""Variet Agent — 진입점. FastAPI 서버 + Discord Bot + APScheduler를 동시 실행합니다. 상시 실행 안정화: 파일 로깅, graceful shutdown, 자가 헬스체크. """ import asyncio import logging import sys import signal from logging.handlers import RotatingFileHandler from pathlib import Path # config를 먼저 import → .env 로드 import config # ────────────────────────────────────────────── # 로깅 설정 (파일 + 콘솔) # ────────────────────────────────────────────── LOG_DIR = Path(__file__).parent / "logs" LOG_DIR.mkdir(exist_ok=True) handlers = [ logging.StreamHandler(sys.stdout), RotatingFileHandler( LOG_DIR / "variet.log", maxBytes=10 * 1024 * 1024, # 10MB backupCount=5, encoding="utf-8", ), ] logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=handlers, ) logger = logging.getLogger("variet") # 외부 라이브러리 로그 레벨 조정 logging.getLogger("discord").setLevel(logging.WARNING) logging.getLogger("uvicorn").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) # ────────────────────────────────────────────── # 서브시스템 # ────────────────────────────────────────────── async def run_api_server(): """FastAPI 서버를 uvicorn으로 실행.""" import uvicorn uvi_config = uvicorn.Config( "api.server:app", host=config.API_HOST, port=config.API_PORT, log_level="warning", reload=False, ) server = uvicorn.Server(uvi_config) await server.serve() async def run_discord_bot(): """Discord Bot 실행.""" from api.discord_bot import start_bot await start_bot() async def run_scheduler(): """APScheduler — 주기적 작업 실행.""" try: from apscheduler.schedulers.asyncio import AsyncIOScheduler except ImportError: logger.warning("APScheduler 미설치 — 스케줄러 비활성화 (pip install apscheduler)") return scheduler = AsyncIOScheduler() # 헬스체크: 5분마다 async def health_check(): logger.debug("헬스체크 OK") scheduler.add_job(health_check, "interval", minutes=5, id="health_check") # TODO: 여기에 주기적 작업 추가 # 예: 편성표 체크, 완료 토렌트 정리 등 # scheduler.add_job(check_schedule, "cron", hour=18, id="anime_schedule") scheduler.start() logger.info(f"스케줄러 시작 — {len(scheduler.get_jobs())}개 작업 등록") # 스케줄러가 종료되지 않도록 대기 try: while True: await asyncio.sleep(3600) except asyncio.CancelledError: scheduler.shutdown(wait=False) logger.info("스케줄러 종료") # ────────────────────────────────────────────── # 메인 # ────────────────────────────────────────────── async def main(): """API 서버 + Discord Bot + 스케줄러 동시 실행.""" logger.info("=" * 50) logger.info("Variet Agent 시작") logger.info(f" API: http://{config.API_HOST}:{config.API_PORT}") logger.info(f" Discord Bot: {'토큰 설정됨' if config.DISCORD_BOT_TOKEN else '⚠ 토큰 없음'}") logger.info(f" 로그: {LOG_DIR / 'variet.log'}") logger.info("=" * 50) tasks = [] # API 서버 tasks.append(asyncio.create_task(run_api_server())) # Discord Bot (토큰이 있을 때만) if config.DISCORD_BOT_TOKEN: tasks.append(asyncio.create_task(run_discord_bot())) else: logger.warning("DISCORD_BOT_TOKEN이 없습니다. Bot 없이 API만 실행합니다.") # 스케줄러 tasks.append(asyncio.create_task(run_scheduler())) # Graceful shutdown 핸들러 shutdown_event = asyncio.Event() def _signal_handler(): logger.info("종료 신호 수신...") shutdown_event.set() loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(sig, _signal_handler) except NotImplementedError: # Windows에서는 add_signal_handler 미지원 pass try: # 어느 하나가 종료되거나 shutdown 신호가 오면 종료 done, pending = await asyncio.wait( [*tasks, asyncio.create_task(shutdown_event.wait())], return_when=asyncio.FIRST_COMPLETED, ) except KeyboardInterrupt: logger.info("KeyboardInterrupt 수신...") except Exception as e: logger.error(f"실행 오류: {e}", exc_info=True) finally: # 모든 태스크 정리 for t in tasks: if not t.done(): t.cancel() # Discord Bot 정리 try: from api.discord_bot import stop_bot await stop_bot() except Exception: pass logger.info("Variet Agent 종료") if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass