[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
617
infrastructure/systemMonitor/systemMonitorAdapters.ts
Normal file
617
infrastructure/systemMonitor/systemMonitorAdapters.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
// System monitor adapter interface and implementations
|
||||
// These adapters parse command output into structured metrics data.
|
||||
// The actual command execution happens via IPC (netcatty:ssh:exec).
|
||||
|
||||
import type {
|
||||
CpuUsage,
|
||||
MemoryUsage,
|
||||
DiskUsage,
|
||||
NetworkTraffic,
|
||||
SystemLoad,
|
||||
ProcessInfo,
|
||||
DockerContainer,
|
||||
ListeningPort,
|
||||
SystemService,
|
||||
SystemMetricsSnapshot,
|
||||
} from '../../domain/systemMonitor';
|
||||
|
||||
/**
|
||||
* Adapter interface for collecting system metrics from a remote host.
|
||||
* Each OS/device type has its own implementation.
|
||||
*/
|
||||
export interface SystemMonitorAdapter {
|
||||
/** OS family this adapter handles */
|
||||
readonly osFamily: 'linux' | 'windows' | 'network';
|
||||
|
||||
/** Build a single command that collects all fast metrics (CPU, mem, load, uptime) */
|
||||
buildFastMetricsCommand(): string;
|
||||
|
||||
/** Parse the output of fast metrics command */
|
||||
parseFastMetrics(output: string): Partial<SystemMetricsSnapshot>;
|
||||
|
||||
/** Build command for disk usage */
|
||||
buildDiskCommand(): string;
|
||||
/** Parse disk usage output */
|
||||
parseDiskOutput(output: string): DiskUsage[];
|
||||
|
||||
/** Build command for network traffic (current snapshot) */
|
||||
buildNetworkCommand(): string;
|
||||
/** Parse network output, needs prev snapshot for speed calc */
|
||||
parseNetworkOutput(output: string, prev?: NetworkTraffic[], intervalMs?: number): NetworkTraffic[];
|
||||
|
||||
/** Build command for process list */
|
||||
buildProcessCommand(limit?: number): string;
|
||||
/** Parse process list output */
|
||||
parseProcessOutput(output: string): ProcessInfo[];
|
||||
|
||||
/** Build command for docker containers */
|
||||
buildDockerCommand(): string;
|
||||
/** Parse docker output */
|
||||
parseDockerOutput(output: string): DockerContainer[];
|
||||
|
||||
/** Build command for listening ports */
|
||||
buildPortsCommand(): string;
|
||||
/** Parse listening ports output */
|
||||
parsePortsOutput(output: string): ListeningPort[];
|
||||
|
||||
/** Build command for system services */
|
||||
buildServicesCommand(): string;
|
||||
/** Parse services output */
|
||||
parseServicesOutput(output: string): SystemService[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Linux adapter
|
||||
// ============================================================================
|
||||
|
||||
export class LinuxMonitorAdapter implements SystemMonitorAdapter {
|
||||
readonly osFamily = 'linux' as const;
|
||||
|
||||
buildFastMetricsCommand(): string {
|
||||
// Collect CPU, memory, load, uptime, hostname in one shot
|
||||
// Using /proc for reliable parsing, plus uptime and hostname
|
||||
return `cat /proc/stat | head -1; echo "---MEM---"; cat /proc/meminfo; echo "---LOAD---"; cat /proc/loadavg; echo "---UPTIME---"; cat /proc/uptime; echo "---HOST---"; hostname; echo "---OS---"; cat /etc/os-release 2>/dev/null | head -5 || uname -r`;
|
||||
}
|
||||
|
||||
parseFastMetrics(output: string): Partial<SystemMetricsSnapshot> {
|
||||
const result: Partial<SystemMetricsSnapshot> = {};
|
||||
const sections = output.split(/^---[A-Z]+---$/m);
|
||||
|
||||
// CPU from /proc/stat (first line)
|
||||
const cpuLine = sections[0]?.trim().split('\n')[0];
|
||||
if (cpuLine) {
|
||||
const parts = cpuLine.trim().split(/\s+/);
|
||||
// cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||
if (parts.length >= 5) {
|
||||
const user = parseInt(parts[1]) || 0;
|
||||
const nice = parseInt(parts[2]) || 0;
|
||||
const system = parseInt(parts[3]) || 0;
|
||||
const idle = parseInt(parts[4]) || 0;
|
||||
const iowait = parts[5] ? parseInt(parts[5]) : 0;
|
||||
const total = user + nice + system + idle + iowait +
|
||||
(parts[6] ? parseInt(parts[6]) : 0) +
|
||||
(parts[7] ? parseInt(parts[7]) : 0) +
|
||||
(parts[8] ? parseInt(parts[8]) : 0);
|
||||
// We can't compute percentage without previous sample, so we store raw
|
||||
// For single snapshot, try to use more clever approach
|
||||
result.cpu = {
|
||||
totalPercent: 0, // will be calculated with delta
|
||||
cores: [],
|
||||
userPercent: 0,
|
||||
systemPercent: 0,
|
||||
idlePercent: 0,
|
||||
};
|
||||
(result.cpu as any)._rawTotal = total;
|
||||
(result.cpu as any)._rawIdle = idle;
|
||||
(result.cpu as any)._rawUser = user;
|
||||
(result.cpu as any)._rawSystem = system;
|
||||
}
|
||||
}
|
||||
|
||||
// Memory from /proc/meminfo
|
||||
const memSection = sections[1];
|
||||
if (memSection) {
|
||||
const memInfo = this.parseMemInfo(memSection);
|
||||
if (memInfo) result.memory = memInfo;
|
||||
}
|
||||
|
||||
// Load average
|
||||
const loadSection = sections[2];
|
||||
if (loadSection) {
|
||||
const loadParts = loadSection.trim().split(/\s+/);
|
||||
if (loadParts.length >= 3) {
|
||||
result.load = {
|
||||
load1: parseFloat(loadParts[0]) || 0,
|
||||
load5: parseFloat(loadParts[1]) || 0,
|
||||
load15: parseFloat(loadParts[2]) || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Uptime
|
||||
const uptimeSection = sections[3];
|
||||
if (uptimeSection) {
|
||||
const uptimeVal = parseFloat(uptimeSection.trim().split(/\s+/)[0]);
|
||||
if (!isNaN(uptimeVal)) result.uptimeSeconds = Math.floor(uptimeVal);
|
||||
}
|
||||
|
||||
// Hostname
|
||||
const hostSection = sections[4];
|
||||
if (hostSection) {
|
||||
result.hostname = hostSection.trim();
|
||||
}
|
||||
|
||||
// OS info
|
||||
const osSection = sections[5];
|
||||
if (osSection) {
|
||||
const prettyMatch = osSection.match(/PRETTY_NAME="?([^"\n]+)"?/);
|
||||
if (prettyMatch) result.osName = prettyMatch[1];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseMemInfo(output: string): MemoryUsage | null {
|
||||
const lines = output.trim().split('\n');
|
||||
const info: Record<string, number> = {};
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^(\S+):\s+(\d+)/);
|
||||
if (m) info[m[1]] = parseInt(m[2]) * 1024; // kB -> bytes
|
||||
}
|
||||
const total = info['MemTotal'] || 0;
|
||||
const free = info['MemFree'] || 0;
|
||||
const buffers = info['Buffers'] || 0;
|
||||
const cached = info['Cached'] || 0;
|
||||
const sreclaimable = info['SReclaimable'] || 0;
|
||||
const used = total - free - buffers - cached - sreclaimable;
|
||||
const percent = total > 0 ? (used / total) * 100 : 0;
|
||||
|
||||
return {
|
||||
totalBytes: total,
|
||||
usedBytes: used,
|
||||
freeBytes: free + buffers + cached + sreclaimable,
|
||||
percent,
|
||||
cachedBytes: cached + buffers + sreclaimable,
|
||||
swapUsedBytes: (info['SwapTotal'] || 0) - (info['SwapFree'] || 0),
|
||||
swapTotalBytes: info['SwapTotal'] || 0,
|
||||
};
|
||||
}
|
||||
|
||||
buildDiskCommand(): string {
|
||||
return `df -PT -x tmpfs -x devtmpfs -x overlay -x squashfs 2>/dev/null | tail -n +2`;
|
||||
}
|
||||
|
||||
parseDiskOutput(output: string): DiskUsage[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const disks: DiskUsage[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
// Filesystem Type Size Used Avail Use% Mounted on
|
||||
if (parts.length >= 7) {
|
||||
const total = parseInt(parts[2]) * 1024; // KB -> bytes
|
||||
const used = parseInt(parts[3]) * 1024;
|
||||
const avail = parseInt(parts[4]) * 1024;
|
||||
const usePercent = parseInt(parts[5]);
|
||||
const mountPoint = parts.slice(6).join(' ');
|
||||
disks.push({
|
||||
filesystem: parts[0],
|
||||
mountPoint,
|
||||
totalBytes: total,
|
||||
usedBytes: used,
|
||||
freeBytes: avail,
|
||||
percent: usePercent,
|
||||
});
|
||||
}
|
||||
}
|
||||
return disks;
|
||||
}
|
||||
|
||||
buildNetworkCommand(): string {
|
||||
return `cat /proc/net/dev | tail -n +3`;
|
||||
}
|
||||
|
||||
parseNetworkOutput(output: string, prev?: NetworkTraffic[], intervalMs: number = 2000): NetworkTraffic[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const current: Map<string, { rx: number; tx: number }> = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/[:\s]+/);
|
||||
if (parts.length >= 10) {
|
||||
const iface = parts[0];
|
||||
if (iface === 'lo' || !iface) continue;
|
||||
const rxBytes = parseInt(parts[1]) || 0;
|
||||
const txBytes = parseInt(parts[9]) || 0;
|
||||
current.set(iface, { rx: rxBytes, tx: txBytes });
|
||||
}
|
||||
}
|
||||
|
||||
const results: NetworkTraffic[] = [];
|
||||
const intervalSec = intervalMs / 1000;
|
||||
|
||||
for (const [iface, curr] of current) {
|
||||
const prevItem = prev?.find(p => p.interfaceName === iface);
|
||||
let rxSpeed = 0;
|
||||
let txSpeed = 0;
|
||||
if (prevItem && prevItem.rxTotalBytes !== undefined && prevItem.txTotalBytes !== undefined) {
|
||||
rxSpeed = Math.max(0, (curr.rx - prevItem.rxTotalBytes) / intervalSec);
|
||||
txSpeed = Math.max(0, (curr.tx - prevItem.txTotalBytes) / intervalSec);
|
||||
}
|
||||
results.push({
|
||||
interfaceName: iface,
|
||||
rxBytesPerSec: rxSpeed,
|
||||
txBytesPerSec: txSpeed,
|
||||
rxTotalBytes: curr.rx,
|
||||
txTotalBytes: curr.tx,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
buildProcessCommand(limit: number = 20): string {
|
||||
return `ps -eo pid,user,%cpu,%mem,rss,comm,stat --sort=-%cpu --no-headers | head -${limit}`;
|
||||
}
|
||||
|
||||
parseProcessOutput(output: string): ProcessInfo[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const processes: ProcessInfo[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 7) {
|
||||
processes.push({
|
||||
pid: parseInt(parts[0]) || 0,
|
||||
user: parts[1],
|
||||
cpuPercent: parseFloat(parts[2]) || 0,
|
||||
memPercent: parseFloat(parts[3]) || 0,
|
||||
memBytes: (parseInt(parts[4]) || 0) * 1024, // KB -> bytes
|
||||
name: parts[5],
|
||||
status: parts[6],
|
||||
});
|
||||
}
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
buildDockerCommand(): string {
|
||||
return `docker ps -a --format "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.State}}" 2>/dev/null || echo "DOCKER_NOT_FOUND"`;
|
||||
}
|
||||
|
||||
parseDockerOutput(output: string): DockerContainer[] {
|
||||
if (output.includes('DOCKER_NOT_FOUND') || output.trim() === '') return [];
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const containers: DockerContainer[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split('|');
|
||||
if (parts.length >= 5) {
|
||||
containers.push({
|
||||
id: parts[0],
|
||||
name: parts[1],
|
||||
image: parts[2],
|
||||
status: parts[3],
|
||||
state: (parts[4].toLowerCase() as DockerContainer['state']) || 'unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
buildPortsCommand(): string {
|
||||
return `ss -tulnp 2>/dev/null || netstat -tulnp 2>/dev/null || echo "PORTS_NOT_AVAILABLE"`;
|
||||
}
|
||||
|
||||
parsePortsOutput(output: string): ListeningPort[] {
|
||||
if (output.includes('PORTS_NOT_AVAILABLE')) return [];
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const ports: ListeningPort[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('State') || line.startsWith('Proto')) continue;
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 5) continue;
|
||||
|
||||
const proto = parts[0].toLowerCase().includes('tcp') ? 'tcp' :
|
||||
parts[0].toLowerCase().includes('udp') ? 'udp' : 'tcp';
|
||||
const localAddr = parts.find(p => p.includes(':') && !p.startsWith('0x'));
|
||||
if (!localAddr) continue;
|
||||
|
||||
const lastColonIdx = localAddr.lastIndexOf(':');
|
||||
const addr = localAddr.substring(0, lastColonIdx).replace(/^\[|\]$/g, '');
|
||||
const portStr = localAddr.substring(lastColonIdx + 1);
|
||||
const port = parseInt(portStr);
|
||||
if (!port || isNaN(port)) continue;
|
||||
|
||||
const key = `${proto}:${port}:${addr}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
// Try to find process info
|
||||
let processName = '';
|
||||
let pid: number | undefined;
|
||||
const procMatch = line.match(/users:\(\("([^"]+)"/);
|
||||
const pidMatch = line.match(/pid=(\d+)/);
|
||||
if (procMatch) processName = procMatch[1];
|
||||
if (pidMatch) pid = parseInt(pidMatch[1]);
|
||||
|
||||
ports.push({
|
||||
port,
|
||||
protocol: proto as 'tcp' | 'udp',
|
||||
address: addr || '0.0.0.0',
|
||||
process: processName || undefined,
|
||||
pid,
|
||||
state: parts[1] || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return ports.sort((a, b) => a.port - b.port);
|
||||
}
|
||||
|
||||
buildServicesCommand(): string {
|
||||
return `systemctl list-units --type=service --all --no-pager --no-legend 2>/dev/null || echo "SYSTEMD_NOT_FOUND"`;
|
||||
}
|
||||
|
||||
parseServicesOutput(output: string): SystemService[] {
|
||||
if (output.includes('SYSTEMD_NOT_FOUND')) return [];
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const services: SystemService[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 4) continue;
|
||||
const name = parts[0];
|
||||
const load = parts[1];
|
||||
const active = parts[2];
|
||||
const sub = parts[3];
|
||||
const desc = parts.slice(4).join(' ');
|
||||
|
||||
let state: SystemService['state'] = 'unknown';
|
||||
if (active === 'active' && (sub === 'running' || sub === 'exited')) state = 'running';
|
||||
else if (active === 'inactive' || active === 'failed') state = active === 'failed' ? 'failed' : 'stopped';
|
||||
|
||||
services.push({
|
||||
name,
|
||||
state,
|
||||
description: desc,
|
||||
});
|
||||
}
|
||||
return services.slice(0, 50); // limit
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Windows adapter (for SSH-connected Windows servers)
|
||||
// ============================================================================
|
||||
|
||||
export class WindowsMonitorAdapter implements SystemMonitorAdapter {
|
||||
readonly osFamily = 'windows' as const;
|
||||
|
||||
buildFastMetricsCommand(): string {
|
||||
return `powershell -NoProfile -Command "
|
||||
$cpu = Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select-Object -ExpandProperty Average;
|
||||
$os = Get-WmiObject Win32_OperatingSystem;
|
||||
$memTotal = $os.TotalVisibleMemorySize * 1024;
|
||||
$memFree = $os.FreePhysicalMemory * 1024;
|
||||
$memUsed = $memTotal - $memFree;
|
||||
$memPct = if ($memTotal -gt 0) { [math]::Round(($memUsed / $memTotal) * 100, 1) } else { 0 };
|
||||
$uptime = (Get-Date) - $os.ConvertToDateTime($os.LastBootUpTime);
|
||||
$hostName = $env:COMPUTERNAME;
|
||||
$osName = $os.Caption;
|
||||
Write-Output \"CPU:$cpu\";
|
||||
Write-Output \"MEM_TOTAL:$memTotal\";
|
||||
Write-Output \"MEM_USED:$memUsed\";
|
||||
Write-Output \"MEM_FREE:$memFree\";
|
||||
Write-Output \"MEM_PCT:$memPct\";
|
||||
Write-Output \"UPTIME:$([int]$uptime.TotalSeconds)\";
|
||||
Write-Output \"HOSTNAME:$hostName\";
|
||||
Write-Output \"OS:$osName\";
|
||||
" 2>$null`;
|
||||
}
|
||||
|
||||
parseFastMetrics(output: string): Partial<SystemMetricsSnapshot> {
|
||||
const result: Partial<SystemMetricsSnapshot> = {};
|
||||
const lines = output.trim().split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('CPU:')) {
|
||||
const val = parseFloat(trimmed.substring(4)) || 0;
|
||||
result.cpu = { totalPercent: val, cores: [] };
|
||||
} else if (trimmed.startsWith('MEM_TOTAL:')) {
|
||||
if (!result.memory) result.memory = { totalBytes: 0, usedBytes: 0, freeBytes: 0, percent: 0 };
|
||||
result.memory.totalBytes = parseInt(trimmed.substring(10)) || 0;
|
||||
} else if (trimmed.startsWith('MEM_USED:')) {
|
||||
if (!result.memory) result.memory = { totalBytes: 0, usedBytes: 0, freeBytes: 0, percent: 0 };
|
||||
result.memory.usedBytes = parseInt(trimmed.substring(9)) || 0;
|
||||
} else if (trimmed.startsWith('MEM_FREE:')) {
|
||||
if (!result.memory) result.memory = { totalBytes: 0, usedBytes: 0, freeBytes: 0, percent: 0 };
|
||||
result.memory.freeBytes = parseInt(trimmed.substring(9)) || 0;
|
||||
} else if (trimmed.startsWith('MEM_PCT:')) {
|
||||
if (!result.memory) result.memory = { totalBytes: 0, usedBytes: 0, freeBytes: 0, percent: 0 };
|
||||
result.memory.percent = parseFloat(trimmed.substring(8)) || 0;
|
||||
} else if (trimmed.startsWith('UPTIME:')) {
|
||||
result.uptimeSeconds = parseInt(trimmed.substring(7)) || 0;
|
||||
} else if (trimmed.startsWith('HOSTNAME:')) {
|
||||
result.hostname = trimmed.substring(9);
|
||||
} else if (trimmed.startsWith('OS:')) {
|
||||
result.osName = trimmed.substring(3);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
buildDiskCommand(): string {
|
||||
return `powershell -NoProfile -Command "Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -gt 0 } | ForEach-Object { $total = $_.Used + $_.Free; $pct = if ($total -gt 0) { [math]::Round(($_.Used / $total) * 100, 1) } else { 0 }; Write-Output '$($_.Name)|$total|$($_.Used)|$($_.Free)|$pct|$($_.Root)' }" 2>$null`;
|
||||
}
|
||||
|
||||
parseDiskOutput(output: string): DiskUsage[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const disks: DiskUsage[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split('|');
|
||||
if (parts.length >= 6) {
|
||||
disks.push({
|
||||
filesystem: parts[0],
|
||||
mountPoint: parts[5],
|
||||
totalBytes: parseInt(parts[1]) || 0,
|
||||
usedBytes: parseInt(parts[2]) || 0,
|
||||
freeBytes: parseInt(parts[3]) || 0,
|
||||
percent: parseFloat(parts[4]) || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
return disks;
|
||||
}
|
||||
|
||||
buildNetworkCommand(): string {
|
||||
return `powershell -NoProfile -Command "Get-NetAdapterStatistics -ErrorAction SilentlyContinue | ForEach-Object { Write-Output '$($_.Name)|$($_.ReceivedBytes)|$($_.SentBytes)' }" 2>$null`;
|
||||
}
|
||||
|
||||
parseNetworkOutput(output: string, prev?: NetworkTraffic[], intervalMs: number = 2000): NetworkTraffic[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const current: Map<string, { rx: number; tx: number }> = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split('|');
|
||||
if (parts.length >= 3) {
|
||||
current.set(parts[0], {
|
||||
rx: parseInt(parts[1]) || 0,
|
||||
tx: parseInt(parts[2]) || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const results: NetworkTraffic[] = [];
|
||||
const intervalSec = intervalMs / 1000;
|
||||
|
||||
for (const [iface, curr] of current) {
|
||||
const prevItem = prev?.find(p => p.interfaceName === iface);
|
||||
let rxSpeed = 0;
|
||||
let txSpeed = 0;
|
||||
if (prevItem && prevItem.rxTotalBytes !== undefined && prevItem.txTotalBytes !== undefined) {
|
||||
rxSpeed = Math.max(0, (curr.rx - prevItem.rxTotalBytes) / intervalSec);
|
||||
txSpeed = Math.max(0, (curr.tx - prevItem.txTotalBytes) / intervalSec);
|
||||
}
|
||||
results.push({
|
||||
interfaceName: iface,
|
||||
rxBytesPerSec: rxSpeed,
|
||||
txBytesPerSec: txSpeed,
|
||||
rxTotalBytes: curr.rx,
|
||||
txTotalBytes: curr.tx,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
buildProcessCommand(limit: number = 20): string {
|
||||
return `powershell -NoProfile -Command "Get-Process | Sort-Object CPU -Descending | Select-Object -First ${limit} | ForEach-Object { $cpuPct = if ($_.CPU -ne $null) { [math]::Round($_.CPU, 1) } else { 0 }; $memPct = 0; $name = $_.ProcessName; Write-Output '$($_.Id)|$($_.UserName ?? '')|$cpuPct|$memPct|$($_.WorkingSet64)|$name|$($_.StartTime)' }" 2>$null`;
|
||||
}
|
||||
|
||||
parseProcessOutput(output: string): ProcessInfo[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const processes: ProcessInfo[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split('|');
|
||||
if (parts.length >= 6) {
|
||||
processes.push({
|
||||
pid: parseInt(parts[0]) || 0,
|
||||
user: parts[1] || undefined,
|
||||
cpuPercent: parseFloat(parts[2]) || 0,
|
||||
memPercent: parseFloat(parts[3]) || 0,
|
||||
memBytes: parseInt(parts[4]) || 0,
|
||||
name: parts[5],
|
||||
});
|
||||
}
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
buildDockerCommand(): string {
|
||||
return `docker ps -a --format "{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.State}}" 2>$null`;
|
||||
}
|
||||
|
||||
parseDockerOutput(output: string): DockerContainer[] {
|
||||
if (output.trim() === '' || output.includes('not found')) return [];
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const containers: DockerContainer[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split('|');
|
||||
if (parts.length >= 5) {
|
||||
containers.push({
|
||||
id: parts[0],
|
||||
name: parts[1],
|
||||
image: parts[2],
|
||||
status: parts[3],
|
||||
state: (parts[4].toLowerCase().trim() as DockerContainer['state']) || 'unknown',
|
||||
});
|
||||
}
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
buildPortsCommand(): string {
|
||||
return `powershell -NoProfile -Command "Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ForEach-Object { Write-Output '$($_.LocalAddress)|$($_.LocalPort)|TCP|$($_.OwningProcess)' }; Get-NetUDPEndpoint -ErrorAction SilentlyContinue | ForEach-Object { Write-Output '$($_.LocalAddress)|$($_.LocalPort)|UDP|$($_.OwningProcess)' }" 2>$null`;
|
||||
}
|
||||
|
||||
parsePortsOutput(output: string): ListeningPort[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const ports: ListeningPort[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
const addr = parts[0].replace(/^\[|\]$/g, '');
|
||||
const port = parseInt(parts[1]);
|
||||
const proto = parts[2].toLowerCase() as 'tcp' | 'udp';
|
||||
const pid = parseInt(parts[3]);
|
||||
|
||||
const key = `${proto}:${port}:${addr}`;
|
||||
if (seen.has(key) || !port) continue;
|
||||
seen.add(key);
|
||||
|
||||
ports.push({
|
||||
port,
|
||||
protocol: proto,
|
||||
address: addr || '0.0.0.0',
|
||||
pid: pid || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return ports.sort((a, b) => a.port - b.port);
|
||||
}
|
||||
|
||||
buildServicesCommand(): string {
|
||||
return `powershell -NoProfile -Command "Get-Service | ForEach-Object { $state = switch ($_.Status) { 'Running' { 'running' } 'Stopped' { 'stopped' } default { 'unknown' } }; $startType = switch ($_.StartType) { 'Automatic' { 'auto' } 'Manual' { 'manual' } 'Disabled' { 'disabled' } default { 'manual' } }; Write-Output '$($_.Name)|$state|$($_.DisplayName)|$startType' }" 2>$null`;
|
||||
}
|
||||
|
||||
parseServicesOutput(output: string): SystemService[] {
|
||||
const lines = output.trim().split('\n').filter(l => l.trim());
|
||||
const services: SystemService[] = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
services.push({
|
||||
name: parts[0],
|
||||
state: parts[1] as SystemService['state'],
|
||||
description: parts[2],
|
||||
startupType: parts[3] as SystemService['startupType'],
|
||||
});
|
||||
}
|
||||
}
|
||||
return services.slice(0, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate monitor adapter based on OS type.
|
||||
*/
|
||||
export function getMonitorAdapter(osType: 'linux' | 'windows' | 'macos' | string): SystemMonitorAdapter {
|
||||
switch (osType) {
|
||||
case 'windows':
|
||||
return new WindowsMonitorAdapter();
|
||||
case 'linux':
|
||||
case 'macos':
|
||||
default:
|
||||
return new LinuxMonitorAdapter();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user