from __future__ import annotations import asyncio import hashlib import json from typing import Any, TYPE_CHECKING from yuxi.utils.logging_config import logger if TYPE_CHECKING: from yuxi.channels.manager import ChannelManager class ConfigWatcher: def __init__(self, channel_manager: ChannelManager): self._manager = channel_manager self._last_config_hash: str | None = None self._watched_prefixes: list[str] = ["channels"] self._noop_prefixes: list[str] = [] self._running = False self._task: asyncio.Task | None = None def set_watched_prefixes(self, prefixes: list[str]) -> None: self._watched_prefixes = list(prefixes) def set_noop_prefixes(self, prefixes: list[str]) -> None: self._noop_prefixes = list(prefixes) async def watch(self, interval: float = 30.0) -> None: if self._running: return self._running = True self._last_config_hash = self._compute_config_hash() self._task = asyncio.create_task(self._watch_loop(interval)) async def stop(self) -> None: self._running = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass self._task = None async def reload_now(self, changed_keys: list[str] | None = None) -> dict[str, Any]: if changed_keys is None: diff = self._compute_config_diff() else: diff = self._compute_diff_from_keys(changed_keys) return await self._apply_diff(diff) async def _watch_loop(self, interval: float) -> None: while self._running: await asyncio.sleep(interval) if not self._running: break try: current = self._compute_config_hash() if current != self._last_config_hash: logger.info("Config change detected, computing diff") diff = self._compute_config_diff() await self._apply_diff(diff) self._last_config_hash = current except asyncio.CancelledError: break except Exception: logger.exception("ConfigWatcher loop error") def _compute_config_hash(self) -> str: raw = json.dumps(self._manager._channels_config, sort_keys=True, default=str) return hashlib.sha256(raw.encode()).hexdigest() def _compute_config_diff(self) -> dict[str, dict[str, Any]]: diff: dict[str, dict[str, Any]] = {} current_channels = set(self._manager._channels_config.keys()) running_channels = set(self._manager._adapters.keys()) added = current_channels - running_channels removed = running_channels - current_channels for channel_id in added: diff[channel_id] = {"action": "start", "config": self._manager._channels_config[channel_id]} for channel_id in removed: diff[channel_id] = {"action": "stop"} for channel_id in current_channels & running_channels: diff[channel_id] = {"action": "restart"} return diff def _compute_diff_from_keys(self, changed_keys: list[str]) -> dict[str, dict[str, Any]]: diff: dict[str, dict[str, Any]] = {} for key in changed_keys: for prefix in self._watched_prefixes: if not key.startswith(prefix): continue if any(key.startswith(noop) for noop in self._noop_prefixes): continue parts = key.split(".") if len(parts) >= 2: channel_id = parts[1] diff[channel_id] = {"action": "restart"} break return diff async def _apply_diff(self, diff: dict) -> dict[str, Any]: results: dict[str, Any] = {} for channel_id, action in diff.items(): try: if action.get("action") == "start": if self._manager._channels_config.get(channel_id, {}).get("enabled", False): await self._manager.start_channel(channel_id, action.get("config")) results[channel_id] = "started" elif action.get("action") == "stop": await self._manager.stop_channel(channel_id) results[channel_id] = "stopped" elif action.get("action") == "restart": await self._manager.restart_channel(channel_id) results[channel_id] = "restarted" except Exception: logger.exception(f"ConfigWatcher failed to apply diff for {channel_id}") results[channel_id] = "failed" return results