Files
NebulaShell/system-monitor/main.py
starlight-apk 5fbc5cc335
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
- nebula create mod/key/list-templates 模组脚手架
- nebula dev 开发模式热重载
- manifest permissions.imports 权限白名单机制
- system-monitor 系统监控仪表盘插件
- 默认端口统一为 10086
- 修复 _init_nbpf 误读 Ed25519 私钥为 RSA 的 bug
- 更新 README.md 文档
2026-05-16 20:20:43 +08:00

431 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
@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:
pass
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 "<h1>System Monitor</h1><p>psutil not available</p>"
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'<div style="height:20px;background:#e9ecef;border-radius:10px;overflow:hidden">' \
f'<div style="height:100%;width:{pct}%;background:{c};transition:width 0.5s"></div></div>'
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"<tr><td>{p['pid']}</td><td>{p['name']}</td>" \
f"<td>{p['cpu']}%</td><td>{p['mem']}%</td><td>{p['status']}</td></tr>"
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>System Monitor</title>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; background:#f8f9fa; color:#333; padding:20px; }}
.container {{ max-width:900px; margin:0 auto; }}
h1 {{ font-size:24px; margin-bottom:5px; }}
.sub {{ color:#666; font-size:14px; margin-bottom:20px; }}
.grid {{ display:grid; grid-template-columns:1fr 1fr; gap:15px; margin-bottom:20px; }}
.card {{ background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 3px rgba(0,0,0,0.1); }}
.card h3 {{ font-size:14px; color:#666; margin-bottom:8px; }}
.card .value {{ font-size:28px; font-weight:700; }}
.card .subtext {{ font-size:12px; color:#999; margin-top:4px; }}
.full {{ grid-column:1/-1; }}
table {{ width:100%; border-collapse:collapse; font-size:13px; }}
th, td {{ padding:6px 8px; text-align:left; border-bottom:1px solid #eee; }}
th {{ color:#666; font-weight:600; }}
a {{ color:#0d6efd; text-decoration:none; }}
a:hover {{ text-decoration:underline; }}
.links {{ margin-bottom:15px; font-size:13px; }}
.links a {{ margin-right:12px; }}
</style>
</head><body>
<div class="container">
<h1>📊 System Monitor</h1>
<p class="sub">v{VERSION} · 运行时间 {uptime_str} · 进程 {procs.get('total', '?')} 个</p>
<div class="links">
<a href="/stats">📄 JSON</a>
<a href="/stats/current">🔄 实时刷新</a>
<a href="/stats/history">📈 历史数据</a>
<a href="/health">💚 健康检查</a>
</div>
<div class="grid">
<div class="card">
<h3>🧠 CPU</h3>
<div class="value">{cpu.get('percent', '?')}%</div>
<div class="subtext">{cpu.get('count', '?')} 核心 · 负载 {cpu.get('load_avg', ['?','?','?'])}</div>
{bar(cpu.get('percent', 0), 'danger' if cpu.get('percent', 0) > 80 else 'warning' if cpu.get('percent', 0) > 60 else 'primary')}
</div>
<div class="card">
<h3>💾 内存</h3>
<div class="value">{mem.get('percent', '?')}%</div>
<div class="subtext">{mem_fmt(mem.get('used', 0))} / {mem_fmt(mem.get('total', 0))}</div>
{bar(mem.get('percent', 0), 'danger' if mem.get('percent', 0) > 80 else 'warning' if mem.get('percent', 0) > 60 else 'success')}
</div>
<div class="card">
<h3>💿 磁盘 /</h3>
<div class="value">{disk.get('percent', '?')}%</div>
<div class="subtext">{mem_fmt(disk.get('used', 0))} / {mem_fmt(disk.get('total', 0))}</div>
{bar(disk.get('percent', 0), 'danger' if disk.get('percent', 0) > 85 else 'warning')}
</div>
<div class="card">
<h3>🌐 网络</h3>
<div class="value">{net.get('connections', '?')}</div>
<div class="subtext">连接数 · ↓ {mem_fmt(net.get('bytes_recv', 0))}{mem_fmt(net.get('bytes_sent', 0))}</div>
</div>
</div>
<div class="card full">
<h3>⚡ TOP 10 进程 (CPU)</h3>
<table>
<thead><tr><th>PID</th><th>名称</th><th>CPU</th><th>内存</th><th>状态</th></tr></thead>
<tbody>{proc_rows}</tbody>
</table>
</div>
<p class="sub" style="text-align:center;margin-top:20px">
数据采集间隔 5秒 · 保留最近 {MAX_HISTORY}
</p>
</div>
</body></html>"""
return html
# ── HTTP 服务器 ──
_http_server = None
_server_thread = None
def _run_http_server(host: str, port: int):
global _http_server
server = HTTPServer((host, port), MonitorHandler)
_http_server = server
try:
server.serve_forever()
except Exception:
pass
# ── 生命周期 ──
def init(deps):
"""模组初始化"""
logger = deps.get("logger")
if logger:
logger.info(f"System Monitor v{VERSION} 初始化")
def start():
"""启动 HTTP 服务 + 数据采集"""
global _http_server, _server_thread, _collector_thread
if not HAS_PSUTIL:
print("[system-monitor] ⚠ psutil 未安装,系统监控不可用")
print("[system-monitor] 💡 请执行: pip install psutil")
return
# 启动数据采集后台线程5秒间隔
_collector_thread = threading.Thread(
target=_collector_loop, args=(5.0,), daemon=True
)
_collector_thread.start()
# 启动 HTTP 服务
port = 10087
host = "0.0.0.0"
_server_thread = threading.Thread(
target=_run_http_server, args=(host, port), daemon=True
)
_server_thread.start()
print(f"[system-monitor] ✅ 系统监控已启动")
print(f"[system-monitor] 🌐 仪表盘: http://localhost:{port}")
print(f"[system-monitor] 📄 API: http://localhost:{port}/stats")
print(f"[system-monitor] 💚 健康: http://localhost:{port}/health")
def stop():
"""停止服务,释放资源"""
global _http_server, _collector_running
_collector_running = False
if _http_server:
try:
_http_server.shutdown()
except Exception:
pass
_http_server = None
_history.clear()
print("[system-monitor] 已停止")
def health() -> dict:
"""健康检查"""
return {
"status": "ok" if HAS_PSUTIL else "degraded",
"version": VERSION,
"uptime": _get_uptime(),
"data_points": len(_history),
}
def stats() -> dict:
"""统计信息"""
return {
"version": VERSION,
"psutil_available": HAS_PSUTIL,
"history_count": len(_history),
"history_max": MAX_HISTORY,
"collector_running": _collector_running,
}
# ── 目录插件兼容(类 + New() 工厂函数) ──
class SystemMonitor:
"""系统监控插件类封装"""
name = NAME
version = VERSION
description = "实时系统监控CPU/内存/磁盘/网络/进程TOP"
def __init__(self):
self._http_server = None
self._server_thread = None
def init(self, deps=None):
return init(deps)
def start(self):
return start()
def stop(self):
return stop()
def health(self) -> dict:
return health()
def stats(self) -> dict:
return stats()
def New():
"""目录插件工厂函数"""
return SystemMonitor()