/** * System Information * * Gathers CPU, RAM, disk, network, and GPU information */ import { runCommand } from '../utils/command.ts'; export interface CpuInfo { model: string; cores: number; usage: number; } export interface MemoryInfo { total: number; used: number; free: number; usagePercent: number; } export interface DiskInfo { device: string; mountpoint: string; total: number; used: number; free: number; usagePercent: number; } export interface NetworkInterface { name: string; ip: string; mac: string; state: 'up' | 'down'; } export interface GpuInfo { name: string; driver: string; } export interface InputDevice { name: string; type: 'keyboard' | 'mouse' | 'touchpad' | 'other'; path: string; } export interface AudioDevice { name: string; description: string; isDefault: boolean; } export interface SystemInfoData { hostname: string; cpu: CpuInfo; memory: MemoryInfo; disks: DiskInfo[]; network: NetworkInterface[]; gpu: GpuInfo[]; uptime: number; inputDevices: InputDevice[]; speakers: AudioDevice[]; microphones: AudioDevice[]; } export class SystemInfo { async getInfo(): Promise { const [hostname, cpu, memory, disks, network, gpu, uptime, inputDevices, speakers, microphones] = await Promise.all([ this.getHostname(), this.getCpuInfo(), this.getMemoryInfo(), this.getDiskInfo(), this.getNetworkInfo(), this.getGpuInfo(), this.getUptime(), this.getInputDevices(), this.getSpeakers(), this.getMicrophones(), ]); return { hostname, cpu, memory, disks, network, gpu, uptime, inputDevices, speakers, microphones }; } private async getHostname(): Promise { try { const result = await runCommand('hostname', []); if (!result.success) return 'unknown'; return result.stdout.trim(); } catch { return 'unknown'; } } private async getCpuInfo(): Promise { try { const cpuinfo = await Deno.readTextFile('/proc/cpuinfo'); const modelMatch = cpuinfo.match(/model name\s*:\s*(.+)/); const coreMatches = cpuinfo.match(/processor\s*:/g); // Get CPU usage from /proc/stat const stat = await Deno.readTextFile('/proc/stat'); const cpuLine = stat.split('\n')[0]; const values = cpuLine.split(/\s+/).slice(1).map(Number); const total = values.reduce((a, b) => a + b, 0); const idle = values[3]; const usage = ((total - idle) / total) * 100; return { model: modelMatch ? modelMatch[1] : 'Unknown', cores: coreMatches ? coreMatches.length : 1, usage: Math.round(usage * 10) / 10, }; } catch { return { model: 'Unknown', cores: 1, usage: 0 }; } } private async getMemoryInfo(): Promise { try { const meminfo = await Deno.readTextFile('/proc/meminfo'); const totalMatch = meminfo.match(/MemTotal:\s*(\d+)/); const freeMatch = meminfo.match(/MemAvailable:\s*(\d+)/); const total = totalMatch ? parseInt(totalMatch[1], 10) * 1024 : 0; const free = freeMatch ? parseInt(freeMatch[1], 10) * 1024 : 0; const used = total - free; return { total, used, free, usagePercent: total > 0 ? Math.round((used / total) * 1000) / 10 : 0, }; } catch { return { total: 0, used: 0, free: 0, usagePercent: 0 }; } } private async getDiskInfo(): Promise { try { const result = await runCommand('df', ['-B1', '--output=source,target,size,used,avail']); if (!result.success) return []; const lines = result.stdout.trim().split('\n').slice(1); const disks: DiskInfo[] = []; for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length >= 5 && parts[0].startsWith('/dev/')) { const total = parseInt(parts[2], 10); const used = parseInt(parts[3], 10); const free = parseInt(parts[4], 10); disks.push({ device: parts[0], mountpoint: parts[1], total, used, free, usagePercent: total > 0 ? Math.round((used / total) * 1000) / 10 : 0, }); } } return disks; } catch { return []; } } private async getNetworkInfo(): Promise { try { const cmdResult = await runCommand('ip', ['-j', 'addr']); if (!cmdResult.success) return []; const interfaces = JSON.parse(cmdResult.stdout); const result: NetworkInterface[] = []; for (const iface of interfaces) { if (iface.ifname === 'lo') continue; let ip = ''; let mac = iface.address || ''; for (const addr of iface.addr_info || []) { if (addr.family === 'inet') { ip = addr.local; break; } } result.push({ name: iface.ifname, ip: ip || 'No IP', mac, state: iface.operstate === 'UP' ? 'up' : 'down', }); } return result; } catch { return []; } } private async getGpuInfo(): Promise { try { const result = await runCommand('lspci', ['-mm']); if (!result.success) return []; const lines = result.stdout.split('\n'); const gpus: GpuInfo[] = []; for (const line of lines) { if (line.includes('VGA') || line.includes('3D') || line.includes('Display')) { const parts = line.split('"'); if (parts.length >= 6) { gpus.push({ name: parts[5] || 'Unknown GPU', driver: 'unknown', }); } } } return gpus; } catch { return []; } } private async getUptime(): Promise { try { const uptime = await Deno.readTextFile('/proc/uptime'); return Math.floor(parseFloat(uptime.split(' ')[0])); } catch { return 0; } } private async getInputDevices(): Promise { try { const content = await Deno.readTextFile('/proc/bus/input/devices'); const devices: InputDevice[] = []; const blocks = content.split('\n\n'); for (const block of blocks) { if (!block.trim()) continue; const nameMatch = block.match(/N: Name="(.+)"/); const handlersMatch = block.match(/H: Handlers=(.+)/); if (nameMatch && handlersMatch) { const name = nameMatch[1]; const handlers = handlersMatch[1]; // Find event device path const eventMatch = handlers.match(/event\d+/); const path = eventMatch ? `/dev/input/${eventMatch[0]}` : ''; // Determine device type let type: 'keyboard' | 'mouse' | 'touchpad' | 'other' = 'other'; const nameLower = name.toLowerCase(); if (handlers.includes('kbd') || nameLower.includes('keyboard')) { type = 'keyboard'; } else if (nameLower.includes('touchpad') || nameLower.includes('trackpad')) { type = 'touchpad'; } else if (handlers.includes('mouse') || nameLower.includes('mouse')) { type = 'mouse'; } // Filter out system/virtual devices if (!nameLower.includes('power button') && !nameLower.includes('sleep button') && !nameLower.includes('pc speaker') && path) { devices.push({ name, type, path }); } } } return devices; } catch { return []; } } private async getSpeakers(): Promise { try { const result = await runCommand('pactl', ['-f', 'json', 'list', 'sinks']); if (!result.success) { // Fallback to aplay if pactl not available return this.getSpeakersFromAplay(); } const sinks = JSON.parse(result.stdout); const defaultResult = await runCommand('pactl', ['get-default-sink']); const defaultSink = defaultResult.success ? defaultResult.stdout.trim() : ''; return sinks.map((sink: { name: string; description: string }) => ({ name: sink.name, description: sink.description || sink.name, isDefault: sink.name === defaultSink, })); } catch { return this.getSpeakersFromAplay(); } } private async getSpeakersFromAplay(): Promise { try { const result = await runCommand('aplay', ['-l']); if (!result.success) return []; const devices: AudioDevice[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { const match = line.match(/card (\d+): (.+?) \[(.+?)\], device (\d+): (.+?) \[(.+?)\]/); if (match) { devices.push({ name: `hw:${match[1]},${match[4]}`, description: `${match[3]} - ${match[6]}`, isDefault: devices.length === 0, }); } } return devices; } catch { return []; } } private async getMicrophones(): Promise { try { const result = await runCommand('pactl', ['-f', 'json', 'list', 'sources']); if (!result.success) { // Fallback to arecord if pactl not available return this.getMicrophonesFromArecord(); } const sources = JSON.parse(result.stdout); const defaultResult = await runCommand('pactl', ['get-default-source']); const defaultSource = defaultResult.success ? defaultResult.stdout.trim() : ''; // Filter out monitor sources (they echo speaker output) return sources .filter((source: { name: string }) => !source.name.includes('.monitor')) .map((source: { name: string; description: string }) => ({ name: source.name, description: source.description || source.name, isDefault: source.name === defaultSource, })); } catch { return this.getMicrophonesFromArecord(); } } private async getMicrophonesFromArecord(): Promise { try { const result = await runCommand('arecord', ['-l']); if (!result.success) return []; const devices: AudioDevice[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { const match = line.match(/card (\d+): (.+?) \[(.+?)\], device (\d+): (.+?) \[(.+?)\]/); if (match) { devices.push({ name: `hw:${match[1]},${match[4]}`, description: `${match[3]} - ${match[6]}`, isDefault: devices.length === 0, }); } } return devices; } catch { return []; } } }