"""Мониторинг сервера: загрузка процессора, память, место на дисках. Без внешних библиотек. Linux (в том числе Docker): /proc/stat, /proc/meminfo; если контейнеру задан лимит памяти (cgroup v2 memory.max) — показываем память контейнера. Windows (локальный запуск): GetSystemTimes и GlobalMemoryStatusEx через ctypes.""" import os import shutil import sys import time from pathlib import Path SAMPLE_SEC = 0.25 def _linux_cpu_times() -> tuple[int, int]: with open("/proc/stat") as f: vals = [int(x) for x in f.readline().split()[1:]] idle = vals[3] + (vals[4] if len(vals) > 4 else 0) # idle + iowait return idle, sum(vals) def _win_cpu_times() -> tuple[int, int]: import ctypes from ctypes import wintypes idle, kernel, user = wintypes.FILETIME(), wintypes.FILETIME(), wintypes.FILETIME() ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kernel), ctypes.byref(user)) v = lambda t: (t.dwHighDateTime << 32) | t.dwLowDateTime return v(idle), v(kernel) + v(user) # время ядра уже включает простой def cpu_percent() -> float | None: get = _win_cpu_times if sys.platform == "win32" else _linux_cpu_times try: i1, t1 = get() time.sleep(SAMPLE_SEC) i2, t2 = get() except (OSError, ValueError, AttributeError): return None dt = t2 - t1 return round(100.0 * (1 - (i2 - i1) / dt), 1) if dt > 0 else 0.0 def _cgroup_memory() -> tuple[int, int] | None: try: limit = Path("/sys/fs/cgroup/memory.max").read_text().strip() if limit == "max": return None return int(Path("/sys/fs/cgroup/memory.current").read_text()), int(limit) except (OSError, ValueError): return None def memory() -> dict | None: """{used, total, scope: host|container} в байтах.""" if sys.platform == "win32": try: import ctypes class MS(ctypes.Structure): _fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong), ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong)] m = MS() m.dwLength = ctypes.sizeof(MS) ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m)) return {"used": m.ullTotalPhys - m.ullAvailPhys, "total": m.ullTotalPhys, "scope": "host"} except (OSError, AttributeError): return None cg = _cgroup_memory() if cg: return {"used": cg[0], "total": cg[1], "scope": "container"} try: info = {} with open("/proc/meminfo") as f: for line in f: k, v = line.split(":", 1) info[k] = int(v.split()[0]) * 1024 total = info["MemTotal"] return {"used": total - info.get("MemAvailable", info.get("MemFree", 0)), "total": total, "scope": "host"} except (OSError, KeyError, ValueError): return None def disks(paths: dict[str, Path]) -> list[dict]: """Место на дисках, где лежат база, фото и т. п. Один и тот же диск показывается один раз.""" out, seen = [], {} for label, p in paths.items(): p = Path(p) while not p.exists() and p.parent != p: p = p.parent try: u = shutil.disk_usage(p) except OSError: continue key = (u.total, os.stat(p).st_dev) if key in seen: seen[key]["labels"].append(label) continue seen[key] = {"labels": [label], "path": str(p), "used": u.used, "total": u.total, "free": u.free} out.append(seen[key]) return out def snapshot(paths: dict[str, Path]) -> dict: try: load = os.getloadavg() except (OSError, AttributeError): load = None return {"cpu": cpu_percent(), "cpu_count": os.cpu_count(), "load": load, "memory": memory(), "disks": disks(paths), "platform": sys.platform}