import os import shutil import time from typing import Any from fastapi import APIRouter, Depends from sqlmodel import Session, select from app.database import get_session from app.models.settings import UserSettings from app.config import UPLOAD_DIR router = APIRouter(tags=["réglages"]) _PREV_CPU_USAGE_USEC: int | None = None _PREV_CPU_TS: float | None = None def _read_int_from_paths(paths: list[str]) -> int | None: for path in paths: try: with open(path, "r", encoding="utf-8") as f: raw = f.read().strip().split()[0] return int(raw) except Exception: continue return None def _read_cgroup_cpu_usage_usec() -> int | None: # cgroup v2 try: with open("/sys/fs/cgroup/cpu.stat", "r", encoding="utf-8") as f: for line in f: if line.startswith("usage_usec "): return int(line.split()[1]) except Exception: pass # cgroup v1 ns = _read_int_from_paths(["/sys/fs/cgroup/cpuacct/cpuacct.usage"]) if ns is not None: return ns // 1000 return None def _cpu_quota_cores() -> float | None: # cgroup v2 try: with open("/sys/fs/cgroup/cpu.max", "r", encoding="utf-8") as f: quota, period = f.read().strip().split()[:2] if quota == "max": return float(os.cpu_count() or 1) q, p = int(quota), int(period) if p > 0: return max(q / p, 0.01) except Exception: pass # cgroup v1 quota = _read_int_from_paths(["/sys/fs/cgroup/cpu/cpu.cfs_quota_us"]) period = _read_int_from_paths(["/sys/fs/cgroup/cpu/cpu.cfs_period_us"]) if quota is not None and period is not None and quota > 0 and period > 0: return max(quota / period, 0.01) return float(os.cpu_count() or 1) def _memory_stats() -> dict[str, Any]: used = _read_int_from_paths( [ "/sys/fs/cgroup/memory.current", # cgroup v2 "/sys/fs/cgroup/memory/memory.usage_in_bytes", # cgroup v1 ] ) limit = _read_int_from_paths( [ "/sys/fs/cgroup/memory.max", # cgroup v2 "/sys/fs/cgroup/memory/memory.limit_in_bytes", # cgroup v1 ] ) # Certaines limites cgroup valent "max" ou des sentinelles tres grandes. if limit is not None and limit >= 9_000_000_000_000_000_000: limit = None pct = None if used is not None and limit and limit > 0: pct = round((used / limit) * 100, 1) return {"used_bytes": used, "limit_bytes": limit, "used_pct": pct} def _disk_stats() -> dict[str, Any]: target = "/data" if os.path.isdir("/data") else "/" total, used, free = shutil.disk_usage(target) uploads_size = None if os.path.isdir(UPLOAD_DIR): try: uploads_size = sum( os.path.getsize(os.path.join(root, name)) for root, _, files in os.walk(UPLOAD_DIR) for name in files ) except Exception: uploads_size = None return { "path": target, "total_bytes": total, "used_bytes": used, "free_bytes": free, "used_pct": round((used / total) * 100, 1) if total else None, "uploads_bytes": uploads_size, } @router.get("/settings") def get_settings(session: Session = Depends(get_session)): rows = session.exec(select(UserSettings)).all() return {r.cle: r.valeur for r in rows} @router.put("/settings") def update_settings(data: dict, session: Session = Depends(get_session)): for cle, valeur in data.items(): row = session.exec(select(UserSettings).where(UserSettings.cle == cle)).first() if row: row.valeur = str(valeur) else: row = UserSettings(cle=cle, valeur=str(valeur)) session.add(row) session.commit() return {"ok": True} @router.get("/settings/debug/system") def get_debug_system_stats() -> dict[str, Any]: """Stats runtime du conteneur (utile pour affichage debug UI).""" global _PREV_CPU_USAGE_USEC, _PREV_CPU_TS now = time.monotonic() usage_usec = _read_cgroup_cpu_usage_usec() quota_cores = _cpu_quota_cores() cpu_pct = None if usage_usec is not None and _PREV_CPU_USAGE_USEC is not None and _PREV_CPU_TS is not None: delta_usage = usage_usec - _PREV_CPU_USAGE_USEC delta_time_usec = (now - _PREV_CPU_TS) * 1_000_000 if delta_time_usec > 0 and quota_cores and quota_cores > 0: cpu_pct = round((delta_usage / (delta_time_usec * quota_cores)) * 100, 1) _PREV_CPU_USAGE_USEC = usage_usec _PREV_CPU_TS = now return { "source": "container-cgroup", "cpu": { "usage_usec_total": usage_usec, "quota_cores": quota_cores, "used_pct": cpu_pct, }, "memory": _memory_stats(), "disk": _disk_stats(), }