diff --git a/firebase_website/__init__.py b/firebase_website/__init__.py index 1edc591..94c3845 100644 --- a/firebase_website/__init__.py +++ b/firebase_website/__init__.py @@ -2,6 +2,10 @@ 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: await bot.load_extension("firebase_website") """ @@ -10,7 +14,7 @@ import logging 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 log = logging.getLogger(__name__) diff --git a/firebase_website/config.py b/firebase_website/config.py index b209e67..5f4b212 100644 --- a/firebase_website/config.py +++ b/firebase_website/config.py @@ -17,12 +17,11 @@ def get_db() -> AsyncClient: """Return the shared Firestore AsyncClient, creating it on first call.""" global _db if _db is None: - creds_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") - if creds_path is None: + if not os.getenv("GOOGLE_APPLICATION_CREDENTIALS"): raise RuntimeError( "GOOGLE_APPLICATION_CREDENTIALS is not set. " "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) return _db diff --git a/firebase_website/sysinfo_sync.py b/firebase_website/sysinfo_sync.py index 207bb93..cfaf39a 100644 --- a/firebase_website/sysinfo_sync.py +++ b/firebase_website/sysinfo_sync.py @@ -11,9 +11,11 @@ from .config import get_db, SYSINFO_INTERVAL 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"): if abs(b) < 1024: return f"{b:.1f} {unit}" @@ -22,12 +24,14 @@ def _fmt_bytes(b: int) -> str: 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: self._task: asyncio.Task | None = None + self._fastfetch: str = "" + self._fastfetch_ts: float = 0 - # ── public API ────────────────────────────────────────────────────────── + # ── public ────────────────────────────────────────────────────────── def start(self) -> None: if self._task is not None: @@ -42,7 +46,7 @@ class SysInfoSync: self._task = None log.info("SysInfo → Firestore sync stopped") - # ── internals ─────────────────────────────────────────────────────────── + # ── loop ──────────────────────────────────────────────────────────── async def _run(self) -> None: while True: @@ -51,51 +55,81 @@ class SysInfoSync: except asyncio.CancelledError: return except Exception: - log.debug("SysInfo Firestore push failed", exc_info=True) + log.warning("SysInfo push failed", exc_info=True) await asyncio.sleep(SYSINFO_INTERVAL) + # ── push ──────────────────────────────────────────────────────────── + async def _push(self) -> None: - cpu = psutil.cpu_percent(interval=1) - mem = psutil.virtual_memory() - 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() + stats = await self._collect_stats() + fetch = await self._get_fastfetch() payload = { "online": True, - "cpu": round(cpu, 1), - "ram": round(mem.percent, 1), - "ramUsed": f"{mem.used // (1024 ** 2)}MB", - "ramTotal": f"{mem.total // (1024 ** 2)}MB", - "disk": round(disk.percent, 1), - "diskUsed": f"{disk.used // (1024 ** 3)}GB", - "diskTotal": f"{disk.total // (1024 ** 3)}GB", - "swap": round(swap.percent, 1), - "swapUsed": f"{swap.used // (1024 ** 2)}MB", - "swapTotal": f"{swap.total // (1024 ** 2)}MB", - "uptime": uptime_str, - "load1": round(load1, 1), - "load5": round(load5, 1), - "load15": round(load15, 1), - "netSent": _fmt_bytes(net.bytes_sent), - "netRecv": _fmt_bytes(net.bytes_recv), - "processes": len(psutil.pids()), - "fetch": await self._get_fastfetch(), + "cpu": stats["cpu"], + "ram": stats["ram"], + "ramUsed": stats["ram_used"], + "ramTotal": stats["ram_total"], + "disk": stats["disk"], + "diskUsed": stats["disk_used"], + "diskTotal": stats["disk_total"], + "swap": stats["swap"], + "swapUsed": stats["swap_used"], + "swapTotal": stats["swap_total"], + "uptime": stats["uptime"], + "load1": stats["load1"], + "load5": stats["load5"], + "load15": stats["load15"], + "netSent": stats["net_sent"], + "netRecv": stats["net_recv"], + "fetch": fetch, } db = get_db() await db.collection("sysinfo").document("server").set(payload, merge=True) - @staticmethod - async def _get_fastfetch() -> str: + # ── collectors ────────────────────────────────────────────────────── + + 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: proc = await asyncio.create_subprocess_exec( "fastfetch", "--pipe", "--logo", "none", @@ -103,8 +137,11 @@ class SysInfoSync: stderr=asyncio.subprocess.DEVNULL, ) 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()] - return "\n".join(lines[:15]) + self._fastfetch = "\n".join(lines[:15]) + self._fastfetch_ts = now except Exception: - return "" + if not self._fastfetch: + self._fastfetch = "" + return self._fastfetch