126 lines
4.7 KiB
Python
126 lines
4.7 KiB
Python
import pkgutil
|
|
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from api.depends import verify_identity_admin
|
|
from core.executor import run_io
|
|
from logger import logger
|
|
from utils.modules_loader import _is_safe_module_name
|
|
from utils.modules_manager import manager
|
|
|
|
|
|
router = APIRouter(prefix="/modules", tags=["Modules"])
|
|
|
|
MODULES_DIR = Path(__file__).resolve().parents[3] / "modules"
|
|
|
|
|
|
class ModuleAction(BaseModel):
|
|
action: Literal["start", "stop", "restart"]
|
|
|
|
|
|
def _available_module_names() -> list[str]:
|
|
"""Имена модулей из папки modules, прошедшие проверку безопасности."""
|
|
candidates: set[str] = set()
|
|
if MODULES_DIR.is_dir():
|
|
for _finder, name, _ispkg in pkgutil.iter_modules([str(MODULES_DIR)]):
|
|
name = (name or "").strip()
|
|
if name and _is_safe_module_name(name):
|
|
candidates.add(name)
|
|
return sorted(n for n in candidates if _is_safe_module_name(n))
|
|
|
|
|
|
def _prune_missing_state(installed: set[str]) -> None:
|
|
"""Удаляет из состояния менеджера модули, которых нет в файловой системе."""
|
|
changed = False
|
|
stale_disabled = {name for name in manager.disabled if name not in installed}
|
|
if stale_disabled:
|
|
for name in stale_disabled:
|
|
manager.disabled.discard(name)
|
|
changed = True
|
|
stale_registry = [name for name in list(manager.registry.keys()) if name not in installed]
|
|
if stale_registry:
|
|
for name in stale_registry:
|
|
manager.registry.pop(name, None)
|
|
changed = True
|
|
if changed:
|
|
save_state = getattr(manager, "_save_state", None)
|
|
if callable(save_state):
|
|
save_state()
|
|
|
|
|
|
def _module_state(name: str) -> dict:
|
|
"""Состояние модуля: enabled, loaded, autostart."""
|
|
normalized = name.strip()
|
|
record = manager.registry.get(normalized)
|
|
is_enabled = manager.is_enabled(normalized)
|
|
return {
|
|
"name": normalized,
|
|
"enabled": is_enabled,
|
|
"loaded": bool(record and record.enabled),
|
|
"autostart": manager.should_autostart(normalized),
|
|
}
|
|
|
|
|
|
def _read_local_module_version(name: str) -> str | None:
|
|
"""Читает VERSION из папки модуля."""
|
|
version_file = MODULES_DIR / name / "VERSION"
|
|
if not version_file.exists() or not version_file.is_file():
|
|
return None
|
|
try:
|
|
with version_file.open("r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
value = line.strip()
|
|
if value:
|
|
return value
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def sync_list_modules() -> list:
|
|
"""Вся синхронная работа со списком модулей (файлы, состояние). Вызывать через run_io()."""
|
|
refresh = getattr(manager, "refresh_state", None) or getattr(manager, "_load_state", None)
|
|
if callable(refresh):
|
|
refresh()
|
|
module_names = _available_module_names()
|
|
_prune_missing_state(set(module_names))
|
|
modules = [_module_state(name) for name in module_names]
|
|
for item in modules:
|
|
name = str(item.get("name") or "").strip()
|
|
item["local_version"] = _read_local_module_version(name)
|
|
return modules
|
|
|
|
|
|
@router.get("/")
|
|
async def list_modules(identity=Depends(verify_identity_admin)):
|
|
"""Список модулей с состоянием и локальной версией."""
|
|
modules = await run_io(sync_list_modules)
|
|
return {"items": modules}
|
|
|
|
|
|
@router.post("/{module_name}/actions")
|
|
async def control_module(module_name: str, payload: ModuleAction, identity=Depends(verify_identity_admin)):
|
|
"""Запуск, остановка или перезапуск модуля."""
|
|
name = (module_name or "").strip()
|
|
if not _is_safe_module_name(name):
|
|
raise HTTPException(status_code=404, detail="Module not found")
|
|
try:
|
|
if payload.action == "start":
|
|
await manager.start(name)
|
|
elif payload.action == "stop":
|
|
await manager.stop(name)
|
|
elif payload.action == "restart":
|
|
await manager.restart(name)
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Unsupported action")
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
except RuntimeError as exc:
|
|
logger.error("[Modules] action failed for {}: {}", name, exc)
|
|
raise HTTPException(status_code=500, detail="Ошибка при выполнении операции модуля") from exc
|
|
return {"item": _module_state(name)}
|