feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
- nebula create mod/key/list-templates 模组脚手架 - nebula dev 开发模式热重载 - manifest permissions.imports 权限白名单机制 - system-monitor 系统监控仪表盘插件 - 默认端口统一为 10086 - 修复 _init_nbpf 误读 Ed25519 私钥为 RSA 的 bug - 更新 README.md 文档
This commit is contained in:
24
system-monitor/README.md
Normal file
24
system-monitor/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# @NebulaShell/system-monitor
|
||||
|
||||
实时系统监控:CPU/内存/磁盘/网络/进程TOP
|
||||
|
||||
## 安装
|
||||
|
||||
将 `system-monitor.nbpf` 放入 NebulaShell 的 `mods/` 目录即可。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 打包
|
||||
nebula nbpf pack ./system-monitor -o system-monitor.nbpf \
|
||||
--ed25519-key ./keys/ed25519.pem \
|
||||
--rsa-key ./keys/rsa.pem \
|
||||
--signer "NebulaShell"
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
430
system-monitor/main.py
Normal file
430
system-monitor/main.py
Normal file
@@ -0,0 +1,430 @@
|
||||
"""
|
||||
@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()
|
||||
45
system-monitor/manifest.json
Normal file
45
system-monitor/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@NebulaShell/system-monitor",
|
||||
"version": "0.1.0",
|
||||
"description": "实时系统监控:CPU/内存/磁盘/网络/进程TOP,提供HTTP仪表盘和REST API",
|
||||
"author": "NebulaShell",
|
||||
"license": "MIT",
|
||||
"type": "tool",
|
||||
"main": "main.py",
|
||||
"enabled": true,
|
||||
"priority": 999,
|
||||
"runtime": {
|
||||
"language": "python",
|
||||
"entry_point": "main.py",
|
||||
"requirements": [
|
||||
"psutil>=5.8.0"
|
||||
]
|
||||
},
|
||||
"capabilities": [
|
||||
"monitoring"
|
||||
],
|
||||
"services": {
|
||||
"provides": [
|
||||
"system-monitor"
|
||||
],
|
||||
"consumes": []
|
||||
},
|
||||
"config": {
|
||||
"port": 10087,
|
||||
"host": "0.0.0.0",
|
||||
"interval": 5,
|
||||
"max_history": 60
|
||||
},
|
||||
"permissions": {
|
||||
"imports": [
|
||||
"os",
|
||||
"threading",
|
||||
"json",
|
||||
"time",
|
||||
"collections",
|
||||
"http",
|
||||
"urllib",
|
||||
"psutil"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user