Files
FunguyBot/plugins/sysinfo.py
T

355 lines
15 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 and resource monitoring.
All blocking calls (psutil, subprocess) run in a thread pool.
"""
import logging
import platform
import os
import asyncio
import psutil
import socket
import datetime
import subprocess
import simplematrixbotlib as botlib
from plugins.common import collapsible_summary, html_escape
async def handle_command(room, message, bot, prefix, config):
"""
Handle !sysinfo command for system information.
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("sysinfo"):
args = match.args()
if args and args[0].lower() == 'help':
await show_usage(room, bot)
return
await get_system_info(room, bot)
async def show_usage(room, bot):
"""Display sysinfo command usage."""
usage = """
<strong>💻 System Information Plugin</strong>
<strong>!sysinfo</strong> - Display comprehensive system information
<strong>!sysinfo help</strong> - Show this help message
<strong>Information Provided:</strong>
• System hardware (CPU, RAM, storage, GPU)
• Operating system and kernel details
• Network configuration and interfaces
• Running processes and resource usage
• Temperature and hardware sensors
• System load and performance metrics
• Docker container status (if available)
"""
await bot.api.send_markdown_message(room.room_id, usage)
# ----- Async wrappers for blocking functions -----
async def _run_blocking(func, *args, **kwargs):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
# ----- Individual data collectors (all sync, run in thread) -----
def _system_overview():
return {
'hostname': socket.gethostname(),
'os': platform.system(),
'os_release': platform.release(),
'os_version': platform.version(),
'architecture': platform.architecture()[0],
'machine': platform.machine(),
'processor': platform.processor(),
'boot_time': datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S"),
'uptime': str(datetime.timedelta(seconds=int((datetime.datetime.now() - datetime.datetime.fromtimestamp(psutil.boot_time())).total_seconds()))),
'users': len(psutil.users())
}
def _cpu_info():
cpu_times = psutil.cpu_times_percent(interval=1)
cpu_freq = psutil.cpu_freq()
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else (0,0,0)
return {
'physical_cores': psutil.cpu_count(logical=False),
'total_cores': psutil.cpu_count(logical=True),
'max_frequency': f"{cpu_freq.max:.1f} MHz" if cpu_freq else "N/A",
'current_frequency': f"{cpu_freq.current:.1f} MHz" if cpu_freq else "N/A",
'usage_percent': psutil.cpu_percent(interval=1),
'user_time': cpu_times.user,
'system_time': cpu_times.system,
'idle_time': cpu_times.idle,
'load_avg': ", ".join(f"{l:.2f}" for l in load_avg)
}
def _memory_info():
mem = psutil.virtual_memory()
swap = psutil.swap_memory()
return {
'total': f"{mem.total / (1024**3):.2f} GB",
'available': f"{mem.available / (1024**3):.2f} GB",
'used': f"{mem.used / (1024**3):.2f} GB",
'usage_percent': mem.percent,
'swap_total': f"{swap.total / (1024**3):.2f} GB",
'swap_used': f"{swap.used / (1024**3):.2f} GB",
'swap_free': f"{swap.free / (1024**3):.2f} GB",
'swap_percent': swap.percent
}
def _storage_info():
partitions = psutil.disk_partitions()
storage_list = []
for part in partitions:
try:
usage = psutil.disk_usage(part.mountpoint)
storage_list.append({
'device': part.device,
'mountpoint': part.mountpoint,
'fstype': part.fstype,
'total': f"{usage.total / (1024**3):.2f} GB",
'used': f"{usage.used / (1024**3):.2f} GB",
'free': f"{usage.free / (1024**3):.2f} GB",
'percent': usage.percent
})
except:
pass
disk_io = psutil.disk_io_counters()
io_info = {
'read_count': disk_io.read_count if disk_io else 0,
'write_count': disk_io.write_count if disk_io else 0,
'read_bytes': f"{disk_io.read_bytes / (1024**3):.2f} GB" if disk_io else "0 GB",
'write_bytes': f"{disk_io.write_bytes / (1024**3):.2f} GB" if disk_io else "0 GB"
}
return {'partitions': storage_list, 'io_stats': io_info}
def _network_info():
interfaces = psutil.net_if_addrs()
io_counters = psutil.net_io_counters(pernic=True)
net_list = []
for iface, addrs in interfaces.items():
if iface == 'lo':
continue
info = {
'interface': iface,
'ipv4': next((a.address for a in addrs if a.family == socket.AF_INET), 'N/A'),
'ipv6': next((a.address for a in addrs if a.family == socket.AF_INET6), 'N/A'),
'mac': next((a.address for a in addrs if a.family == psutil.AF_LINK), 'N/A'),
}
io = io_counters.get(iface)
if io:
info['bytes_sent'] = f"{io.bytes_sent / (1024**2):.2f} MB"
info['bytes_recv'] = f"{io.bytes_recv / (1024**2):.2f} MB"
else:
info['bytes_sent'] = 'N/A'
info['bytes_recv'] = 'N/A'
net_list.append(info)
return net_list
def _process_info():
procs = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
try:
procs.append(proc.info)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
top_cpu = sorted(procs, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:5]
return {'total_processes': len(procs), 'top_cpu': top_cpu}
def _docker_info():
try:
result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
if result.returncode != 0:
return {'available': False}
result = subprocess.run(['docker', 'ps', '--format', '{{.Names}}|{{.Status}}|{{.Ports}}'],
capture_output=True, text=True)
containers = []
for line in result.stdout.strip().split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 2:
containers.append({'name': parts[0], 'status': parts[1], 'ports': parts[2] if len(parts)>2 else 'N/A'})
return {'available': True, 'containers': containers, 'total_running': len(containers)}
except:
return {'available': False}
def _sensor_info():
temps = psutil.sensors_temperatures()
fans = psutil.sensors_fans()
battery = psutil.sensors_battery()
sensor = {'temperatures': {}, 'fans': {}, 'battery': {}}
if temps:
for name, entries in temps.items():
sensor['temperatures'][name] = [f"{e.current}°C" for e in entries[:2]]
if fans:
for name, entries in fans.items():
sensor['fans'][name] = [f"{e.current} RPM" for e in entries[:2]]
if battery:
sensor['battery'] = {
'percent': battery.percent,
'power_plugged': battery.power_plugged,
'time_left': f"{battery.secsleft // 3600}h {(battery.secsleft % 3600) // 60}m" if battery.secsleft != psutil.POWER_TIME_UNLIMITED else "Unknown"
}
return sensor
def _gpu_info():
gpu_data = {}
# NVIDIA
try:
res = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,memory.free,temperature.gpu,utilization.gpu',
'--format=csv,noheader,nounits'], capture_output=True, text=True)
if res.returncode == 0:
nvidia = []
for line in res.stdout.strip().split('\n'):
parts = [p.strip() for p in line.split(',')]
if len(parts) >= 6:
nvidia.append({
'name': parts[0],
'memory_total': f"{parts[1]} MB",
'memory_used': f"{parts[2]} MB",
'memory_free': f"{parts[3]} MB",
'temperature': f"{parts[4]}°C",
'utilization': f"{parts[5]}%"
})
if nvidia:
gpu_data['nvidia'] = nvidia
except:
pass
# lspci fallback
try:
res = subprocess.run(['lspci'], capture_output=True, text=True)
if res.returncode == 0:
gpu_lines = [l for l in res.stdout.split('\n') if 'VGA' in l or '3D' in l]
if gpu_lines:
gpu_data['detected'] = gpu_lines[:3]
except:
pass
return gpu_data
# ----- Main info gatherer -----
async def get_system_info(room, bot):
await bot.api.send_text_message(room.room_id, "🔍 Gathering system information...")
# Run all blocking collectors concurrently
system = await _run_blocking(_system_overview)
cpu = await _run_blocking(_cpu_info)
memory = await _run_blocking(_memory_info)
storage = await _run_blocking(_storage_info)
network = await _run_blocking(_network_info)
processes = await _run_blocking(_process_info)
docker = await _run_blocking(_docker_info)
sensors = await _run_blocking(_sensor_info)
gpu = await _run_blocking(_gpu_info)
# Build output HTML
output = await format_system_info(system, cpu, memory, storage, network, processes, docker, sensors, gpu)
await bot.api.send_markdown_message(room.room_id, output)
logging.info("Sent system information")
async def format_system_info(system, cpu, memory, storage, network, processes, docker, sensors, gpu):
hostname = html_escape(system.get('hostname', 'Unknown'))
body = "<strong>💻 System Information</strong><br><br>"
# System Overview
body += "<strong>🖥️ System Overview</strong><br>"
body += f" • <strong>Hostname:</strong> {hostname}<br>"
body += f" • <strong>OS:</strong> {html_escape(system['os'])} {html_escape(system['os_release'])}<br>"
body += f" • <strong>Architecture:</strong> {html_escape(system['architecture'])}<br>"
body += f" • <strong>Uptime:</strong> {html_escape(system['uptime'])}<br>"
body += f" • <strong>Boot Time:</strong> {html_escape(system['boot_time'])}<br>"
body += f" • <strong>Users:</strong> {system['users']}<br><br>"
# CPU
body += "<strong>⚡ CPU Information</strong><br>"
body += f" • <strong>Cores:</strong> {cpu['physical_cores']} physical, {cpu['total_cores']} logical<br>"
body += f" • <strong>Frequency:</strong> {html_escape(cpu['current_frequency'])} (max: {html_escape(cpu['max_frequency'])})<br>"
body += f" • <strong>Usage:</strong> {cpu['usage_percent']}%<br>"
body += f" • <strong>Load Average:</strong> {html_escape(cpu['load_avg'])}<br><br>"
# Memory
body += "<strong>🧠 Memory Information</strong><br>"
body += f" • <strong>Total:</strong> {html_escape(memory['total'])}<br>"
body += f" • <strong>Used:</strong> {html_escape(memory['used'])} ({memory['usage_percent']}%)<br>"
body += f" • <strong>Available:</strong> {html_escape(memory['available'])}<br>"
body += f" • <strong>Swap:</strong> {html_escape(memory['swap_used'])} / {html_escape(memory['swap_total'])} ({memory['swap_percent']}%)<br><br>"
# Storage
if storage and 'error' not in storage:
body += "<strong>💾 Storage Information</strong><br>"
for p in storage['partitions'][:3]:
body += f" • <strong>{html_escape(p['device'])}:</strong> {p['used']} / {p['total']} ({p['percent']}%)<br>"
# IO stats if wanted
io = storage.get('io_stats')
if io:
body += f" • <strong>Disk I/O:</strong> read {io['read_bytes']}, write {io['write_bytes']}<br>"
body += "<br>"
# GPU
if gpu:
if 'nvidia' in gpu:
body += "<strong>🎮 GPU Information (NVIDIA)</strong><br>"
for g in gpu['nvidia']:
body += f" • <strong>{html_escape(g['name'])}:</strong> {g['utilization']} usage, {g['temperature']}<br>"
body += "<br>"
elif 'detected' in gpu:
body += "<strong>🎮 GPU Information</strong><br>"
for line in gpu['detected'][:2]:
body += f"{html_escape(line)}<br>"
body += "<br>"
# Network
if network:
body += "<strong>🌐 Network Information</strong><br>"
for iface in network[:2]:
body += f" • <strong>{html_escape(iface['interface'])}:</strong> {html_escape(iface['ipv4'])}<br>"
body += "<br>"
# Top Processes
if processes:
body += "<strong>🔄 Top Processes (by CPU)</strong><br>"
for proc in processes['top_cpu'][:3]:
name = html_escape(proc.get('name', 'N/A'))
cpu_p = proc.get('cpu_percent', 0) or 0
mem_p = proc.get('memory_percent', 0) or 0
body += f" • <strong>{name}:</strong> {cpu_p:.1f}% CPU, {mem_p:.1f}% RAM<br>"
body += f" • <strong>Total Processes:</strong> {processes['total_processes']}<br><br>"
# Docker
if docker and docker.get('available'):
body += "<strong>🐳 Docker Containers</strong><br>"
for c in docker['containers'][:3]:
body += f" • <strong>{html_escape(c['name'])}:</strong> {html_escape(c['status'])}<br>"
body += f" • <strong>Total Running:</strong> {docker['total_running']}<br><br>"
# Sensors
if sensors and 'error' not in sensors:
if sensors.get('temperatures'):
body += "<strong>🌡️ Temperature Sensors</strong><br>"
for sensor, temps in list(sensors['temperatures'].items())[:2]:
body += f" • <strong>{html_escape(sensor)}:</strong> {', '.join(temps[:2])}<br>"
body += "<br>"
if sensors.get('battery'):
bat = sensors['battery']
body += "<strong>🔋 Battery Information</strong><br>"
body += f" • <strong>Charge:</strong> {bat['percent']}%<br>"
body += f" • <strong>Plugged In:</strong> {'Yes' if bat['power_plugged'] else 'No'}<br>"
if bat.get('time_left'):
body += f" • <strong>Time Left:</strong> {bat['time_left']}<br>"
body += "<br>"
# Timestamp
body += f"<em>Last updated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em>"
return collapsible_summary(f"💻 System Information - {hostname}", body)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "Comprehensive system information and monitoring"
__help__ = """
<details>
<summary><strong>!sysinfo</strong> System information</summary>
<p>Displays CPU, RAM, storage, network, Docker, GPU, sensors, and top processes.</p>
</details>
"""