314 lines
12 KiB
Python
314 lines
12 KiB
Python
"""
|
||
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>
|
||
"""
|