mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
- Introduced a new function to configure Unix signal handlers for SIGTERM, SIGINT, and SIGHUP, allowing for graceful shutdown and in-process configuration reload. - Updated the main function to utilize the new signal handling setup, improving the bot's responsiveness to system signals. - Enhanced documentation in the service installation guide to clarify the use of the reload command for configuration changes without restarting the service. These changes improve the bot's operational flexibility and user experience during configuration updates.
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MeshCore Bot using the meshcore-cli and meshcore.py packages
|
|
Uses a modular structure for command creation and organization
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import signal
|
|
import sys
|
|
|
|
|
|
def _configure_unix_signal_handlers(loop, bot, shutdown_event: asyncio.Event) -> None:
|
|
"""Register Unix signal handlers for shutdown and config reload."""
|
|
|
|
def shutdown_handler():
|
|
"""Signal handler for graceful shutdown."""
|
|
print("\nShutting down...")
|
|
shutdown_event.set()
|
|
|
|
def reload_handler():
|
|
"""Reload config on SIGHUP without exiting."""
|
|
bot.logger.info("Received SIGHUP, reloading configuration...")
|
|
success, msg = bot.reload_config()
|
|
if success:
|
|
bot.logger.info("SIGHUP config reload succeeded: %s", msg)
|
|
else:
|
|
bot.logger.warning("SIGHUP config reload failed: %s", msg)
|
|
|
|
# Register shutdown signals
|
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
loop.add_signal_handler(sig, shutdown_handler)
|
|
|
|
# Register config reload signal (Unix daemons convention)
|
|
if hasattr(signal, "SIGHUP"):
|
|
loop.add_signal_handler(signal.SIGHUP, reload_handler)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="MeshCore Bot - Mesh network bot for MeshCore devices"
|
|
)
|
|
parser.add_argument(
|
|
"--config",
|
|
default="config.ini",
|
|
help="Path to configuration file (default: config.ini)",
|
|
)
|
|
parser.add_argument(
|
|
"--validate-config",
|
|
action="store_true",
|
|
help="Validate config section names and exit before starting the bot (exit 1 on errors)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.validate_config:
|
|
from modules.config_validation import (
|
|
SEVERITY_ERROR,
|
|
SEVERITY_INFO,
|
|
SEVERITY_WARNING,
|
|
validate_config,
|
|
)
|
|
results = validate_config(args.config)
|
|
has_error = False
|
|
for severity, message in results:
|
|
if severity == SEVERITY_ERROR:
|
|
print(f"Error: {message}", file=sys.stderr)
|
|
has_error = True
|
|
elif severity == SEVERITY_WARNING:
|
|
print(f"Warning: {message}", file=sys.stderr)
|
|
else:
|
|
print(f"Info: {message}", file=sys.stderr)
|
|
sys.exit(1 if has_error else 0)
|
|
|
|
from modules.core import MeshCoreBot
|
|
bot = MeshCoreBot(config_file=args.config)
|
|
|
|
# Use asyncio.run() which handles KeyboardInterrupt properly
|
|
# For SIGTERM, we'll handle it in the async context
|
|
async def run_bot():
|
|
"""Run bot with proper signal handling"""
|
|
loop = asyncio.get_running_loop()
|
|
|
|
def meshcore_task_exception_handler(loop, context):
|
|
"""Log unhandled exceptions from asyncio tasks (e.g. meshcore reader)."""
|
|
exc = context.get('exception')
|
|
msg = context.get('message', 'Unhandled exception in task')
|
|
if exc is not None:
|
|
bot.logger.warning(
|
|
"%s: %s",
|
|
msg,
|
|
exc,
|
|
exc_info=(type(exc), exc, exc.__traceback__),
|
|
)
|
|
else:
|
|
bot.logger.warning("%s: %s", msg, context)
|
|
|
|
loop.set_exception_handler(meshcore_task_exception_handler)
|
|
|
|
# Set up signal handlers for graceful shutdown (Unix only)
|
|
if sys.platform != 'win32':
|
|
shutdown_event = asyncio.Event()
|
|
bot_task = None
|
|
|
|
try:
|
|
# Register signal handlers
|
|
_configure_unix_signal_handlers(loop, bot, shutdown_event)
|
|
|
|
# Start bot
|
|
bot_task = asyncio.create_task(bot.start())
|
|
|
|
# Wait for shutdown or completion
|
|
done, pending = await asyncio.wait(
|
|
[bot_task, asyncio.create_task(shutdown_event.wait())],
|
|
return_when=asyncio.FIRST_COMPLETED
|
|
)
|
|
|
|
# Cancel pending tasks
|
|
for task in pending:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
# Handle bot task completion
|
|
if bot_task:
|
|
if shutdown_event.is_set() and not bot_task.done():
|
|
# Shutdown triggered: cancel if still running
|
|
bot_task.cancel()
|
|
|
|
# Always await bot_task to ensure proper cleanup
|
|
# This is necessary because:
|
|
# 1. If the task completed normally, we need to await to surface exceptions
|
|
# 2. If the task was cancelled, it only becomes "done" after being awaited
|
|
# (cancellation is not immediate - the task must be awaited for the
|
|
# CancelledError to be raised and the task to fully terminate)
|
|
try:
|
|
await bot_task
|
|
except asyncio.CancelledError:
|
|
# Expected when cancelled, ignore
|
|
pass
|
|
finally:
|
|
# Always ensure cleanup happens
|
|
await bot.stop()
|
|
else:
|
|
# Windows: just run and catch KeyboardInterrupt
|
|
try:
|
|
await bot.start()
|
|
finally:
|
|
await bot.stop()
|
|
|
|
try:
|
|
asyncio.run(run_bot())
|
|
except KeyboardInterrupt:
|
|
# Cleanup already handled in run_bot's finally block
|
|
print("\nShutdown complete.")
|
|
except Exception as e:
|
|
# Cleanup already handled in run_bot's finally block
|
|
print(f"Error: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
|
|
|