Files
eco_os/ecoos_daemon/ts/daemon/system-info.ts
2026-01-08 18:33:14 +00:00

385 lines
10 KiB
TypeScript

/**
* 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<SystemInfoData> {
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<string> {
try {
const result = await runCommand('hostname', []);
if (!result.success) return 'unknown';
return result.stdout.trim();
} catch {
return 'unknown';
}
}
private async getCpuInfo(): Promise<CpuInfo> {
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<MemoryInfo> {
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<DiskInfo[]> {
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<NetworkInterface[]> {
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<GpuInfo[]> {
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<number> {
try {
const uptime = await Deno.readTextFile('/proc/uptime');
return Math.floor(parseFloat(uptime.split(' ')[0]));
} catch {
return 0;
}
}
private async getInputDevices(): Promise<InputDevice[]> {
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<AudioDevice[]> {
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<AudioDevice[]> {
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<AudioDevice[]> {
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<AudioDevice[]> {
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 [];
}
}
}