mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: node-MainThread (#1300)
https://github.com/Termix-SSH/Support/issues/1300
This commit is contained in:
1 parent
a9a3bbc5f4
commit
beb5145475
3 files changed
+144
-57
No files matched your search
@@ -25,13 +25,19 @@ import type {
|
||||
LogEntry,
|
||||
ConnectionStage,
|
||||
} from "../../../types/connection-log.js";
|
||||
import { collectCpuMetrics } from "./widgets/cpu-collector.js";
|
||||
import {
|
||||
collectCpuMetrics,
|
||||
clearCpuSampleCache,
|
||||
} from "./widgets/cpu-collector.js";
|
||||
import { collectMemoryMetrics } from "./widgets/memory-collector.js";
|
||||
import {
|
||||
collectDiskMetrics,
|
||||
type DiskFilesystem,
|
||||
} from "./widgets/disk-collector.js";
|
||||
import { collectNetworkMetrics } from "./widgets/network-collector.js";
|
||||
import {
|
||||
collectNetworkMetrics,
|
||||
clearNetworkSampleCache,
|
||||
} from "./widgets/network-collector.js";
|
||||
import { collectUptimeMetrics } from "./widgets/uptime-collector.js";
|
||||
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
||||
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
||||
@@ -515,6 +521,8 @@ class PollingManager {
|
||||
this.syncPollConcurrency();
|
||||
this.statusStore.delete(host.id);
|
||||
this.metricsStore.delete(host.id);
|
||||
clearCpuSampleCache(host.id);
|
||||
clearNetworkSampleCache(host.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -848,6 +856,8 @@ class PollingManager {
|
||||
if (clearData) {
|
||||
this.statusStore.delete(hostId);
|
||||
this.metricsStore.delete(hostId);
|
||||
clearCpuSampleCache(hostId);
|
||||
clearNetworkSampleCache(hostId);
|
||||
}
|
||||
}
|
||||
hostPollCache.invalidate(hostId);
|
||||
@@ -917,6 +927,8 @@ class PollingManager {
|
||||
if (!currentHostIds.has(hostId)) {
|
||||
this.statusStore.delete(hostId);
|
||||
this.metricsStore.delete(hostId);
|
||||
clearCpuSampleCache(hostId);
|
||||
clearNetworkSampleCache(hostId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1771,7 +1783,7 @@ async function collectMetrics(
|
||||
const collectFn = async (client: Client) => {
|
||||
onAuthenticated?.();
|
||||
const platform = await detectPlatform(client);
|
||||
const cpu = await collectCpuMetrics(client, platform);
|
||||
const cpu = await collectCpuMetrics(client, platform, host.id);
|
||||
const memory = await collectMemoryMetrics(client, platform);
|
||||
const disk = await collectDiskMetrics(
|
||||
client,
|
||||
@@ -1779,7 +1791,7 @@ async function collectMetrics(
|
||||
monitoredMounts,
|
||||
platform,
|
||||
);
|
||||
const network = await collectNetworkMetrics(client, platform);
|
||||
const network = await collectNetworkMetrics(client, platform, host.id);
|
||||
const uptime = await collectUptimeMetrics(client, platform);
|
||||
const processes = await collectProcessesMetrics(client);
|
||||
const system = await collectSystemMetrics(client, platform);
|
||||
|
||||
@@ -6,6 +6,24 @@ import {
|
||||
type HostPlatform,
|
||||
} from "./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 {
|
||||
@@ -130,6 +148,7 @@ async function collectWindowsCpuMetrics(client: Client): Promise<{
|
||||
export async function collectCpuMetrics(
|
||||
client: Client,
|
||||
platform?: HostPlatform,
|
||||
hostId?: number,
|
||||
): Promise<{
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
@@ -147,7 +166,8 @@ export async function collectCpuMetrics(
|
||||
let loadTriplet: [number, number, number] | null = null;
|
||||
|
||||
try {
|
||||
const [stat1, loadAvgOut, coresOut] = await Promise.race([
|
||||
const readAt = Date.now();
|
||||
const [statOut, loadAvgOut, coresOut] = await Promise.race([
|
||||
Promise.all([
|
||||
execCommand(client, "cat /proc/stat"),
|
||||
execCommand(client, "cat /proc/loadavg"),
|
||||
@@ -164,24 +184,22 @@ export async function collectCpuMetrics(
|
||||
),
|
||||
]);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
const stat2 = await execCommand(client, "cat /proc/stat");
|
||||
|
||||
const cpuLine1 = (
|
||||
stat1.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
||||
const cpuLine = (
|
||||
statOut.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
||||
).trim();
|
||||
const cpuLine2 = (
|
||||
stat2.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
||||
).trim();
|
||||
const a = parseCpuLine(cpuLine1);
|
||||
const b = parseCpuLine(cpuLine2);
|
||||
if (a && b) {
|
||||
const totalDiff = b.total - a.total;
|
||||
const idleDiff = b.idle - a.idle;
|
||||
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) {
|
||||
|
||||
@@ -44,6 +44,31 @@ export function counterRate(
|
||||
return Math.round((second - first) / elapsedSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate is derived from the previous poll's counters rather than a same-poll
|
||||
* double-sample, so a poll no longer has to block for a fixed settle window.
|
||||
* Keyed by host, since counters from one host are meaningless against another.
|
||||
*/
|
||||
const previousSamples = new Map<
|
||||
number,
|
||||
{ counters: Map<string, NetworkCounters>; timestamp: number }
|
||||
>();
|
||||
|
||||
const previousWindowsSamples = new Map<
|
||||
number,
|
||||
{ rows: WindowsAdapterRow[]; timestamp: number }
|
||||
>();
|
||||
|
||||
export function clearNetworkSampleCache(hostId?: number): void {
|
||||
if (hostId === undefined) {
|
||||
previousSamples.clear();
|
||||
previousWindowsSamples.clear();
|
||||
return;
|
||||
}
|
||||
previousSamples.delete(hostId);
|
||||
previousWindowsSamples.delete(hostId);
|
||||
}
|
||||
|
||||
export function parseDarwinIfconfig(
|
||||
output: string,
|
||||
): Map<string, { ip: string; state: string }> {
|
||||
@@ -85,7 +110,10 @@ export function parseDarwinNetstat(
|
||||
return counters;
|
||||
}
|
||||
|
||||
async function collectDarwinNetworkMetrics(client: Client): Promise<{
|
||||
async function collectDarwinNetworkMetrics(
|
||||
client: Client,
|
||||
hostId?: number,
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
@@ -111,31 +139,35 @@ async function collectDarwinNetworkMetrics(client: Client): Promise<{
|
||||
const ifMap = parseDarwinIfconfig(ifconfigOut.stdout);
|
||||
|
||||
try {
|
||||
const firstReadAt = Date.now();
|
||||
const netstat1 = await execCommand(client, "netstat -ib 2>/dev/null");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const netstat2 = await execCommand(client, "netstat -ib 2>/dev/null");
|
||||
const elapsedSeconds = (Date.now() - firstReadAt) / 1000;
|
||||
const before = parseDarwinNetstat(netstat1.stdout);
|
||||
const after = parseDarwinNetstat(netstat2.stdout);
|
||||
const readAt = Date.now();
|
||||
const netstatOut = await execCommand(client, "netstat -ib 2>/dev/null");
|
||||
const current = parseDarwinNetstat(netstatOut.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousSamples.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous
|
||||
? (readAt - previous.timestamp) / 1000
|
||||
: 0;
|
||||
if (ifMap.size === 0) {
|
||||
for (const name of before.keys()) {
|
||||
for (const name of current.keys()) {
|
||||
ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
||||
}
|
||||
}
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
const b = before.get(name);
|
||||
const a = after.get(name);
|
||||
const c = current.get(name);
|
||||
const p = previous?.counters.get(name);
|
||||
interfaces.push({
|
||||
name,
|
||||
ip: data.ip,
|
||||
state: data.state,
|
||||
rxBytes: b?.rx ?? null,
|
||||
txBytes: b?.tx ?? null,
|
||||
rxRateBps: counterRate(b?.rx, a?.rx, elapsedSeconds),
|
||||
txRateBps: counterRate(b?.tx, a?.tx, elapsedSeconds),
|
||||
rxBytes: c?.rx ?? null,
|
||||
txBytes: c?.tx ?? null,
|
||||
rxRateBps: p ? counterRate(p.rx, c?.rx, elapsedSeconds) : null,
|
||||
txRateBps: p ? counterRate(p.tx, c?.tx, elapsedSeconds) : null,
|
||||
});
|
||||
}
|
||||
if (hostId !== undefined) {
|
||||
previousSamples.set(hostId, { counters: current, timestamp: readAt });
|
||||
}
|
||||
} catch {
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
interfaces.push({
|
||||
@@ -191,7 +223,10 @@ export function parseWindowsAdapterJson(output: string): WindowsAdapterRow[] {
|
||||
}
|
||||
}
|
||||
|
||||
async function collectWindowsNetworkMetrics(client: Client): Promise<{
|
||||
async function collectWindowsNetworkMetrics(
|
||||
client: Client,
|
||||
hostId?: number,
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
@@ -213,27 +248,37 @@ async function collectWindowsNetworkMetrics(client: Client): Promise<{
|
||||
}> = [];
|
||||
|
||||
try {
|
||||
const firstReadAt = Date.now();
|
||||
const before = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const after = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
||||
const elapsedSeconds = (Date.now() - firstReadAt) / 1000;
|
||||
|
||||
const beforeRows = parseWindowsAdapterJson(before.stdout);
|
||||
const afterMap = new Map(
|
||||
parseWindowsAdapterJson(after.stdout).map((row) => [row.name, row]),
|
||||
const readAt = Date.now();
|
||||
const result = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
||||
const currentRows = parseWindowsAdapterJson(result.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousWindowsSamples.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous ? (readAt - previous.timestamp) / 1000 : 0;
|
||||
const previousMap = new Map(
|
||||
(previous?.rows ?? []).map((row) => [row.name, row]),
|
||||
);
|
||||
|
||||
for (const row of beforeRows) {
|
||||
const afterRow = afterMap.get(row.name);
|
||||
for (const row of currentRows) {
|
||||
const previousRow = previousMap.get(row.name);
|
||||
interfaces.push({
|
||||
name: row.name,
|
||||
ip: row.ip,
|
||||
state: row.state,
|
||||
rxBytes: row.rx || null,
|
||||
txBytes: row.tx || null,
|
||||
rxRateBps: counterRate(row.rx, afterRow?.rx, elapsedSeconds),
|
||||
txRateBps: counterRate(row.tx, afterRow?.tx, elapsedSeconds),
|
||||
rxRateBps: previousRow
|
||||
? counterRate(previousRow.rx, row.rx, elapsedSeconds)
|
||||
: null,
|
||||
txRateBps: previousRow
|
||||
? counterRate(previousRow.tx, row.tx, elapsedSeconds)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (hostId !== undefined) {
|
||||
previousWindowsSamples.set(hostId, {
|
||||
rows: currentRows,
|
||||
timestamp: readAt,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
@@ -246,6 +291,7 @@ async function collectWindowsNetworkMetrics(client: Client): Promise<{
|
||||
export async function collectNetworkMetrics(
|
||||
client: Client,
|
||||
platform?: HostPlatform,
|
||||
hostId?: number,
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
@@ -258,10 +304,10 @@ export async function collectNetworkMetrics(
|
||||
}>;
|
||||
}> {
|
||||
if (platform === "darwin") {
|
||||
return collectDarwinNetworkMetrics(client);
|
||||
return collectDarwinNetworkMetrics(client, hostId);
|
||||
}
|
||||
if (platform === "windows") {
|
||||
return collectWindowsNetworkMetrics(client);
|
||||
return collectWindowsNetworkMetrics(client, hostId);
|
||||
}
|
||||
|
||||
const interfaces: Array<{
|
||||
@@ -318,13 +364,14 @@ export async function collectNetworkMetrics(
|
||||
}
|
||||
|
||||
try {
|
||||
const firstReadAt = Date.now();
|
||||
const readAt = Date.now();
|
||||
const procNet = await execCommand(client, "cat /proc/net/dev");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const procNetAfter = await execCommand(client, "cat /proc/net/dev");
|
||||
const elapsedSeconds = (Date.now() - firstReadAt) / 1000;
|
||||
const rxTxMap = parseNetworkCounters(procNet.stdout);
|
||||
const afterMap = parseNetworkCounters(procNetAfter.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousSamples.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous
|
||||
? (readAt - previous.timestamp) / 1000
|
||||
: 0;
|
||||
if (ifMap.size === 0) {
|
||||
for (const name of rxTxMap.keys()) {
|
||||
if (name !== "lo") ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
||||
@@ -332,15 +379,25 @@ export async function collectNetworkMetrics(
|
||||
}
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
const rxTx = rxTxMap.get(name);
|
||||
const after = afterMap.get(name);
|
||||
const previousCounters = previous?.counters.get(name);
|
||||
interfaces.push({
|
||||
name,
|
||||
ip: data.ip,
|
||||
state: data.state,
|
||||
rxBytes: rxTx?.rx ?? null,
|
||||
txBytes: rxTx?.tx ?? null,
|
||||
rxRateBps: counterRate(rxTx?.rx, after?.rx, elapsedSeconds),
|
||||
txRateBps: counterRate(rxTx?.tx, after?.tx, elapsedSeconds),
|
||||
rxRateBps: previousCounters
|
||||
? counterRate(previousCounters.rx, rxTx?.rx, elapsedSeconds)
|
||||
: null,
|
||||
txRateBps: previousCounters
|
||||
? counterRate(previousCounters.tx, rxTx?.tx, elapsedSeconds)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
if (hostId !== undefined) {
|
||||
previousSamples.set(hostId, {
|
||||
counters: rxTxMap,
|
||||
timestamp: readAt,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user