385 lines
10 KiB
TypeScript
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 [];
|
|
}
|
|
}
|
|
}
|