clean up firebase_website: offload cpu_percent to thread, cache fastfetch, fix regex, drop processes

This commit is contained in:
esef committed 2026-07-22 22:21:23 +03:00
1 parent ab3265667c
commit 7c2fd2a1ac
3 files changed
+86 -46

No files matched your search

+5 -1
View File
@@ -2,6 +2,10 @@
Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON. Requires GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service-account JSON.
This package is loaded as a Discord extension purely for lifecycle hooks
(setup / teardown). It does not register a Cog — it starts a background
SysInfoSync task instead.
Usage from main.py: Usage from main.py:
await bot.load_extension("firebase_website") await bot.load_extension("firebase_website")
""" """
@@ -10,7 +14,7 @@ import logging
from discord.ext import commands from discord.ext import commands
from .config import get_db # noqa: re‑export for other modules from .config import get_db # noqa: re‑export for calendar, bucketlist, etc.
from .sysinfo_sync import SysInfoSync from .sysinfo_sync import SysInfoSync
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+2 -3
View File
@@ -17,12 +17,11 @@ def get_db() -> AsyncClient:
"""Return the shared Firestore AsyncClient, creating it on first call.""" """Return the shared Firestore AsyncClient, creating it on first call."""
global _db global _db
if _db is None: if _db is None:
creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") if not os.getenv("GOOGLE_APPLICATION_CREDENTIALS"):
if creds_path is None:
raise RuntimeError( raise RuntimeError(
"GOOGLE_APPLICATION_CREDENTIALS is not set. " "GOOGLE_APPLICATION_CREDENTIALS is not set. "
"Point it to your service-account JSON file." "Point it to your service-account JSON file."
) )
_db = AsyncClient() _db = AsyncClient(project=PROJECT_ID)
log.info("Firestore Admin SDK initialised (project %s)", _db.project) log.info("Firestore Admin SDK initialised (project %s)", _db.project)
return _db return _db
+79 -42
View File
@@ -11,9 +11,11 @@ from .config import get_db, SYSINFO_INTERVAL
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FASTFETCH_CACHE_SECS = 300 # refresh fastfetch every 5 minutes
def _fmt_bytes(b: int) -> str:
"""Format a byte count as a human-readable string (e.g. '1.2 GB').""" def _fmt_size(b: int) -> str:
"""Bytes → human-readable string (auto-scales B → TB)."""
for unit in ("B", "KB", "MB", "GB", "TB"): for unit in ("B", "KB", "MB", "GB", "TB"):
if abs(b) < 1024: if abs(b) < 1024:
return f"{b:.1f} {unit}" return f"{b:.1f} {unit}"
@@ -22,12 +24,14 @@ def _fmt_bytes(b: int) -> str:
class SysInfoSync: class SysInfoSync:
"""Background task that collects system stats and pushes them to Firestore.""" """Background task: collect system stats and push to Firestore."""
def __init__(self) -> None: def __init__(self) -> None:
self._task: asyncio.Task | None = None self._task: asyncio.Task | None = None
self._fastfetch: str = ""
self._fastfetch_ts: float = 0
# ── public API ────────────────────────────────────────────────────────── # ── public ──────────────────────────────────────────────────────────
def start(self) -> None: def start(self) -> None:
if self._task is not None: if self._task is not None:
@@ -42,7 +46,7 @@ class SysInfoSync:
self._task = None self._task = None
log.info("SysInfo → Firestore sync stopped") log.info("SysInfo → Firestore sync stopped")
# ── internals ─────────────────────────────────────────────────────────── # ── loop ────────────────────────────────────────────────────────────
async def _run(self) -> None: async def _run(self) -> None:
while True: while True:
@@ -51,51 +55,81 @@ class SysInfoSync:
except asyncio.CancelledError: except asyncio.CancelledError:
return return
except Exception: except Exception:
log.debug("SysInfo Firestore push failed", exc_info=True) log.warning("SysInfo push failed", exc_info=True)
await asyncio.sleep(SYSINFO_INTERVAL) await asyncio.sleep(SYSINFO_INTERVAL)
# ── push ────────────────────────────────────────────────────────────
async def _push(self) -> None: async def _push(self) -> None:
cpu = psutil.cpu_percent(interval=1) stats = await self._collect_stats()
mem = psutil.virtual_memory() fetch = await self._get_fastfetch()
disk = psutil.disk_usage("/")
uptime_seconds = int(time.time() - psutil.boot_time())
days, r = divmod(uptime_seconds, 86400)
hours, r = divmod(r, 3600)
minutes, _ = divmod(r, 60)
uptime_str = f"{days}d {hours}h {minutes}m" if days > 0 else f"{hours}h {minutes}m"
load1, load5, load15 = psutil.getloadavg()
net = psutil.net_io_counters()
swap = psutil.swap_memory()
payload = { payload = {
"online": True, "online": True,
"cpu": round(cpu, 1), "cpu": stats["cpu"],
"ram": round(mem.percent, 1), "ram": stats["ram"],
"ramUsed": f"{mem.used // (1024 ** 2)}MB", "ramUsed": stats["ram_used"],
"ramTotal": f"{mem.total // (1024 ** 2)}MB", "ramTotal": stats["ram_total"],
"disk": round(disk.percent, 1), "disk": stats["disk"],
"diskUsed": f"{disk.used // (1024 ** 3)}GB", "diskUsed": stats["disk_used"],
"diskTotal": f"{disk.total // (1024 ** 3)}GB", "diskTotal": stats["disk_total"],
"swap": round(swap.percent, 1), "swap": stats["swap"],
"swapUsed": f"{swap.used // (1024 ** 2)}MB", "swapUsed": stats["swap_used"],
"swapTotal": f"{swap.total // (1024 ** 2)}MB", "swapTotal": stats["swap_total"],
"uptime": uptime_str, "uptime": stats["uptime"],
"load1": round(load1, 1), "load1": stats["load1"],
"load5": round(load5, 1), "load5": stats["load5"],
"load15": round(load15, 1), "load15": stats["load15"],
"netSent": _fmt_bytes(net.bytes_sent), "netSent": stats["net_sent"],
"netRecv": _fmt_bytes(net.bytes_recv), "netRecv": stats["net_recv"],
"processes": len(psutil.pids()), "fetch": fetch,
"fetch": await self._get_fastfetch(),
} }
db = get_db() db = get_db()
await db.collection("sysinfo").document("server").set(payload, merge=True) await db.collection("sysinfo").document("server").set(payload, merge=True)
@staticmethod # ── collectors ──────────────────────────────────────────────────────
async def _get_fastfetch() -> str:
async def _collect_stats(self) -> dict:
"""Gather all psutil stats; offloads blocking calls to a thread."""
cpu = await asyncio.to_thread(psutil.cpu_percent, interval=1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
swap = psutil.swap_memory()
load1, load5, load15 = psutil.getloadavg()
net = psutil.net_io_counters()
uptime_seconds = int(time.time() - psutil.boot_time())
days, r = divmod(uptime_seconds, 86400)
hours, r = divmod(r, 3600)
minutes, _ = divmod(r, 60)
uptime_str = f"{days}d {hours}h {minutes}m" if days else f"{hours}h {minutes}m"
return {
"cpu": round(cpu, 1),
"ram": round(mem.percent, 1),
"ram_used": _fmt_size(mem.used),
"ram_total": _fmt_size(mem.total),
"disk": round(disk.percent, 1),
"disk_used": _fmt_size(disk.used),
"disk_total": _fmt_size(disk.total),
"swap": round(swap.percent, 1),
"swap_used": _fmt_size(swap.used),
"swap_total": _fmt_size(swap.total),
"uptime": uptime_str,
"load1": round(load1, 1),
"load5": round(load5, 1),
"load15": round(load15, 1),
"net_sent": _fmt_size(net.bytes_sent),
"net_recv": _fmt_size(net.bytes_recv),
}
async def _get_fastfetch(self) -> str:
"""Return cached fastfetch output; refreshed every 5 minutes."""
now = time.monotonic()
if self._fastfetch and (now - self._fastfetch_ts) < FASTFETCH_CACHE_SECS:
return self._fastfetch
try: try:
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
"fastfetch", "--pipe", "--logo", "none", "fastfetch", "--pipe", "--logo", "none",
@@ -103,8 +137,11 @@ class SysInfoSync:
stderr=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
) )
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]|\[[0-9]+[A-Z]", "", stdout.decode()) clean = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", stdout.decode())
lines = [line for line in clean.strip().splitlines() if line.strip()] lines = [line for line in clean.strip().splitlines() if line.strip()]
return "\n".join(lines[:15]) self._fastfetch = "\n".join(lines[:15])
self._fastfetch_ts = now
except Exception: except Exception:
return "" if not self._fastfetch:
self._fastfetch = ""
return self._fastfetch