""" @NebulaShell/system-monitor 实时系统监控模组 - CPU/内存/磁盘/网络/进程TOP 提供 HTTP REST API,默认端口 10087。 可在 manifest.json 的 config.args 中自定义端口。 """ import json import os import threading import time from collections import deque from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False NAME = "system-monitor" VERSION = "0.1.0" # ── 历史数据存储 ── MAX_HISTORY = 60 # 保留最近60条(每分钟一条,即1小时) _history: deque = deque(maxlen=MAX_HISTORY) _collector_thread = None _collector_running = False def _collect_stats() -> dict: """采集一次系统状态快照""" if not HAS_PSUTIL: return {"error": "psutil not installed"} now = time.time() cpu_percent = psutil.cpu_percent(interval=0.1) cpu_count = psutil.cpu_count() mem = psutil.virtual_memory() swap = psutil.swap_memory() disk = psutil.disk_usage("/") net = psutil.net_io_counters() net_conns = len(psutil.net_connections()) # TOP 10 进程(按CPU排序) top_processes = [] for proc in sorted(psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent", "status"]), key=lambda p: p.info.get("cpu_percent", 0) or 0, reverse=True)[:10]: try: top_processes.append({ "pid": proc.info["pid"], "name": proc.info["name"] or "?", "cpu": round(proc.info["cpu_percent"] or 0, 1), "mem": round(proc.info["memory_percent"] or 0, 1), "status": proc.info["status"] or "?", }) except (psutil.NoSuchProcess, psutil.AccessDenied): continue # 开机时间 boot_time = psutil.boot_time() return { "timestamp": now, "datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)), "uptime": round(now - boot_time), "cpu": { "percent": round(cpu_percent, 1), "count": cpu_count, "load_avg": [round(x, 2) for x in os.getloadavg()] if hasattr(os, "getloadavg") else None, }, "memory": { "total": mem.total, "available": mem.available, "used": mem.used, "percent": round(mem.percent, 1), "swap_total": swap.total, "swap_used": swap.used, "swap_percent": round(swap.percent, 1), }, "disk": { "total": disk.total, "used": disk.used, "free": disk.free, "percent": round(disk.percent, 1), }, "network": { "bytes_sent": net.bytes_sent, "bytes_recv": net.bytes_recv, "packets_sent": net.packets_sent, "packets_recv": net.packets_recv, "connections": net_conns, }, "processes": { "total": len(psutil.pids()), "running": sum(1 for p in psutil.process_iter(["status"]) if p.info.get("status") == "running"), "top": top_processes, }, } def _collector_loop(interval: float = 5.0): """后台采集线程""" global _collector_running _collector_running = True while _collector_running: try: stats = _collect_stats() _history.append(stats) except Exception as e: print(f"[SystemMonitor] 监控数据采集错误: {e}") time.sleep(interval) # ── HTTP 服务 ── class MonitorHandler(BaseHTTPRequestHandler): """HTTP 请求处理器""" def _json_response(self, data: dict, status: int = 200): self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")) def _html_response(self, html: str): self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(html.encode("utf-8")) def do_GET(self): parsed = urlparse(self.path) path = parsed.path.rstrip("/") if path == "/" or path == "": self._html_response(_render_dashboard()) elif path == "/health": self._json_response({"status": "ok", "version": VERSION, "uptime": _get_uptime()}) elif path == "/stats": stats = _collect_stats() if not _history else _history[-1] self._json_response(stats) elif path == "/stats/current": self._json_response(_collect_stats()) elif path == "/stats/history": self._json_response({ "count": len(_history), "max": MAX_HISTORY, "data": list(_history), }) elif path == "/stats/cpu": s = _collect_stats() self._json_response(s.get("cpu", {})) elif path == "/stats/memory": s = _collect_stats() self._json_response(s.get("memory", {})) elif path == "/stats/disk": s = _collect_stats() self._json_response(s.get("disk", {})) elif path == "/stats/network": s = _collect_stats() self._json_response(s.get("network", {})) elif path == "/stats/processes": s = _collect_stats() self._json_response(s.get("processes", {})) else: self._json_response({"error": "Not Found", "path": path}, 404) def log_message(self, format, *args): """静默日志,避免刷屏""" pass def _get_uptime() -> float: try: return round(time.time() - psutil.boot_time()) except Exception: return 0 def _render_dashboard() -> str: """渲染简易仪表盘 HTML""" stats = _collect_stats() if not _history else _history[-1] if not stats or "error" in stats: return "
psutil not available
" cpu = stats.get("cpu", {}) mem = stats.get("memory", {}) disk = stats.get("disk", {}) net = stats.get("network", {}) procs = stats.get("processes", {}) uptime = stats.get("uptime", 0) # 格式化时间 days, rem = divmod(uptime, 86400) hours, rem = divmod(rem, 3600) mins = rem // 60 uptime_str = f"{int(days)}天 {int(hours)}时 {int(mins)}分" def bar(pct, color="primary"): color_map = { "primary": "#0d6efd", "success": "#198754", "warning": "#ffc107", "danger": "#dc3545", } c = color_map.get(color, color_map["primary"]) return f'' def mem_fmt(b): for unit in ["B", "KB", "MB", "GB", "TB"]: if b < 1024: return f"{b:.1f} {unit}" b /= 1024 return f"{b:.1f} PB" # 进程TOP表格行 proc_rows = "" for p in procs.get("top", []): proc_rows += f"v{VERSION} · 运行时间 {uptime_str} · 进程 {procs.get('total', '?')} 个
| PID | 名称 | CPU | 内存 | 状态 |
|---|
数据采集间隔 5秒 · 保留最近 {MAX_HISTORY} 条