- crypto.py: 8个_imp_*方法改为_ModuleCache类缓存导入 - crypto.py: outer/inner加解密合并为_layer_encrypt/decrypt - crypto.py: 提取公共摘要计算方法,拆分长方法 - compiler.py: 删除_obfuscate_code中未使用的死代码 - loader.py: 3次ZIP扫描合并为1次缓存读取 - format.py: 更新为使用_ModuleCache - 合计减少205行代码(1707→1502)
431 lines
13 KiB
Python
431 lines
13 KiB
Python
"""
|
||
@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 "<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 as e:
|
||
print(f"[SystemMonitor] 服务启动错误: {e}")
|
||
|
||
|
||
# ── 生命周期 ──
|
||
|
||
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 as e:
|
||
print(f"[SystemMonitor] 服务停止错误: {e}")
|
||
_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()
|