refactor: async I/O, input sanitisation, and shared utilities cleanup

This commit is contained in:
2026-05-08 22:59:31 -05:00
parent 52a9621d50
commit f822d6a450
21 changed files with 1351 additions and 2709 deletions
+267 -368
View File
@@ -1,41 +1,29 @@
"""
This plugin provides comprehensive system information and resource monitoring.
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 simplematrixbotlib as botlib
import subprocess
import sys
import simplematrixbotlib as botlib
from plugins.common import collapsible_summary, html_escape
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle !sysinfo command for system information.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
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"):
logging.info("Received !sysinfo command")
args = match.args()
if len(args) > 0 and args[0].lower() == 'help':
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):
@@ -57,396 +45,307 @@ async def show_usage(room, bot):
"""
await bot.api.send_markdown_message(room.room_id, usage)
async def get_system_info(room, bot):
"""Collect and display comprehensive system information."""
try:
await bot.api.send_text_message(room.room_id, "🔍 Gathering system information...")
# ----- 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))
sysinfo = {
'system': await get_system_info_basic(),
'cpu': await get_cpu_info(),
'memory': await get_memory_info(),
'storage': await get_storage_info(),
'network': await get_network_info(),
'processes': await get_process_info(),
'docker': await get_docker_info(),
'sensors': await get_sensor_info(),
'gpu': await get_gpu_info()
# ----- 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
output = await format_system_info(sysinfo)
await bot.api.send_markdown_message(room.room_id, output)
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}
logging.info("Sent system information")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error gathering system info: {str(e)}")
logging.error(f"Error in get_system_info: {e}")
async def get_system_info_basic():
"""Get basic system information."""
def _docker_info():
try:
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=psutil.boot_time() - datetime.datetime.now().timestamp())).split('.')[0],
'users': len(psutil.users())
}
except Exception as e:
return {'error': str(e)}
async def get_cpu_info():
"""Get CPU information and usage."""
try:
cpu_times = psutil.cpu_times_percent(interval=1)
cpu_freq = psutil.cpu_freq()
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': os.getloadavg() if hasattr(os, 'getloadavg') else "N/A"
}
except Exception as e:
return {'error': str(e)}
async def get_memory_info():
"""Get memory and swap information."""
try:
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
return {
'total': f"{memory.total / (1024**3):.2f} GB",
'available': f"{memory.available / (1024**3):.2f} GB",
'used': f"{memory.used / (1024**3):.2f} GB",
'usage_percent': memory.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
}
except Exception as e:
return {'error': str(e)}
async def get_storage_info():
"""Get storage device information."""
try:
partitions = psutil.disk_partitions()
storage_info = []
for partition in partitions:
try:
usage = psutil.disk_usage(partition.mountpoint)
storage_info.append({
'device': partition.device,
'mountpoint': partition.mountpoint,
'fstype': partition.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:
continue
# Get disk I/O statistics
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_info,
'io_stats': io_info
}
except Exception as e:
return {'error': str(e)}
async def get_network_info():
"""Get network interface information."""
try:
interfaces = psutil.net_if_addrs()
io_counters = psutil.net_io_counters(pernic=True)
network_info = []
for interface, addrs in interfaces.items():
if interface not in ['lo']: # Skip loopback
interface_io = io_counters.get(interface, None)
network_info.append({
'interface': interface,
'ipv4': next((addr.address for addr in addrs if addr.family == socket.AF_INET), 'N/A'),
'ipv6': next((addr.address for addr in addrs if addr.family == socket.AF_INET6), 'N/A'),
'mac': next((addr.address for addr in addrs if addr.family == psutil.AF_LINK), 'N/A'),
'bytes_sent': f"{interface_io.bytes_sent / (1024**2):.2f} MB" if interface_io else "N/A",
'bytes_recv': f"{interface_io.bytes_recv / (1024**2):.2f} MB" if interface_io else "N/A"
})
return network_info
except Exception as e:
return {'error': str(e)}
async def get_process_info():
"""Get process and system load information."""
try:
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
try:
processes.append(proc.info)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# Sort by CPU usage and get top 5
top_processes = sorted(processes, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:5]
return {
'total_processes': len(processes),
'top_cpu': top_processes
}
except Exception as e:
return {'error': str(e)}
async def get_docker_info():
"""Get Docker container information if available."""
try:
# Check if docker is available
result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
if result.returncode != 0:
return {'available': False}
# Get running containers
result = subprocess.run(['docker', 'ps', '--format', '{{.Names}}|{{.Status}}|{{.Ports}}'],
capture_output=True, text=True)
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({
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],
'status': parts[1],
'ports': parts[2] if len(parts) > 2 else 'N/A'
'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]}%"
})
return {
'available': True,
'containers': containers,
'total_running': len(containers)
}
except Exception as e:
return {'available': False, 'error': str(e)}
async def get_sensor_info():
"""Get hardware sensor information."""
if nvidia:
gpu_data['nvidia'] = nvidia
except:
pass
# lspci fallback
try:
temperatures = psutil.sensors_temperatures()
fans = psutil.sensors_fans()
battery = psutil.sensors_battery()
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
sensor_info = {
'temperatures': {},
'fans': {},
'battery': {}
}
# ----- Main info gatherer -----
async def get_system_info(room, bot):
await bot.api.send_text_message(room.room_id, "🔍 Gathering system information...")
# Temperature sensors
if temperatures:
for name, entries in temperatures.items():
sensor_info['temperatures'][name] = [
f"{entry.current}°C" for entry in entries[:2] # Show first 2 sensors per type
]
# 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)
# Fan speeds
if fans:
for name, entries in fans.items():
sensor_info['fans'][name] = [
f"{entry.current} RPM" for entry in entries[:2]
]
# 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")
# Battery information
if battery:
sensor_info['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_info
except Exception as e:
return {'error': str(e)}
async def get_gpu_info():
"""Get GPU information using various methods."""
try:
gpu_info = {}
# Try nvidia-smi first
try:
result = 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 result.returncode == 0:
nvidia_gpus = []
for line in result.stdout.strip().split('\n'):
if line:
parts = [part.strip() for part in line.split(',')]
if len(parts) >= 6:
nvidia_gpus.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]}%"
})
gpu_info['nvidia'] = nvidia_gpus
except:
pass
# Try lspci for generic GPU detection
try:
result = subprocess.run(['lspci'], capture_output=True, text=True)
if result.returncode == 0:
gpu_lines = [line for line in result.stdout.split('\n') if 'VGA' in line or '3D' in line]
gpu_info['detected'] = gpu_lines[:3] # Show first 3 GPUs
except:
pass
return gpu_info
except Exception as e:
return {'error': str(e)}
async def format_system_info(sysinfo):
"""Format system information for display."""
output = "<strong>💻 System Information</strong><br><br>"
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
system = sysinfo.get('system', {})
output += "<strong>🖥️ System Overview</strong><br>"
output += f" • <strong>Hostname:</strong> {system.get('hostname', 'N/A')}<br>"
output += f" • <strong>OS:</strong> {system.get('os', 'N/A')} {system.get('os_release', '')}<br>"
output += f" • <strong>Architecture:</strong> {system.get('architecture', 'N/A')}<br>"
output += f" • <strong>Uptime:</strong> {system.get('uptime', 'N/A')}<br>"
output += f" • <strong>Boot Time:</strong> {system.get('boot_time', 'N/A')}<br>"
output += f" • <strong>Users:</strong> {system.get('users', 'N/A')}<br>"
output += "<br>"
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 Information
cpu = sysinfo.get('cpu', {})
if 'error' not in cpu:
output += "<strong>⚡ CPU Information</strong><br>"
output += f" • <strong>Cores:</strong> {cpu.get('physical_cores', 'N/A')} physical, {cpu.get('total_cores', 'N/A')} logical<br>"
output += f" • <strong>Frequency:</strong> {cpu.get('current_frequency', 'N/A')} (max: {cpu.get('max_frequency', 'N/A')})<br>"
output += f" • <strong>Usage:</strong> {cpu.get('usage_percent', 'N/A')}%<br>"
if cpu.get('load_avg') != "N/A":
output += f" • <strong>Load Average:</strong> {', '.join([f'{load:.2f}' for load in cpu.get('load_avg', [0,0,0])])}<br>"
output += "<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 Information
memory = sysinfo.get('memory', {})
if 'error' not in memory:
output += "<strong>🧠 Memory Information</strong><br>"
output += f" • <strong>Total:</strong> {memory.get('total', 'N/A')}<br>"
output += f" • <strong>Used:</strong> {memory.get('used', 'N/A')} ({memory.get('usage_percent', 'N/A')}%)<br>"
output += f" • <strong>Available:</strong> {memory.get('available', 'N/A')}<br>"
output += f" • <strong>Swap:</strong> {memory.get('swap_used', 'N/A')} / {memory.get('swap_total', 'N/A')} ({memory.get('swap_percent', 'N/A')}%)<br>"
output += "<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 Information
storage = sysinfo.get('storage', {})
if 'error' not in storage:
output += "<strong>💾 Storage Information</strong><br>"
partitions = storage.get('partitions', [])
for partition in partitions[:3]: # Show first 3 partitions
output += f" • <strong>{partition.get('device', 'N/A')}:</strong> {partition.get('used', 'N/A')} / {partition.get('total', 'N/A')} ({partition.get('percent', 'N/A')}%)<br>"
output += "<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 Information
gpu = sysinfo.get('gpu', {})
if gpu.get('nvidia'):
output += "<strong>🎮 GPU Information (NVIDIA)</strong><br>"
for gpu_info in gpu['nvidia']:
output += f" • <strong>{gpu_info.get('name', 'N/A')}:</strong> {gpu_info.get('utilization', 'N/A')} usage, {gpu_info.get('temperature', 'N/A')}<br>"
output += "<br>"
elif gpu.get('detected'):
output += "<strong>🎮 GPU Information</strong><br>"
for gpu_line in gpu['detected'][:2]:
output += f"{gpu_line}<br>"
output += "<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 Information
network = sysinfo.get('network', [])
if network and 'error' not in network:
output += "<strong>🌐 Network Information</strong><br>"
for interface in network[:2]: # Show first 2 interfaces
output += f" • <strong>{interface.get('interface', 'N/A')}:</strong> {interface.get('ipv4', 'N/A')}<br>"
output += "<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>"
# Process Information
processes = sysinfo.get('processes', {})
if 'error' not in processes:
output += "<strong>🔄 Top Processes (by CPU)</strong><br>"
for proc in processes.get('top_cpu', [])[:3]:
output += f" • <strong>{proc.get('name', 'N/A')}:</strong> {proc.get('cpu_percent', 0):.1f}% CPU, {proc.get('memory_percent', 0):.1f}% RAM<br>"
output += f" • <strong>Total Processes:</strong> {processes.get('total_processes', 'N/A')}<br>"
output += "<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 Information
docker = sysinfo.get('docker', {})
if docker.get('available'):
output += "<strong>🐳 Docker Containers</strong><br>"
for container in docker.get('containers', [])[:3]:
output += f" • <strong>{container.get('name', 'N/A')}:</strong> {container.get('status', 'N/A')}<br>"
output += f" • <strong>Total Running:</strong> {docker.get('total_running', 'N/A')}<br>"
output += "<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>"
# Sensor Information
sensors = sysinfo.get('sensors', {})
if 'error' not in sensors:
# Sensors
if sensors and 'error' not in sensors:
if sensors.get('temperatures'):
output += "<strong>🌡️ Temperature Sensors</strong><br>"
body += "<strong>🌡️ Temperature Sensors</strong><br>"
for sensor, temps in list(sensors['temperatures'].items())[:2]:
output += f" • <strong>{sensor}:</strong> {', '.join(temps[:2])}<br>"
output += "<br>"
body += f" • <strong>{html_escape(sensor)}:</strong> {', '.join(temps[:2])}<br>"
body += "<br>"
if sensors.get('battery'):
battery = sensors['battery']
output += "<strong>🔋 Battery Information</strong><br>"
output += f" • <strong>Charge:</strong> {battery.get('percent', 'N/A')}%<br>"
output += f" • <strong>Plugged In:</strong> {'Yes' if battery.get('power_plugged') else 'No'}<br>"
if battery.get('time_left'):
output += f" • <strong>Time Left:</strong> {battery.get('time_left', 'N/A')}<br>"
output += "<br>"
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>"
# Add timestamp
output += f"<em>Last updated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em>"
# Timestamp
body += f"<em>Last updated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em>"
# Wrap in collapsible due to comprehensive output
output = f"<details><summary><strong>💻 System Information - {system.get('hostname', 'Unknown')}</strong></summary>{output}</details>"
return output
return collapsible_summary(f"💻 System Information - {hostname}", body)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__version__ = "1.0.1"
__author__ = "Funguy Bot"
__description__ = "System information and monitoring"
__description__ = "Comprehensive system information and monitoring"
__help__ = """
<details>
<summary><strong>!sysinfo</strong> System information</summary>