Files
Termix/plugins/host-metrics/backend/managers/simple-reads.ts
T

139 lines
4.1 KiB
TypeScript

import type { Express } from "express";
import { execCommand } from "../../../../src/backend/hosts/metrics-shared/common-utils.js";
import { managerHandler } from "./route-helpers.js";
import type { ManagerRoutesDeps } from "./types.js";
// ─── Top by memory ──────────────────────────────────────────────────────────
export interface MemProcessRow {
pid: number;
user: string;
mem: number;
rss: number;
command: string;
}
const TOP_MEM_CMD =
"ps -eo pid,user:20,pmem,rss,comm --sort=-pmem --no-headers 2>/dev/null | head -n 20";
export function parseTopMemory(output: string): MemProcessRow[] {
const rows: MemProcessRow[] = [];
for (const raw of output.split("\n")) {
const line = raw.trim();
if (!line) continue;
const m = line.match(/^(\d+)\s+(\S+)\s+([\d.]+)\s+(\d+)\s+(.*)$/);
if (!m) continue;
rows.push({
pid: Number(m[1]),
user: m[2],
mem: Number(m[3]),
rss: Number(m[4]),
command: m[5],
});
}
return rows;
}
// ─── Systemd timers ─────────────────────────────────────────────────────────
export interface TimerRow {
next: string;
left: string;
last: string;
unit: string;
activates: string;
}
const TIMERS_CMD =
"systemctl list-timers --all --no-legend --no-pager 2>/dev/null";
export function parseTimers(output: string): TimerRow[] {
const rows: TimerRow[] = [];
for (const raw of output.split("\n")) {
const line = raw.trim();
if (!line || line.startsWith("NEXT")) continue;
// NEXT(3) LEFT(2) LAST(3) PASSED(2) UNIT ACTIVATES -> columns vary; grab
// the trailing UNIT + ACTIVATES which always end the line.
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const activates = parts[parts.length - 1];
const unit = parts[parts.length - 2];
if (!unit.endsWith(".timer")) continue;
rows.push({
next: parts.slice(0, 3).join(" "),
left: "",
last: "",
unit,
activates,
});
}
return rows;
}
// ─── Disk breakdown (per-mount) ─────────────────────────────────────────────
export interface MountUsage {
filesystem: string;
sizeKb: number;
usedKb: number;
availKb: number;
usePct: number;
mount: string;
}
const DF_CMD = "df -Pk 2>/dev/null | tail -n +2";
export function parseDfMounts(output: string): MountUsage[] {
const mounts: MountUsage[] = [];
for (const raw of output.split("\n")) {
const line = raw.trim();
if (!line) continue;
const m = line.match(/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$/);
if (!m) continue;
const fs = m[1];
// Skip pseudo/virtual filesystems that clutter the view.
if (/^(tmpfs|devtmpfs|overlay|udev|none|shm)$/.test(fs)) continue;
mounts.push({
filesystem: fs,
sizeKb: Number(m[2]),
usedKb: Number(m[3]),
availKb: Number(m[4]),
usePct: Number(m[5]),
mount: m[6],
});
}
return mounts;
}
export function registerSimpleReadRoutes(
app: Express,
{ validateHostId, runOnHost }: ManagerRoutesDeps,
): void {
app.get(
"/host-metrics/managers/top-memory/:id",
validateHostId,
managerHandler(runOnHost, "connect", "top_memory", async (client) => {
const { stdout } = await execCommand(client, TOP_MEM_CMD, 15000);
return { processes: parseTopMemory(stdout) };
}),
);
app.get(
"/host-metrics/managers/timers/:id",
validateHostId,
managerHandler(runOnHost, "connect", "systemd_timers", async (client) => {
const { stdout } = await execCommand(client, TIMERS_CMD, 15000);
return { timers: parseTimers(stdout) };
}),
);
app.get(
"/host-metrics/managers/disk-breakdown/:id",
validateHostId,
managerHandler(runOnHost, "connect", "disk_breakdown", async (client) => {
const { stdout } = await execCommand(client, DF_CMD, 15000);
return { mounts: parseDfMounts(stdout) };
}),
);
}