Files
FunguyBot/plugins/sysinfo.py
T

314 lines
12 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.
"""
Comprehensive system information code block with emoji + aligned columns.
All blocking calls run in thread pool.
"""
import logging, platform, os, asyncio, psutil, socket, datetime, subprocess
import simplematrixbotlib as botlib
from plugins.common import collapsible_summary, html_escape, code_block
async def _run_blocking(func, *args, **kwargs):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
# ---------- Data collectors (unchanged) ----------
def _system_overview():
boot = datetime.datetime.fromtimestamp(psutil.boot_time())
uptime_delta = datetime.datetime.now() - boot
uptime_str = str(datetime.timedelta(seconds=int(uptime_delta.total_seconds())))
return {
"hostname": socket.gethostname(),
"os": f"{platform.system()} {platform.release()}",
"architecture": platform.architecture()[0],
"machine": platform.machine(),
"processor": platform.processor(),
"boot_time": boot.strftime("%Y-%m-%d %H:%M:%S"),
"uptime": uptime_str,
"users": len(psutil.users())
}
def _cpu_info():
cpu_freq = psutil.cpu_freq()
load = os.getloadavg() if hasattr(os, "getloadavg") else (0,0,0)
return {
"physical_cores": psutil.cpu_count(logical=False),
"logical_cores": psutil.cpu_count(logical=True),
"max_freq": f"{cpu_freq.max:.0f} MHz" if cpu_freq else "N/A",
"current_freq": f"{cpu_freq.current:.0f} MHz" if cpu_freq else "N/A",
"usage": f"{psutil.cpu_percent(interval=1)}%",
"load_avg": f"{load[0]:.2f} {load[1]:.2f} {load[2]:.2f}"
}
def _memory_info():
mem = psutil.virtual_memory()
swap = psutil.swap_memory()
return {
"total_ram": f"{mem.total / (1024**3):.1f} GB",
"used_ram": f"{mem.used / (1024**3):.1f} GB",
"ram_percent": f"{mem.percent}%",
"available_ram": f"{mem.available / (1024**3):.1f} GB",
"total_swap": f"{swap.total / (1024**3):.1f} GB" if swap.total > 0 else "N/A",
"used_swap": f"{swap.used / (1024**3):.1f} GB" if swap.total > 0 else "N/A",
"swap_percent": f"{swap.percent}%" if swap.total > 0 else "N/A"
}
def _disk_info():
partitions = psutil.disk_partitions()
mounted = []
for p in partitions:
try:
usage = psutil.disk_usage(p.mountpoint)
mounted.append({
"mount": p.mountpoint,
"used": f"{usage.used / (1024**3):.1f} GB",
"total": f"{usage.total / (1024**3):.1f} GB",
"percent": usage.percent
})
except:
pass
io = psutil.disk_io_counters()
io_read = f"{io.read_bytes / (1024**3):.2f} GB" if io else "0 GB"
io_write = f"{io.write_bytes / (1024**3):.2f} GB" if io else "0 GB"
return mounted, io_read, io_write
def _network_info():
ifaces = psutil.net_if_addrs()
io_counters = psutil.net_io_counters(pernic=True)
net = []
for name, addrs in ifaces.items():
if name == "lo":
continue
ip4 = next((a.address for a in addrs if a.family == socket.AF_INET), None)
if ip4:
stats = io_counters.get(name)
sent = f"{stats.bytes_sent / (1024**2):.1f} MB" if stats else "0 MB"
recv = f"{stats.bytes_recv / (1024**2):.1f} MB" if stats else "0 MB"
net.append((name, ip4, sent, recv))
return net
def _top_processes():
procs = []
for p in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
try:
procs.append(p.info)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
top_cpu = sorted(procs, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:5]
return top_cpu, len(procs)
def _gpu_info():
info = {}
try:
res = subprocess.run(
['nvidia-smi', '--query-gpu=name,memory.used,memory.total,temperature.gpu,utilization.gpu',
'--format=csv,noheader,nounits'],
capture_output=True, text=True
)
if res.returncode == 0:
gpus = []
for line in res.stdout.strip().split('\n'):
parts = [p.strip() for p in line.split(',')]
if len(parts) >= 5:
gpus.append({
"name": parts[0],
"mem_used": f"{parts[1]} MB",
"mem_total": f"{parts[2]} MB",
"temp": f"{parts[3]}°C",
"usage": f"{parts[4]}%"
})
if gpus:
info["nvidia"] = gpus
except:
pass
try:
res = subprocess.run(['lspci'], capture_output=True, text=True)
if res.returncode == 0:
lines = [l for l in res.stdout.split('\n') if 'VGA' in l or '3D' in l]
if lines:
info["detected"] = lines[:2]
except:
pass
return info
def _docker_info():
try:
ver = subprocess.run(['docker', '--version'], capture_output=True, text=True)
if ver.returncode != 0:
return None
ps_res = subprocess.run(
['docker', 'ps', '--format', '{{.Names}}|{{.Status}}'],
capture_output=True, text=True
)
containers = []
for line in ps_res.stdout.strip().split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 2:
containers.append({"name": parts[0], "status": parts[1]})
return containers
except:
return None
def _sensor_info():
temps = psutil.sensors_temperatures()
fans = psutil.sensors_fans()
battery = psutil.sensors_battery()
data = {"temps": [], "fans": [], "battery": None}
if temps:
for chip, entries in temps.items():
for e in entries[:2]:
data["temps"].append(f"{e.label or chip}: {e.current}°C")
if fans:
for chip, entries in fans.items():
for e in entries[:2]:
data["fans"].append(f"{e.label or chip}: {e.current} RPM")
if battery:
rem = ""
if battery.secsleft != psutil.POWER_TIME_UNLIMITED and battery.secsleft > 0:
h = battery.secsleft // 3600
m = (battery.secsleft % 3600) // 60
rem = f" ({h}h {m}m left)"
plugged = " 🔌" if battery.power_plugged else ""
data["battery"] = f"{battery.percent}%{plugged}{rem}"
return data
# -------------------------------------------------------------------
# Main builder
# -------------------------------------------------------------------
async def get_system_info(room, bot):
await bot.api.send_text_message(room.room_id, "🔍 Gathering system information...")
system = await _run_blocking(_system_overview)
cpu = await _run_blocking(_cpu_info)
mem = await _run_blocking(_memory_info)
disks, io_read, io_write = await _run_blocking(_disk_info)
net = await _run_blocking(_network_info)
top_procs, total_procs = await _run_blocking(_top_processes)
gpu = await _run_blocking(_gpu_info)
docker = await _run_blocking(_docker_info)
sensors = await _run_blocking(_sensor_info)
sections = []
# System Overview
sys_rows = [
("💻", "Hostname", system["hostname"]),
("🖥️", "OS", system["os"]),
("📐", "Architecture", system["architecture"]),
("⚙️", "Machine", system["machine"]),
("🔧", "Processor", system["processor"]),
("", "Uptime", system["uptime"]),
("📅", "Boot Time", system["boot_time"]),
("👥", "Users", str(system["users"]))
]
sections.append({"title": "🖥️ System Overview", "rows": sys_rows})
# CPU
cpu_rows = [
("", "CPU Cores", f"{cpu['physical_cores']} physical, {cpu['logical_cores']} logical"),
("📈", "Freq (Max/Cur)", f"{cpu['max_freq']} / {cpu['current_freq']}"),
("📊", "CPU Usage", cpu["usage"]),
("⚖️", "Load Avg", cpu["load_avg"])
]
sections.append({"title": "⚡ CPU", "rows": cpu_rows})
# Memory
mem_rows = [
("🧠", "RAM", f"{mem['used_ram']} / {mem['total_ram']} ({mem['ram_percent']})")
]
if mem["total_swap"] != "N/A":
mem_rows.append(("💾", "Swap", f"{mem['used_swap']} / {mem['total_swap']} ({mem['swap_percent']})"))
sections.append({"title": "🧠 Memory", "rows": mem_rows})
# Storage
disk_rows = []
for d in disks[:5]:
disk_rows.append(("💽", d['mount'], f"{d['used']} / {d['total']} ({d['percent']}%)"))
disk_rows.append(("📀", "Disk I/O", f"Read {io_read} / Write {io_write}"))
sections.append({"title": "💾 Storage", "rows": disk_rows})
# Network
net_rows = []
if net:
for idx, (name, ip, sent, recv) in enumerate(net[:3]):
emoji = "🌐" if idx == 0 else ""
label = "Network" if idx == 0 else ""
net_rows.append((emoji, label, f"{name} - {ip} | ↓{recv}{sent}"))
else:
net_rows.append(("🌐", "Network", "No active interfaces"))
sections.append({"title": "🌐 Network", "rows": net_rows})
# GPU
gpu_rows = []
if "nvidia" in gpu:
for g in gpu["nvidia"]:
gpu_rows.append(("🎮", "GPU", f"{g['name']} | {g['mem_used']}/{g['mem_total']} | {g['temp']} | {g['usage']} util"))
elif "detected" in gpu:
for line in gpu["detected"]:
gpu_rows.append(("🎮", "GPU", line))
else:
gpu_rows.append(("🎮", "GPU", "No dedicated GPU detected"))
sections.append({"title": "🎮 GPU", "rows": gpu_rows})
# Processes
proc_rows = [("🔄", "Processes", f"Total: {total_procs}")]
for p in top_procs:
name = p.get('name', '?')
cpu_p = p.get('cpu_percent') or 0
mem_p = p.get('memory_percent') or 0
proc_rows.append(("", "", f"{name} - CPU {cpu_p:.1f}% / RAM {mem_p:.1f}%"))
sections.append({"title": "🔄 Top Processes", "rows": proc_rows})
# Docker
docker_rows = []
if docker is not None:
if docker:
for c in docker[:5]:
docker_rows.append(("🐳", "Docker", f"{c['name']} - {c['status']}"))
else:
docker_rows.append(("🐳", "Docker", "No containers running"))
else:
docker_rows.append(("🐳", "Docker", "Docker not available"))
sections.append({"title": "🐳 Docker", "rows": docker_rows})
# Sensors
sensor_rows = []
if sensors["temps"]:
sensor_rows.append(("🌡️", "Temperature", ", ".join(sensors["temps"])))
if sensors["fans"]:
sensor_rows.append(("🌀", "Fans", ", ".join(sensors["fans"])))
if sensors["battery"]:
sensor_rows.append(("🔋", "Battery", sensors["battery"]))
if sensor_rows:
sections.append({"title": "🌡️ Sensors", "rows": sensor_rows})
block = code_block(f"💻 System Info: {system['hostname']}", sections)
output = collapsible_summary(f"💻 System Info {html_escape(system['hostname'])}", block)
await bot.api.send_markdown_message(room.room_id, output)
logging.info("Sent system information")
async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("sysinfo"):
if match.args() and match.args()[0].lower() == 'help':
usage = """
<strong>💻 System Information</strong>
<code>!sysinfo</code> display comprehensive system info in a clean code block.
"""
await bot.api.send_markdown_message(room.room_id, usage)
return
await get_system_info(room, bot)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.3.1"
__author__ = "Funguy Bot"
__description__ = "System information plugin"
__help__ = """
<details>
<summary><strong>!sysinfo</strong> System information</summary>
<p>Displays CPU, RAM, storage, network, GPU, sensors, top processes, and more in a clean, aligned code block.</p>
</details>
"""