Files
Solo_bot/utils/modules_manager.py
2026-02-11 19:54:17 +03:00

173 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import importlib
import json
import os
import sys
from aiogram import Router
from hooks.hooks import unregister_module_hooks
from logger import logger
IGNORE_SUBMODULES = {"models", "schemas", "db"}
STATE_FILE = os.getenv("MODULES_STATE_FILE", "storage/modules_state.json")
def _normalize_module_name(name: str | None) -> str:
return (name or "").strip()
class ModuleRecord:
def __init__(self, name: str, pkg: str) -> None:
self.name = name
self.pkg = pkg
self.router: Router | None = None
self.enabled: bool = False
class ModulesManager:
def __init__(self, base: str = "modules") -> None:
self.base = base
self.registry: dict[str, ModuleRecord] = {}
self.disabled: set[str] = set()
self._load_state()
def pkg(self, name: str) -> str:
return f"{self.base}.{name}"
def _load_state(self) -> None:
try:
if os.path.isfile(STATE_FILE):
with open(STATE_FILE, encoding="utf-8") as f:
data = json.load(f)
raw = data.get("disabled", [])
self.disabled = {_normalize_module_name(n) for n in raw if _normalize_module_name(n)}
else:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
self._save_state()
except Exception as e:
logger.warning(f"[Modules] Не удалось загрузить состояние: {e}")
def _save_state(self) -> None:
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump({"disabled": sorted(self.disabled)}, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.warning(f"[Modules] Не удалось сохранить состояние: {e}")
def adopt(self, name: str, router: Router):
name = _normalize_module_name(name)
rec = self.registry.get(name) or ModuleRecord(name, self.pkg(name))
rec.router = router
rec.enabled = True
self.registry[name] = rec
def _is_safe_module_name(self, name: str) -> bool:
return bool(name and name.isidentifier() and "." not in name and "/" not in name and "\\" not in name)
async def start(self, name: str) -> None:
name = _normalize_module_name(name)
if not self._is_safe_module_name(name):
raise ValueError(f"[Modules] Недопустимое имя модуля: {name!r}")
rec = self.registry.get(name) or ModuleRecord(name, self.pkg(name))
if rec.enabled:
logger.info(f"[Modules] {name} уже активен.")
return
try:
unregister_module_hooks(name)
except Exception:
pass
self.purge_selective(rec.pkg)
mod = importlib.import_module(f"{rec.pkg}.router")
router = getattr(mod, "router", None)
if not isinstance(router, Router):
raise RuntimeError(f"[Modules] В модуле {name} не найден router")
from utils.modules_loader import modules_hub
modules_hub.include_router(router)
rec.router = router
rec.enabled = True
self.registry[name] = rec
if name in self.disabled:
self.disabled.discard(name)
self._save_state()
logger.info(f"[Modules] {name} запущен.")
async def stop(self, name: str) -> None:
name = _normalize_module_name(name)
rec = self.registry.get(name)
if not rec or not rec.enabled:
logger.info(f"[Modules] {name} уже остановлен или не найден.")
if name not in self.disabled:
self.disabled.add(name)
self._save_state()
return
try:
unregister_module_hooks(name)
except Exception:
pass
from utils.modules_loader import modules_hub
sub = getattr(modules_hub, "_sub_routers", None) or getattr(modules_hub, "sub_routers", None)
if sub and rec.router in sub:
sub.remove(rec.router)
rec.router = None
rec.enabled = False
if name not in self.disabled:
self.disabled.add(name)
self._save_state()
logger.info(f"[Modules] {name} остановлен.")
async def restart(self, name: str) -> None:
name = _normalize_module_name(name)
logger.info(f"[Modules] Перезапуск {name}...")
await self.stop(name)
await self.start(name)
def purge_selective(self, root_pkg: str) -> None:
to_del = []
for m in list(sys.modules):
if m == root_pkg or m.startswith(root_pkg + "."):
tail = m[len(root_pkg) :].lstrip(".")
top = tail.split(".", 1)[0] if tail else ""
if top and top in IGNORE_SUBMODULES:
continue
to_del.append(m)
for m in to_del:
sys.modules.pop(m, None)
importlib.invalidate_caches()
def is_enabled(self, name: str) -> bool:
name = _normalize_module_name(name)
rec = self.registry.get(name)
if not rec or not rec.router:
return False
try:
from utils.modules_loader import modules_hub
except Exception:
return bool(rec.enabled)
sub = getattr(modules_hub, "_sub_routers", None) or getattr(modules_hub, "sub_routers", None)
return bool(sub and rec.router in sub)
def is_disabled(self, name: str) -> bool:
return _normalize_module_name(name) in self.disabled
def should_autostart(self, name: str) -> bool:
return _normalize_module_name(name) not in self.disabled
manager = ModulesManager()