""" 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.

"""