355 lines
15 KiB
Python
355 lines
15 KiB
Python
"""
|
||
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>
|
||
"""
|