pyre-cli 9.5.0 → 9.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/blender-7UT5J7IP.js +7 -0
- package/dist/{checkCmd-ULNENOPZ.js → checkCmd-T7O4KXXA.js} +4 -3
- package/dist/chunk-25I533OF.js +133 -0
- package/dist/{chunk-MOAEGMFP.js → chunk-3TE7MJLS.js} +168 -36
- package/dist/{chunk-RVGKEJXT.js → chunk-CXL4GYGQ.js} +3 -0
- package/dist/{chunk-PV4MXTAJ.js → chunk-N4WJVF5M.js} +289 -23
- package/dist/{collectors-VYUH3UWY.js → collectors-JB4OGUEF.js} +4 -1
- package/dist/{config-4ZLF4HTS.js → config-KS5ZZ3VN.js} +1 -1
- package/dist/{doctor-L6FZYA7F.js → doctor-UV7TWIHX.js} +1 -1
- package/dist/index.js +419 -156
- package/dist/{pipeCmd-BWNMUV2X.js → pipeCmd-TYLJHQ5B.js} +3 -2
- package/dist/{wizard-GVIAHIVV.js → wizard-Z3RGA3VB.js} +1 -1
- package/package.json +1 -1
- package/src/anomalies.ts +114 -60
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +100 -13
- package/src/formatters/render.ts +240 -28
- package/src/formatters/themes.ts +8 -0
- package/src/formatters/types.ts +4 -1
- package/src/index.ts +53 -26
- package/src/live/input.ts +62 -4
- package/src/live/render.ts +31 -0
- package/src/live/state.ts +65 -52
- package/src/live/types.ts +2 -2
- package/src/monitors/blender.ts +144 -0
- package/src/monitors/collectors.ts +287 -40
- package/src/monitors/dns_cache.ts +48 -0
- package/src/monitors/index.ts +3 -2
- package/src/monitors/smc.ts +27 -0
- package/src/monitors/types.ts +97 -14
- package/src/state/config.ts +6 -0
- /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
|
@@ -15,7 +15,9 @@ import chalk from 'chalk';
|
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import { run } from './run.js';
|
|
17
17
|
import { getSmcMetrics, parseSuffix } from './smc.js';
|
|
18
|
-
import
|
|
18
|
+
import { resolveIp } from './dns_cache.js';
|
|
19
|
+
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, NetworkConnection, BlenderRenderData, ProtocolStats, ConnectionStateStats, RemoteHostInfo } from './types.js';
|
|
20
|
+
import { collectBlenderRenders } from './blender.js';
|
|
19
21
|
|
|
20
22
|
const SP_TTL_MS = 10_000;
|
|
21
23
|
const NETSTAT_TTL_MS = 1000;
|
|
@@ -30,6 +32,8 @@ let netstatIbCache: { raw: string; ts: number } | null = null;
|
|
|
30
32
|
let routeCache: { iface: string; ip: string; ts: number } | null = null;
|
|
31
33
|
const sysctlCache = new Map<string, { value: string; ts: number }>();
|
|
32
34
|
|
|
35
|
+
let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
|
|
36
|
+
|
|
33
37
|
async function cachedSysctl(key: string, fallback: string): Promise<string> {
|
|
34
38
|
const now = Date.now();
|
|
35
39
|
const hit = sysctlCache.get(key);
|
|
@@ -127,7 +131,7 @@ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmp
|
|
|
127
131
|
* complete {@link StatsData} snapshot.
|
|
128
132
|
*/
|
|
129
133
|
export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
|
|
130
|
-
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
|
|
134
|
+
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders] = await Promise.all([
|
|
131
135
|
collectCpu(),
|
|
132
136
|
collectGpu(opts.detailed),
|
|
133
137
|
collectMemory(),
|
|
@@ -140,6 +144,8 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
140
144
|
collectPower(),
|
|
141
145
|
collectPackets(),
|
|
142
146
|
collectTasks(),
|
|
147
|
+
collectContainers(),
|
|
148
|
+
collectBlenderRenders(),
|
|
143
149
|
]);
|
|
144
150
|
|
|
145
151
|
return {
|
|
@@ -161,9 +167,34 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
161
167
|
timestamp: new Date().toISOString(),
|
|
162
168
|
packets,
|
|
163
169
|
tasks,
|
|
170
|
+
containers,
|
|
171
|
+
blenderRenders,
|
|
164
172
|
};
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
export async function collectContainers(): Promise<ContainerData[]> {
|
|
176
|
+
try {
|
|
177
|
+
const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', '')).trim();
|
|
178
|
+
if (!raw) return [];
|
|
179
|
+
|
|
180
|
+
return raw.split('\n').map(line => {
|
|
181
|
+
const parts = line.split('|');
|
|
182
|
+
return {
|
|
183
|
+
id: parts[0],
|
|
184
|
+
name: parts[1],
|
|
185
|
+
cpuPercent: parseFloat(parts[2].replace('%', '')) || 0,
|
|
186
|
+
memUsage: parts[3],
|
|
187
|
+
memPercent: parseFloat(parts[4].replace('%', '')) || 0,
|
|
188
|
+
netIO: parts[5],
|
|
189
|
+
blockIO: parts[6],
|
|
190
|
+
pids: parseInt(parts[7], 10) || 0
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
167
198
|
/**
|
|
168
199
|
* Read power-draw metrics (CPU watts, GPU watts, combined)
|
|
169
200
|
* from the SMC via powermetrics. Returns `null` when no
|
|
@@ -829,9 +860,166 @@ export async function collectNetwork(): Promise<NetworkData> {
|
|
|
829
860
|
}
|
|
830
861
|
}
|
|
831
862
|
|
|
832
|
-
|
|
863
|
+
const now = Date.now();
|
|
864
|
+
let rxRate = 0;
|
|
865
|
+
let txRate = 0;
|
|
866
|
+
if (prevNetSample && prevNetSample.ts > 0) {
|
|
867
|
+
const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
|
|
868
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
869
|
+
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
870
|
+
}
|
|
871
|
+
prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
872
|
+
|
|
873
|
+
const protocolStats = await getProtocolStats();
|
|
874
|
+
const connectionStates = await getConnectionStates();
|
|
875
|
+
const listeningPorts = await getListeningPorts();
|
|
876
|
+
const establishedConnections = connectionStates.established;
|
|
877
|
+
const topRemoteHosts = await getTopRemoteHosts();
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
interface: iface,
|
|
881
|
+
ip,
|
|
882
|
+
rxBytes,
|
|
883
|
+
txBytes,
|
|
884
|
+
rxPackets,
|
|
885
|
+
txPackets,
|
|
886
|
+
rxRate,
|
|
887
|
+
txRate,
|
|
888
|
+
connections: establishedConnections,
|
|
889
|
+
protocols: protocolStats,
|
|
890
|
+
connectionStates,
|
|
891
|
+
listeningPorts,
|
|
892
|
+
establishedConnections,
|
|
893
|
+
topRemoteHosts,
|
|
894
|
+
};
|
|
895
|
+
} catch {
|
|
896
|
+
return {
|
|
897
|
+
interface: 'unknown',
|
|
898
|
+
ip: '0.0.0.0',
|
|
899
|
+
rxBytes: 0,
|
|
900
|
+
txBytes: 0,
|
|
901
|
+
rxPackets: 0,
|
|
902
|
+
txPackets: 0,
|
|
903
|
+
rxRate: 0,
|
|
904
|
+
txRate: 0,
|
|
905
|
+
connections: 0,
|
|
906
|
+
protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
|
|
907
|
+
connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
|
|
908
|
+
listeningPorts: [],
|
|
909
|
+
establishedConnections: 0,
|
|
910
|
+
topRemoteHosts: [],
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function getProtocolStats(): Promise<ProtocolStats> {
|
|
916
|
+
try {
|
|
917
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", '');
|
|
918
|
+
const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
|
|
919
|
+
const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
|
|
920
|
+
|
|
921
|
+
let tcpRxBytes = 0;
|
|
922
|
+
let tcpTxBytes = 0;
|
|
923
|
+
let udpRxBytes = 0;
|
|
924
|
+
let udpTxBytes = 0;
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
const netstat = await cachedNetstatIb();
|
|
928
|
+
for (const line of netstat.split('\n')) {
|
|
929
|
+
const parts = line.split(/\s+/);
|
|
930
|
+
if (parts.length >= 9 && parts[0] && !parts[0].startsWith('Name')) {
|
|
931
|
+
const iface = parts[0];
|
|
932
|
+
if (iface.includes('lo')) continue;
|
|
933
|
+
const rx = parseInt(parts[5]) || 0;
|
|
934
|
+
const tx = parseInt(parts[7]) || 0;
|
|
935
|
+
tcpRxBytes += rx;
|
|
936
|
+
tcpTxBytes += tx;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
} catch {
|
|
940
|
+
// ignore
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
tcp: {
|
|
945
|
+
connections: parseInt(tcpConnections) || 0,
|
|
946
|
+
rxBytes: tcpRxBytes,
|
|
947
|
+
txBytes: tcpTxBytes,
|
|
948
|
+
},
|
|
949
|
+
udp: {
|
|
950
|
+
connections: parseInt(udpConnections) || 0,
|
|
951
|
+
rxBytes: udpRxBytes,
|
|
952
|
+
txBytes: udpTxBytes,
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
} catch {
|
|
956
|
+
return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function getConnectionStates(): Promise<ConnectionStateStats> {
|
|
961
|
+
try {
|
|
962
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", '');
|
|
963
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
964
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
965
|
+
|
|
966
|
+
for (const line of lines) {
|
|
967
|
+
const match = line.match(/(\d+)\s+(\S+)/);
|
|
968
|
+
if (!match) continue;
|
|
969
|
+
const count = parseInt(match[1]);
|
|
970
|
+
const state = match[2].toUpperCase();
|
|
971
|
+
|
|
972
|
+
if (state.includes('ESTABLISHED')) stats.established += count;
|
|
973
|
+
else if (state.includes('LISTEN')) stats.listening += count;
|
|
974
|
+
else if (state.includes('TIME_WAIT')) stats.timeWait += count;
|
|
975
|
+
else if (state.includes('CLOSE_WAIT')) stats.closeWait += count;
|
|
976
|
+
else stats.other += count;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return stats;
|
|
980
|
+
} catch {
|
|
981
|
+
return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function getListeningPorts(): Promise<number[]> {
|
|
986
|
+
try {
|
|
987
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", '');
|
|
988
|
+
const ports = raw.split('\n')
|
|
989
|
+
.map(p => parseInt(p, 10))
|
|
990
|
+
.filter(p => !isNaN(p) && p > 0);
|
|
991
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
992
|
+
} catch {
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function getTopRemoteHosts(): Promise<RemoteHostInfo[]> {
|
|
998
|
+
try {
|
|
999
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", '');
|
|
1000
|
+
const ports = raw.split('\n').filter(Boolean);
|
|
1001
|
+
const hostMap = new Map<string, { count: number; bytes: number }>();
|
|
1002
|
+
|
|
1003
|
+
for (const port of ports) {
|
|
1004
|
+
const host = port.split(':')[0];
|
|
1005
|
+
if (!host || host === '127.0.0.1' || host === '::1') continue;
|
|
1006
|
+
const existing = hostMap.get(host) || { count: 0, bytes: 0 };
|
|
1007
|
+
hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const result: RemoteHostInfo[] = [];
|
|
1011
|
+
for (const [host, info] of hostMap.entries()) {
|
|
1012
|
+
result.push({
|
|
1013
|
+
host,
|
|
1014
|
+
ip: host,
|
|
1015
|
+
bytesTransferred: info.bytes,
|
|
1016
|
+
connectionCount: info.count,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
|
|
833
1021
|
} catch {
|
|
834
|
-
return
|
|
1022
|
+
return [];
|
|
835
1023
|
}
|
|
836
1024
|
}
|
|
837
1025
|
|
|
@@ -886,47 +1074,106 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
886
1074
|
}
|
|
887
1075
|
|
|
888
1076
|
let connections = 0;
|
|
1077
|
+
let allProcesses: NetworkProcess[] = [];
|
|
1078
|
+
|
|
889
1079
|
try {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (
|
|
904
|
-
|
|
1080
|
+
const nettop = (await run('nettop -L 1 -J bytes_in,bytes_out,state 2>/dev/null', '', 2000)).trim();
|
|
1081
|
+
const rows = nettop.split('\n');
|
|
1082
|
+
|
|
1083
|
+
let currentProc: NetworkProcess | null = null;
|
|
1084
|
+
|
|
1085
|
+
for (const row of rows) {
|
|
1086
|
+
if (!row || row.startsWith(',state,')) continue;
|
|
1087
|
+
|
|
1088
|
+
const cols = row.split(',');
|
|
1089
|
+
const nameCol = cols[0];
|
|
1090
|
+
|
|
1091
|
+
// If it doesn't contain <->, it's a process row
|
|
1092
|
+
if (!nameCol.includes('<->')) {
|
|
1093
|
+
if (currentProc) {
|
|
1094
|
+
if (currentProc.connections && currentProc.connections.length > 0) {
|
|
1095
|
+
allProcesses.push(currentProc);
|
|
1096
|
+
}
|
|
905
1097
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1098
|
+
|
|
1099
|
+
// Extract PID and Name (e.g., "syslogd.369")
|
|
1100
|
+
const lastDot = nameCol.lastIndexOf('.');
|
|
1101
|
+
let pid = 0;
|
|
1102
|
+
let command = nameCol;
|
|
1103
|
+
if (lastDot > 0) {
|
|
1104
|
+
const possiblePid = parseInt(nameCol.substring(lastDot + 1));
|
|
1105
|
+
if (!isNaN(possiblePid)) {
|
|
1106
|
+
pid = possiblePid;
|
|
1107
|
+
command = nameCol.substring(0, lastDot);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const rx = parseInt(cols[2]) || 0;
|
|
1112
|
+
const tx = parseInt(cols[3]) || 0;
|
|
1113
|
+
|
|
1114
|
+
currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
|
|
1115
|
+
} else {
|
|
1116
|
+
// Connection row
|
|
1117
|
+
if (currentProc) {
|
|
1118
|
+
const state = cols[1];
|
|
1119
|
+
const rx = parseInt(cols[2]) || 0;
|
|
1120
|
+
const tx = parseInt(cols[3]) || 0;
|
|
1121
|
+
if (state === 'Established') connections++;
|
|
1122
|
+
|
|
1123
|
+
// Parse endpoints (e.g. "tcp4 172.16.0.2:49844<->17.242.13.5:5223")
|
|
1124
|
+
const spaceIdx = nameCol.indexOf(' ');
|
|
1125
|
+
const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : '';
|
|
1126
|
+
const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
|
|
1127
|
+
const arrowIdx = endpoints.indexOf('<->');
|
|
1128
|
+
let local = '';
|
|
1129
|
+
let remote = '';
|
|
1130
|
+
if (arrowIdx > 0) {
|
|
1131
|
+
local = endpoints.substring(0, arrowIdx);
|
|
1132
|
+
remote = endpoints.substring(arrowIdx + 3);
|
|
1133
|
+
|
|
1134
|
+
// Attempt DNS resolution on remote IP
|
|
1135
|
+
remote = resolveIp(remote);
|
|
1136
|
+
} else {
|
|
1137
|
+
local = endpoints;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Ignore purely local communication if you want, but for now include all
|
|
1141
|
+
currentProc.connections!.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
|
|
1146
|
+
allProcesses.push(currentProc);
|
|
1147
|
+
}
|
|
910
1148
|
} catch {
|
|
911
|
-
topProcesses = [];
|
|
912
1149
|
allProcesses = [];
|
|
913
1150
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1151
|
+
|
|
1152
|
+
// Sort processes by total bandwidth (tx + rx) to get top processes
|
|
1153
|
+
const topProcesses = [...allProcesses].sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
|
|
1154
|
+
|
|
1155
|
+
const protocolStats = await getProtocolStats();
|
|
1156
|
+
const connectionStates = await getConnectionStates();
|
|
1157
|
+
const listeningPorts = await getListeningPorts();
|
|
1158
|
+
|
|
1159
|
+
return {
|
|
1160
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
1161
|
+
rxPackets: totalRxPackets,
|
|
1162
|
+
txPackets: totalTxPackets,
|
|
1163
|
+
rxRate: 0,
|
|
1164
|
+
txRate: 0,
|
|
1165
|
+
connections,
|
|
1166
|
+
topProcesses,
|
|
1167
|
+
interfaces: ifaceStats,
|
|
1168
|
+
allProcesses,
|
|
1169
|
+
protocolStats,
|
|
1170
|
+
connectionStates,
|
|
1171
|
+
listeningPorts,
|
|
1172
|
+
};
|
|
1173
|
+
} catch {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
930
1177
|
|
|
931
1178
|
export async function collectTasks(limit = 12): Promise<TaskData[]> {
|
|
932
1179
|
try {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import dns from 'node:dns';
|
|
2
|
+
|
|
3
|
+
const cache = new Map<string, string>();
|
|
4
|
+
const pending = new Set<string>();
|
|
5
|
+
|
|
6
|
+
export function resolveIp(ip: string): string {
|
|
7
|
+
// Extract IP if it has port (e.g., 17.242.13.5:5223 or [2606:4700...]:443)
|
|
8
|
+
let cleanIp = ip;
|
|
9
|
+
if (cleanIp.includes(']:')) {
|
|
10
|
+
cleanIp = cleanIp.substring(1, cleanIp.indexOf(']'));
|
|
11
|
+
} else if (cleanIp.includes(':') && cleanIp.split(':').length === 2) {
|
|
12
|
+
cleanIp = cleanIp.split(':')[0];
|
|
13
|
+
} else if (cleanIp.includes(':') && !cleanIp.includes(']')) {
|
|
14
|
+
// ipv6 without port or brackets?
|
|
15
|
+
// If there's more than one colon, it's ipv6.
|
|
16
|
+
const lastColon = cleanIp.lastIndexOf(':');
|
|
17
|
+
const isPort = !cleanIp.includes(':', cleanIp.indexOf(':') + 1); // Only 1 colon means it's a port. Wait, ipv6 has many colons.
|
|
18
|
+
// E.g., 2606:4700:103::2.443 -> nettop uses `.` for port in ipv6!
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle nettop's IPv6 port format: 2606:4700:103::2.443 (the last dot separates the port)
|
|
22
|
+
if (cleanIp.includes(':') && cleanIp.includes('.')) {
|
|
23
|
+
const lastDot = cleanIp.lastIndexOf('.');
|
|
24
|
+
if (lastDot > cleanIp.lastIndexOf(':')) {
|
|
25
|
+
cleanIp = cleanIp.substring(0, lastDot);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (cache.has(cleanIp)) return cache.get(cleanIp)!;
|
|
30
|
+
if (pending.has(cleanIp)) return ip;
|
|
31
|
+
|
|
32
|
+
// Don't resolve local/multicast/broadcast
|
|
33
|
+
if (cleanIp.startsWith('127.') || cleanIp.startsWith('192.168.') || cleanIp.startsWith('10.') || cleanIp.startsWith('172.16.') || cleanIp.startsWith('ff02:') || cleanIp === '*:*' || cleanIp === '*' || cleanIp.startsWith('fe80:')) {
|
|
34
|
+
return ip;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pending.add(cleanIp);
|
|
38
|
+
dns.reverse(cleanIp, (err, hostnames) => {
|
|
39
|
+
if (!err && hostnames && hostnames.length > 0) {
|
|
40
|
+
cache.set(cleanIp, hostnames[0]);
|
|
41
|
+
} else {
|
|
42
|
+
cache.set(cleanIp, cleanIp); // don't try again
|
|
43
|
+
}
|
|
44
|
+
pending.delete(cleanIp);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return ip;
|
|
48
|
+
}
|
package/src/monitors/index.ts
CHANGED
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
* - `collectors.ts` — individual metric collectors and the orchestrator
|
|
11
11
|
* - `run.ts` — shared shell-execution helper
|
|
12
12
|
*/
|
|
13
|
-
export { collectAll, collectPower, collectPackets, collectTasks } from './collectors.js';
|
|
14
|
-
export
|
|
13
|
+
export { collectAll, collectPower, collectPackets, collectTasks, collectContainers } from './collectors.js';
|
|
14
|
+
export { collectBlenderRenders } from './blender.js';
|
|
15
|
+
export type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, BlenderRenderData } from './types.js';
|
package/src/monitors/smc.ts
CHANGED
|
@@ -99,6 +99,33 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// 2b. AppleSmartBattery fallback — works without sudo on all MacBooks
|
|
103
|
+
// VirtualTemperature reflects the SoC/die temperature; Temperature is
|
|
104
|
+
// the battery cell temperature. Both are reported in centi-Celsius
|
|
105
|
+
// (e.g. 3539 → 35.39 °C).
|
|
106
|
+
if (result.temps['cpu_die'] === undefined) {
|
|
107
|
+
try {
|
|
108
|
+
const battRaw = await run('ioreg -r -n AppleSmartBattery 2>/dev/null', '');
|
|
109
|
+
if (battRaw) {
|
|
110
|
+
// Prefer VirtualTemperature (closer to die temp) over Temperature (battery cell)
|
|
111
|
+
const virtMatch = battRaw.match(/"VirtualTemperature"\s*=\s*(\d+)/);
|
|
112
|
+
const cellMatch = battRaw.match(/"Temperature"\s*=\s*(\d+)/);
|
|
113
|
+
const rawVirt = virtMatch ? parseFloat(virtMatch[1]) : undefined;
|
|
114
|
+
const rawCell = cellMatch ? parseFloat(cellMatch[1]) : undefined;
|
|
115
|
+
// Convert centi-Celsius to Celsius (e.g. 3085 → 30.85 → 30.9)
|
|
116
|
+
const virtC = rawVirt !== undefined ? Math.round(rawVirt / 10) / 10 : undefined;
|
|
117
|
+
const cellC = rawCell !== undefined ? Math.round(rawCell / 10) / 10 : undefined;
|
|
118
|
+
if (virtC !== undefined && virtC >= 15 && virtC <= 115) {
|
|
119
|
+
result.temps['cpu_die'] = virtC;
|
|
120
|
+
} else if (cellC !== undefined && cellC >= 15 && cellC <= 115) {
|
|
121
|
+
result.temps['cpu_die'] = cellC;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
// 3. Linux /sys/class/thermal or /sys/class/hwmon fallback
|
|
103
130
|
if (result.temps['cpu_die'] === undefined) {
|
|
104
131
|
try {
|
package/src/monitors/types.ts
CHANGED
|
@@ -21,6 +21,19 @@ export interface StatsData {
|
|
|
21
21
|
timestamp: string;
|
|
22
22
|
packets: PacketData | null;
|
|
23
23
|
tasks: TaskData[];
|
|
24
|
+
containers?: ContainerData[];
|
|
25
|
+
blenderRenders?: BlenderRenderData[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ContainerData {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
cpuPercent: number;
|
|
32
|
+
memUsage: string;
|
|
33
|
+
memPercent: number;
|
|
34
|
+
netIO: string;
|
|
35
|
+
blockIO: string;
|
|
36
|
+
pids: number;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export interface GpuData {
|
|
@@ -38,18 +51,20 @@ export interface GpuData {
|
|
|
38
51
|
combinedWatts?: number;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
export interface PacketData {
|
|
55
|
+
totalPackets: number;
|
|
56
|
+
rxPackets: number;
|
|
57
|
+
txPackets: number;
|
|
58
|
+
rxRate: number;
|
|
59
|
+
txRate: number;
|
|
60
|
+
connections: number;
|
|
61
|
+
topProcesses: NetworkProcess[];
|
|
62
|
+
allProcesses: NetworkProcess[];
|
|
63
|
+
interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
|
|
64
|
+
protocolStats?: ProtocolStats;
|
|
65
|
+
connectionStates?: ConnectionStateStats;
|
|
66
|
+
listeningPorts?: number[];
|
|
67
|
+
}
|
|
53
68
|
|
|
54
69
|
export interface TaskData {
|
|
55
70
|
pid: number;
|
|
@@ -138,10 +153,60 @@ export interface NetworkData {
|
|
|
138
153
|
txBytes: number;
|
|
139
154
|
rxPackets: number;
|
|
140
155
|
txPackets: number;
|
|
156
|
+
rxRate: number;
|
|
157
|
+
txRate: number;
|
|
141
158
|
rxPacketsPerSec?: number;
|
|
142
159
|
txPacketsPerSec?: number;
|
|
143
160
|
connections?: number;
|
|
144
161
|
topProcesses?: NetworkProcess[];
|
|
162
|
+
protocols?: ProtocolStats;
|
|
163
|
+
connectionStates?: ConnectionStateStats;
|
|
164
|
+
listeningPorts?: number[];
|
|
165
|
+
establishedConnections?: number;
|
|
166
|
+
topRemoteHosts?: RemoteHostInfo[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ProtocolStats {
|
|
170
|
+
tcp: { connections: number; rxBytes: number; txBytes: number };
|
|
171
|
+
udp: { connections: number; rxBytes: number; txBytes: number };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ConnectionStateStats {
|
|
175
|
+
established: number;
|
|
176
|
+
listening: number;
|
|
177
|
+
timeWait: number;
|
|
178
|
+
closeWait: number;
|
|
179
|
+
other: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface RemoteHostInfo {
|
|
183
|
+
host: string;
|
|
184
|
+
ip: string;
|
|
185
|
+
country?: string;
|
|
186
|
+
city?: string;
|
|
187
|
+
asn?: string;
|
|
188
|
+
bytesTransferred: number;
|
|
189
|
+
connectionCount: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface DnsQuery {
|
|
193
|
+
timestamp: number;
|
|
194
|
+
domain: string;
|
|
195
|
+
type: string;
|
|
196
|
+
process: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface NetworkConnection {
|
|
200
|
+
protocol: string;
|
|
201
|
+
local: string;
|
|
202
|
+
remote: string;
|
|
203
|
+
state: string;
|
|
204
|
+
rxBytes: number;
|
|
205
|
+
txBytes: number;
|
|
206
|
+
remoteHost?: string;
|
|
207
|
+
country?: string;
|
|
208
|
+
city?: string;
|
|
209
|
+
asn?: string;
|
|
145
210
|
}
|
|
146
211
|
|
|
147
212
|
export interface NetworkProcess {
|
|
@@ -149,8 +214,11 @@ export interface NetworkProcess {
|
|
|
149
214
|
command: string;
|
|
150
215
|
rxBytes: number;
|
|
151
216
|
txBytes: number;
|
|
152
|
-
|
|
153
|
-
|
|
217
|
+
rxRate?: number;
|
|
218
|
+
txRate?: number;
|
|
219
|
+
rxPackets?: number;
|
|
220
|
+
txPackets?: number;
|
|
221
|
+
connections?: NetworkConnection[];
|
|
154
222
|
}
|
|
155
223
|
|
|
156
224
|
export interface ProcessData {
|
|
@@ -164,4 +232,19 @@ export interface ProcessData {
|
|
|
164
232
|
threads: number;
|
|
165
233
|
runtime: number;
|
|
166
234
|
isGpuOrMlAttributed?: boolean;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface BlenderRenderData {
|
|
238
|
+
pid: number;
|
|
239
|
+
blendFile: string;
|
|
240
|
+
renderEngine: string;
|
|
241
|
+
frameStart: number;
|
|
242
|
+
frameEnd: number;
|
|
243
|
+
currentFrame: number;
|
|
244
|
+
totalFrames: number;
|
|
245
|
+
elapsedSec: number;
|
|
246
|
+
completionPercent: number;
|
|
247
|
+
status: 'rendering' | 'finalizing' | 'loading' | 'idle' | 'unknown';
|
|
248
|
+
outputPath: string;
|
|
249
|
+
sampleCount?: number;
|
|
167
250
|
}
|
package/src/state/config.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface PyreConfig {
|
|
|
41
41
|
watchdogMem?: number;
|
|
42
42
|
webhookUrl?: string;
|
|
43
43
|
alertCmd?: string;
|
|
44
|
+
dockerModeConfirmed?: boolean;
|
|
44
45
|
visiblePanels?: {
|
|
45
46
|
cpu?: boolean;
|
|
46
47
|
mem?: boolean;
|
|
@@ -53,6 +54,8 @@ export interface PyreConfig {
|
|
|
53
54
|
tasks?: boolean;
|
|
54
55
|
disk?: boolean;
|
|
55
56
|
process?: boolean;
|
|
57
|
+
containers?: boolean;
|
|
58
|
+
blender?: boolean;
|
|
56
59
|
tree?: boolean;
|
|
57
60
|
};
|
|
58
61
|
panelLayout?: string[];
|
|
@@ -83,6 +86,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
83
86
|
splashAnimation: 'classic',
|
|
84
87
|
webhookUrl: '',
|
|
85
88
|
alertCmd: '',
|
|
89
|
+
dockerModeConfirmed: false,
|
|
86
90
|
visiblePanels: {
|
|
87
91
|
cpu: true,
|
|
88
92
|
mem: true,
|
|
@@ -95,6 +99,8 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
|
|
|
95
99
|
tasks: true,
|
|
96
100
|
disk: true,
|
|
97
101
|
process: true,
|
|
102
|
+
containers: true,
|
|
103
|
+
blender: true,
|
|
98
104
|
tree: false,
|
|
99
105
|
},
|
|
100
106
|
panelLayout: ['mem', 'disk', 'net'],
|
|
File without changes
|