mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-01 08:05:23 +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,
|
LogEntry,
|
||||||
ConnectionStage,
|
ConnectionStage,
|
||||||
} from "../../../types/connection-log.js";
|
} 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 { collectMemoryMetrics } from "./widgets/memory-collector.js";
|
||||||
import {
|
import {
|
||||||
collectDiskMetrics,
|
collectDiskMetrics,
|
||||||
type DiskFilesystem,
|
type DiskFilesystem,
|
||||||
} from "./widgets/disk-collector.js";
|
} 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 { collectUptimeMetrics } from "./widgets/uptime-collector.js";
|
||||||
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
||||||
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
||||||
@@ -515,6 +521,8 @@ class PollingManager {
|
|||||||
this.syncPollConcurrency();
|
this.syncPollConcurrency();
|
||||||
this.statusStore.delete(host.id);
|
this.statusStore.delete(host.id);
|
||||||
this.metricsStore.delete(host.id);
|
this.metricsStore.delete(host.id);
|
||||||
|
clearCpuSampleCache(host.id);
|
||||||
|
clearNetworkSampleCache(host.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,6 +856,8 @@ class PollingManager {
|
|||||||
if (clearData) {
|
if (clearData) {
|
||||||
this.statusStore.delete(hostId);
|
this.statusStore.delete(hostId);
|
||||||
this.metricsStore.delete(hostId);
|
this.metricsStore.delete(hostId);
|
||||||
|
clearCpuSampleCache(hostId);
|
||||||
|
clearNetworkSampleCache(hostId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostPollCache.invalidate(hostId);
|
hostPollCache.invalidate(hostId);
|
||||||
@@ -917,6 +927,8 @@ class PollingManager {
|
|||||||
if (!currentHostIds.has(hostId)) {
|
if (!currentHostIds.has(hostId)) {
|
||||||
this.statusStore.delete(hostId);
|
this.statusStore.delete(hostId);
|
||||||
this.metricsStore.delete(hostId);
|
this.metricsStore.delete(hostId);
|
||||||
|
clearCpuSampleCache(hostId);
|
||||||
|
clearNetworkSampleCache(hostId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1771,7 +1783,7 @@ async function collectMetrics(
|
|||||||
const collectFn = async (client: Client) => {
|
const collectFn = async (client: Client) => {
|
||||||
onAuthenticated?.();
|
onAuthenticated?.();
|
||||||
const platform = await detectPlatform(client);
|
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 memory = await collectMemoryMetrics(client, platform);
|
||||||
const disk = await collectDiskMetrics(
|
const disk = await collectDiskMetrics(
|
||||||
client,
|
client,
|
||||||
@@ -1779,7 +1791,7 @@ async function collectMetrics(
|
|||||||
monitoredMounts,
|
monitoredMounts,
|
||||||
platform,
|
platform,
|
||||||
);
|
);
|
||||||
const network = await collectNetworkMetrics(client, platform);
|
const network = await collectNetworkMetrics(client, platform, host.id);
|
||||||
const uptime = await collectUptimeMetrics(client, platform);
|
const uptime = await collectUptimeMetrics(client, platform);
|
||||||
const processes = await collectProcessesMetrics(client);
|
const processes = await collectProcessesMetrics(client);
|
||||||
const system = await collectSystemMetrics(client, platform);
|
const system = await collectSystemMetrics(client, platform);
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ import {
|
|||||||
type HostPlatform,
|
type HostPlatform,
|
||||||
} from "./common-utils.js";
|
} 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(
|
export function parseCpuLine(
|
||||||
cpuLine: string,
|
cpuLine: string,
|
||||||
): { total: number; idle: number } | undefined {
|
): { total: number; idle: number } | undefined {
|
||||||
@@ -130,6 +148,7 @@ async function collectWindowsCpuMetrics(client: Client): Promise<{
|
|||||||
export async function collectCpuMetrics(
|
export async function collectCpuMetrics(
|
||||||
client: Client,
|
client: Client,
|
||||||
platform?: HostPlatform,
|
platform?: HostPlatform,
|
||||||
|
hostId?: number,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
percent: number | null;
|
percent: number | null;
|
||||||
cores: number | null;
|
cores: number | null;
|
||||||
@@ -147,7 +166,8 @@ export async function collectCpuMetrics(
|
|||||||
let loadTriplet: [number, number, number] | null = null;
|
let loadTriplet: [number, number, number] | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [stat1, loadAvgOut, coresOut] = await Promise.race([
|
const readAt = Date.now();
|
||||||
|
const [statOut, loadAvgOut, coresOut] = await Promise.race([
|
||||||
Promise.all([
|
Promise.all([
|
||||||
execCommand(client, "cat /proc/stat"),
|
execCommand(client, "cat /proc/stat"),
|
||||||
execCommand(client, "cat /proc/loadavg"),
|
execCommand(client, "cat /proc/loadavg"),
|
||||||
@@ -164,24 +184,22 @@ export async function collectCpuMetrics(
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
const cpuLine = (
|
||||||
const stat2 = await execCommand(client, "cat /proc/stat");
|
statOut.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
||||||
|
|
||||||
const cpuLine1 = (
|
|
||||||
stat1.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
|
||||||
).trim();
|
).trim();
|
||||||
const cpuLine2 = (
|
const current = parseCpuLine(cpuLine);
|
||||||
stat2.stdout.split("\n").find((l) => l.startsWith("cpu ")) || ""
|
const previous =
|
||||||
).trim();
|
hostId !== undefined ? previousStat.get(hostId) : undefined;
|
||||||
const a = parseCpuLine(cpuLine1);
|
if (current && previous) {
|
||||||
const b = parseCpuLine(cpuLine2);
|
const totalDiff = current.total - previous.total;
|
||||||
if (a && b) {
|
const idleDiff = current.idle - previous.idle;
|
||||||
const totalDiff = b.total - a.total;
|
|
||||||
const idleDiff = b.idle - a.idle;
|
|
||||||
const used = totalDiff - idleDiff;
|
const used = totalDiff - idleDiff;
|
||||||
if (totalDiff > 0)
|
if (totalDiff > 0)
|
||||||
cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100));
|
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+/);
|
const laParts = loadAvgOut.stdout.trim().split(/\s+/);
|
||||||
if (laParts.length >= 3) {
|
if (laParts.length >= 3) {
|
||||||
|
|||||||
@@ -44,6 +44,31 @@ export function counterRate(
|
|||||||
return Math.round((second - first) / elapsedSeconds);
|
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(
|
export function parseDarwinIfconfig(
|
||||||
output: string,
|
output: string,
|
||||||
): Map<string, { ip: string; state: string }> {
|
): Map<string, { ip: string; state: string }> {
|
||||||
@@ -85,7 +110,10 @@ export function parseDarwinNetstat(
|
|||||||
return counters;
|
return counters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function collectDarwinNetworkMetrics(client: Client): Promise<{
|
async function collectDarwinNetworkMetrics(
|
||||||
|
client: Client,
|
||||||
|
hostId?: number,
|
||||||
|
): Promise<{
|
||||||
interfaces: Array<{
|
interfaces: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -111,31 +139,35 @@ async function collectDarwinNetworkMetrics(client: Client): Promise<{
|
|||||||
const ifMap = parseDarwinIfconfig(ifconfigOut.stdout);
|
const ifMap = parseDarwinIfconfig(ifconfigOut.stdout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const firstReadAt = Date.now();
|
const readAt = Date.now();
|
||||||
const netstat1 = await execCommand(client, "netstat -ib 2>/dev/null");
|
const netstatOut = await execCommand(client, "netstat -ib 2>/dev/null");
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
const current = parseDarwinNetstat(netstatOut.stdout);
|
||||||
const netstat2 = await execCommand(client, "netstat -ib 2>/dev/null");
|
const previous =
|
||||||
const elapsedSeconds = (Date.now() - firstReadAt) / 1000;
|
hostId !== undefined ? previousSamples.get(hostId) : undefined;
|
||||||
const before = parseDarwinNetstat(netstat1.stdout);
|
const elapsedSeconds = previous
|
||||||
const after = parseDarwinNetstat(netstat2.stdout);
|
? (readAt - previous.timestamp) / 1000
|
||||||
|
: 0;
|
||||||
if (ifMap.size === 0) {
|
if (ifMap.size === 0) {
|
||||||
for (const name of before.keys()) {
|
for (const name of current.keys()) {
|
||||||
ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [name, data] of ifMap.entries()) {
|
for (const [name, data] of ifMap.entries()) {
|
||||||
const b = before.get(name);
|
const c = current.get(name);
|
||||||
const a = after.get(name);
|
const p = previous?.counters.get(name);
|
||||||
interfaces.push({
|
interfaces.push({
|
||||||
name,
|
name,
|
||||||
ip: data.ip,
|
ip: data.ip,
|
||||||
state: data.state,
|
state: data.state,
|
||||||
rxBytes: b?.rx ?? null,
|
rxBytes: c?.rx ?? null,
|
||||||
txBytes: b?.tx ?? null,
|
txBytes: c?.tx ?? null,
|
||||||
rxRateBps: counterRate(b?.rx, a?.rx, elapsedSeconds),
|
rxRateBps: p ? counterRate(p.rx, c?.rx, elapsedSeconds) : null,
|
||||||
txRateBps: counterRate(b?.tx, a?.tx, elapsedSeconds),
|
txRateBps: p ? counterRate(p.tx, c?.tx, elapsedSeconds) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (hostId !== undefined) {
|
||||||
|
previousSamples.set(hostId, { counters: current, timestamp: readAt });
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
for (const [name, data] of ifMap.entries()) {
|
for (const [name, data] of ifMap.entries()) {
|
||||||
interfaces.push({
|
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<{
|
interfaces: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -213,27 +248,37 @@ async function collectWindowsNetworkMetrics(client: Client): Promise<{
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const firstReadAt = Date.now();
|
const readAt = Date.now();
|
||||||
const before = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
const result = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
const currentRows = parseWindowsAdapterJson(result.stdout);
|
||||||
const after = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
const previous =
|
||||||
const elapsedSeconds = (Date.now() - firstReadAt) / 1000;
|
hostId !== undefined ? previousWindowsSamples.get(hostId) : undefined;
|
||||||
|
const elapsedSeconds = previous ? (readAt - previous.timestamp) / 1000 : 0;
|
||||||
const beforeRows = parseWindowsAdapterJson(before.stdout);
|
const previousMap = new Map(
|
||||||
const afterMap = new Map(
|
(previous?.rows ?? []).map((row) => [row.name, row]),
|
||||||
parseWindowsAdapterJson(after.stdout).map((row) => [row.name, row]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const row of beforeRows) {
|
for (const row of currentRows) {
|
||||||
const afterRow = afterMap.get(row.name);
|
const previousRow = previousMap.get(row.name);
|
||||||
interfaces.push({
|
interfaces.push({
|
||||||
name: row.name,
|
name: row.name,
|
||||||
ip: row.ip,
|
ip: row.ip,
|
||||||
state: row.state,
|
state: row.state,
|
||||||
rxBytes: row.rx || null,
|
rxBytes: row.rx || null,
|
||||||
txBytes: row.tx || null,
|
txBytes: row.tx || null,
|
||||||
rxRateBps: counterRate(row.rx, afterRow?.rx, elapsedSeconds),
|
rxRateBps: previousRow
|
||||||
txRateBps: counterRate(row.tx, afterRow?.tx, elapsedSeconds),
|
? 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 {
|
} catch {
|
||||||
@@ -246,6 +291,7 @@ async function collectWindowsNetworkMetrics(client: Client): Promise<{
|
|||||||
export async function collectNetworkMetrics(
|
export async function collectNetworkMetrics(
|
||||||
client: Client,
|
client: Client,
|
||||||
platform?: HostPlatform,
|
platform?: HostPlatform,
|
||||||
|
hostId?: number,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
interfaces: Array<{
|
interfaces: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -258,10 +304,10 @@ export async function collectNetworkMetrics(
|
|||||||
}>;
|
}>;
|
||||||
}> {
|
}> {
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
return collectDarwinNetworkMetrics(client);
|
return collectDarwinNetworkMetrics(client, hostId);
|
||||||
}
|
}
|
||||||
if (platform === "windows") {
|
if (platform === "windows") {
|
||||||
return collectWindowsNetworkMetrics(client);
|
return collectWindowsNetworkMetrics(client, hostId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const interfaces: Array<{
|
const interfaces: Array<{
|
||||||
@@ -318,13 +364,14 @@ export async function collectNetworkMetrics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const firstReadAt = Date.now();
|
const readAt = Date.now();
|
||||||
const procNet = await execCommand(client, "cat /proc/net/dev");
|
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 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) {
|
if (ifMap.size === 0) {
|
||||||
for (const name of rxTxMap.keys()) {
|
for (const name of rxTxMap.keys()) {
|
||||||
if (name !== "lo") ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
if (name !== "lo") ifMap.set(name, { ip: "", state: "UNKNOWN" });
|
||||||
@@ -332,15 +379,25 @@ export async function collectNetworkMetrics(
|
|||||||
}
|
}
|
||||||
for (const [name, data] of ifMap.entries()) {
|
for (const [name, data] of ifMap.entries()) {
|
||||||
const rxTx = rxTxMap.get(name);
|
const rxTx = rxTxMap.get(name);
|
||||||
const after = afterMap.get(name);
|
const previousCounters = previous?.counters.get(name);
|
||||||
interfaces.push({
|
interfaces.push({
|
||||||
name,
|
name,
|
||||||
ip: data.ip,
|
ip: data.ip,
|
||||||
state: data.state,
|
state: data.state,
|
||||||
rxBytes: rxTx?.rx ?? null,
|
rxBytes: rxTx?.rx ?? null,
|
||||||
txBytes: rxTx?.tx ?? null,
|
txBytes: rxTx?.tx ?? null,
|
||||||
rxRateBps: counterRate(rxTx?.rx, after?.rx, elapsedSeconds),
|
rxRateBps: previousCounters
|
||||||
txRateBps: counterRate(rxTx?.tx, after?.tx, elapsedSeconds),
|
? 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 {
|
} catch {
|
||||||
|
|||||||
Reference in new issue
Block a user