2025-02-24 14:26:48 -08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import signal
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
2025-02-24 21:48:51 -08:00
|
|
|
import time
|
2025-02-24 14:26:48 -08:00
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
# ANSI color codes for terminal output if needed
|
|
|
|
|
class Colors:
|
|
|
|
|
COLORS = [
|
|
|
|
|
'\033[94m', # BLUE
|
|
|
|
|
'\033[92m', # GREEN
|
|
|
|
|
'\033[95m', # MAGENTA
|
|
|
|
|
'\033[93m', # YELLOW
|
|
|
|
|
'\033[96m', # CYAN
|
|
|
|
|
'\033[91m', # RED
|
|
|
|
|
'\033[38;5;208m', # ORANGE
|
|
|
|
|
'\033[38;5;165m', # PURPLE
|
|
|
|
|
'\033[38;5;39m', # DEEP BLUE
|
|
|
|
|
'\033[38;5;82m', # LIME
|
|
|
|
|
]
|
|
|
|
|
ENDC = '\033[0m'
|
|
|
|
|
BOLD = '\033[1m'
|
|
|
|
|
|
|
|
|
|
# Additional colors for output
|
|
|
|
|
GREY = '\033[37m'
|
|
|
|
|
WHITE = '\033[97m'
|
|
|
|
|
RED = '\033[91m'
|
|
|
|
|
ORANGE = '\033[38;5;208m' # Using 256-color code for orange
|
|
|
|
|
CYAN = '\033[96m'
|
|
|
|
|
MAGENTA = '\033[95m'
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
class BotManager:
|
|
|
|
|
"""Manages multiple Icecast IRC bot instances."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.bots: Dict[str, asyncio.subprocess.Process] = {}
|
2025-02-24 21:48:51 -08:00
|
|
|
self.config_dir = Path('.') # Use current directory instead of /etc/icecast-irc-bot
|
2025-02-24 14:26:48 -08:00
|
|
|
self.socket_dir = Path(tempfile.gettempdir())
|
2025-02-24 21:48:51 -08:00
|
|
|
self.state_file = Path(tempfile.gettempdir()) / 'icecast-irc-bots.json'
|
|
|
|
|
self.monitor_task = None
|
|
|
|
|
self.venv_python = os.getenv('VIRTUAL_ENV', '/opt/icecast-irc-bot/venv') + '/bin/python3'
|
|
|
|
|
# If venv_python doesn't exist, use the current Python interpreter
|
|
|
|
|
if not os.path.exists(self.venv_python):
|
|
|
|
|
self.venv_python = sys.executable
|
|
|
|
|
|
|
|
|
|
def _save_state(self):
|
|
|
|
|
"""Save the current state of running bots."""
|
|
|
|
|
try:
|
|
|
|
|
# Load existing state
|
|
|
|
|
existing_state = self._load_state()
|
|
|
|
|
|
|
|
|
|
# Update with current bots
|
|
|
|
|
current_state = {
|
|
|
|
|
bot_id: {
|
|
|
|
|
'pid': process.pid,
|
|
|
|
|
'config': str(self.config_dir / f"{bot_id}.yaml")
|
|
|
|
|
}
|
|
|
|
|
for bot_id, process in self.bots.items()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Merge states, with current bots taking precedence
|
|
|
|
|
merged_state = {**existing_state, **current_state}
|
|
|
|
|
|
|
|
|
|
# Save merged state
|
|
|
|
|
with open(self.state_file, 'w') as f:
|
|
|
|
|
json.dump(merged_state, f)
|
|
|
|
|
|
|
|
|
|
print(f"Saved state with {len(merged_state)} bots")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error saving state: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def _load_state(self):
|
|
|
|
|
"""Load the state of running bots."""
|
|
|
|
|
try:
|
|
|
|
|
if self.state_file.exists():
|
|
|
|
|
with open(self.state_file, 'r') as f:
|
|
|
|
|
state = json.load(f)
|
|
|
|
|
return state
|
|
|
|
|
return {}
|
|
|
|
|
except Exception:
|
|
|
|
|
return {}
|
2025-02-24 14:26:48 -08:00
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
async def monitor_processes(self):
|
|
|
|
|
"""Monitor running bot processes and clean up dead ones."""
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
for bot_id in list(self.bots.keys()):
|
|
|
|
|
process = self.bots[bot_id]
|
|
|
|
|
try:
|
|
|
|
|
# Check if process exists in system
|
|
|
|
|
os.kill(process.pid, 0)
|
|
|
|
|
# Check if process has terminated
|
|
|
|
|
if process.returncode is not None:
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
del self.bots[bot_id]
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
del self.bots[bot_id]
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
await asyncio.sleep(5) # Check every 5 seconds
|
|
|
|
|
|
|
|
|
|
async def _cleanup_process(self, process: asyncio.subprocess.Process):
|
|
|
|
|
"""Clean up a process and its resources.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
process: The process to clean up
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
print(f"Cleaning up process with PID {process.pid}")
|
|
|
|
|
# Wait for the process to finish if it hasn't
|
|
|
|
|
if process.returncode is None:
|
|
|
|
|
try:
|
|
|
|
|
print(f"Terminating process {process.pid}")
|
|
|
|
|
process.terminate()
|
|
|
|
|
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
|
|
|
except (asyncio.TimeoutError, ProcessLookupError) as e:
|
|
|
|
|
print(f"Error terminating process: {e}")
|
|
|
|
|
try:
|
|
|
|
|
print(f"Killing process {process.pid}")
|
|
|
|
|
process.kill()
|
|
|
|
|
await process.wait()
|
|
|
|
|
except ProcessLookupError as e:
|
|
|
|
|
print(f"Process already gone: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Drain any remaining output to prevent deadlocks
|
|
|
|
|
if process.stdout:
|
|
|
|
|
try:
|
|
|
|
|
data = await process.stdout.read()
|
|
|
|
|
if data:
|
|
|
|
|
print(f"Remaining stdout: {data.decode().strip()}")
|
|
|
|
|
except (ValueError, IOError) as e:
|
|
|
|
|
print(f"Error reading stdout: {e}")
|
|
|
|
|
pass
|
|
|
|
|
if process.stderr:
|
|
|
|
|
try:
|
|
|
|
|
data = await process.stderr.read()
|
|
|
|
|
if data:
|
|
|
|
|
print(f"Remaining stderr: {data.decode().strip()}")
|
|
|
|
|
except (ValueError, IOError) as e:
|
|
|
|
|
print(f"Error reading stderr: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Exception during cleanup: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
2025-02-24 14:26:48 -08:00
|
|
|
async def start_bot(self, config_path: Path) -> bool:
|
2025-02-24 21:48:51 -08:00
|
|
|
"""Start a new bot instance with the given config file.
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
Args:
|
2025-02-24 21:48:51 -08:00
|
|
|
config_path: Path to the config file
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True if bot was started successfully
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2025-02-24 21:48:51 -08:00
|
|
|
# Generate a unique ID for this bot based on the config file name
|
2025-02-24 14:26:48 -08:00
|
|
|
bot_id = config_path.stem
|
|
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
# Check if a bot with this ID is already running
|
|
|
|
|
state = self._load_state()
|
|
|
|
|
if bot_id in state:
|
|
|
|
|
try:
|
|
|
|
|
pid = state[bot_id]['pid']
|
|
|
|
|
os.kill(pid, 0) # Check if process exists
|
|
|
|
|
return False
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
# Process doesn't exist, remove from state
|
|
|
|
|
pass
|
2025-02-24 14:26:48 -08:00
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
# Start the bot process using venv Python
|
2025-02-24 14:26:48 -08:00
|
|
|
process = await asyncio.create_subprocess_exec(
|
2025-02-24 21:48:51 -08:00
|
|
|
self.venv_python, 'main.py',
|
2025-02-24 14:26:48 -08:00
|
|
|
'--config', str(config_path),
|
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
|
stderr=asyncio.subprocess.PIPE
|
|
|
|
|
)
|
|
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
# Verify process started successfully
|
|
|
|
|
try:
|
|
|
|
|
os.kill(process.pid, 0)
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
return False
|
2025-02-24 14:26:48 -08:00
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
# Start the monitor task if not already running
|
|
|
|
|
if self.monitor_task is None:
|
|
|
|
|
self.monitor_task = asyncio.create_task(self.monitor_processes())
|
|
|
|
|
|
|
|
|
|
# Monitor the process output for a short time to ensure it starts properly
|
|
|
|
|
try:
|
|
|
|
|
startup_timeout = 5.0 # Give it 5 seconds to start
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
success = False
|
|
|
|
|
|
|
|
|
|
while time.time() - start_time < startup_timeout:
|
|
|
|
|
# Check if process has exited
|
|
|
|
|
if process.returncode is not None:
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Read any available output during startup
|
|
|
|
|
try:
|
|
|
|
|
line = await asyncio.wait_for(process.stderr.readline(), timeout=0.1)
|
|
|
|
|
if line:
|
|
|
|
|
line = line.decode().strip()
|
|
|
|
|
print(f"Bot output: {line}")
|
|
|
|
|
# Consider the bot started successfully after a short delay
|
|
|
|
|
# instead of looking for specific output
|
|
|
|
|
success = True
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
# No output available, continue monitoring
|
|
|
|
|
continue
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error reading bot output: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Consider the bot started successfully if it's still running after the timeout
|
|
|
|
|
if not success and process.returncode is None:
|
|
|
|
|
success = True
|
|
|
|
|
|
|
|
|
|
# Start background task to monitor process output
|
|
|
|
|
async def monitor_output(process, bot_id):
|
|
|
|
|
try:
|
|
|
|
|
while True:
|
|
|
|
|
if process.returncode is not None:
|
|
|
|
|
break
|
|
|
|
|
try:
|
|
|
|
|
line = await process.stderr.readline()
|
|
|
|
|
if not line:
|
|
|
|
|
break
|
|
|
|
|
# Print the bot's output
|
|
|
|
|
print(f"Bot {bot_id} output: {line.decode().strip()}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error monitoring bot {bot_id}: {e}")
|
|
|
|
|
break
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Monitor task exception for bot {bot_id}: {e}")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
asyncio.create_task(monitor_output(process, bot_id))
|
|
|
|
|
|
|
|
|
|
# Store the process
|
|
|
|
|
self.bots[bot_id] = process
|
|
|
|
|
self._save_state()
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
except Exception:
|
2025-02-24 14:26:48 -08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def stop_bot(self, bot_id: str) -> bool:
|
|
|
|
|
"""Stop a running bot instance.
|
|
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
Args:
|
|
|
|
|
bot_id: ID of the bot to stop, or "all" to stop all bots
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True if bot(s) were stopped successfully
|
|
|
|
|
"""
|
|
|
|
|
if bot_id == "all":
|
|
|
|
|
print("Stopping all bots...")
|
|
|
|
|
success = True
|
|
|
|
|
state = self._load_state()
|
|
|
|
|
for bid in list(state.keys()):
|
|
|
|
|
print(f"Stopping bot {bid}...")
|
|
|
|
|
if not await self._stop_single_bot(bid):
|
|
|
|
|
success = False
|
|
|
|
|
return success
|
|
|
|
|
|
|
|
|
|
# Stop a single bot
|
|
|
|
|
return await self._stop_single_bot(bot_id)
|
|
|
|
|
|
|
|
|
|
async def _stop_single_bot(self, bot_id: str) -> bool:
|
|
|
|
|
"""Stop a single bot instance.
|
|
|
|
|
|
2025-02-24 14:26:48 -08:00
|
|
|
Args:
|
|
|
|
|
bot_id: ID of the bot to stop
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True if bot was stopped successfully
|
|
|
|
|
"""
|
2025-02-24 21:48:51 -08:00
|
|
|
# Check both local bots and state file
|
|
|
|
|
state = self._load_state()
|
|
|
|
|
process = None
|
|
|
|
|
|
|
|
|
|
if bot_id in self.bots:
|
2025-02-24 14:26:48 -08:00
|
|
|
process = self.bots[bot_id]
|
2025-02-24 21:48:51 -08:00
|
|
|
elif bot_id in state:
|
2025-02-24 14:26:48 -08:00
|
|
|
try:
|
2025-02-24 21:48:51 -08:00
|
|
|
pid = state[bot_id]['pid']
|
|
|
|
|
print(f"Stopping bot {bot_id} with PID {pid}")
|
|
|
|
|
# Try to kill the process
|
|
|
|
|
try:
|
|
|
|
|
# First try a gentle termination
|
|
|
|
|
os.kill(pid, signal.SIGTERM)
|
|
|
|
|
print(f"Sent SIGTERM to process {pid}")
|
|
|
|
|
|
|
|
|
|
# Wait a bit for the process to terminate
|
|
|
|
|
for i in range(50): # 5 seconds
|
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
|
try:
|
|
|
|
|
os.kill(pid, 0) # Check if process exists
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
print(f"Process {pid} terminated successfully")
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
# Process didn't terminate, force kill
|
|
|
|
|
print(f"Process {pid} didn't terminate, sending SIGKILL")
|
|
|
|
|
try:
|
|
|
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
|
print(f"Sent SIGKILL to process {pid}")
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
print(f"Process {pid} already terminated")
|
|
|
|
|
pass
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
print(f"Process {pid} not found")
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Remove only this bot from state
|
|
|
|
|
if bot_id in state:
|
|
|
|
|
print(f"Removing {bot_id} from state file")
|
|
|
|
|
del state[bot_id]
|
|
|
|
|
with open(self.state_file, 'w') as f:
|
|
|
|
|
json.dump(state, f)
|
|
|
|
|
print(f"State file updated, remaining bots: {list(state.keys())}")
|
|
|
|
|
return True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error stopping bot {bot_id}: {e}")
|
|
|
|
|
return False
|
|
|
|
|
else:
|
|
|
|
|
print(f"Bot {bot_id} not found")
|
2025-02-24 14:26:48 -08:00
|
|
|
return False
|
2025-02-24 21:48:51 -08:00
|
|
|
|
|
|
|
|
if process:
|
|
|
|
|
try:
|
|
|
|
|
print(f"Cleaning up process for bot {bot_id}")
|
|
|
|
|
await self._cleanup_process(process)
|
|
|
|
|
del self.bots[bot_id]
|
|
|
|
|
|
|
|
|
|
# Update state file - only remove this bot
|
|
|
|
|
state = self._load_state()
|
|
|
|
|
if bot_id in state:
|
|
|
|
|
print(f"Removing {bot_id} from state file")
|
|
|
|
|
del state[bot_id]
|
|
|
|
|
with open(self.state_file, 'w') as f:
|
|
|
|
|
json.dump(state, f)
|
|
|
|
|
print(f"State file updated, remaining bots: {list(state.keys())}")
|
|
|
|
|
return True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error cleaning up process for bot {bot_id}: {e}")
|
|
|
|
|
return False
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
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():
|
|
|
|
|
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)
|
|
|
|
|
|
2025-02-24 21:48:51 -08:00
|
|
|
async def list_bots(self) -> bool:
|
|
|
|
|
"""List all running bots.
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
Returns:
|
2025-02-24 21:48:51 -08:00
|
|
|
bool: True if any bots are running
|
2025-02-24 14:26:48 -08:00
|
|
|
"""
|
2025-02-24 21:48:51 -08:00
|
|
|
state = self._load_state()
|
|
|
|
|
|
|
|
|
|
# Check if any bots are running
|
|
|
|
|
if not state:
|
|
|
|
|
print("No bots running")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Track unique PIDs to avoid duplicates
|
|
|
|
|
seen_pids = set()
|
|
|
|
|
unique_bots = {}
|
|
|
|
|
|
|
|
|
|
# Filter out duplicates based on PID
|
|
|
|
|
for bot_id, info in state.items():
|
|
|
|
|
pid = info.get('pid')
|
|
|
|
|
if pid and pid not in seen_pids:
|
|
|
|
|
seen_pids.add(pid)
|
|
|
|
|
unique_bots[bot_id] = info
|
|
|
|
|
|
|
|
|
|
if not unique_bots:
|
|
|
|
|
print("No bots running")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Print header
|
|
|
|
|
print("\nRunning Bots:")
|
|
|
|
|
print("-" * 80)
|
|
|
|
|
print(f"{'ID':<20} {'PID':<8} {'Status':<10} {'Command':<40}")
|
|
|
|
|
print("-" * 80)
|
|
|
|
|
|
|
|
|
|
# Print each bot's status
|
|
|
|
|
for bot_id, info in unique_bots.items():
|
|
|
|
|
pid = info.get('pid')
|
|
|
|
|
config = info.get('config', '')
|
|
|
|
|
|
|
|
|
|
# Check if process is still running
|
|
|
|
|
try:
|
|
|
|
|
os.kill(pid, 0)
|
|
|
|
|
status = "running"
|
|
|
|
|
except ProcessLookupError:
|
|
|
|
|
status = "stopped"
|
|
|
|
|
continue # Skip stopped processes
|
|
|
|
|
except Exception:
|
|
|
|
|
status = "unknown"
|
|
|
|
|
|
|
|
|
|
# Get command line
|
|
|
|
|
try:
|
|
|
|
|
with open(f"/proc/{pid}/cmdline", 'rb') as f:
|
|
|
|
|
cmdline = f.read().replace(b'\0', b' ').decode()
|
|
|
|
|
except Exception:
|
|
|
|
|
cmdline = f"Unknown (PID: {pid})"
|
|
|
|
|
|
|
|
|
|
print(f"{bot_id:<20} {pid:<8} {status:<10} {cmdline:<40}")
|
|
|
|
|
|
|
|
|
|
print("-" * 80)
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
return True
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
async def cleanup(self):
|
|
|
|
|
"""Clean up all running bots."""
|
2025-02-24 21:48:51 -08:00
|
|
|
if self.monitor_task:
|
|
|
|
|
self.monitor_task.cancel()
|
|
|
|
|
try:
|
|
|
|
|
await self.monitor_task
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
await self.stop_bot("all")
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
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')
|
2025-02-24 21:48:51 -08:00
|
|
|
parser.add_argument('bot_id', nargs='?', help='Bot ID for start/stop/restart commands, or "all" to stop all bots')
|
|
|
|
|
|
2025-02-24 14:26:48 -08:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
manager = BotManager()
|
2025-02-24 21:48:51 -08:00
|
|
|
should_cleanup = False # Only cleanup for certain commands
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if args.command == 'list':
|
2025-02-24 21:48:51 -08:00
|
|
|
if await manager.list_bots():
|
|
|
|
|
# If list_bots returns True, we've printed the list
|
|
|
|
|
pass
|
2025-02-24 14:26:48 -08:00
|
|
|
elif args.command == 'start':
|
2025-02-24 21:48:51 -08:00
|
|
|
should_cleanup = True # Need cleanup for start command
|
2025-02-24 14:26:48 -08:00
|
|
|
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
|
2025-02-24 21:48:51 -08:00
|
|
|
success = True
|
2025-02-24 14:26:48 -08:00
|
|
|
for config_file in config_path.glob('*.yaml'):
|
2025-02-24 21:48:51 -08:00
|
|
|
if not await manager.start_bot(config_file):
|
|
|
|
|
success = False
|
|
|
|
|
if not success:
|
|
|
|
|
sys.exit(1)
|
2025-02-24 14:26:48 -08:00
|
|
|
else:
|
|
|
|
|
# Start single bot
|
2025-02-24 21:48:51 -08:00
|
|
|
if not await manager.start_bot(config_path):
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
# If we started any bots successfully, keep running until interrupted
|
|
|
|
|
if manager.bots:
|
|
|
|
|
try:
|
|
|
|
|
# Keep the manager running
|
|
|
|
|
while True:
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
if not manager.bots:
|
|
|
|
|
break
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
pass
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
elif args.command == 'stop':
|
2025-02-24 21:48:51 -08:00
|
|
|
# Don't need cleanup for stop command as it already cleans up
|
|
|
|
|
should_cleanup = False
|
2025-02-24 14:26:48 -08:00
|
|
|
if not args.bot_id:
|
2025-02-24 21:48:51 -08:00
|
|
|
print("Error: bot_id required for stop command (use 'all' to stop all bots)")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
if not await manager.stop_bot(args.bot_id):
|
2025-02-24 14:26:48 -08:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
elif args.command == 'restart':
|
2025-02-24 21:48:51 -08:00
|
|
|
should_cleanup = True # Need cleanup for restart command
|
2025-02-24 14:26:48 -08:00
|
|
|
if not args.bot_id:
|
|
|
|
|
print("Error: bot_id required for restart command")
|
|
|
|
|
sys.exit(1)
|
2025-02-24 21:48:51 -08:00
|
|
|
if args.bot_id == "all":
|
|
|
|
|
print("Error: restart all is not supported")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
if not await manager.restart_bot(args.bot_id):
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
# If we restarted successfully, keep running until interrupted
|
|
|
|
|
if manager.bots:
|
|
|
|
|
try:
|
|
|
|
|
# Keep the manager running
|
|
|
|
|
while True:
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
if not manager.bots:
|
|
|
|
|
break
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
pass
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
2025-02-24 21:48:51 -08:00
|
|
|
should_cleanup = True # Need cleanup for keyboard interrupt
|
|
|
|
|
except Exception:
|
|
|
|
|
should_cleanup = True # Need cleanup for errors
|
2025-02-24 14:26:48 -08:00
|
|
|
sys.exit(1)
|
2025-02-24 21:48:51 -08:00
|
|
|
finally:
|
|
|
|
|
# Only clean up if we need to
|
|
|
|
|
if should_cleanup:
|
|
|
|
|
await manager.cleanup()
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2025-02-24 21:48:51 -08:00
|
|
|
# Set up signal handlers for graceful shutdown
|
|
|
|
|
def signal_handler(sig, frame):
|
|
|
|
|
# Don't exit immediately, let the cleanup happen
|
|
|
|
|
asyncio.get_event_loop().stop()
|
|
|
|
|
|
2025-02-24 14:26:48 -08:00
|
|
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
2025-02-24 21:48:51 -08:00
|
|
|
signal.signal(sig, signal_handler)
|
2025-02-24 14:26:48 -08:00
|
|
|
|
|
|
|
|
# Run the manager
|
|
|
|
|
asyncio.run(main())
|