Files
Termix/src/backend/hosts/metrics/widgets/disk-collector.ts
T
2026-07-20 00:38:13 -05:00

116 lines
3.1 KiB
TypeScript

import type { Client } from "ssh2";
import { execCommand, toFixedNum } from "./common-utils.js";
const PSEUDO_FS_RE = /^(tmpfs|devtmpfs|overlay|udev|none|shm)$/;
export interface DfRow {
filesystem: string;
mount: string;
parts: string[];
}
export function parseDfLines(output: string): DfRow[] {
return output
.split("\n")
.map((l) => l.trim())
.filter(Boolean)
.map((line) => {
const parts = line.split(/\s+/);
return { filesystem: parts[0] || "", mount: parts[5] || "", parts };
})
.filter(
(row) => row.parts.length >= 6 && !PSEUDO_FS_RE.test(row.filesystem),
);
}
// Finds the index of the most-utilized real filesystem in a `df -B1`-style
// row set (parts[1] = total bytes, parts[2] = used bytes), so a nearly-full
// secondary mount (e.g. /data) isn't hidden behind a healthy root filesystem.
export function findWorstMountIndex(bytesRows: DfRow[]): {
index: number;
usedBytes: number;
totalBytes: number;
} {
let worstIndex = -1;
let worstUsedBytes = -1;
let worstTotalBytes = 0;
bytesRows.forEach((row, index) => {
const totalBytes = Number(row.parts[1]);
const usedBytes = Number(row.parts[2]);
if (
!Number.isFinite(totalBytes) ||
!Number.isFinite(usedBytes) ||
totalBytes <= 0
) {
return;
}
const usedRatio = usedBytes / totalBytes;
const worstRatio =
worstTotalBytes > 0 ? worstUsedBytes / worstTotalBytes : -1;
if (usedRatio > worstRatio) {
worstIndex = index;
worstUsedBytes = usedBytes;
worstTotalBytes = totalBytes;
}
});
return {
index: worstIndex,
usedBytes: worstUsedBytes,
totalBytes: worstTotalBytes,
};
}
export async function collectDiskMetrics(client: Client): Promise<{
percent: number | null;
usedHuman: string | null;
totalHuman: string | null;
availableHuman: string | null;
}> {
let diskPercent: number | null = null;
let usedHuman: string | null = null;
let totalHuman: string | null = null;
let availableHuman: string | null = null;
try {
const [diskOutHuman, diskOutBytes] = await Promise.all([
execCommand(client, "df -h -P | tail -n +2"),
execCommand(client, "df -B1 -P | tail -n +2"),
]);
const humanRows = parseDfLines(diskOutHuman.stdout);
const bytesRows = parseDfLines(diskOutBytes.stdout);
const worst = findWorstMountIndex(bytesRows);
if (worst.totalBytes > 0) {
diskPercent = Math.max(
0,
Math.min(100, (worst.usedBytes / worst.totalBytes) * 100),
);
const humanRow =
humanRows.length === bytesRows.length
? humanRows[worst.index]
: humanRows.find((row) => row.mount === bytesRows[worst.index].mount);
if (humanRow) {
totalHuman = humanRow.parts[1] || null;
usedHuman = humanRow.parts[2] || null;
availableHuman = humanRow.parts[3] || null;
}
}
} catch {
diskPercent = null;
usedHuman = null;
totalHuman = null;
availableHuman = null;
}
return {
percent: toFixedNum(diskPercent, 0),
usedHuman,
totalHuman,
availableHuman,
};
}