#!/usr/bin/env python3 import argparse import asyncio import json import logging import os import signal import sys import tempfile from pathlib import Path from typing import Dict, List, Optional # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger('icecast-irc-bot-manager') class BotManager: """Manages multiple Icecast IRC bot instances.""" def __init__(self): self.bots: Dict[str, asyncio.subprocess.Process] = {} self.config_dir = Path('/etc/icecast-irc-bot') self.socket_dir = Path(tempfile.gettempdir()) async def start_bot(self, config_path: Path) -> bool: """Start a bot instance with the given config. Args: config_path: Path to the bot's config file Returns: bool: True if bot was started successfully """ try: # Create unique name for this bot instance bot_id = config_path.stem # Check if bot is already running if bot_id in self.bots: logger.warning(f"Bot {bot_id} is already running") return False # Start the bot process process = await asyncio.create_subprocess_exec( sys.executable, '-m', 'main', '--config', str(config_path), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) self.bots[bot_id] = process logger.info(f"Started bot {bot_id} (PID: {process.pid})") return True except Exception as e: logger.error(f"Failed to start bot with config {config_path}: {e}") return False async def stop_bot(self, bot_id: str) -> bool: """Stop a running bot instance. Args: bot_id: ID of the bot to stop Returns: bool: True if bot was stopped successfully """ if bot_id not in self.bots: logger.warning(f"Bot {bot_id} is not running") return False try: process = self.bots[bot_id] process.terminate() try: await asyncio.wait_for(process.wait(), timeout=5.0) except asyncio.TimeoutError: process.kill() await process.wait() del self.bots[bot_id] logger.info(f"Stopped bot {bot_id}") return True except Exception as e: logger.error(f"Failed to stop bot {bot_id}: {e}") return False async def restart_bot(self, bot_id: str) -> bool: """Restart a running bot instance. Args: bot_id: ID of the bot to restart Returns: bool: True if bot was restarted successfully """ # Find the config file for this bot config_path = self.config_dir / f"{bot_id}.yaml" if not config_path.exists(): logger.error(f"Config file not found for bot {bot_id}") return False # Stop the bot if it's running if bot_id in self.bots: if not await self.stop_bot(bot_id): return False # Start the bot with the same config return await self.start_bot(config_path) async def list_bots(self) -> List[Dict]: """List all running bot instances. Returns: List[Dict]: List of bot info dictionaries """ bot_info = [] for bot_id, process in self.bots.items(): info = { 'id': bot_id, 'pid': process.pid, 'running': process.returncode is None } bot_info.append(info) return bot_info async def cleanup(self): """Clean up all running bots.""" for bot_id in list(self.bots.keys()): await self.stop_bot(bot_id) async def main(): parser = argparse.ArgumentParser(description='Icecast IRC Bot Manager') parser.add_argument('--config', type=str, help='Path to config file or directory') parser.add_argument('command', choices=['start', 'stop', 'restart', 'list'], help='Command to execute') parser.add_argument('bot_id', nargs='?', help='Bot ID for start/stop/restart commands') args = parser.parse_args() manager = BotManager() try: if args.command == 'list': bot_info = await manager.list_bots() if bot_info: print(json.dumps(bot_info, indent=2)) else: print("No bots running") elif args.command == 'start': if not args.config: print("Error: --config required for start command") sys.exit(1) config_path = Path(args.config) if config_path.is_dir(): # Start all bots in directory for config_file in config_path.glob('*.yaml'): await manager.start_bot(config_file) else: # Start single bot await manager.start_bot(config_path) elif args.command == 'stop': if not args.bot_id: print("Error: bot_id required for stop command") sys.exit(1) await manager.stop_bot(args.bot_id) elif args.command == 'restart': if not args.bot_id: print("Error: bot_id required for restart command") sys.exit(1) await manager.restart_bot(args.bot_id) except KeyboardInterrupt: logger.info("Shutting down...") await manager.cleanup() except Exception as e: logger.error(f"Unhandled error: {e}") sys.exit(1) if __name__ == '__main__': # Set up signal handlers for sig in (signal.SIGTERM, signal.SIGINT): signal.signal(sig, lambda s, f: sys.exit(0)) # Run the manager asyncio.run(main())