mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
116 lines
3.1 KiB
TypeScript
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,
|
|
};
|
|
}
|