diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..d07f217 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-alpine +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN mkdir -p data logs +EXPOSE 4200 4240 +ENV SENSU_ADMIN_PASSWORD=changeme +ENV SENSU_API_PASSWORD=changeme +CMD ["python3", "main.py", "--headless"] diff --git a/deploy/sensu.service b/deploy/sensu.service new file mode 100644 index 0000000..c7a8399 --- /dev/null +++ b/deploy/sensu.service @@ -0,0 +1,18 @@ +[Unit] +Description=SenSu Plugin Framework +After=network.target + +[Service] +Type=simple +User=aska +WorkingDirectory=/data/data/com.termux/files/home/Proj/SenSu_workspace +ExecStart=/data/data/com.termux/files/usr/bin/python3 /data/data/com.termux/files/home/Proj/SenSu_workspace/start.py --headless +Restart=on-failure +RestartSec=5 +Environment=SENSU_ADMIN_PASSWORD=changeme +Environment=SENSU_API_PASSWORD=changeme +Environment=SENSU_PANEL_USER=admin +Environment=SENSU_PANEL_PASS=changeme + +[Install] +WantedBy=multi-user.target diff --git a/services/process_isolated.py b/services/process_isolated.py new file mode 100644 index 0000000..4654993 --- /dev/null +++ b/services/process_isolated.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""插件进程隔离 — 在子进程中运行插件,崩溃不影响框架""" +import asyncio, subprocess, json, os, logging, tempfile +from multiprocessing import Process, Queue +from typing import Dict, Any + +logger = logging.getLogger(__name__) + +def _plugin_runner(plugin_path: str, config_json: str, cmd_queue: Queue, result_queue: Queue): + """子进程入口 - 加载插件并监听命令队列""" + import sys, importlib.util, json as j + sys.path.insert(0, os.path.dirname(plugin_path)) + spec = importlib.util.spec_from_file_location("plugin", plugin_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + plugin = mod.Plugin(os.path.basename(os.path.dirname(plugin_path)), + j.loads(config_json), None) + asyncio.run(plugin.initialize()) + + while True: + cmd = cmd_queue.get() + if cmd == "__SHUTDOWN__": + asyncio.run(plugin.shutdown()) + break + try: + method = getattr(plugin, cmd.get("method", ""), None) + if method: + result = method(*cmd.get("args", [])) + result_queue.put({"ok": True, "result": str(result)}) + else: + result_queue.put({"ok": False, "error": f"method not found: {cmd.get(method)}"}) + except Exception as e: + result_queue.put({"ok": False, "error": str(e)}) + + +class IsolatedPlugin: + """进程隔离插件包装器""" + def __init__(self, plugin_name: str, plugin_path: str, config: dict): + self.name = plugin_name + self.cmd_queue = Queue() + self.result_queue = Queue() + self.process = Process( + target=_plugin_runner, + args=(plugin_path, json.dumps(config), self.cmd_queue, self.result_queue), + daemon=True + ) + self.process.start() + logger.info(f"隔离插件已启动: {plugin_name} (PID={self.process.pid})") + + def call(self, method: str, *args, timeout: float = 30): + self.cmd_queue.put({"method": method, "args": args}) + try: + result = self.result_queue.get(timeout=timeout) + return result + except: + return {"ok": False, "error": "timeout"} + + def shutdown(self): + self.cmd_queue.put("__SHUTDOWN__") + self.process.join(timeout=10) + if self.process.is_alive(): + self.process.terminate() + logger.info(f"隔离插件已关闭: {self.name}") + + @property + def pid(self): + return self.process.pid if self.process.is_alive() else None diff --git a/services/sysmon_widget.py b/services/sysmon_widget.py new file mode 100644 index 0000000..d970c2c --- /dev/null +++ b/services/sysmon_widget.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""SenSu TUI 系统监控组件 — CPU/内存/磁盘/插件状态""" +import psutil, time, asyncio, logging +from textual.widgets import Static +from textual.reactive import reactive + +logger = logging.getLogger(__name__) + +def get_system_stats(): + """获取系统实时状态""" + try: + cpu = psutil.cpu_percent(interval=0.1) + mem = psutil.virtual_memory() + disk = psutil.disk_usage("/") + net = psutil.net_io_counters() + return { + "cpu": cpu, + "mem_total": mem.total, + "mem_used": mem.used, + "mem_percent": mem.percent, + "disk_total": disk.total, + "disk_used": disk.used, + "disk_percent": disk.percent, + "net_sent": net.bytes_sent, + "net_recv": net.bytes_recv, + "time": time.time(), + } + except Exception as e: + return {"error": str(e)} + +def format_bytes(b): + if b < 1024: return f"{b}B" + if b < 1024**2: return f"{b/1024:.1f}K" + if b < 1024**3: return f"{b/1024**2:.1f}M" + return f"{b/1024**3:.1f}G" + +def make_bar(percent, width=20, filled="█", empty="░"): + n = int(percent / 100 * width) + return filled * n + empty * (width - n) + +class SysMonWidget(Static): + """系统监控显示组件""" + stats = reactive({}) + plugin_info = reactive("") + + def __init__(self): + super().__init__("系统监控初始化中...") + self._refresh_task = None + + def on_mount(self): + self._refresh_task = asyncio.create_task(self._periodic_refresh()) + + async def _periodic_refresh(self): + while True: + try: + s = get_system_stats() + if "error" not in s: + cpu_bar = make_bar(s["cpu"], 20) + mem_bar = make_bar(s["mem_percent"], 20) + disk_bar = make_bar(s["disk_percent"], 20) + + text = ( + f"[bold cyan]━━━ 系统状态 ━━━[/]\n" + f"CPU {cpu_bar} {s['cpu']:5.1f}%\n" + f"MEM {mem_bar} {s['mem_percent']:5.1f}% " + f"({format_bytes(s['mem_used'])}/{format_bytes(s['mem_total'])})\n" + f"DISK {disk_bar} {s['disk_percent']:5.1f}% " + f"({format_bytes(s['disk_used'])}/{format_bytes(s['disk_total'])})\n" + f"NET ↑{format_bytes(s['net_sent'])} ↓{format_bytes(s['net_recv'])}" + ) + if self.plugin_info: + text += f"\n\n[bold yellow]━━━ 插件状态 ━━━[/]\n{self.plugin_info}" + self.update(text) + await asyncio.sleep(2) + except asyncio.CancelledError: + break + except Exception as e: + logger.debug(f"SysMon refresh error: {e}") + await asyncio.sleep(5) + + def update_plugins(self, info: str): + self.plugin_info = info + + def on_unmount(self): + if self._refresh_task: + self._refresh_task.cancel() diff --git a/services/tui_service.py b/services/tui_service.py index b755c70..2691dad 100644 --- a/services/tui_service.py +++ b/services/tui_service.py @@ -9,6 +9,7 @@ from textual.app import App from textual.containers import Container, ScrollableContainer from textual.widgets import Static, Input, Header, Footer from textual.reactive import reactive +from services.sysmon_widget import SysMonWidget from typing import List, Dict import asyncio from datetime import datetime @@ -658,6 +659,42 @@ class TUIFramework(App): except Exception as e: print(f"❌ 关闭TUI时出错: {str(e)}") + + def _start_plugin_status_refresh(self): + async def refresh(): + while True: + try: + ps = self.service_manager.get_service("plugin") + if ps and hasattr(ps, "plugin_status"): + lines = [] + for name, status in ps.plugin_status.items(): + icon = {"running":"🟢","error":"🔴","loading":"🟡","unloaded":"⚫"}.get(str(status),"⚪") + lines.append(f"{icon} {name}: {status}") + widget = self.tui_app.query_one(SysMonWidget) + widget.update_plugins("\n".join(lines) if lines else "无已加载插件") + except: pass + await asyncio.sleep(3) + import asyncio + asyncio.create_task(refresh()) + + + def _get_suggester(self): + from textual.suggester import Suggester + class CmdSuggester(Suggester): + async def get_suggestion(self, value): + try: + cs = self.app.query_one("#input-area").app + names = ["help","status","history","testlog","netdiag", + "permissions","pmpending","pmhelp","echo","plugin_status", + "scroll","autoscroll","create-plugin","exit","quit"] + if not value: return None + for n in names: + if n.startswith(value) and n != value: + return n + except: pass + return None + return CmdSuggester() + class TuiService: """TUI服务"""