Spaces:
Running
Running
| import asyncio | |
| import threading | |
| import os | |
| from datetime import datetime | |
| from extensions import db # β Safe import | |
| from models import BotStatus, BotLog | |
| from discord_bot import run_bot | |
| class BotManager: | |
| def __init__(self): | |
| self.bot_thread = None | |
| self.loop = None | |
| self.is_running = False | |
| self._stop_event = None | |
| # Sync with database after all attributes are set | |
| try: | |
| self._sync_with_database() | |
| except Exception as e: | |
| print(f"Could not sync with database during init: {e}") | |
| def log_callback(self, level, message): | |
| """Callback function to handle bot logs""" | |
| from app import app # π Lazy import to avoid circular dependency | |
| with app.app_context(): | |
| try: | |
| log_entry = BotLog(level=level, message=message) | |
| db.session.add(log_entry) | |
| db.session.commit() | |
| # Keep only last 1000 log entries | |
| total_logs = BotLog.query.count() | |
| if total_logs > 1000: | |
| oldest_logs = BotLog.query.order_by(BotLog.id).limit(total_logs - 1000).all() | |
| for log in oldest_logs: | |
| db.session.delete(log) | |
| db.session.commit() | |
| except Exception as e: | |
| print(f"Failed to log to database: {e}") | |
| def _run_bot_in_thread(self): | |
| """Run the bot in a separate thread with its own event loop""" | |
| from app import app # π Lazy import | |
| try: | |
| self.loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(self.loop) | |
| self._stop_event = asyncio.Event() | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| status.is_running = True | |
| status.started_at = datetime.utcnow() | |
| status.last_error = None | |
| db.session.commit() | |
| self.log_callback("INFO", "Starting Discord bot...") | |
| self.loop.run_until_complete(run_bot(self.log_callback)) | |
| except Exception as e: | |
| self.log_callback("ERROR", f"Bot thread error: {e}") | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| status.is_running = False | |
| status.last_error = str(e) | |
| db.session.commit() | |
| finally: | |
| self.is_running = False | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| status.is_running = False | |
| db.session.commit() | |
| def start_bot(self): | |
| """Start the Discord bot in a separate thread""" | |
| from app import app # π Lazy import | |
| if self.is_running: | |
| return False, "Bot is already running" | |
| # Check if required environment variables are set | |
| if not os.getenv("DISCORD_TOKEN"): | |
| return False, "DISCORD_TOKEN not set in environment variables" | |
| if not os.getenv("SPACE_URL"): | |
| return False, "SPACE_URL not set in environment variables" | |
| try: | |
| self.bot_thread = threading.Thread(target=self._run_bot_in_thread, daemon=True) | |
| self.bot_thread.start() | |
| self.is_running = True | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| status.restart_count += 1 | |
| db.session.commit() | |
| return True, "Bot started successfully" | |
| except Exception as e: | |
| self.log_callback("ERROR", f"Failed to start bot: {e}") | |
| return False, f"Failed to start bot: {e}" | |
| def stop_bot(self): | |
| """Stop the Discord bot""" | |
| from app import app # π Lazy import | |
| if not self.is_running: | |
| return False, "Bot is not running" | |
| try: | |
| self.log_callback("INFO", "Stopping Discord bot...") | |
| if self.loop and not self.loop.is_closed(): | |
| # Schedule the stop event to be set | |
| asyncio.run_coroutine_threadsafe(self._stop_bot_async(), self.loop) | |
| # Wait for the thread to finish | |
| if self.bot_thread and self.bot_thread.is_alive(): | |
| self.bot_thread.join(timeout=10) | |
| self.is_running = False | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| status.is_running = False | |
| db.session.commit() | |
| return True, "Bot stopped successfully" | |
| except Exception as e: | |
| self.log_callback("ERROR", f"Failed to stop bot: {e}") | |
| return False, f"Failed to stop bot: {e}" | |
| async def _stop_bot_async(self): | |
| """Async method to stop the bot gracefully""" | |
| if self._stop_event: | |
| self._stop_event.set() | |
| def restart_bot(self): | |
| """Restart the Discord bot""" | |
| self.log_callback("INFO", "Restarting Discord bot...") | |
| stop_success, stop_msg = self.stop_bot() | |
| if not stop_success: | |
| return False, f"Failed to stop bot: {stop_msg}" | |
| # Wait a moment for cleanup | |
| import time | |
| time.sleep(2) | |
| start_success, start_msg = self.start_bot() | |
| if not start_success: | |
| return False, f"Failed to start bot: {start_msg}" | |
| return True, "Bot restarted successfully" | |
| def _sync_with_database(self): | |
| """Sync bot manager state with database on startup""" | |
| from app import app # π Lazy import | |
| try: | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| # If database shows bot is running but manager thinks it's not, reset database | |
| if status.is_running and not self.is_running: | |
| status.is_running = False | |
| status.last_error = "Bot was reset due to application restart" | |
| db.session.commit() | |
| self.log_callback("INFO", "Reset bot status after application restart") | |
| except Exception as e: | |
| print(f"Could not sync with database: {e}") | |
| def get_status(self): | |
| """Get current bot status""" | |
| from app import app # π Lazy import | |
| with app.app_context(): | |
| status = BotStatus.get_current_status() | |
| return { | |
| 'is_running': self.is_running, | |
| 'database_status': status.to_dict(), | |
| 'thread_alive': self.bot_thread.is_alive() if self.bot_thread else False | |
| } | |
| # Global bot manager instance | |
| bot_manager = BotManager() |