"""
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 = """
💻 System Information Plugin!sysinfo - Display comprehensive system information
!sysinfo help - Show this help message
Information Provided:
• 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 = "💻 System Information
"
# System Overview
body += "🖥️ System Overview "
body += f" • Hostname: {hostname} "
body += f" • OS: {html_escape(system['os'])} {html_escape(system['os_release'])} "
body += f" • Architecture: {html_escape(system['architecture'])} "
body += f" • Uptime: {html_escape(system['uptime'])} "
body += f" • Boot Time: {html_escape(system['boot_time'])} "
body += f" • Users: {system['users']}
"
# CPU
body += "⚡ CPU Information "
body += f" • Cores: {cpu['physical_cores']} physical, {cpu['total_cores']} logical "
body += f" • Frequency: {html_escape(cpu['current_frequency'])} (max: {html_escape(cpu['max_frequency'])}) "
body += f" • Usage: {cpu['usage_percent']}% "
body += f" • Load Average: {html_escape(cpu['load_avg'])}
"
# Memory
body += "🧠 Memory Information "
body += f" • Total: {html_escape(memory['total'])} "
body += f" • Used: {html_escape(memory['used'])} ({memory['usage_percent']}%) "
body += f" • Available: {html_escape(memory['available'])} "
body += f" • Swap: {html_escape(memory['swap_used'])} / {html_escape(memory['swap_total'])} ({memory['swap_percent']}%)
"
# Storage
if storage and 'error' not in storage:
body += "💾 Storage Information "
for p in storage['partitions'][:3]:
body += f" • {html_escape(p['device'])}: {p['used']} / {p['total']} ({p['percent']}%) "
# IO stats if wanted
io = storage.get('io_stats')
if io:
body += f" • Disk I/O: read {io['read_bytes']}, write {io['write_bytes']} "
body += " "
# GPU
if gpu:
if 'nvidia' in gpu:
body += "🎮 GPU Information (NVIDIA) "
for g in gpu['nvidia']:
body += f" • {html_escape(g['name'])}: {g['utilization']} usage, {g['temperature']} "
body += " "
elif 'detected' in gpu:
body += "🎮 GPU Information "
for line in gpu['detected'][:2]:
body += f" • {html_escape(line)} "
body += " "
# Network
if network:
body += "🌐 Network Information "
for iface in network[:2]:
body += f" • {html_escape(iface['interface'])}: {html_escape(iface['ipv4'])} "
body += " "
# Top Processes
if processes:
body += "🔄 Top Processes (by CPU) "
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" • {name}: {cpu_p:.1f}% CPU, {mem_p:.1f}% RAM "
body += f" • Total Processes: {processes['total_processes']}
"
# Docker
if docker and docker.get('available'):
body += "🐳 Docker Containers "
for c in docker['containers'][:3]:
body += f" • {html_escape(c['name'])}: {html_escape(c['status'])} "
body += f" • Total Running: {docker['total_running']}
"
# Sensors
if sensors and 'error' not in sensors:
if sensors.get('temperatures'):
body += "🌡️ Temperature Sensors "
for sensor, temps in list(sensors['temperatures'].items())[:2]:
body += f" • {html_escape(sensor)}: {', '.join(temps[:2])} "
body += " "
if sensors.get('battery'):
bat = sensors['battery']
body += "🔋 Battery Information "
body += f" • Charge: {bat['percent']}% "
body += f" • Plugged In: {'Yes' if bat['power_plugged'] else 'No'} "
if bat.get('time_left'):
body += f" • Time Left: {bat['time_left']} "
body += " "
# Timestamp
body += f"Last updated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
return collapsible_summary(f"💻 System Information - {hostname}", body)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "Comprehensive system information and monitoring"
__help__ = """
!sysinfo – System information
Displays CPU, RAM, storage, network, Docker, GPU, sensors, and top processes.