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
618 lines
22 KiB
TypeScript
618 lines
22 KiB
TypeScript
// 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();
|
|
}
|
|
}
|