initial
This commit is contained in:
384
ecoos_daemon/ts/daemon/system-info.ts
Normal file
384
ecoos_daemon/ts/daemon/system-info.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user