mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35:24 +00:00
feat: make each plugin an npm workspace built by a termix-plugin CLI
This commit is contained in:
1 parent
4758016f41
commit
5796186a1c
448 files changed
+3087
-2003
No files matched your search
@@ -0,0 +1,229 @@
|
||||
import type { Client } from "ssh2";
|
||||
import {
|
||||
execCommand,
|
||||
execPowerShell,
|
||||
toFixedNum,
|
||||
type HostPlatform,
|
||||
} from "../../../../../src/backend/hosts/metrics-shared/common-utils.js";
|
||||
|
||||
/**
|
||||
* CPU percent is derived from the delta against the previous poll's
|
||||
* /proc/stat sample rather than a same-poll double-read, so a poll no longer
|
||||
* has to block for a fixed settle window. Keyed by host.
|
||||
*/
|
||||
const previousStat = new Map<
|
||||
number,
|
||||
{ total: number; idle: number; timestamp: number }
|
||||
>();
|
||||
|
||||
export function clearCpuSampleCache(hostId?: number): void {
|
||||
if (hostId === undefined) {
|
||||
previousStat.clear();
|
||||
return;
|
||||
}
|
||||
previousStat.delete(hostId);
|
||||
}
|
||||
|
||||
export function parseCpuLine(
|
||||
cpuLine: string,
|
||||
): { total: number; idle: number } | undefined {
|
||||
const parts = cpuLine.trim().split(/\s+/);
|
||||
if (parts[0] !== "cpu") return undefined;
|
||||
const nums = parts
|
||||
.slice(1)
|
||||
.map((n) => Number(n))
|
||||
.filter((n) => Number.isFinite(n));
|
||||
if (nums.length < 4) return undefined;
|
||||
const idle = (nums[3] ?? 0) + (nums[4] ?? 0);
|
||||
const total = nums.reduce((a, b) => a + b, 0);
|
||||
return { total, idle };
|
||||
}
|
||||
|
||||
// Parses macOS `top -l 1 -n 0` output, e.g.
|
||||
// "CPU usage: 12.34% user, 5.67% sys, 81.99% idle"
|
||||
export function parseDarwinCpuUsageLine(
|
||||
line: string,
|
||||
): { percent: number } | undefined {
|
||||
const match = line.match(
|
||||
/([\d.]+)%\s*user,\s*([\d.]+)%\s*sys,\s*([\d.]+)%\s*idle/i,
|
||||
);
|
||||
if (!match) return undefined;
|
||||
const user = Number(match[1]);
|
||||
const sys = Number(match[2]);
|
||||
if (!Number.isFinite(user) || !Number.isFinite(sys)) return undefined;
|
||||
return { percent: Math.max(0, Math.min(100, user + sys)) };
|
||||
}
|
||||
|
||||
async function collectDarwinCpuMetrics(client: Client): Promise<{
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
}> {
|
||||
let cpuPercent: number | null = null;
|
||||
let cores: number | null = null;
|
||||
let loadTriplet: [number, number, number] | null = null;
|
||||
|
||||
try {
|
||||
const [topOut, coresOut, loadOut] = await Promise.race([
|
||||
Promise.all([
|
||||
execCommand(client, "top -l 1 -n 0"),
|
||||
execCommand(client, "sysctl -n hw.ncpu"),
|
||||
execCommand(client, "sysctl -n vm.loadavg"),
|
||||
]),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("CPU metrics collection timeout")),
|
||||
25000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const usageLine = topOut.stdout
|
||||
.split("\n")
|
||||
.find((l) => l.includes("CPU usage:"));
|
||||
const parsed = usageLine ? parseDarwinCpuUsageLine(usageLine) : undefined;
|
||||
if (parsed) cpuPercent = parsed.percent;
|
||||
|
||||
const coresNum = Number((coresOut.stdout || "").trim());
|
||||
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
|
||||
|
||||
// sysctl -n vm.loadavg -> "{ 1.23 1.45 1.67 }"
|
||||
const loadMatch = loadOut.stdout.match(/([\d.]+)\s+([\d.]+)\s+([\d.]+)/);
|
||||
if (loadMatch) {
|
||||
loadTriplet = [
|
||||
Number(loadMatch[1]),
|
||||
Number(loadMatch[2]),
|
||||
Number(loadMatch[3]),
|
||||
].map((v) => (Number.isFinite(v) ? Number(v) : 0)) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
}
|
||||
} catch {
|
||||
cpuPercent = null;
|
||||
loadTriplet = null;
|
||||
}
|
||||
|
||||
return {
|
||||
percent: toFixedNum(cpuPercent, 0),
|
||||
cores,
|
||||
load: loadTriplet,
|
||||
};
|
||||
}
|
||||
|
||||
// No POSIX-style load average exists on Windows, so `load` always stays null.
|
||||
async function collectWindowsCpuMetrics(client: Client): Promise<{
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
}> {
|
||||
let cpuPercent: number | null = null;
|
||||
let cores: number | null = null;
|
||||
|
||||
try {
|
||||
const { stdout } = await execPowerShell(
|
||||
client,
|
||||
"$cpu=(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; $cores=(Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors; [PSCustomObject]@{cpu=$cpu; cores=$cores} | ConvertTo-Json -Compress",
|
||||
25000,
|
||||
);
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
const cpuNum = Number(parsed?.cpu);
|
||||
const coresNum = Number(parsed?.cores);
|
||||
cpuPercent = Number.isFinite(cpuNum)
|
||||
? Math.max(0, Math.min(100, cpuNum))
|
||||
: null;
|
||||
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
|
||||
} catch {
|
||||
cpuPercent = null;
|
||||
}
|
||||
|
||||
return {
|
||||
percent: toFixedNum(cpuPercent, 0),
|
||||
cores,
|
||||
load: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function collectCpuMetrics(
|
||||
client: Client,
|
||||
platform?: HostPlatform,
|
||||
hostId?: number,
|
||||
): Promise<{
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
}> {
|
||||
if (platform === "darwin") {
|
||||
return collectDarwinCpuMetrics(client);
|
||||
}
|
||||
if (platform === "windows") {
|
||||
return collectWindowsCpuMetrics(client);
|
||||
}
|
||||
|
||||
let cpuPercent: number | null = null;
|
||||
let cores: number | null = null;
|
||||
let loadTriplet: [number, number, number] | null = null;
|
||||
|
||||
try {
|
||||
const readAt = Date.now();
|
||||
const [statOut, loadAvgOut, coresOut] = await Promise.race([
|
||||
Promise.all([
|
||||
execCommand(client, "cat /proc/stat"),
|
||||
execCommand(client, "cat /proc/loadavg"),
|
||||
execCommand(
|
||||
client,
|
||||
"nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo",
|
||||
),
|
||||
]),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("CPU metrics collection timeout")),
|
||||
25000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const cpuLine = (
|
||||
statOut.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
||||
).trim();
|
||||
const current = parseCpuLine(cpuLine);
|
||||
const previous =
|
||||
hostId !== undefined ? previousStat.get(hostId) : undefined;
|
||||
if (current && previous) {
|
||||
const totalDiff = current.total - previous.total;
|
||||
const idleDiff = current.idle - previous.idle;
|
||||
const used = totalDiff - idleDiff;
|
||||
if (totalDiff > 0)
|
||||
cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100));
|
||||
}
|
||||
if (current && hostId !== undefined) {
|
||||
previousStat.set(hostId, { ...current, timestamp: readAt });
|
||||
}
|
||||
|
||||
const laParts = loadAvgOut.stdout.trim().split(/\s+/);
|
||||
if (laParts.length >= 3) {
|
||||
loadTriplet = [
|
||||
Number(laParts[0]),
|
||||
Number(laParts[1]),
|
||||
Number(laParts[2]),
|
||||
].map((v) => (Number.isFinite(v) ? Number(v) : 0)) as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
}
|
||||
|
||||
const coresNum = Number((coresOut.stdout || "").trim());
|
||||
cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
|
||||
} catch {
|
||||
cpuPercent = null;
|
||||
loadTriplet = null;
|
||||
}
|
||||
|
||||
return {
|
||||
percent: toFixedNum(cpuPercent, 0),
|
||||
cores,
|
||||
load: loadTriplet,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user