diff --git a/bot/bot.py b/bot/bot.py index 1c90610..f032c38 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -874,10 +874,82 @@ def save_autonomous_state(): except Exception as e: logger.error(f"Failed to save autonomous context on shutdown: {e}") -# Register shutdown handlers +async def graceful_shutdown(): + """ + Perform a full async cleanup before the bot exits. + + Shutdown sequence: + 1. End active voice sessions (disconnect, release GPU locks) + 2. Save autonomous engine state + 3. Stop the APScheduler + 4. Cancel all tracked background tasks + 5. Close the Discord gateway connection + """ + logger.warning("🛑 Graceful shutdown initiated...") + + # 1. End active voice session (cleans up audio, STT, GPU locks, etc.) + try: + from utils.voice_manager import VoiceSessionManager + session_mgr = VoiceSessionManager() + if session_mgr.active_session: + logger.info("🎙️ Ending active voice session...") + await session_mgr.end_session() + logger.info("✓ Voice session ended") + except Exception as e: + logger.error(f"Error ending voice session during shutdown: {e}") + + # 2. Persist autonomous engine state + save_autonomous_state() + + # 3. Shut down the APScheduler + try: + if globals.scheduler.running: + globals.scheduler.shutdown(wait=False) + logger.info("✓ Scheduler stopped") + except Exception as e: + logger.error(f"Error stopping scheduler: {e}") + + # 4. Cancel all tracked background tasks + try: + from utils.task_tracker import _active_tasks + pending = [t for t in _active_tasks if not t.done()] + if pending: + logger.info(f"Cancelling {len(pending)} background tasks...") + for t in pending: + t.cancel() + await asyncio.gather(*pending, return_exceptions=True) + logger.info("✓ Background tasks cancelled") + except Exception as e: + logger.error(f"Error cancelling background tasks: {e}") + + # 5. Close the Discord gateway connection + try: + if not globals.client.is_closed(): + await globals.client.close() + logger.info("✓ Discord client closed") + except Exception as e: + logger.error(f"Error closing Discord client: {e}") + + logger.warning("🛑 Graceful shutdown complete") + +def _handle_shutdown_signal(sig, _frame): + """Schedule the async shutdown from a sync signal handler.""" + sig_name = signal.Signals(sig).name + logger.warning(f"Received {sig_name}, scheduling graceful shutdown...") + # Schedule the coroutine on the running event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(graceful_shutdown()) + else: + # Fallback: just save state synchronously + save_autonomous_state() + +# Register signal handlers (async-aware) +signal.signal(signal.SIGTERM, _handle_shutdown_signal) +signal.signal(signal.SIGINT, _handle_shutdown_signal) + +# Keep atexit as a last-resort sync fallback atexit.register(save_autonomous_state) -signal.signal(signal.SIGTERM, lambda s, f: save_autonomous_state()) -signal.signal(signal.SIGINT, lambda s, f: save_autonomous_state()) threading.Thread(target=start_api, daemon=True).start() globals.client.run(globals.DISCORD_BOT_TOKEN)