pyre-cli 9.5.2 → 10.0.0
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 +73 -16
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/{blender-7UT5J7IP.js → blender-FGH3ASEQ.js} +2 -2
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-T7O4KXXA.js → checkCmd-RKW6QGDR.js} +9 -11
- package/dist/{chunk-CXL4GYGQ.js → chunk-53O2RGSL.js} +44 -17
- package/dist/{chunk-3TE7MJLS.js → chunk-6UHZP4QP.js} +36 -10
- package/dist/chunk-GHRDW646.js +2506 -0
- package/dist/{prometheus-LFYHF6VO.js → chunk-HT4VQ4OH.js} +1 -0
- package/dist/chunk-M73HCQD2.js +95 -0
- package/dist/chunk-MZO5QKUR.js +32 -0
- package/dist/{chunk-25I533OF.js → chunk-VIACN2RU.js} +1 -1
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-JB4OGUEF.js → collectors-H43PN4PS.js} +6 -3
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-KS5ZZ3VN.js → config-GYGJXU2Q.js} +9 -1
- package/dist/doctor-GYHFTNZQ.js +387 -0
- package/dist/explainCmd-MF6PIMLW.js +222 -0
- package/dist/extensions-XJGHG7X3.js +134 -0
- package/dist/index.js +460 -88
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-TYLJHQ5B.js → pipeCmd-7REDBLBU.js} +4 -3
- package/dist/profileProcCmd-N4H6MCBT.js +107 -0
- package/dist/prometheus-5BL5PVLC.js +6 -0
- package/dist/prometheusCmd-PL64BLN5.js +93 -0
- package/dist/replayCmd-Z3PYXY7V.js +164 -0
- package/dist/run-HOGQDDTA.js +8 -0
- package/dist/serveCmd-2H5VLF3P.js +153 -0
- package/dist/service-Q74BBILO.js +181 -0
- package/dist/smartCmd-ODWKSFNG.js +166 -0
- package/dist/syncCmd-A2W3QB3S.js +126 -0
- package/dist/topoCmd-DU2HHZNH.js +148 -0
- package/dist/watchdogCmd-UF53JPZB.js +161 -0
- package/dist/{wizard-Z3RGA3VB.js → wizard-TD7BGX6O.js} +1 -1
- package/install.ps1 +29 -0
- package/install.sh +10 -9
- package/package.json +7 -3
- package/src/ai/models.ts +138 -0
- package/src/alertChannels.ts +119 -0
- package/src/brewHealth.ts +157 -40
- package/src/buildCmd.ts +48 -0
- package/src/checkCmd.ts +5 -5
- package/src/completions.ts +35 -0
- package/src/doctor.ts +235 -7
- package/src/easy_features.test.ts +40 -0
- package/src/explainCmd.ts +261 -0
- package/src/extensions.ts +71 -7
- package/src/formatters/render.ts +34 -10
- package/src/formatters/types.ts +2 -0
- package/src/index.ts +251 -36
- package/src/live/input.ts +60 -0
- package/src/live/render.ts +67 -0
- package/src/live/state.ts +5 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +342 -178
- package/src/monitors/ioreport.py +72 -0
- package/src/monitors/platform/linux.ts +756 -0
- package/src/monitors/platform/windows.ts +519 -0
- package/src/monitors/run.ts +27 -3
- package/src/monitors/smc.ts +21 -1
- package/src/monitors/types.ts +15 -0
- package/src/netUsageCmd.ts +121 -0
- package/src/platform.test.ts +99 -0
- package/src/plugins/index.ts +143 -0
- package/src/profileProcCmd.ts +133 -0
- package/src/prometheusCmd.ts +107 -0
- package/src/replayCmd.ts +186 -0
- package/src/serveCmd.ts +180 -0
- package/src/service.ts +125 -7
- package/src/smartCmd.ts +205 -0
- package/src/state/config.ts +58 -17
- package/src/syncCmd.ts +166 -0
- package/src/topoCmd.ts +176 -0
- package/src/watchdogCmd.ts +198 -0
- package/src/xbar.ts +22 -11
- package/dist/brewHealth-F2BXF5M6.js +0 -74
- package/dist/chunk-FH5WDJBF.js +0 -15
- package/dist/chunk-N4WJVF5M.js +0 -1184
- package/dist/doctor-UV7TWIHX.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
|
@@ -0,0 +1,2506 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectBuilds
|
|
3
|
+
} from "./chunk-M73HCQD2.js";
|
|
4
|
+
import {
|
|
5
|
+
collectBlenderRenders
|
|
6
|
+
} from "./chunk-VIACN2RU.js";
|
|
7
|
+
import {
|
|
8
|
+
run
|
|
9
|
+
} from "./chunk-MZO5QKUR.js";
|
|
10
|
+
|
|
11
|
+
// src/monitors/collectors.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import os3 from "os";
|
|
14
|
+
import path3 from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import fs3 from "fs";
|
|
17
|
+
|
|
18
|
+
// src/monitors/smc.ts
|
|
19
|
+
var smcCache = null;
|
|
20
|
+
async function getSmcMetrics() {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (smcCache && now - smcCache.ts < 4e3) return smcCache.data;
|
|
23
|
+
const result = { temps: {}, power: {}, freq: {}, fans: [] };
|
|
24
|
+
try {
|
|
25
|
+
const pm = (await run(
|
|
26
|
+
"sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
|
|
27
|
+
"",
|
|
28
|
+
2e3
|
|
29
|
+
)).trim();
|
|
30
|
+
if (pm) {
|
|
31
|
+
const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
|
|
32
|
+
if (cpuTemp) result.temps["cpu_die"] = parseFloat(cpuTemp[1]);
|
|
33
|
+
const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
|
|
34
|
+
if (gpuTemp) result.temps["gpu_die"] = parseFloat(gpuTemp[1]);
|
|
35
|
+
const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
|
|
36
|
+
if (smcTemp) result.temps["smc_die"] = parseFloat(smcTemp[1]);
|
|
37
|
+
const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
|
|
38
|
+
if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1e3;
|
|
39
|
+
const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
40
|
+
if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1e3;
|
|
41
|
+
const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
|
|
42
|
+
if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1e3;
|
|
43
|
+
const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
44
|
+
if (eFreq) result.freq["e_cluster"] = parseFloat(eFreq[1]);
|
|
45
|
+
const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
46
|
+
if (pFreq) result.freq["p_cluster"] = parseFloat(pFreq[1]);
|
|
47
|
+
const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
|
|
48
|
+
if (cpuFreq) result.freq["cpu"] = parseFloat(cpuFreq[1]);
|
|
49
|
+
const fanMatches = pm.matchAll(/Fan\s+(\d+)\s+speed:\s*(\d+)\s*rpm/gi);
|
|
50
|
+
for (const m of fanMatches) {
|
|
51
|
+
const id = parseInt(m[1], 10);
|
|
52
|
+
const rpm = parseInt(m[2], 10);
|
|
53
|
+
if (!isNaN(id) && !isNaN(rpm)) {
|
|
54
|
+
result.fans.push({ id, rpm });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (result.fans.length === 0) {
|
|
58
|
+
const fanMatches2 = pm.matchAll(/Fan\s+(\d+):\s*(\d+)\s*rpm/gi);
|
|
59
|
+
for (const m of fanMatches2) {
|
|
60
|
+
const id = parseInt(m[1], 10);
|
|
61
|
+
const rpm = parseInt(m[2], 10);
|
|
62
|
+
if (!isNaN(id) && !isNaN(rpm)) result.fans.push({ id, rpm });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
69
|
+
try {
|
|
70
|
+
const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
|
|
71
|
+
if (ioregRaw) {
|
|
72
|
+
const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
|
|
73
|
+
for (const match of matches) {
|
|
74
|
+
let val = parseFloat(match[1]);
|
|
75
|
+
if (val > 1e3) val = val / 100;
|
|
76
|
+
if (val >= 15 && val <= 115) {
|
|
77
|
+
if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
78
|
+
else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
|
|
79
|
+
else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
87
|
+
try {
|
|
88
|
+
const battRaw = await run("ioreg -r -n AppleSmartBattery 2>/dev/null", "");
|
|
89
|
+
if (battRaw) {
|
|
90
|
+
const virtMatch = battRaw.match(/"VirtualTemperature"\s*=\s*(\d+)/);
|
|
91
|
+
const cellMatch = battRaw.match(/"Temperature"\s*=\s*(\d+)/);
|
|
92
|
+
const rawVirt = virtMatch ? parseFloat(virtMatch[1]) : void 0;
|
|
93
|
+
const rawCell = cellMatch ? parseFloat(cellMatch[1]) : void 0;
|
|
94
|
+
const virtC = rawVirt !== void 0 ? Math.round(rawVirt / 10) / 10 : void 0;
|
|
95
|
+
const cellC = rawCell !== void 0 ? Math.round(rawCell / 10) / 10 : void 0;
|
|
96
|
+
if (virtC !== void 0 && virtC >= 15 && virtC <= 115) {
|
|
97
|
+
result.temps["cpu_die"] = virtC;
|
|
98
|
+
} else if (cellC !== void 0 && cellC >= 15 && cellC <= 115) {
|
|
99
|
+
result.temps["cpu_die"] = cellC;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
106
|
+
try {
|
|
107
|
+
const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
|
|
108
|
+
if (tz0) {
|
|
109
|
+
const rawVal = parseFloat(tz0);
|
|
110
|
+
const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
|
|
111
|
+
if (val >= 10 && val <= 120) {
|
|
112
|
+
result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (result.temps["cpu_die"] === void 0) {
|
|
119
|
+
const loadAvg = (await import("os")).loadavg();
|
|
120
|
+
const ncpus = (await import("os")).cpus().length || 1;
|
|
121
|
+
const loadUsage = Math.min(100, loadAvg[0] / ncpus * 100);
|
|
122
|
+
const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
|
|
123
|
+
result.temps["cpu_die"] = estCpu;
|
|
124
|
+
if (result.temps["gpu_die"] === void 0) {
|
|
125
|
+
result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
|
|
126
|
+
}
|
|
127
|
+
if (result.temps["smc_die"] === void 0) {
|
|
128
|
+
result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
smcCache = { data: result, ts: now };
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function parseSuffix(s) {
|
|
135
|
+
return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/monitors/dns_cache.ts
|
|
139
|
+
import dns from "dns";
|
|
140
|
+
var cache = /* @__PURE__ */ new Map();
|
|
141
|
+
var pending = /* @__PURE__ */ new Set();
|
|
142
|
+
function resolveIp(ip) {
|
|
143
|
+
let cleanIp = ip;
|
|
144
|
+
if (cleanIp.includes("]:")) {
|
|
145
|
+
cleanIp = cleanIp.substring(1, cleanIp.indexOf("]"));
|
|
146
|
+
} else if (cleanIp.includes(":") && cleanIp.split(":").length === 2) {
|
|
147
|
+
cleanIp = cleanIp.split(":")[0];
|
|
148
|
+
} else if (cleanIp.includes(":") && !cleanIp.includes("]")) {
|
|
149
|
+
const lastColon = cleanIp.lastIndexOf(":");
|
|
150
|
+
const isPort = !cleanIp.includes(":", cleanIp.indexOf(":") + 1);
|
|
151
|
+
}
|
|
152
|
+
if (cleanIp.includes(":") && cleanIp.includes(".")) {
|
|
153
|
+
const lastDot = cleanIp.lastIndexOf(".");
|
|
154
|
+
if (lastDot > cleanIp.lastIndexOf(":")) {
|
|
155
|
+
cleanIp = cleanIp.substring(0, lastDot);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (cache.has(cleanIp)) return cache.get(cleanIp);
|
|
159
|
+
if (pending.has(cleanIp)) return ip;
|
|
160
|
+
if (cleanIp.startsWith("127.") || cleanIp.startsWith("192.168.") || cleanIp.startsWith("10.") || cleanIp.startsWith("172.16.") || cleanIp.startsWith("ff02:") || cleanIp === "*:*" || cleanIp === "*" || cleanIp.startsWith("fe80:")) {
|
|
161
|
+
return ip;
|
|
162
|
+
}
|
|
163
|
+
pending.add(cleanIp);
|
|
164
|
+
dns.reverse(cleanIp, (err, hostnames) => {
|
|
165
|
+
if (!err && hostnames && hostnames.length > 0) {
|
|
166
|
+
cache.set(cleanIp, hostnames[0]);
|
|
167
|
+
} else {
|
|
168
|
+
cache.set(cleanIp, cleanIp);
|
|
169
|
+
}
|
|
170
|
+
pending.delete(cleanIp);
|
|
171
|
+
});
|
|
172
|
+
return ip;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/monitors/platform/linux.ts
|
|
176
|
+
import fs from "fs";
|
|
177
|
+
import os from "os";
|
|
178
|
+
import path from "path";
|
|
179
|
+
var prevNetSample = null;
|
|
180
|
+
var prevDiskIoSample = null;
|
|
181
|
+
var cachedDiskIo = null;
|
|
182
|
+
function safeReadFile(filePath) {
|
|
183
|
+
try {
|
|
184
|
+
return fs.readFileSync(filePath, "utf8");
|
|
185
|
+
} catch {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function collectLinuxSystem() {
|
|
190
|
+
const hostname = os.hostname();
|
|
191
|
+
let osName = "Linux";
|
|
192
|
+
const osRelease = safeReadFile("/etc/os-release");
|
|
193
|
+
if (osRelease) {
|
|
194
|
+
const prettyMatch = osRelease.match(/PRETTY_NAME="?([^"\n]+)"?/);
|
|
195
|
+
if (prettyMatch) {
|
|
196
|
+
osName = prettyMatch[1];
|
|
197
|
+
} else {
|
|
198
|
+
const nameMatch = osRelease.match(/NAME="?([^"\n]+)"?/);
|
|
199
|
+
const verMatch = osRelease.match(/VERSION="?([^"\n]+)"?/);
|
|
200
|
+
if (nameMatch) {
|
|
201
|
+
osName = verMatch ? `${nameMatch[1]} ${verMatch[1]}` : nameMatch[1];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
osName = `Linux ${os.release()}`;
|
|
206
|
+
}
|
|
207
|
+
const uptimeSec = Math.floor(os.uptime());
|
|
208
|
+
const days = Math.floor(uptimeSec / 86400);
|
|
209
|
+
const hours = Math.floor(uptimeSec % 86400 / 3600);
|
|
210
|
+
const minutes = Math.floor(uptimeSec % 3600 / 60);
|
|
211
|
+
let uptime = "";
|
|
212
|
+
if (days > 0) uptime += `${days} ${days === 1 ? "day" : "days"}, `;
|
|
213
|
+
uptime += `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
|
214
|
+
return { hostname, os: osName, uptime };
|
|
215
|
+
}
|
|
216
|
+
async function collectLinuxCpu(prevCpuTimesRef) {
|
|
217
|
+
const cpus = os.cpus();
|
|
218
|
+
const cores = cpus.length || 1;
|
|
219
|
+
let brand = cpus[0]?.model || "Linux CPU";
|
|
220
|
+
let frequency = cpus[0]?.speed || 0;
|
|
221
|
+
const cpuInfo = safeReadFile("/proc/cpuinfo");
|
|
222
|
+
if (cpuInfo) {
|
|
223
|
+
const brandMatch = cpuInfo.match(/model name\s*:\s*(.+)/i);
|
|
224
|
+
if (brandMatch) brand = brandMatch[1].trim();
|
|
225
|
+
const mhzMatch = cpuInfo.match(/cpu MHz\s*:\s*([\d.]+)/i);
|
|
226
|
+
if (mhzMatch) frequency = Math.round(parseFloat(mhzMatch[1]));
|
|
227
|
+
}
|
|
228
|
+
if (frequency === 0) {
|
|
229
|
+
const curFreq = safeReadFile("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq").trim();
|
|
230
|
+
if (curFreq) {
|
|
231
|
+
frequency = Math.round(parseInt(curFreq, 10) / 1e3);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let physicalCores = cores;
|
|
235
|
+
if (cpuInfo) {
|
|
236
|
+
const coreIds = /* @__PURE__ */ new Set();
|
|
237
|
+
const blocks = cpuInfo.split(/\n\s*\n/);
|
|
238
|
+
for (const b of blocks) {
|
|
239
|
+
const physMatch = b.match(/physical id\s*:\s*(\d+)/);
|
|
240
|
+
const coreMatch = b.match(/core id\s*:\s*(\d+)/);
|
|
241
|
+
if (physMatch && coreMatch) {
|
|
242
|
+
coreIds.add(`${physMatch[1]}:${coreMatch[1]}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (coreIds.size > 0) physicalCores = coreIds.size;
|
|
246
|
+
}
|
|
247
|
+
const current = cpus.map((cpu) => ({
|
|
248
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
249
|
+
idle: cpu.times.idle
|
|
250
|
+
}));
|
|
251
|
+
let coreUsage = [];
|
|
252
|
+
let usage = 0;
|
|
253
|
+
if (prevCpuTimesRef.current && prevCpuTimesRef.current.length === current.length) {
|
|
254
|
+
coreUsage = current.map((c, i) => {
|
|
255
|
+
const prev = prevCpuTimesRef.current[i];
|
|
256
|
+
const totalDelta = c.total - prev.total;
|
|
257
|
+
const idleDelta = c.idle - prev.idle;
|
|
258
|
+
if (totalDelta <= 0) return 0;
|
|
259
|
+
return Math.min(100, Math.max(0, Math.round((totalDelta - idleDelta) / totalDelta * 100)));
|
|
260
|
+
});
|
|
261
|
+
if (coreUsage.length > 0) {
|
|
262
|
+
usage = Math.round(coreUsage.reduce((a, b) => a + b, 0) / coreUsage.length);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
prevCpuTimesRef.current = current;
|
|
266
|
+
let temperature;
|
|
267
|
+
const thermZone = safeReadFile("/sys/class/thermal/thermal_zone0/temp").trim();
|
|
268
|
+
if (thermZone) {
|
|
269
|
+
const rawVal = parseFloat(thermZone);
|
|
270
|
+
const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
|
|
271
|
+
if (val >= 10 && val <= 125) temperature = Math.round(val * 10) / 10;
|
|
272
|
+
}
|
|
273
|
+
if (temperature === void 0) {
|
|
274
|
+
try {
|
|
275
|
+
const hwmonDirs = fs.readdirSync("/sys/class/hwmon");
|
|
276
|
+
for (const d of hwmonDirs) {
|
|
277
|
+
const tempFile = path.join("/sys/class/hwmon", d, "temp1_input");
|
|
278
|
+
if (fs.existsSync(tempFile)) {
|
|
279
|
+
const raw = parseFloat(safeReadFile(tempFile).trim());
|
|
280
|
+
const val = raw > 1e3 ? raw / 1e3 : raw;
|
|
281
|
+
if (val >= 10 && val <= 125) {
|
|
282
|
+
temperature = Math.round(val * 10) / 10;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const loadAvg = os.loadavg();
|
|
291
|
+
return {
|
|
292
|
+
brand,
|
|
293
|
+
cores,
|
|
294
|
+
physicalCores,
|
|
295
|
+
frequency,
|
|
296
|
+
usage,
|
|
297
|
+
loadAvg,
|
|
298
|
+
temperature,
|
|
299
|
+
coreUsage
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function collectLinuxMemory() {
|
|
303
|
+
const meminfo = safeReadFile("/proc/meminfo");
|
|
304
|
+
let total = os.totalmem();
|
|
305
|
+
let free = os.freemem();
|
|
306
|
+
let available = free;
|
|
307
|
+
let swapTotal = 0;
|
|
308
|
+
let swapFree = 0;
|
|
309
|
+
let buffers = 0;
|
|
310
|
+
let cached = 0;
|
|
311
|
+
let shmem = 0;
|
|
312
|
+
let sreclaimable = 0;
|
|
313
|
+
if (meminfo) {
|
|
314
|
+
const parseKb = (key) => {
|
|
315
|
+
const m = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)\\s+kB`, "m"));
|
|
316
|
+
return m ? parseInt(m[1], 10) * 1024 : 0;
|
|
317
|
+
};
|
|
318
|
+
const t = parseKb("MemTotal");
|
|
319
|
+
if (t > 0) total = t;
|
|
320
|
+
free = parseKb("MemFree");
|
|
321
|
+
const a = parseKb("MemAvailable");
|
|
322
|
+
if (a > 0) available = a;
|
|
323
|
+
else available = free;
|
|
324
|
+
buffers = parseKb("Buffers");
|
|
325
|
+
cached = parseKb("Cached");
|
|
326
|
+
shmem = parseKb("Shmem");
|
|
327
|
+
sreclaimable = parseKb("SReclaimable");
|
|
328
|
+
swapTotal = parseKb("SwapTotal");
|
|
329
|
+
swapFree = parseKb("SwapFree");
|
|
330
|
+
}
|
|
331
|
+
const used = Math.max(0, total - available);
|
|
332
|
+
const usagePercent = total > 0 ? Math.round(used / total * 100) : 0;
|
|
333
|
+
const swapUsed = Math.max(0, swapTotal - swapFree);
|
|
334
|
+
let pressureLevel = "Normal";
|
|
335
|
+
if (usagePercent > 92) pressureLevel = "Critical";
|
|
336
|
+
else if (usagePercent > 82) pressureLevel = "Warning";
|
|
337
|
+
return {
|
|
338
|
+
total,
|
|
339
|
+
used,
|
|
340
|
+
free,
|
|
341
|
+
swapTotal,
|
|
342
|
+
swapUsed,
|
|
343
|
+
swapFree,
|
|
344
|
+
pageSize: 4096,
|
|
345
|
+
usagePercent,
|
|
346
|
+
wiredBytes: buffers,
|
|
347
|
+
compressedBytes: cached + sreclaimable,
|
|
348
|
+
purgeableBytes: shmem,
|
|
349
|
+
pressureLevel
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async function collectLinuxDisk() {
|
|
353
|
+
const rawDf = await run("df -k -P 2>/dev/null", "");
|
|
354
|
+
const lines = rawDf.trim().split("\n").slice(1);
|
|
355
|
+
const result = [];
|
|
356
|
+
for (const line of lines) {
|
|
357
|
+
const parts = line.split(/\s+/);
|
|
358
|
+
if (parts.length >= 6) {
|
|
359
|
+
const filesystem = parts[0];
|
|
360
|
+
const totalKb = parseInt(parts[1], 10);
|
|
361
|
+
const usedKb = parseInt(parts[2], 10);
|
|
362
|
+
const availKb = parseInt(parts[3], 10);
|
|
363
|
+
const capacity = parts[4];
|
|
364
|
+
const mountpoint = parts[5];
|
|
365
|
+
if (filesystem.startsWith("/dev/") || mountpoint === "/" || totalKb > 1024 * 1024) {
|
|
366
|
+
if (!filesystem.includes("loop")) {
|
|
367
|
+
result.push({
|
|
368
|
+
filesystem,
|
|
369
|
+
size: formatKb(totalKb),
|
|
370
|
+
used: formatKb(usedKb),
|
|
371
|
+
available: formatKb(availKb),
|
|
372
|
+
capacity,
|
|
373
|
+
mountpoint,
|
|
374
|
+
readBytesSec: 0,
|
|
375
|
+
writeBytesSec: 0
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const diskstats = safeReadFile("/proc/diskstats");
|
|
382
|
+
if (diskstats) {
|
|
383
|
+
const now = Date.now();
|
|
384
|
+
let totalSectors = 0;
|
|
385
|
+
for (const line of diskstats.split("\n")) {
|
|
386
|
+
const parts = line.trim().split(/\s+/);
|
|
387
|
+
if (parts.length >= 10 && (parts[2].startsWith("sd") || parts[2].startsWith("nvme") || parts[2].startsWith("vd"))) {
|
|
388
|
+
const readSectors = parseInt(parts[5], 10) || 0;
|
|
389
|
+
const writeSectors = parseInt(parts[9], 10) || 0;
|
|
390
|
+
totalSectors += readSectors + writeSectors;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const totalBytes = totalSectors * 512;
|
|
394
|
+
if (prevDiskIoSample && prevDiskIoSample.ts > 0) {
|
|
395
|
+
const dt = Math.max((now - prevDiskIoSample.ts) / 1e3, 1e-3);
|
|
396
|
+
const bytesSec = Math.max(0, (totalBytes - prevDiskIoSample.totalBytes) / dt);
|
|
397
|
+
cachedDiskIo = {
|
|
398
|
+
readBytesSec: Math.round(bytesSec * 0.6),
|
|
399
|
+
writeBytesSec: Math.round(bytesSec * 0.4),
|
|
400
|
+
ts: now
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
prevDiskIoSample = { totalBytes, ts: now };
|
|
404
|
+
if (cachedDiskIo && result.length > 0) {
|
|
405
|
+
result[0].readBytesSec = cachedDiskIo.readBytesSec;
|
|
406
|
+
result[0].writeBytesSec = cachedDiskIo.writeBytesSec;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return result.length > 0 ? result : [
|
|
410
|
+
{
|
|
411
|
+
filesystem: "/dev/root",
|
|
412
|
+
size: "50G",
|
|
413
|
+
used: "10G",
|
|
414
|
+
available: "40G",
|
|
415
|
+
capacity: "20%",
|
|
416
|
+
mountpoint: "/",
|
|
417
|
+
readBytesSec: 0,
|
|
418
|
+
writeBytesSec: 0
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
function formatKb(kb) {
|
|
423
|
+
if (isNaN(kb)) return "0B";
|
|
424
|
+
const bytes = kb * 1024;
|
|
425
|
+
if (bytes >= 1024 * 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1) + "T";
|
|
426
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
|
|
427
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + "M";
|
|
428
|
+
return (bytes / 1024).toFixed(0) + "K";
|
|
429
|
+
}
|
|
430
|
+
async function collectLinuxBattery() {
|
|
431
|
+
const baseDir = "/sys/class/power_supply";
|
|
432
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
433
|
+
try {
|
|
434
|
+
const entries = fs.readdirSync(baseDir);
|
|
435
|
+
const batDir = entries.find((e) => /^BAT\d*$/i.test(e));
|
|
436
|
+
if (!batDir) return null;
|
|
437
|
+
const batPath = path.join(baseDir, batDir);
|
|
438
|
+
const capStr = safeReadFile(path.join(batPath, "capacity")).trim();
|
|
439
|
+
if (!capStr) return null;
|
|
440
|
+
const level = parseInt(capStr, 10) || 0;
|
|
441
|
+
const status = safeReadFile(path.join(batPath, "status")).trim().toLowerCase();
|
|
442
|
+
const isCharging = status === "charging";
|
|
443
|
+
const isFull = status === "full";
|
|
444
|
+
const state = isFull ? "charged" : isCharging ? "charging" : "discharging";
|
|
445
|
+
const acDir = entries.find((e) => /^AC|^ADP/i.test(e));
|
|
446
|
+
let powerSource = "Battery";
|
|
447
|
+
if (acDir) {
|
|
448
|
+
const online = safeReadFile(path.join(baseDir, acDir, "online")).trim();
|
|
449
|
+
if (online === "1") powerSource = "AC";
|
|
450
|
+
}
|
|
451
|
+
let powerWatts;
|
|
452
|
+
const powerNowStr = safeReadFile(path.join(batPath, "power_now")).trim();
|
|
453
|
+
if (powerNowStr) {
|
|
454
|
+
const uW = parseInt(powerNowStr, 10);
|
|
455
|
+
if (uW > 0) powerWatts = Math.round(uW / 1e6 * 100) / 100;
|
|
456
|
+
} else {
|
|
457
|
+
const curNowStr = safeReadFile(path.join(batPath, "current_now")).trim();
|
|
458
|
+
const voltNowStr = safeReadFile(path.join(batPath, "voltage_now")).trim();
|
|
459
|
+
if (curNowStr && voltNowStr) {
|
|
460
|
+
const uA = parseInt(curNowStr, 10);
|
|
461
|
+
const uV = parseInt(voltNowStr, 10);
|
|
462
|
+
if (uA > 0 && uV > 0) {
|
|
463
|
+
powerWatts = Math.round(uA / 1e6 * (uV / 1e6) * 100) / 100;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
let cycles;
|
|
468
|
+
const cycleStr = safeReadFile(path.join(batPath, "cycle_count")).trim();
|
|
469
|
+
if (cycleStr) cycles = parseInt(cycleStr, 10) || void 0;
|
|
470
|
+
return {
|
|
471
|
+
level,
|
|
472
|
+
state,
|
|
473
|
+
timeRemaining: state === "discharging" ? "calculating" : "\u221E",
|
|
474
|
+
health: "Good",
|
|
475
|
+
powerSource,
|
|
476
|
+
cycles,
|
|
477
|
+
powerWatts
|
|
478
|
+
};
|
|
479
|
+
} catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function collectLinuxThermal() {
|
|
484
|
+
const temperatures = {};
|
|
485
|
+
const fans = [];
|
|
486
|
+
try {
|
|
487
|
+
const thermalDirs = fs.readdirSync("/sys/class/thermal");
|
|
488
|
+
for (const d of thermalDirs) {
|
|
489
|
+
if (d.startsWith("thermal_zone")) {
|
|
490
|
+
const typeStr = safeReadFile(`/sys/class/thermal/${d}/type`).trim() || d;
|
|
491
|
+
const tempStr = safeReadFile(`/sys/class/thermal/${d}/temp`).trim();
|
|
492
|
+
if (tempStr) {
|
|
493
|
+
const raw = parseFloat(tempStr);
|
|
494
|
+
const c = raw > 1e3 ? Math.round(raw / 1e3 * 10) / 10 : Math.round(raw * 10) / 10;
|
|
495
|
+
if (c >= 10 && c <= 125) {
|
|
496
|
+
temperatures[typeStr] = c;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const hwmonDirs = fs.readdirSync("/sys/class/hwmon");
|
|
505
|
+
for (const d of hwmonDirs) {
|
|
506
|
+
const dirPath = path.join("/sys/class/hwmon", d);
|
|
507
|
+
const files = fs.readdirSync(dirPath);
|
|
508
|
+
for (const f of files) {
|
|
509
|
+
if (/^temp\d+_input$/.test(f)) {
|
|
510
|
+
const idx = f.match(/\d+/)?.[0] || "1";
|
|
511
|
+
const labelFile = path.join(dirPath, `temp${idx}_label`);
|
|
512
|
+
const label = safeReadFile(labelFile).trim() || `sensor_${idx}`;
|
|
513
|
+
const raw = parseFloat(safeReadFile(path.join(dirPath, f)).trim());
|
|
514
|
+
const c = raw > 1e3 ? Math.round(raw / 1e3 * 10) / 10 : Math.round(raw * 10) / 10;
|
|
515
|
+
if (c >= 10 && c <= 125) temperatures[label] = c;
|
|
516
|
+
} else if (/^fan\d+_input$/.test(f)) {
|
|
517
|
+
const idx = parseInt(f.match(/\d+/)?.[0] || "0", 10);
|
|
518
|
+
const rpm = parseInt(safeReadFile(path.join(dirPath, f)).trim(), 10);
|
|
519
|
+
if (!isNaN(rpm)) fans.push({ id: idx, rpm });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
const temps = Object.values(temperatures).filter((t) => typeof t === "number");
|
|
526
|
+
const maxTemp = temps.length > 0 ? Math.max(...temps) : 45;
|
|
527
|
+
let state = "Nominal";
|
|
528
|
+
let pressureLevel = 0;
|
|
529
|
+
if (maxTemp >= 100) {
|
|
530
|
+
state = "Critical";
|
|
531
|
+
pressureLevel = 3;
|
|
532
|
+
} else if (maxTemp >= 90) {
|
|
533
|
+
state = "Serious";
|
|
534
|
+
pressureLevel = 2;
|
|
535
|
+
} else if (maxTemp >= 80) {
|
|
536
|
+
state = "Fair";
|
|
537
|
+
pressureLevel = 1;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
state,
|
|
541
|
+
detail: `${state} (${maxTemp}\xB0C)`,
|
|
542
|
+
pressureLevel,
|
|
543
|
+
temperatures: Object.keys(temperatures).length ? temperatures : void 0,
|
|
544
|
+
fans: fans.length ? fans : void 0
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
async function collectLinuxPower() {
|
|
548
|
+
const raplDir = "/sys/class/powercap/intel-rapl/intel-rapl:0";
|
|
549
|
+
if (fs.existsSync(raplDir)) {
|
|
550
|
+
try {
|
|
551
|
+
const e1Str = safeReadFile(path.join(raplDir, "energy_uj")).trim();
|
|
552
|
+
const t1 = Date.now();
|
|
553
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
554
|
+
const e2Str = safeReadFile(path.join(raplDir, "energy_uj")).trim();
|
|
555
|
+
const t2 = Date.now();
|
|
556
|
+
const e1 = parseInt(e1Str, 10);
|
|
557
|
+
const e2 = parseInt(e2Str, 10);
|
|
558
|
+
if (e2 > e1 && t2 > t1) {
|
|
559
|
+
const dtSec = (t2 - t1) / 1e3;
|
|
560
|
+
const watts = Math.round((e2 - e1) / 1e6 / dtSec * 100) / 100;
|
|
561
|
+
if (watts > 0 && watts < 500) {
|
|
562
|
+
return { cpuWatts: watts, combinedWatts: watts };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const batt = await collectLinuxBattery();
|
|
570
|
+
if (batt && batt.powerWatts) {
|
|
571
|
+
return { combinedWatts: batt.powerWatts };
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
async function collectLinuxGpu() {
|
|
578
|
+
const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
|
|
579
|
+
if (nvRaw) {
|
|
580
|
+
const parts = nvRaw.split(",").map((s) => s.trim());
|
|
581
|
+
if (parts.length >= 3) {
|
|
582
|
+
const model = parts[0];
|
|
583
|
+
const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
584
|
+
const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
585
|
+
const temperature = parts[3] ? parseFloat(parts[3]) : void 0;
|
|
586
|
+
return { model, memory, utilization, temperature, processes: 0 };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const drmDir = "/sys/class/drm";
|
|
591
|
+
if (fs.existsSync(drmDir)) {
|
|
592
|
+
const cards = fs.readdirSync(drmDir).filter((c) => /^card\d+$/i.test(c));
|
|
593
|
+
for (const card of cards) {
|
|
594
|
+
const busyFile = path.join(drmDir, card, "device", "gpu_busy_percent");
|
|
595
|
+
if (fs.existsSync(busyFile)) {
|
|
596
|
+
const util = parseInt(safeReadFile(busyFile).trim(), 10) || 0;
|
|
597
|
+
return { model: "Linux DRM GPU", memory: 0, utilization: util, processes: 0 };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
async function collectLinuxNetwork() {
|
|
606
|
+
const netDev = safeReadFile("/proc/net/dev");
|
|
607
|
+
let iface = "eth0";
|
|
608
|
+
let rxBytes = 0;
|
|
609
|
+
let txBytes = 0;
|
|
610
|
+
let rxPackets = 0;
|
|
611
|
+
let txPackets = 0;
|
|
612
|
+
const route = safeReadFile("/proc/net/route");
|
|
613
|
+
if (route) {
|
|
614
|
+
for (const line of route.split("\n").slice(1)) {
|
|
615
|
+
const parts = line.trim().split(/\s+/);
|
|
616
|
+
if (parts[1] === "00000000" && parts[0]) {
|
|
617
|
+
iface = parts[0];
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
let ip = "127.0.0.1";
|
|
623
|
+
const interfaces = os.networkInterfaces();
|
|
624
|
+
if (interfaces[iface]) {
|
|
625
|
+
const addr = interfaces[iface]?.find((a) => a.family === "IPv4" && !a.internal);
|
|
626
|
+
if (addr) ip = addr.address;
|
|
627
|
+
}
|
|
628
|
+
if (ip === "127.0.0.1") {
|
|
629
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
630
|
+
const addr = addrs?.find((a) => a.family === "IPv4" && !a.internal);
|
|
631
|
+
if (addr) {
|
|
632
|
+
iface = name;
|
|
633
|
+
ip = addr.address;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (netDev) {
|
|
639
|
+
for (const line of netDev.split("\n").slice(2)) {
|
|
640
|
+
const trimmed = line.trim();
|
|
641
|
+
if (!trimmed) continue;
|
|
642
|
+
const colonIdx = trimmed.indexOf(":");
|
|
643
|
+
if (colonIdx === -1) continue;
|
|
644
|
+
const devName = trimmed.slice(0, colonIdx).trim();
|
|
645
|
+
const stats = trimmed.slice(colonIdx + 1).trim().split(/\s+/);
|
|
646
|
+
if (stats.length >= 16) {
|
|
647
|
+
const rxB = parseInt(stats[0], 10) || 0;
|
|
648
|
+
const rxP = parseInt(stats[1], 10) || 0;
|
|
649
|
+
const txB = parseInt(stats[8], 10) || 0;
|
|
650
|
+
const txP = parseInt(stats[9], 10) || 0;
|
|
651
|
+
if (devName === iface || !iface && devName !== "lo") {
|
|
652
|
+
rxBytes = rxB;
|
|
653
|
+
rxPackets = rxP;
|
|
654
|
+
txBytes = txB;
|
|
655
|
+
txPackets = txP;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
let rxRate = 0;
|
|
662
|
+
let txRate = 0;
|
|
663
|
+
if (prevNetSample && prevNetSample.ts > 0) {
|
|
664
|
+
const dt = Math.max((now - prevNetSample.ts) / 1e3, 1e-3);
|
|
665
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
666
|
+
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
667
|
+
}
|
|
668
|
+
prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
669
|
+
const connectionStates = await getLinuxConnectionStates();
|
|
670
|
+
return {
|
|
671
|
+
interface: iface,
|
|
672
|
+
ip,
|
|
673
|
+
rxBytes,
|
|
674
|
+
txBytes,
|
|
675
|
+
rxPackets,
|
|
676
|
+
txPackets,
|
|
677
|
+
rxRate,
|
|
678
|
+
txRate,
|
|
679
|
+
connections: connectionStates.established,
|
|
680
|
+
protocols: {
|
|
681
|
+
tcp: { connections: connectionStates.established, rxBytes: 0, txBytes: 0 },
|
|
682
|
+
udp: { connections: 0, rxBytes: 0, txBytes: 0 }
|
|
683
|
+
},
|
|
684
|
+
connectionStates,
|
|
685
|
+
listeningPorts: [],
|
|
686
|
+
establishedConnections: connectionStates.established,
|
|
687
|
+
topRemoteHosts: []
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async function getLinuxConnectionStates() {
|
|
691
|
+
const stats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
692
|
+
const tcpFile = safeReadFile("/proc/net/tcp");
|
|
693
|
+
if (tcpFile) {
|
|
694
|
+
for (const line of tcpFile.split("\n").slice(1)) {
|
|
695
|
+
const parts = line.trim().split(/\s+/);
|
|
696
|
+
if (parts.length >= 4) {
|
|
697
|
+
const stateHex = parts[3];
|
|
698
|
+
if (stateHex === "01") stats.established++;
|
|
699
|
+
else if (stateHex === "0A") stats.listening++;
|
|
700
|
+
else if (stateHex === "06") stats.timeWait++;
|
|
701
|
+
else if (stateHex === "08") stats.closeWait++;
|
|
702
|
+
else stats.other++;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return stats;
|
|
707
|
+
}
|
|
708
|
+
async function collectLinuxPackets() {
|
|
709
|
+
const netDev = safeReadFile("/proc/net/dev");
|
|
710
|
+
let totalRxPackets = 0;
|
|
711
|
+
let totalTxPackets = 0;
|
|
712
|
+
const ifaceStats = [];
|
|
713
|
+
if (netDev) {
|
|
714
|
+
for (const line of netDev.split("\n").slice(2)) {
|
|
715
|
+
const trimmed = line.trim();
|
|
716
|
+
if (!trimmed) continue;
|
|
717
|
+
const colonIdx = trimmed.indexOf(":");
|
|
718
|
+
if (colonIdx === -1) continue;
|
|
719
|
+
const devName = trimmed.slice(0, colonIdx).trim();
|
|
720
|
+
const stats = trimmed.slice(colonIdx + 1).trim().split(/\s+/);
|
|
721
|
+
if (stats.length >= 16) {
|
|
722
|
+
const rxB = parseInt(stats[0], 10) || 0;
|
|
723
|
+
const rxP = parseInt(stats[1], 10) || 0;
|
|
724
|
+
const txB = parseInt(stats[8], 10) || 0;
|
|
725
|
+
const txP = parseInt(stats[9], 10) || 0;
|
|
726
|
+
totalRxPackets += rxP;
|
|
727
|
+
totalTxPackets += txP;
|
|
728
|
+
if (!devName.includes("lo")) {
|
|
729
|
+
ifaceStats.push({ iface: devName, rxPackets: rxP, txPackets: txP, rxBytes: rxB, txBytes: txB });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const connectionStates = await getLinuxConnectionStates();
|
|
735
|
+
return {
|
|
736
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
737
|
+
rxPackets: totalRxPackets,
|
|
738
|
+
txPackets: totalTxPackets,
|
|
739
|
+
rxRate: 0,
|
|
740
|
+
txRate: 0,
|
|
741
|
+
connections: connectionStates.established,
|
|
742
|
+
topProcesses: [],
|
|
743
|
+
interfaces: ifaceStats,
|
|
744
|
+
allProcesses: [],
|
|
745
|
+
connectionStates,
|
|
746
|
+
listeningPorts: []
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function collectLinuxProcesses(limit) {
|
|
750
|
+
try {
|
|
751
|
+
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
|
|
752
|
+
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm --sort=-pcpu 2>/dev/null${headClause}`, "")).trim();
|
|
753
|
+
if (!raw) return [];
|
|
754
|
+
const lines = raw.split("\n").slice(1);
|
|
755
|
+
return lines.map((line) => {
|
|
756
|
+
const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
757
|
+
if (!parts) return null;
|
|
758
|
+
const runtimeSec = parts[7].split(":").reduce((acc, val, idx) => acc + parseInt(val, 10) * Math.pow(60, 2 - idx), 0);
|
|
759
|
+
const command = parts[8];
|
|
760
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python/i.test(command);
|
|
761
|
+
return {
|
|
762
|
+
pid: parseInt(parts[1], 10),
|
|
763
|
+
ppid: parseInt(parts[2], 10),
|
|
764
|
+
user: parts[3],
|
|
765
|
+
cpu: parseFloat(parts[4]),
|
|
766
|
+
mem: parseFloat(parts[5]),
|
|
767
|
+
state: parts[6],
|
|
768
|
+
threads: 0,
|
|
769
|
+
runtime: runtimeSec,
|
|
770
|
+
command,
|
|
771
|
+
isGpuOrMlAttributed: isGpuOrMl
|
|
772
|
+
};
|
|
773
|
+
}).filter((p) => p !== null && p.pid > 0);
|
|
774
|
+
} catch {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function collectLinuxTasks(limit = 12) {
|
|
779
|
+
try {
|
|
780
|
+
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm --sort=-pcpu 2>/dev/null | head -n ${limit + 1}`, "")).trim();
|
|
781
|
+
if (!raw) return [];
|
|
782
|
+
const lines = raw.split("\n").slice(1);
|
|
783
|
+
return lines.map((line) => {
|
|
784
|
+
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
785
|
+
if (!parts) return null;
|
|
786
|
+
const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val, 10) * Math.pow(60, 2 - idx), 0);
|
|
787
|
+
return {
|
|
788
|
+
pid: parseInt(parts[1], 10),
|
|
789
|
+
user: parts[2],
|
|
790
|
+
cpu: parseFloat(parts[3]),
|
|
791
|
+
mem: parseFloat(parts[4]),
|
|
792
|
+
state: parts[5],
|
|
793
|
+
runtime: runtimeSec,
|
|
794
|
+
command: parts[7]
|
|
795
|
+
};
|
|
796
|
+
}).filter((t) => t !== null && t.pid > 0);
|
|
797
|
+
} catch {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
async function getLinuxDisplayInfo() {
|
|
802
|
+
const xrandr = await run("xrandr --query 2>/dev/null", "");
|
|
803
|
+
if (!xrandr) return [];
|
|
804
|
+
const displays = [];
|
|
805
|
+
for (const line of xrandr.split("\n")) {
|
|
806
|
+
const match = line.match(/^(\S+)\s+(connected)\s+(primary\s+)?(\d+x\d+)/);
|
|
807
|
+
if (match) {
|
|
808
|
+
displays.push({
|
|
809
|
+
name: match[1],
|
|
810
|
+
resolution: match[4],
|
|
811
|
+
isMain: !!match[3]
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return displays;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/monitors/platform/windows.ts
|
|
819
|
+
import os2 from "os";
|
|
820
|
+
var prevNetSample2 = null;
|
|
821
|
+
async function collectWindowsSystem() {
|
|
822
|
+
const hostname = os2.hostname();
|
|
823
|
+
const rel = os2.release();
|
|
824
|
+
const build = parseInt(rel.split(".")[2] || "0", 10);
|
|
825
|
+
let osName = "Windows";
|
|
826
|
+
if (build >= 22e3) {
|
|
827
|
+
osName = `Windows 11 (${rel})`;
|
|
828
|
+
} else if (rel.startsWith("10.0")) {
|
|
829
|
+
osName = `Windows 10 (${rel})`;
|
|
830
|
+
} else {
|
|
831
|
+
osName = `Windows (${rel})`;
|
|
832
|
+
}
|
|
833
|
+
const uptimeSec = Math.floor(os2.uptime());
|
|
834
|
+
const days = Math.floor(uptimeSec / 86400);
|
|
835
|
+
const hours = Math.floor(uptimeSec % 86400 / 3600);
|
|
836
|
+
const minutes = Math.floor(uptimeSec % 3600 / 60);
|
|
837
|
+
let uptime = "";
|
|
838
|
+
if (days > 0) uptime += `${days} ${days === 1 ? "day" : "days"}, `;
|
|
839
|
+
uptime += `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
|
840
|
+
return { hostname, os: osName, uptime };
|
|
841
|
+
}
|
|
842
|
+
async function collectWindowsCpu(prevCpuTimesRef) {
|
|
843
|
+
const cpus = os2.cpus();
|
|
844
|
+
const cores = cpus.length || 1;
|
|
845
|
+
const brand = cpus[0]?.model || "Windows CPU";
|
|
846
|
+
const frequency = cpus[0]?.speed || 0;
|
|
847
|
+
const current = cpus.map((cpu) => ({
|
|
848
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
849
|
+
idle: cpu.times.idle
|
|
850
|
+
}));
|
|
851
|
+
let coreUsage = [];
|
|
852
|
+
let usage = 0;
|
|
853
|
+
if (prevCpuTimesRef.current && prevCpuTimesRef.current.length === current.length) {
|
|
854
|
+
coreUsage = current.map((c, i) => {
|
|
855
|
+
const prev = prevCpuTimesRef.current[i];
|
|
856
|
+
const totalDelta = c.total - prev.total;
|
|
857
|
+
const idleDelta = c.idle - prev.idle;
|
|
858
|
+
if (totalDelta <= 0) return 0;
|
|
859
|
+
return Math.min(100, Math.max(0, Math.round((totalDelta - idleDelta) / totalDelta * 100)));
|
|
860
|
+
});
|
|
861
|
+
if (coreUsage.length > 0) {
|
|
862
|
+
usage = Math.round(coreUsage.reduce((a, b) => a + b, 0) / coreUsage.length);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
prevCpuTimesRef.current = current;
|
|
866
|
+
const activeLoad = Math.round(usage / 100 * cores * 100) / 100;
|
|
867
|
+
const loadAvg = [activeLoad, activeLoad, activeLoad];
|
|
868
|
+
let temperature;
|
|
869
|
+
try {
|
|
870
|
+
const rawWmi = (await run("wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul", "", 1500)).trim();
|
|
871
|
+
const match = rawWmi.match(/(\d{3,4})/);
|
|
872
|
+
if (match) {
|
|
873
|
+
const kelvinTenths = parseInt(match[1], 10);
|
|
874
|
+
const c = Math.round((kelvinTenths - 2732) / 10 * 10) / 10;
|
|
875
|
+
if (c >= 15 && c <= 115) temperature = c;
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
if (temperature === void 0) {
|
|
880
|
+
temperature = Math.round((38 + usage * 0.42) * 10) / 10;
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
brand,
|
|
884
|
+
cores,
|
|
885
|
+
physicalCores: cores,
|
|
886
|
+
frequency,
|
|
887
|
+
usage,
|
|
888
|
+
loadAvg,
|
|
889
|
+
temperature,
|
|
890
|
+
coreUsage
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function collectWindowsMemory() {
|
|
894
|
+
const total = os2.totalmem();
|
|
895
|
+
const free = os2.freemem();
|
|
896
|
+
const used = Math.max(0, total - free);
|
|
897
|
+
const usagePercent = total > 0 ? Math.round(used / total * 100) : 0;
|
|
898
|
+
let swapTotal = 0;
|
|
899
|
+
let swapUsed = 0;
|
|
900
|
+
let swapFree = 0;
|
|
901
|
+
try {
|
|
902
|
+
const rawPage = (await run("wmic pagefile get AllocatedBaseSize,CurrentUsage 2>nul", "", 1500)).trim();
|
|
903
|
+
const lines = rawPage.split("\n").filter((l) => l.trim().length > 0);
|
|
904
|
+
if (lines.length >= 2) {
|
|
905
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
906
|
+
if (parts.length >= 2) {
|
|
907
|
+
const allocMb = parseInt(parts[0], 10) || 0;
|
|
908
|
+
const usedMb = parseInt(parts[1], 10) || 0;
|
|
909
|
+
swapTotal = allocMb * 1024 * 1024;
|
|
910
|
+
swapUsed = usedMb * 1024 * 1024;
|
|
911
|
+
swapFree = Math.max(0, swapTotal - swapUsed);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
let pressureLevel = "Normal";
|
|
917
|
+
if (usagePercent > 92) pressureLevel = "Critical";
|
|
918
|
+
else if (usagePercent > 82) pressureLevel = "Warning";
|
|
919
|
+
return {
|
|
920
|
+
total,
|
|
921
|
+
used,
|
|
922
|
+
free,
|
|
923
|
+
swapTotal,
|
|
924
|
+
swapUsed,
|
|
925
|
+
swapFree,
|
|
926
|
+
pageSize: 4096,
|
|
927
|
+
usagePercent,
|
|
928
|
+
pressureLevel
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function collectWindowsDisk() {
|
|
932
|
+
try {
|
|
933
|
+
const raw = (await run("wmic logicaldisk get Caption,FileSystem,FreeSpace,Size 2>nul", "", 2500)).trim();
|
|
934
|
+
const lines = raw.split("\n").slice(1);
|
|
935
|
+
const disks = [];
|
|
936
|
+
for (const line of lines) {
|
|
937
|
+
const parts = line.trim().split(/\s+/);
|
|
938
|
+
if (parts.length >= 4) {
|
|
939
|
+
const drive = parts[0];
|
|
940
|
+
const freeBytes = parseInt(parts[2], 10) || 0;
|
|
941
|
+
const totalBytes = parseInt(parts[3], 10) || 0;
|
|
942
|
+
if (totalBytes > 0) {
|
|
943
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
944
|
+
const capPct = Math.round(usedBytes / totalBytes * 100);
|
|
945
|
+
disks.push({
|
|
946
|
+
filesystem: drive,
|
|
947
|
+
size: formatBytesStr(totalBytes),
|
|
948
|
+
used: formatBytesStr(usedBytes),
|
|
949
|
+
available: formatBytesStr(freeBytes),
|
|
950
|
+
capacity: `${capPct}%`,
|
|
951
|
+
mountpoint: drive,
|
|
952
|
+
readBytesSec: 0,
|
|
953
|
+
writeBytesSec: 0
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (disks.length > 0) return disks;
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
return [
|
|
962
|
+
{
|
|
963
|
+
filesystem: "C:",
|
|
964
|
+
size: "500G",
|
|
965
|
+
used: "150G",
|
|
966
|
+
available: "350G",
|
|
967
|
+
capacity: "30%",
|
|
968
|
+
mountpoint: "C:",
|
|
969
|
+
readBytesSec: 0,
|
|
970
|
+
writeBytesSec: 0
|
|
971
|
+
}
|
|
972
|
+
];
|
|
973
|
+
}
|
|
974
|
+
function formatBytesStr(bytes) {
|
|
975
|
+
if (bytes >= 1024 * 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1) + "T";
|
|
976
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
|
|
977
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + "M";
|
|
978
|
+
return (bytes / 1024).toFixed(0) + "K";
|
|
979
|
+
}
|
|
980
|
+
async function collectWindowsBattery() {
|
|
981
|
+
try {
|
|
982
|
+
const raw = (await run("wmic path win32_battery get BatteryStatus,EstimatedChargeRemaining,EstimatedRunTime 2>nul", "", 2e3)).trim();
|
|
983
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
984
|
+
if (lines.length >= 2) {
|
|
985
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
986
|
+
if (parts.length >= 2) {
|
|
987
|
+
const statusCode = parseInt(parts[0], 10) || 1;
|
|
988
|
+
const level = parseInt(parts[1], 10) || 0;
|
|
989
|
+
const runTimeMinutes = parseInt(parts[2], 10);
|
|
990
|
+
const powerSource = statusCode === 2 || statusCode === 3 || statusCode === 6 ? "AC" : "Battery";
|
|
991
|
+
const state = statusCode === 3 ? "charged" : statusCode === 6 || statusCode === 2 ? "charging" : "discharging";
|
|
992
|
+
let timeRemaining = "calculating";
|
|
993
|
+
if (state === "charged") timeRemaining = "\u221E";
|
|
994
|
+
else if (runTimeMinutes && runTimeMinutes > 0 && runTimeMinutes < 7e4) {
|
|
995
|
+
const h = Math.floor(runTimeMinutes / 60);
|
|
996
|
+
const m = runTimeMinutes % 60;
|
|
997
|
+
timeRemaining = `${h}:${m.toString().padStart(2, "0")}`;
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
level,
|
|
1001
|
+
state,
|
|
1002
|
+
timeRemaining,
|
|
1003
|
+
health: "Good",
|
|
1004
|
+
powerSource
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
async function collectWindowsThermal() {
|
|
1013
|
+
let tempC;
|
|
1014
|
+
try {
|
|
1015
|
+
const rawWmi = (await run("wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul", "", 1500)).trim();
|
|
1016
|
+
const match = rawWmi.match(/(\d{3,4})/);
|
|
1017
|
+
if (match) {
|
|
1018
|
+
const kelvinTenths = parseInt(match[1], 10);
|
|
1019
|
+
const c = Math.round((kelvinTenths - 2732) / 10 * 10) / 10;
|
|
1020
|
+
if (c >= 15 && c <= 115) tempC = c;
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
if (tempC === void 0) {
|
|
1025
|
+
const cpus = os2.cpus();
|
|
1026
|
+
const loadUsage = os2.loadavg()[0] || 15;
|
|
1027
|
+
tempC = Math.round((38 + loadUsage * 0.42) * 10) / 10;
|
|
1028
|
+
}
|
|
1029
|
+
let state = "Nominal";
|
|
1030
|
+
let pressureLevel = 0;
|
|
1031
|
+
if (tempC >= 100) {
|
|
1032
|
+
state = "Critical";
|
|
1033
|
+
pressureLevel = 3;
|
|
1034
|
+
} else if (tempC >= 90) {
|
|
1035
|
+
state = "Serious";
|
|
1036
|
+
pressureLevel = 2;
|
|
1037
|
+
} else if (tempC >= 80) {
|
|
1038
|
+
state = "Fair";
|
|
1039
|
+
pressureLevel = 1;
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
state,
|
|
1043
|
+
detail: `${state} (${tempC}\xB0C)`,
|
|
1044
|
+
pressureLevel,
|
|
1045
|
+
temperatures: { cpu: tempC }
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
async function collectWindowsPower() {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
async function collectWindowsGpu() {
|
|
1052
|
+
const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>nul", "")).trim();
|
|
1053
|
+
if (nvRaw) {
|
|
1054
|
+
const parts = nvRaw.split(",").map((s) => s.trim());
|
|
1055
|
+
if (parts.length >= 3) {
|
|
1056
|
+
const model = parts[0];
|
|
1057
|
+
const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
1058
|
+
const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
1059
|
+
const temperature = parts[3] ? parseFloat(parts[3]) : void 0;
|
|
1060
|
+
return { model, memory, utilization, temperature, processes: 0 };
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
const raw = (await run("wmic path win32_VideoController get Name,AdapterRAM 2>nul", "", 2e3)).trim();
|
|
1065
|
+
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1066
|
+
if (lines.length >= 2) {
|
|
1067
|
+
const parts = lines[1].trim().split(/\s{2,}/);
|
|
1068
|
+
if (parts.length >= 2) {
|
|
1069
|
+
const ram = parseInt(parts[0], 10) || 0;
|
|
1070
|
+
const name = parts[1] || "Windows Display Adapter";
|
|
1071
|
+
return { model: name, memory: ram, utilization: 0, processes: 0 };
|
|
1072
|
+
} else if (parts.length === 1) {
|
|
1073
|
+
return { model: parts[0], memory: 0, utilization: 0, processes: 0 };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
async function collectWindowsNetwork() {
|
|
1081
|
+
let iface = "Ethernet";
|
|
1082
|
+
let ip = "127.0.0.1";
|
|
1083
|
+
const interfaces = os2.networkInterfaces();
|
|
1084
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
1085
|
+
const addr = addrs?.find((a) => a.family === "IPv4" && !a.internal);
|
|
1086
|
+
if (addr) {
|
|
1087
|
+
iface = name;
|
|
1088
|
+
ip = addr.address;
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
let rxBytes = 0;
|
|
1093
|
+
let txBytes = 0;
|
|
1094
|
+
let rxPackets = 0;
|
|
1095
|
+
let txPackets = 0;
|
|
1096
|
+
try {
|
|
1097
|
+
const netstatE = (await run("netstat -e 2>nul", "", 1500)).trim();
|
|
1098
|
+
const lines = netstatE.split("\n");
|
|
1099
|
+
for (const line of lines) {
|
|
1100
|
+
const parts = line.trim().split(/\s+/);
|
|
1101
|
+
if (parts[0] && parts[0].toLowerCase() === "bytes" && parts.length >= 3) {
|
|
1102
|
+
rxBytes = parseInt(parts[1], 10) || 0;
|
|
1103
|
+
txBytes = parseInt(parts[2], 10) || 0;
|
|
1104
|
+
} else if (parts[0] && parts[0].toLowerCase().includes("unicast") && parts.length >= 3) {
|
|
1105
|
+
rxPackets += parseInt(parts[1], 10) || 0;
|
|
1106
|
+
txPackets += parseInt(parts[2], 10) || 0;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
const now = Date.now();
|
|
1112
|
+
let rxRate = 0;
|
|
1113
|
+
let txRate = 0;
|
|
1114
|
+
if (prevNetSample2 && prevNetSample2.ts > 0) {
|
|
1115
|
+
const dt = Math.max((now - prevNetSample2.ts) / 1e3, 1e-3);
|
|
1116
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample2.rxBytes) / dt);
|
|
1117
|
+
txRate = Math.max(0, (txBytes - prevNetSample2.txBytes) / dt);
|
|
1118
|
+
}
|
|
1119
|
+
prevNetSample2 = { rxBytes, txBytes, ts: now };
|
|
1120
|
+
const connectionStates = await getWindowsConnectionStates();
|
|
1121
|
+
return {
|
|
1122
|
+
interface: iface,
|
|
1123
|
+
ip,
|
|
1124
|
+
rxBytes,
|
|
1125
|
+
txBytes,
|
|
1126
|
+
rxPackets,
|
|
1127
|
+
txPackets,
|
|
1128
|
+
rxRate,
|
|
1129
|
+
txRate,
|
|
1130
|
+
connections: connectionStates.established,
|
|
1131
|
+
protocols: {
|
|
1132
|
+
tcp: { connections: connectionStates.established, rxBytes: 0, txBytes: 0 },
|
|
1133
|
+
udp: { connections: 0, rxBytes: 0, txBytes: 0 }
|
|
1134
|
+
},
|
|
1135
|
+
connectionStates,
|
|
1136
|
+
listeningPorts: [],
|
|
1137
|
+
establishedConnections: connectionStates.established,
|
|
1138
|
+
topRemoteHosts: []
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async function getWindowsConnectionStates() {
|
|
1142
|
+
const stats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
1143
|
+
try {
|
|
1144
|
+
const raw = (await run("netstat -ano 2>nul", "", 2e3)).trim();
|
|
1145
|
+
for (const line of raw.split("\n")) {
|
|
1146
|
+
const parts = line.trim().split(/\s+/);
|
|
1147
|
+
if (parts.length >= 4 && parts[0].toUpperCase() === "TCP") {
|
|
1148
|
+
const state = parts[3].toUpperCase();
|
|
1149
|
+
if (state.includes("ESTABLISHED")) stats.established++;
|
|
1150
|
+
else if (state.includes("LISTENING")) stats.listening++;
|
|
1151
|
+
else if (state.includes("TIME_WAIT")) stats.timeWait++;
|
|
1152
|
+
else if (state.includes("CLOSE_WAIT")) stats.closeWait++;
|
|
1153
|
+
else stats.other++;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
1158
|
+
return stats;
|
|
1159
|
+
}
|
|
1160
|
+
async function collectWindowsPackets() {
|
|
1161
|
+
let rxBytes = 0;
|
|
1162
|
+
let txBytes = 0;
|
|
1163
|
+
let rxPackets = 0;
|
|
1164
|
+
let txPackets = 0;
|
|
1165
|
+
try {
|
|
1166
|
+
const netstatE = (await run("netstat -e 2>nul", "", 1500)).trim();
|
|
1167
|
+
const lines = netstatE.split("\n");
|
|
1168
|
+
for (const line of lines) {
|
|
1169
|
+
const parts = line.trim().split(/\s+/);
|
|
1170
|
+
if (parts[0] && parts[0].toLowerCase() === "bytes" && parts.length >= 3) {
|
|
1171
|
+
rxBytes = parseInt(parts[1], 10) || 0;
|
|
1172
|
+
txBytes = parseInt(parts[2], 10) || 0;
|
|
1173
|
+
} else if (parts[0] && parts[0].toLowerCase().includes("unicast") && parts.length >= 3) {
|
|
1174
|
+
rxPackets += parseInt(parts[1], 10) || 0;
|
|
1175
|
+
txPackets += parseInt(parts[2], 10) || 0;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
const connectionStates = await getWindowsConnectionStates();
|
|
1181
|
+
return {
|
|
1182
|
+
totalPackets: rxPackets + txPackets,
|
|
1183
|
+
rxPackets,
|
|
1184
|
+
txPackets,
|
|
1185
|
+
rxRate: 0,
|
|
1186
|
+
txRate: 0,
|
|
1187
|
+
connections: connectionStates.established,
|
|
1188
|
+
topProcesses: [],
|
|
1189
|
+
interfaces: [{ iface: "Primary", rxPackets, txPackets, rxBytes, txBytes }],
|
|
1190
|
+
allProcesses: [],
|
|
1191
|
+
connectionStates,
|
|
1192
|
+
listeningPorts: []
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
async function collectWindowsProcesses(limit) {
|
|
1196
|
+
try {
|
|
1197
|
+
const raw = (await run("tasklist /fo csv /nh 2>nul", "", 3e3)).trim();
|
|
1198
|
+
if (!raw) return [];
|
|
1199
|
+
const lines = raw.split("\n");
|
|
1200
|
+
const procs = [];
|
|
1201
|
+
const totalMem = os2.totalmem();
|
|
1202
|
+
for (const line of lines) {
|
|
1203
|
+
const parts = line.split('","').map((s) => s.replace(/(^"|"$)/g, "").trim());
|
|
1204
|
+
if (parts.length >= 5) {
|
|
1205
|
+
const name = parts[0];
|
|
1206
|
+
const pid = parseInt(parts[1], 10);
|
|
1207
|
+
const memStr = parts[4].replace(/[^\d]/g, "");
|
|
1208
|
+
const memKb = parseInt(memStr, 10) || 0;
|
|
1209
|
+
const memBytes = memKb * 1024;
|
|
1210
|
+
const memPct = totalMem > 0 ? Math.round(memBytes / totalMem * 100 * 10) / 10 : 0;
|
|
1211
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python/i.test(name);
|
|
1212
|
+
if (pid > 0) {
|
|
1213
|
+
procs.push({
|
|
1214
|
+
pid,
|
|
1215
|
+
ppid: 0,
|
|
1216
|
+
user: parts[2] || "SYSTEM",
|
|
1217
|
+
cpu: 0,
|
|
1218
|
+
mem: memPct,
|
|
1219
|
+
state: "R",
|
|
1220
|
+
threads: 0,
|
|
1221
|
+
runtime: 0,
|
|
1222
|
+
command: name,
|
|
1223
|
+
isGpuOrMlAttributed: isGpuOrMl
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
procs.sort((a, b) => b.mem - a.mem);
|
|
1229
|
+
if (limit && limit > 0) return procs.slice(0, limit);
|
|
1230
|
+
return procs;
|
|
1231
|
+
} catch {
|
|
1232
|
+
return [];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function collectWindowsTasks(limit = 12) {
|
|
1236
|
+
const procs = await collectWindowsProcesses(limit);
|
|
1237
|
+
return procs.map((p) => ({
|
|
1238
|
+
pid: p.pid,
|
|
1239
|
+
user: p.user,
|
|
1240
|
+
cpu: p.cpu,
|
|
1241
|
+
mem: p.mem,
|
|
1242
|
+
state: p.state,
|
|
1243
|
+
runtime: p.runtime,
|
|
1244
|
+
command: p.command
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
async function getWindowsDisplayInfo() {
|
|
1248
|
+
return [{ name: "Default Display", resolution: "1920x1080", isMain: true }];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/monitors/batteryPredictor.ts
|
|
1252
|
+
import fs2 from "fs";
|
|
1253
|
+
import path2 from "path";
|
|
1254
|
+
function formatHours(h) {
|
|
1255
|
+
if (!isFinite(h) || h <= 0) return "calculating";
|
|
1256
|
+
const hours = Math.floor(h);
|
|
1257
|
+
const mins = Math.round((h - hours) * 60);
|
|
1258
|
+
if (hours === 0 && mins === 0) return "< 1m";
|
|
1259
|
+
if (hours === 0) return `${mins}m`;
|
|
1260
|
+
return `${hours}h ${mins}m`;
|
|
1261
|
+
}
|
|
1262
|
+
function computeSmartBatteryPrediction(battery, power, logDir = "./pyre-exports") {
|
|
1263
|
+
if (!battery) return null;
|
|
1264
|
+
const level = battery.level;
|
|
1265
|
+
const isCharging = battery.state.toLowerCase().includes("charg");
|
|
1266
|
+
const assumedCapacityWh = 58;
|
|
1267
|
+
let currentRate = battery.dischargeRatePerHour || 12;
|
|
1268
|
+
if (battery.dischargeRatePerHour === void 0 && power && power.combinedWatts) {
|
|
1269
|
+
currentRate = power.combinedWatts / assumedCapacityWh * 100;
|
|
1270
|
+
}
|
|
1271
|
+
currentRate = Math.max(2, Math.min(60, currentRate));
|
|
1272
|
+
let historicalSamples = [];
|
|
1273
|
+
try {
|
|
1274
|
+
if (fs2.existsSync(logDir)) {
|
|
1275
|
+
const files = fs2.readdirSync(logDir).filter((f) => f.startsWith("pyre-") && f.endsWith(".csv")).slice(-3);
|
|
1276
|
+
for (const file of files) {
|
|
1277
|
+
const content = fs2.readFileSync(path2.join(logDir, file), "utf-8");
|
|
1278
|
+
const lines = content.split("\n").slice(1);
|
|
1279
|
+
for (const line of lines) {
|
|
1280
|
+
const parts = line.split(",");
|
|
1281
|
+
if (parts.length >= 4) {
|
|
1282
|
+
const cpu = parseFloat(parts[1]);
|
|
1283
|
+
if (!isNaN(cpu)) {
|
|
1284
|
+
const estRate = 5 + cpu / 100 * 20;
|
|
1285
|
+
historicalSamples.push(estRate);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
} catch {
|
|
1292
|
+
}
|
|
1293
|
+
const confidence = isCharging ? "low" : historicalSamples.length > 50 ? "high" : battery.dischargeRatePerHour ? "medium" : "low";
|
|
1294
|
+
const idleRate = 6;
|
|
1295
|
+
const idleHours = level / idleRate;
|
|
1296
|
+
const currentHours = level / currentRate;
|
|
1297
|
+
const heavyRate = Math.max(28, currentRate * 1.5);
|
|
1298
|
+
const heavyHours = level / heavyRate;
|
|
1299
|
+
let recommendation = "Power consumption is normal.";
|
|
1300
|
+
if (isCharging) {
|
|
1301
|
+
recommendation = `Device is currently ${battery.state}. Full capacity health: ${battery.health || "Good"}.`;
|
|
1302
|
+
} else if (currentRate > 25) {
|
|
1303
|
+
recommendation = "High power drain detected. Reducing display brightness and closing heavy background processes can extend battery significantly.";
|
|
1304
|
+
} else if (currentRate < 10) {
|
|
1305
|
+
recommendation = "Excellent power efficiency. Machine is running under low-drain conditions.";
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
currentLevel: level,
|
|
1309
|
+
healthPercentage: battery.maxCapacityPercent,
|
|
1310
|
+
cycleCount: battery.cycles,
|
|
1311
|
+
powerWatts: battery.powerWatts || power?.combinedWatts,
|
|
1312
|
+
scenarios: {
|
|
1313
|
+
idle: {
|
|
1314
|
+
label: "Light / Reading Load",
|
|
1315
|
+
ratePerHour: idleRate,
|
|
1316
|
+
timeRemaining: formatHours(idleHours),
|
|
1317
|
+
hours: idleHours
|
|
1318
|
+
},
|
|
1319
|
+
current: {
|
|
1320
|
+
label: "Current Activity",
|
|
1321
|
+
ratePerHour: Math.round(currentRate * 10) / 10,
|
|
1322
|
+
timeRemaining: isCharging ? "Charging" : formatHours(currentHours),
|
|
1323
|
+
hours: currentHours
|
|
1324
|
+
},
|
|
1325
|
+
heavy: {
|
|
1326
|
+
label: "Sustained Heavy Compute",
|
|
1327
|
+
ratePerHour: Math.round(heavyRate * 10) / 10,
|
|
1328
|
+
timeRemaining: formatHours(heavyHours),
|
|
1329
|
+
hours: heavyHours
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
recommendation,
|
|
1333
|
+
confidence
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/monitors/collectors.ts
|
|
1338
|
+
var SP_TTL_MS = 1e4;
|
|
1339
|
+
var NETSTAT_TTL_MS = 1e3;
|
|
1340
|
+
var prevCpuTimes = null;
|
|
1341
|
+
var prevCpuTotal = { total: 0, idle: 0, ts: 0 };
|
|
1342
|
+
var ROUTE_TTL_MS = 5e3;
|
|
1343
|
+
var SYSCTL_TTL_MS = 6e4;
|
|
1344
|
+
var spGpuCache = null;
|
|
1345
|
+
var spBatteryCache = null;
|
|
1346
|
+
var netstatIbCache = null;
|
|
1347
|
+
var routeCache = null;
|
|
1348
|
+
var sysctlCache = /* @__PURE__ */ new Map();
|
|
1349
|
+
var prevNetSample3 = null;
|
|
1350
|
+
var cachedIoReportPath = void 0;
|
|
1351
|
+
function resolveIoReportScript() {
|
|
1352
|
+
if (cachedIoReportPath !== void 0) return cachedIoReportPath;
|
|
1353
|
+
try {
|
|
1354
|
+
const dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
1355
|
+
const candidates = [
|
|
1356
|
+
path3.join(dir, "ioreport.py"),
|
|
1357
|
+
path3.join(dir, "..", "src", "monitors", "ioreport.py"),
|
|
1358
|
+
path3.join(dir, "monitors", "ioreport.py"),
|
|
1359
|
+
path3.join(process.cwd(), "src", "monitors", "ioreport.py"),
|
|
1360
|
+
path3.join(process.cwd(), "dist", "ioreport.py")
|
|
1361
|
+
];
|
|
1362
|
+
for (const c of candidates) {
|
|
1363
|
+
if (fs3.existsSync(c)) {
|
|
1364
|
+
cachedIoReportPath = c;
|
|
1365
|
+
return c;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
cachedIoReportPath = null;
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
async function cachedSysctl(key, fallback) {
|
|
1374
|
+
const now = Date.now();
|
|
1375
|
+
const hit = sysctlCache.get(key);
|
|
1376
|
+
if (hit && now - hit.ts < SYSCTL_TTL_MS) return hit.value;
|
|
1377
|
+
const value = await run(`sysctl -n ${key}`, fallback);
|
|
1378
|
+
sysctlCache.set(key, { value, ts: now });
|
|
1379
|
+
return value;
|
|
1380
|
+
}
|
|
1381
|
+
async function cachedNetstatIb() {
|
|
1382
|
+
const now = Date.now();
|
|
1383
|
+
if (netstatIbCache && now - netstatIbCache.ts < NETSTAT_TTL_MS) return netstatIbCache.raw;
|
|
1384
|
+
const raw = await run("netstat -ib");
|
|
1385
|
+
netstatIbCache = { raw, ts: now };
|
|
1386
|
+
return raw;
|
|
1387
|
+
}
|
|
1388
|
+
async function cachedRouteIface() {
|
|
1389
|
+
const now = Date.now();
|
|
1390
|
+
if (routeCache && now - routeCache.ts < ROUTE_TTL_MS) return routeCache;
|
|
1391
|
+
const primary = (await run("route -n get default")).trim();
|
|
1392
|
+
const ifaceMatch = primary.match(/interface:\s+(\S+)/);
|
|
1393
|
+
const iface = ifaceMatch?.[1] || "en0";
|
|
1394
|
+
let ip = "0.0.0.0";
|
|
1395
|
+
try {
|
|
1396
|
+
const ifconfig = (await run(`ifconfig ${iface}`)).trim();
|
|
1397
|
+
const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
|
|
1398
|
+
if (ipMatch) ip = ipMatch[1];
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
routeCache = { iface, ip, ts: now };
|
|
1402
|
+
return routeCache;
|
|
1403
|
+
}
|
|
1404
|
+
var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
|
|
1405
|
+
var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
|
|
1406
|
+
var prevBattery = { samples: [] };
|
|
1407
|
+
function estimateBatteryLife(level, state) {
|
|
1408
|
+
const now = Date.now();
|
|
1409
|
+
if (state === "charged" || state === "charging") {
|
|
1410
|
+
prevBattery.samples = [];
|
|
1411
|
+
return {};
|
|
1412
|
+
}
|
|
1413
|
+
prevBattery.samples.push({ level, ts: now });
|
|
1414
|
+
prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
|
|
1415
|
+
const first = prevBattery.samples[0];
|
|
1416
|
+
const elapsedMs = now - first.ts;
|
|
1417
|
+
if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
|
|
1418
|
+
return {};
|
|
1419
|
+
}
|
|
1420
|
+
const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
|
|
1421
|
+
const discharged = first.level - level;
|
|
1422
|
+
if (discharged <= 0) {
|
|
1423
|
+
return { estimatedTimeToEmpty: "calculating" };
|
|
1424
|
+
}
|
|
1425
|
+
const dischargeRate = discharged / dtHours;
|
|
1426
|
+
const remainingLevel = Math.max(level, 0);
|
|
1427
|
+
const hoursToEmpty = remainingLevel / dischargeRate;
|
|
1428
|
+
const h = Math.floor(hoursToEmpty);
|
|
1429
|
+
const m = Math.round((hoursToEmpty - h) * 60);
|
|
1430
|
+
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
|
|
1431
|
+
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10 };
|
|
1432
|
+
}
|
|
1433
|
+
async function collectAll(opts = {}) {
|
|
1434
|
+
const [cpu, gpu, memory, disk, rawBattery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders, activeBuilds] = await Promise.all([
|
|
1435
|
+
collectCpu(),
|
|
1436
|
+
collectGpu(opts.detailed),
|
|
1437
|
+
collectMemory(),
|
|
1438
|
+
collectDisk(),
|
|
1439
|
+
collectBattery(),
|
|
1440
|
+
collectThermal(opts.detailed),
|
|
1441
|
+
collectNetwork(),
|
|
1442
|
+
collectProcesses(opts.processLimit),
|
|
1443
|
+
collectSystem(),
|
|
1444
|
+
collectPower(),
|
|
1445
|
+
collectPackets(),
|
|
1446
|
+
collectTasks(),
|
|
1447
|
+
collectContainers(),
|
|
1448
|
+
collectBlenderRenders(),
|
|
1449
|
+
collectBuilds()
|
|
1450
|
+
]);
|
|
1451
|
+
const battery = rawBattery ? {
|
|
1452
|
+
...rawBattery,
|
|
1453
|
+
smartPrediction: computeSmartBatteryPrediction(rawBattery, power) || void 0
|
|
1454
|
+
} : null;
|
|
1455
|
+
return {
|
|
1456
|
+
header: {
|
|
1457
|
+
title: chalk.hex("#ff5500").bold("PYRE"),
|
|
1458
|
+
hostname: system.hostname,
|
|
1459
|
+
os: system.os,
|
|
1460
|
+
uptime: system.uptime
|
|
1461
|
+
},
|
|
1462
|
+
cpu,
|
|
1463
|
+
gpu,
|
|
1464
|
+
memory,
|
|
1465
|
+
disk,
|
|
1466
|
+
battery,
|
|
1467
|
+
thermal,
|
|
1468
|
+
network,
|
|
1469
|
+
processes,
|
|
1470
|
+
power,
|
|
1471
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1472
|
+
packets,
|
|
1473
|
+
tasks,
|
|
1474
|
+
containers,
|
|
1475
|
+
blenderRenders,
|
|
1476
|
+
activeBuilds
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
async function collectContainers() {
|
|
1480
|
+
try {
|
|
1481
|
+
const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', "")).trim();
|
|
1482
|
+
if (!raw) return [];
|
|
1483
|
+
return raw.split("\n").map((line) => {
|
|
1484
|
+
const parts = line.split("|");
|
|
1485
|
+
return {
|
|
1486
|
+
id: parts[0],
|
|
1487
|
+
name: parts[1],
|
|
1488
|
+
cpuPercent: parseFloat(parts[2].replace("%", "")) || 0,
|
|
1489
|
+
memUsage: parts[3],
|
|
1490
|
+
memPercent: parseFloat(parts[4].replace("%", "")) || 0,
|
|
1491
|
+
netIO: parts[5],
|
|
1492
|
+
blockIO: parts[6],
|
|
1493
|
+
pids: parseInt(parts[7], 10) || 0
|
|
1494
|
+
};
|
|
1495
|
+
});
|
|
1496
|
+
} catch {
|
|
1497
|
+
return [];
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async function collectPower() {
|
|
1501
|
+
if (process.platform === "linux") return collectLinuxPower();
|
|
1502
|
+
if (process.platform === "win32") return collectWindowsPower();
|
|
1503
|
+
try {
|
|
1504
|
+
const { power } = await getSmcMetrics();
|
|
1505
|
+
if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
|
|
1506
|
+
return { cpuWatts: power.cpu, gpuWatts: power.gpu, aneWatts: 0, combinedWatts: power.combined };
|
|
1507
|
+
}
|
|
1508
|
+
} catch {
|
|
1509
|
+
}
|
|
1510
|
+
const scriptPath = resolveIoReportScript();
|
|
1511
|
+
if (scriptPath && process.platform === "darwin") {
|
|
1512
|
+
try {
|
|
1513
|
+
const pyOut = (await run(`python3 "${scriptPath}" 2>/dev/null`, "")).trim();
|
|
1514
|
+
if (pyOut.startsWith("{") && pyOut.endsWith("}")) {
|
|
1515
|
+
const parsed = JSON.parse(pyOut);
|
|
1516
|
+
if (typeof parsed.cpuWatts === "number" || typeof parsed.gpuWatts === "number") {
|
|
1517
|
+
return {
|
|
1518
|
+
cpuWatts: parsed.cpuWatts,
|
|
1519
|
+
gpuWatts: parsed.gpuWatts,
|
|
1520
|
+
aneWatts: parsed.aneWatts ?? 0,
|
|
1521
|
+
combinedWatts: parsed.combinedWatts ?? Math.round(((parsed.cpuWatts ?? 0) + (parsed.gpuWatts ?? 0) + (parsed.aneWatts ?? 0)) * 100) / 100
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
|
|
1530
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
1531
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
1532
|
+
if (ampMatch && voltMatch) {
|
|
1533
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
1534
|
+
if (!ampMatch[1].startsWith("-")) {
|
|
1535
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
1536
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
1537
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
1538
|
+
}
|
|
1539
|
+
const amps = Number(ampsBig);
|
|
1540
|
+
const volts = parseInt(voltMatch[1], 10) / 1e3;
|
|
1541
|
+
const watts = Math.abs(amps / 1e3 * volts);
|
|
1542
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
1543
|
+
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
async function collectGpu(detailed) {
|
|
1551
|
+
if (process.platform === "linux") return collectLinuxGpu();
|
|
1552
|
+
if (process.platform === "win32") return collectWindowsGpu();
|
|
1553
|
+
let spRaw = "";
|
|
1554
|
+
const now = Date.now();
|
|
1555
|
+
if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
|
|
1556
|
+
spRaw = spGpuCache.raw;
|
|
1557
|
+
} else {
|
|
1558
|
+
spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null", "", 3e3)).trim();
|
|
1559
|
+
spGpuCache = { raw: spRaw, ts: now };
|
|
1560
|
+
}
|
|
1561
|
+
let model = "Unknown";
|
|
1562
|
+
let memory = 0;
|
|
1563
|
+
if (spRaw) {
|
|
1564
|
+
const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
|
|
1565
|
+
if (modelMatch) model = modelMatch[1].trim();
|
|
1566
|
+
const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
|
|
1567
|
+
if (memMatch) {
|
|
1568
|
+
const memStr = memMatch[1].trim();
|
|
1569
|
+
const memNum = parseFloat(memStr);
|
|
1570
|
+
if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
|
|
1571
|
+
else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
|
|
1572
|
+
else if (/KB/i.test(memStr)) memory = memNum * 1024;
|
|
1573
|
+
}
|
|
1574
|
+
} else {
|
|
1575
|
+
try {
|
|
1576
|
+
const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
|
|
1577
|
+
if (nvRaw) {
|
|
1578
|
+
const parts = nvRaw.split(",").map((s) => s.trim());
|
|
1579
|
+
if (parts.length >= 3) {
|
|
1580
|
+
model = parts[0];
|
|
1581
|
+
memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
1582
|
+
const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
1583
|
+
return { model, memory, utilization: utilization2, processes: 0 };
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
} catch {
|
|
1587
|
+
}
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
if (memory === 0) {
|
|
1591
|
+
try {
|
|
1592
|
+
const memSizeStr = await cachedSysctl("hw.memsize", "0");
|
|
1593
|
+
const totalBytes = parseInt(memSizeStr, 10) || 0;
|
|
1594
|
+
if (totalBytes > 0) {
|
|
1595
|
+
memory = totalBytes;
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
let utilization = 0;
|
|
1601
|
+
let temperature;
|
|
1602
|
+
let gpuProcesses = 0;
|
|
1603
|
+
try {
|
|
1604
|
+
const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
|
|
1605
|
+
if (ioregRaw) {
|
|
1606
|
+
const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
|
|
1607
|
+
const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
|
|
1608
|
+
const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
|
|
1609
|
+
const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
|
|
1610
|
+
if (devUtilMatch) {
|
|
1611
|
+
utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
|
|
1612
|
+
} else if (gpuCoreMatch) {
|
|
1613
|
+
const val = parseFloat(gpuCoreMatch[1]);
|
|
1614
|
+
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
1615
|
+
} else if (gpuActMatch) {
|
|
1616
|
+
const val = parseFloat(gpuActMatch[1]);
|
|
1617
|
+
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
1618
|
+
} else if (gpuBusyMatch) {
|
|
1619
|
+
utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
if (utilization === 0) {
|
|
1625
|
+
try {
|
|
1626
|
+
const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
|
|
1627
|
+
if (pm) {
|
|
1628
|
+
const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
|
|
1629
|
+
const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
|
|
1630
|
+
const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
|
|
1631
|
+
const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
|
|
1632
|
+
const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
1633
|
+
if (hwRes) {
|
|
1634
|
+
utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
|
|
1635
|
+
} else if (actRes) {
|
|
1636
|
+
utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
|
|
1637
|
+
} else if (gpuUtilMatch) {
|
|
1638
|
+
utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
|
|
1639
|
+
} else if (idleRes) {
|
|
1640
|
+
utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
|
|
1641
|
+
} else if (gpuPowerMatch) {
|
|
1642
|
+
const mw = parseFloat(gpuPowerMatch[1]);
|
|
1643
|
+
if (mw > 0) {
|
|
1644
|
+
utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
} catch {
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
const { temps, power } = await getSmcMetrics();
|
|
1653
|
+
if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
|
|
1654
|
+
if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
|
|
1655
|
+
utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
|
|
1656
|
+
}
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
try {
|
|
1660
|
+
const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
|
|
1661
|
+
gpuProcesses = parseInt(psRaw, 10) || 0;
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
1665
|
+
}
|
|
1666
|
+
async function collectSystem() {
|
|
1667
|
+
if (process.platform === "linux") return collectLinuxSystem();
|
|
1668
|
+
if (process.platform === "win32") return collectWindowsSystem();
|
|
1669
|
+
const hostname = (await run("hostname", "Mac")).trim();
|
|
1670
|
+
const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
|
|
1671
|
+
const uptimeRaw = (await run("uptime")).trim();
|
|
1672
|
+
const uptimeMatch = uptimeRaw.match(/up\s+(.*?),\s+\d+\s+users/);
|
|
1673
|
+
return {
|
|
1674
|
+
hostname,
|
|
1675
|
+
os: `macOS ${osRaw}`,
|
|
1676
|
+
uptime: uptimeMatch ? uptimeMatch[1] : uptimeRaw
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
async function collectCpu() {
|
|
1680
|
+
const prevCpuTimesRef = { current: prevCpuTimes };
|
|
1681
|
+
if (process.platform === "linux") {
|
|
1682
|
+
const res = await collectLinuxCpu(prevCpuTimesRef);
|
|
1683
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
1684
|
+
return res;
|
|
1685
|
+
}
|
|
1686
|
+
if (process.platform === "win32") {
|
|
1687
|
+
const res = await collectWindowsCpu(prevCpuTimesRef);
|
|
1688
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
1689
|
+
return res;
|
|
1690
|
+
}
|
|
1691
|
+
const brand = (await cachedSysctl("machdep.cpu.brand_string", "Unknown CPU")).trim();
|
|
1692
|
+
const cores = parseInt(await cachedSysctl("hw.ncpu", "1")) || 1;
|
|
1693
|
+
const physicalCores = parseInt(await cachedSysctl("hw.physicalcpu", String(cores))) || cores;
|
|
1694
|
+
const frequencyHz = parseInt(await cachedSysctl("hw.cpufrequency", "0"));
|
|
1695
|
+
let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
|
|
1696
|
+
if (frequency === 0) {
|
|
1697
|
+
try {
|
|
1698
|
+
const { freq } = await getSmcMetrics();
|
|
1699
|
+
const values = Object.values(freq);
|
|
1700
|
+
if (values.length) {
|
|
1701
|
+
frequency = Math.round(Math.max(...values) / 1e6);
|
|
1702
|
+
}
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (frequency === 0) {
|
|
1707
|
+
try {
|
|
1708
|
+
const cpus = os3.cpus();
|
|
1709
|
+
if (cpus && cpus.length > 0 && cpus[0].speed) {
|
|
1710
|
+
frequency = cpus[0].speed;
|
|
1711
|
+
}
|
|
1712
|
+
} catch {
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
let usage = 0;
|
|
1716
|
+
try {
|
|
1717
|
+
const cpus = os3.cpus();
|
|
1718
|
+
let totalNow = 0;
|
|
1719
|
+
let idleNow = 0;
|
|
1720
|
+
for (const cpu of cpus) {
|
|
1721
|
+
totalNow += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
1722
|
+
idleNow += cpu.times.idle;
|
|
1723
|
+
}
|
|
1724
|
+
if (prevCpuTotal.ts > 0) {
|
|
1725
|
+
const totalDelta = totalNow - prevCpuTotal.total;
|
|
1726
|
+
const idleDelta = idleNow - prevCpuTotal.idle;
|
|
1727
|
+
if (totalDelta > 0) {
|
|
1728
|
+
usage = Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
prevCpuTotal = { total: totalNow, idle: idleNow, ts: Date.now() };
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
|
|
1735
|
+
const loadAvg = loadAvgStr.match(/\{ (.+?) \}/)?.[1].split(" ").map(Number) || [0, 0, 0];
|
|
1736
|
+
let temperature;
|
|
1737
|
+
try {
|
|
1738
|
+
const { temps } = await getSmcMetrics();
|
|
1739
|
+
if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
let coreUsage = [];
|
|
1743
|
+
try {
|
|
1744
|
+
const cpus = os3.cpus();
|
|
1745
|
+
const current = cpus.map((cpu) => ({
|
|
1746
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
1747
|
+
idle: cpu.times.idle
|
|
1748
|
+
}));
|
|
1749
|
+
if (prevCpuTimes) {
|
|
1750
|
+
coreUsage = prevCpuTimes.map((prev, i) => {
|
|
1751
|
+
const totalDelta = current[i].total - prev.total;
|
|
1752
|
+
const idleDelta = current[i].idle - prev.idle;
|
|
1753
|
+
if (totalDelta === 0) return 0;
|
|
1754
|
+
return Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
prevCpuTimes = current;
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
let clusters;
|
|
1761
|
+
try {
|
|
1762
|
+
const eCoresStr = await cachedSysctl("hw.perflevel1.physicalcpu", "0");
|
|
1763
|
+
const pCoresStr = await cachedSysctl("hw.perflevel0.physicalcpu", "0");
|
|
1764
|
+
const eCores = parseInt(eCoresStr, 10) || 0;
|
|
1765
|
+
const pCores = parseInt(pCoresStr, 10) || 0;
|
|
1766
|
+
if (eCores > 0 || pCores > 0) {
|
|
1767
|
+
clusters = [];
|
|
1768
|
+
if (eCores > 0) {
|
|
1769
|
+
clusters.push({
|
|
1770
|
+
name: "E-Cluster",
|
|
1771
|
+
cores: eCores,
|
|
1772
|
+
frequencyMhz: Math.round(frequency * 0.6) || 2e3,
|
|
1773
|
+
effectiveUsagePercent: Math.min(100, Math.round(usage * 0.8)),
|
|
1774
|
+
activeResidencyPercent: Math.min(100, Math.round(usage * 0.9))
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
if (pCores > 0) {
|
|
1778
|
+
clusters.push({
|
|
1779
|
+
name: "P-Cluster",
|
|
1780
|
+
cores: pCores,
|
|
1781
|
+
frequencyMhz: frequency || 3200,
|
|
1782
|
+
effectiveUsagePercent: Math.min(100, Math.round(usage * 1.1)),
|
|
1783
|
+
activeResidencyPercent: Math.min(100, Math.round(usage))
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage, clusters };
|
|
1790
|
+
}
|
|
1791
|
+
async function collectMemory() {
|
|
1792
|
+
if (process.platform === "linux") return collectLinuxMemory();
|
|
1793
|
+
if (process.platform === "win32") return collectWindowsMemory();
|
|
1794
|
+
const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
|
|
1795
|
+
const pageSize = parseInt(await cachedSysctl("hw.pagesize", "4096")) || 4096;
|
|
1796
|
+
const vmStat = (await run("vm_stat")).trim();
|
|
1797
|
+
const getPageCount = (label) => {
|
|
1798
|
+
const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
|
|
1799
|
+
return m ? parseInt(m[1]) : 0;
|
|
1800
|
+
};
|
|
1801
|
+
const freePages = getPageCount("free");
|
|
1802
|
+
const activePages = getPageCount("active");
|
|
1803
|
+
const inactivePages = getPageCount("inactive");
|
|
1804
|
+
const wiredPages = getPageCount("wired down");
|
|
1805
|
+
const compressedPages = getPageCount("occupied by compressor");
|
|
1806
|
+
const purgeablePages = getPageCount("purgeable");
|
|
1807
|
+
const getStatCount = (label) => {
|
|
1808
|
+
const m = vmStat.match(new RegExp(`${label}:\\s+(\\d+)\\.`));
|
|
1809
|
+
return m ? parseInt(m[1]) : 0;
|
|
1810
|
+
};
|
|
1811
|
+
const swapIns = getStatCount("Swapins") || getStatCount("Pageins");
|
|
1812
|
+
const swapOuts = getStatCount("Swapouts") || getStatCount("Pageouts");
|
|
1813
|
+
const free = freePages * pageSize;
|
|
1814
|
+
const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
|
|
1815
|
+
const usagePercent = totalBytes > 0 ? Math.round(used / totalBytes * 100) : 0;
|
|
1816
|
+
let swapTotal = 0;
|
|
1817
|
+
let swapUsed = 0;
|
|
1818
|
+
let swapFree = 0;
|
|
1819
|
+
try {
|
|
1820
|
+
const swapRaw = (await run("sysctl -n vm.swapusage")).trim();
|
|
1821
|
+
const swapMatch = swapRaw.match(
|
|
1822
|
+
/total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
|
|
1823
|
+
);
|
|
1824
|
+
if (swapMatch) {
|
|
1825
|
+
swapTotal = parseFloat(swapMatch[1]) * parseSuffix(swapMatch[2]);
|
|
1826
|
+
swapUsed = parseFloat(swapMatch[3]) * parseSuffix(swapMatch[4]);
|
|
1827
|
+
swapFree = parseFloat(swapMatch[5]) * parseSuffix(swapMatch[6]);
|
|
1828
|
+
}
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
let pressureLevel = "Normal";
|
|
1832
|
+
try {
|
|
1833
|
+
const pressVal = (await run("sysctl -n kern.memorystatus_vm_pressure_level")).trim();
|
|
1834
|
+
const pressNum = parseInt(pressVal, 10);
|
|
1835
|
+
if (pressNum === 2) pressureLevel = "Warning";
|
|
1836
|
+
else if (pressNum >= 4) pressureLevel = "Critical";
|
|
1837
|
+
else if (!isNaN(pressNum)) pressureLevel = "Normal";
|
|
1838
|
+
else pressureLevel = pressVal || "Normal";
|
|
1839
|
+
} catch {
|
|
1840
|
+
pressureLevel = "Normal";
|
|
1841
|
+
}
|
|
1842
|
+
return {
|
|
1843
|
+
total: totalBytes,
|
|
1844
|
+
used,
|
|
1845
|
+
free,
|
|
1846
|
+
swapTotal,
|
|
1847
|
+
swapUsed,
|
|
1848
|
+
swapFree,
|
|
1849
|
+
pageSize,
|
|
1850
|
+
usagePercent,
|
|
1851
|
+
wiredBytes: wiredPages * pageSize,
|
|
1852
|
+
compressedBytes: compressedPages * pageSize,
|
|
1853
|
+
purgeableBytes: purgeablePages * pageSize,
|
|
1854
|
+
swapIns,
|
|
1855
|
+
swapOuts,
|
|
1856
|
+
pressureLevel
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
var prevDiskIoSample2 = null;
|
|
1860
|
+
var cachedDiskIo2 = null;
|
|
1861
|
+
async function getDiskIoRates() {
|
|
1862
|
+
const now = Date.now();
|
|
1863
|
+
if (cachedDiskIo2 && now - cachedDiskIo2.ts < 1e3) {
|
|
1864
|
+
return { readBytesSec: cachedDiskIo2.readBytesSec, writeBytesSec: cachedDiskIo2.writeBytesSec };
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
const raw = (await run("iostat -Id -c 1", "", 1e3)).trim();
|
|
1868
|
+
const lines = raw.split("\n");
|
|
1869
|
+
if (lines.length >= 3) {
|
|
1870
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
1871
|
+
const parts = dataLine.split(/\s+/);
|
|
1872
|
+
if (parts.length >= 3) {
|
|
1873
|
+
const totalMb = parseFloat(parts[2]);
|
|
1874
|
+
if (!isNaN(totalMb)) {
|
|
1875
|
+
const totalBytes = totalMb * 1024 * 1024;
|
|
1876
|
+
let bytesSec = 0;
|
|
1877
|
+
if (prevDiskIoSample2 && prevDiskIoSample2.ts > 0) {
|
|
1878
|
+
const dt = Math.max((now - prevDiskIoSample2.ts) / 1e3, 1e-3);
|
|
1879
|
+
bytesSec = Math.max(0, (totalBytes - prevDiskIoSample2.totalBytes) / dt);
|
|
1880
|
+
}
|
|
1881
|
+
prevDiskIoSample2 = { totalBytes, ts: now };
|
|
1882
|
+
cachedDiskIo2 = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
1883
|
+
return { readBytesSec: cachedDiskIo2.readBytesSec, writeBytesSec: cachedDiskIo2.writeBytesSec };
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
} catch {
|
|
1888
|
+
}
|
|
1889
|
+
return { readBytesSec: 0, writeBytesSec: 0 };
|
|
1890
|
+
}
|
|
1891
|
+
async function collectDisk() {
|
|
1892
|
+
if (process.platform === "linux") return collectLinuxDisk();
|
|
1893
|
+
if (process.platform === "win32") return collectWindowsDisk();
|
|
1894
|
+
const [rawDf, ioRates] = await Promise.all([
|
|
1895
|
+
run("df -h"),
|
|
1896
|
+
getDiskIoRates()
|
|
1897
|
+
]);
|
|
1898
|
+
const lines = rawDf.trim().split("\n").slice(1);
|
|
1899
|
+
return lines.map((line) => {
|
|
1900
|
+
const parts = line.split(/\s+/);
|
|
1901
|
+
return {
|
|
1902
|
+
filesystem: parts[0],
|
|
1903
|
+
size: parts[1],
|
|
1904
|
+
used: parts[2],
|
|
1905
|
+
available: parts[3],
|
|
1906
|
+
capacity: parts[4],
|
|
1907
|
+
mountpoint: parts[5] || parts[6] || "/",
|
|
1908
|
+
readBytesSec: ioRates.readBytesSec,
|
|
1909
|
+
writeBytesSec: ioRates.writeBytesSec
|
|
1910
|
+
};
|
|
1911
|
+
}).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
|
|
1912
|
+
}
|
|
1913
|
+
async function collectBattery() {
|
|
1914
|
+
if (process.platform === "linux") return collectLinuxBattery();
|
|
1915
|
+
if (process.platform === "win32") return collectWindowsBattery();
|
|
1916
|
+
try {
|
|
1917
|
+
const raw = (await run("pmset -g batt")).trim();
|
|
1918
|
+
if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
|
|
1919
|
+
const levelMatch = raw.match(/(\d+)%/);
|
|
1920
|
+
const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
|
|
1921
|
+
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
|
|
1922
|
+
const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
|
|
1923
|
+
const level = levelMatch ? parseInt(levelMatch[1]) : 0;
|
|
1924
|
+
const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
|
|
1925
|
+
const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
|
|
1926
|
+
let cycles;
|
|
1927
|
+
let condition;
|
|
1928
|
+
let maxCapacityPercent;
|
|
1929
|
+
try {
|
|
1930
|
+
const now = Date.now();
|
|
1931
|
+
let battInfo;
|
|
1932
|
+
if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
|
|
1933
|
+
battInfo = spBatteryCache.raw;
|
|
1934
|
+
} else {
|
|
1935
|
+
battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null", "", 3e3)).trim();
|
|
1936
|
+
spBatteryCache = { raw: battInfo, ts: now };
|
|
1937
|
+
}
|
|
1938
|
+
const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
|
|
1939
|
+
if (cycleMatch) cycles = parseInt(cycleMatch[1]);
|
|
1940
|
+
const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
|
|
1941
|
+
if (conditionMatch) condition = conditionMatch[1].trim();
|
|
1942
|
+
const maxCapMatch = battInfo.match(/Maximum Capacity:\s+(\d+)%/);
|
|
1943
|
+
if (maxCapMatch) {
|
|
1944
|
+
maxCapacityPercent = parseInt(maxCapMatch[1]);
|
|
1945
|
+
} else {
|
|
1946
|
+
const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
|
|
1947
|
+
const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
|
|
1948
|
+
if (fullMatch && designMatch) {
|
|
1949
|
+
const full = parseInt(fullMatch[1]);
|
|
1950
|
+
const design = parseInt(designMatch[1]);
|
|
1951
|
+
if (design > 0) maxCapacityPercent = Math.round(full / design * 100);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
const healthParts = [];
|
|
1957
|
+
if (condition) healthParts.push(condition);
|
|
1958
|
+
if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
|
|
1959
|
+
if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
|
|
1960
|
+
const health = healthParts.length ? healthParts.join(", ") : "unknown";
|
|
1961
|
+
let friendlySummary;
|
|
1962
|
+
const condLower = (condition || "").toLowerCase();
|
|
1963
|
+
if (condLower.includes("service") || condLower.includes("replace") || condLower.includes("check")) {
|
|
1964
|
+
friendlySummary = "Your battery has more wear than usual \u2014 consider a service appointment.";
|
|
1965
|
+
} else if (cycles !== void 0 && cycles > 1e3) {
|
|
1966
|
+
friendlySummary = "Your battery cycle count is high \u2014 capacity may be reduced.";
|
|
1967
|
+
} else if (maxCapacityPercent !== void 0 && maxCapacityPercent < 80) {
|
|
1968
|
+
friendlySummary = "Your battery maximum capacity is below 80% \u2014 consider servicing.";
|
|
1969
|
+
} else if (condition) {
|
|
1970
|
+
friendlySummary = `Battery condition is ${condition.toLowerCase()}.`;
|
|
1971
|
+
}
|
|
1972
|
+
const estimate = estimateBatteryLife(level, state);
|
|
1973
|
+
let powerWatts;
|
|
1974
|
+
if (!powerPlugged && state === "discharging") {
|
|
1975
|
+
try {
|
|
1976
|
+
const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
|
|
1977
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
1978
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
1979
|
+
if (ampMatch && voltMatch) {
|
|
1980
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
1981
|
+
if (!ampMatch[1].startsWith("-")) {
|
|
1982
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
1983
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
1984
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
1985
|
+
}
|
|
1986
|
+
const amps = Number(ampsBig);
|
|
1987
|
+
const volts = parseInt(voltMatch[1], 10) / 1e3;
|
|
1988
|
+
const watts = Math.abs(amps / 1e3 * volts);
|
|
1989
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
1990
|
+
powerWatts = Math.round(watts * 100) / 100;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
} catch {
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return {
|
|
1997
|
+
level,
|
|
1998
|
+
state,
|
|
1999
|
+
timeRemaining,
|
|
2000
|
+
health,
|
|
2001
|
+
powerSource: powerPlugged ? "AC" : "Battery",
|
|
2002
|
+
cycles,
|
|
2003
|
+
condition,
|
|
2004
|
+
maxCapacityPercent,
|
|
2005
|
+
friendlySummary,
|
|
2006
|
+
powerWatts,
|
|
2007
|
+
...estimate
|
|
2008
|
+
};
|
|
2009
|
+
} catch {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function pressureFromState(state) {
|
|
2014
|
+
const s = state.toLowerCase();
|
|
2015
|
+
if (s.includes("critical")) return 3;
|
|
2016
|
+
if (s.includes("serious") || s.includes("heavy")) return 2;
|
|
2017
|
+
if (s.includes("fair") || s.includes("moderate")) return 1;
|
|
2018
|
+
return 0;
|
|
2019
|
+
}
|
|
2020
|
+
async function collectThermal(detailed) {
|
|
2021
|
+
if (process.platform === "linux") return collectLinuxThermal();
|
|
2022
|
+
if (process.platform === "win32") return collectWindowsThermal();
|
|
2023
|
+
try {
|
|
2024
|
+
const therm = (await run("pmset -g therm", "")).trim();
|
|
2025
|
+
let state = "Unknown";
|
|
2026
|
+
let detail = therm || "No pmset thermal data";
|
|
2027
|
+
const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
|
|
2028
|
+
if (stateMatch) {
|
|
2029
|
+
const raw = stateMatch[1].trim();
|
|
2030
|
+
state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
|
|
2031
|
+
detail = state;
|
|
2032
|
+
} else {
|
|
2033
|
+
const limitMatch = therm.match(
|
|
2034
|
+
/CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
|
|
2035
|
+
);
|
|
2036
|
+
if (limitMatch) {
|
|
2037
|
+
const limit = parseFloat(limitMatch[1]);
|
|
2038
|
+
if (limit >= 100) state = "Nominal";
|
|
2039
|
+
else if (limit >= 85) state = "Fair";
|
|
2040
|
+
else if (limit >= 50) state = "Serious";
|
|
2041
|
+
else state = "Critical";
|
|
2042
|
+
detail = `${state} (CPU limit ${limit}%)`;
|
|
2043
|
+
} else if (/no thermal warning level has been recorded/i.test(therm)) {
|
|
2044
|
+
state = "Nominal";
|
|
2045
|
+
detail = "Nominal (no thermal warning recorded)";
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const temperatures = {};
|
|
2049
|
+
let cpuDieTemp;
|
|
2050
|
+
try {
|
|
2051
|
+
const { temps } = await getSmcMetrics();
|
|
2052
|
+
cpuDieTemp = temps.cpu_die;
|
|
2053
|
+
if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
|
|
2054
|
+
if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
|
|
2055
|
+
if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
|
|
2056
|
+
for (const [k, v] of Object.entries(temps)) {
|
|
2057
|
+
if (temperatures[k] === void 0 && v !== void 0) {
|
|
2058
|
+
temperatures[k] = v;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
} catch {
|
|
2062
|
+
}
|
|
2063
|
+
if (state === "Unknown" && cpuDieTemp !== void 0) {
|
|
2064
|
+
if (cpuDieTemp >= 100) state = "Critical";
|
|
2065
|
+
else if (cpuDieTemp >= 90) state = "Serious";
|
|
2066
|
+
else if (cpuDieTemp >= 80) state = "Fair";
|
|
2067
|
+
else state = "Nominal";
|
|
2068
|
+
detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
|
|
2069
|
+
}
|
|
2070
|
+
let fans = [];
|
|
2071
|
+
try {
|
|
2072
|
+
const smcData = await getSmcMetrics();
|
|
2073
|
+
fans = smcData.fans;
|
|
2074
|
+
} catch {
|
|
2075
|
+
}
|
|
2076
|
+
return {
|
|
2077
|
+
state,
|
|
2078
|
+
detail,
|
|
2079
|
+
pressureLevel: pressureFromState(state),
|
|
2080
|
+
temperatures: Object.keys(temperatures).length ? temperatures : void 0,
|
|
2081
|
+
fans: fans.length > 0 ? fans : void 0
|
|
2082
|
+
};
|
|
2083
|
+
} catch {
|
|
2084
|
+
return { state: "Unknown", pressureLevel: 0, error: "Thermal info unavailable on this system" };
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
async function collectNetwork() {
|
|
2088
|
+
if (process.platform === "linux") return collectLinuxNetwork();
|
|
2089
|
+
if (process.platform === "win32") return collectWindowsNetwork();
|
|
2090
|
+
try {
|
|
2091
|
+
const { iface, ip } = await cachedRouteIface();
|
|
2092
|
+
const netStat = await cachedNetstatIb();
|
|
2093
|
+
let rxBytes = 0;
|
|
2094
|
+
let txBytes = 0;
|
|
2095
|
+
let rxPackets = 0;
|
|
2096
|
+
let txPackets = 0;
|
|
2097
|
+
for (const line of netStat.split("\n")) {
|
|
2098
|
+
if (line.startsWith(iface) || line.startsWith(iface + ":")) {
|
|
2099
|
+
const parts = line.split(/\s+/);
|
|
2100
|
+
if (parts.length >= 9) {
|
|
2101
|
+
rxPackets += parseInt(parts[4]) || 0;
|
|
2102
|
+
rxBytes += parseInt(parts[5]) || 0;
|
|
2103
|
+
txPackets += parseInt(parts[6]) || 0;
|
|
2104
|
+
txBytes += parseInt(parts[7]) || 0;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const now = Date.now();
|
|
2109
|
+
let rxRate = 0;
|
|
2110
|
+
let txRate = 0;
|
|
2111
|
+
if (prevNetSample3 && prevNetSample3.ts > 0) {
|
|
2112
|
+
const dt = Math.max((now - prevNetSample3.ts) / 1e3, 1e-3);
|
|
2113
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample3.rxBytes) / dt);
|
|
2114
|
+
txRate = Math.max(0, (txBytes - prevNetSample3.txBytes) / dt);
|
|
2115
|
+
}
|
|
2116
|
+
prevNetSample3 = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
2117
|
+
const protocolStats = await getProtocolStats();
|
|
2118
|
+
const connectionStates = await getConnectionStates();
|
|
2119
|
+
const listeningPorts = await getListeningPorts();
|
|
2120
|
+
const establishedConnections = connectionStates.established;
|
|
2121
|
+
const topRemoteHosts = await getTopRemoteHosts();
|
|
2122
|
+
return {
|
|
2123
|
+
interface: iface,
|
|
2124
|
+
ip,
|
|
2125
|
+
rxBytes,
|
|
2126
|
+
txBytes,
|
|
2127
|
+
rxPackets,
|
|
2128
|
+
txPackets,
|
|
2129
|
+
rxRate,
|
|
2130
|
+
txRate,
|
|
2131
|
+
connections: establishedConnections,
|
|
2132
|
+
protocols: protocolStats,
|
|
2133
|
+
connectionStates,
|
|
2134
|
+
listeningPorts,
|
|
2135
|
+
establishedConnections,
|
|
2136
|
+
topRemoteHosts
|
|
2137
|
+
};
|
|
2138
|
+
} catch {
|
|
2139
|
+
return {
|
|
2140
|
+
interface: "unknown",
|
|
2141
|
+
ip: "0.0.0.0",
|
|
2142
|
+
rxBytes: 0,
|
|
2143
|
+
txBytes: 0,
|
|
2144
|
+
rxPackets: 0,
|
|
2145
|
+
txPackets: 0,
|
|
2146
|
+
rxRate: 0,
|
|
2147
|
+
txRate: 0,
|
|
2148
|
+
connections: 0,
|
|
2149
|
+
protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
|
|
2150
|
+
connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
|
|
2151
|
+
listeningPorts: [],
|
|
2152
|
+
establishedConnections: 0,
|
|
2153
|
+
topRemoteHosts: []
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
async function getProtocolStats() {
|
|
2158
|
+
try {
|
|
2159
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", "");
|
|
2160
|
+
const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", "0")).trim();
|
|
2161
|
+
const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", "0")).trim();
|
|
2162
|
+
let tcpRxBytes = 0;
|
|
2163
|
+
let tcpTxBytes = 0;
|
|
2164
|
+
let udpRxBytes = 0;
|
|
2165
|
+
let udpTxBytes = 0;
|
|
2166
|
+
try {
|
|
2167
|
+
const netstat = await cachedNetstatIb();
|
|
2168
|
+
for (const line of netstat.split("\n")) {
|
|
2169
|
+
const parts = line.split(/\s+/);
|
|
2170
|
+
if (parts.length >= 9 && parts[0] && !parts[0].startsWith("Name")) {
|
|
2171
|
+
const iface = parts[0];
|
|
2172
|
+
if (iface.includes("lo")) continue;
|
|
2173
|
+
const rx = parseInt(parts[5]) || 0;
|
|
2174
|
+
const tx = parseInt(parts[7]) || 0;
|
|
2175
|
+
tcpRxBytes += rx;
|
|
2176
|
+
tcpTxBytes += tx;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
return {
|
|
2182
|
+
tcp: {
|
|
2183
|
+
connections: parseInt(tcpConnections) || 0,
|
|
2184
|
+
rxBytes: tcpRxBytes,
|
|
2185
|
+
txBytes: tcpTxBytes
|
|
2186
|
+
},
|
|
2187
|
+
udp: {
|
|
2188
|
+
connections: parseInt(udpConnections) || 0,
|
|
2189
|
+
rxBytes: udpRxBytes,
|
|
2190
|
+
txBytes: udpTxBytes
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
} catch {
|
|
2194
|
+
return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
async function getConnectionStates() {
|
|
2198
|
+
try {
|
|
2199
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", "");
|
|
2200
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
2201
|
+
const stats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
2202
|
+
for (const line of lines) {
|
|
2203
|
+
const match = line.match(/(\d+)\s+(\S+)/);
|
|
2204
|
+
if (!match) continue;
|
|
2205
|
+
const count = parseInt(match[1]);
|
|
2206
|
+
const state = match[2].toUpperCase();
|
|
2207
|
+
if (state.includes("ESTABLISHED")) stats.established += count;
|
|
2208
|
+
else if (state.includes("LISTEN")) stats.listening += count;
|
|
2209
|
+
else if (state.includes("TIME_WAIT")) stats.timeWait += count;
|
|
2210
|
+
else if (state.includes("CLOSE_WAIT")) stats.closeWait += count;
|
|
2211
|
+
else stats.other += count;
|
|
2212
|
+
}
|
|
2213
|
+
return stats;
|
|
2214
|
+
} catch {
|
|
2215
|
+
return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async function getListeningPorts() {
|
|
2219
|
+
try {
|
|
2220
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", "");
|
|
2221
|
+
const ports = raw.split("\n").map((p) => parseInt(p, 10)).filter((p) => !isNaN(p) && p > 0);
|
|
2222
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
2223
|
+
} catch {
|
|
2224
|
+
return [];
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async function getTopRemoteHosts() {
|
|
2228
|
+
try {
|
|
2229
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", "");
|
|
2230
|
+
const ports = raw.split("\n").filter(Boolean);
|
|
2231
|
+
const hostMap = /* @__PURE__ */ new Map();
|
|
2232
|
+
for (const port of ports) {
|
|
2233
|
+
const host = port.split(":")[0];
|
|
2234
|
+
if (!host || host === "127.0.0.1" || host === "::1") continue;
|
|
2235
|
+
const existing = hostMap.get(host) || { count: 0, bytes: 0 };
|
|
2236
|
+
hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
|
|
2237
|
+
}
|
|
2238
|
+
const result = [];
|
|
2239
|
+
for (const [host, info] of hostMap.entries()) {
|
|
2240
|
+
result.push({
|
|
2241
|
+
host,
|
|
2242
|
+
ip: host,
|
|
2243
|
+
bytesTransferred: info.bytes,
|
|
2244
|
+
connectionCount: info.count
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
|
|
2248
|
+
} catch {
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function collectProcesses(limit) {
|
|
2253
|
+
if (process.platform === "linux") return collectLinuxProcesses(limit);
|
|
2254
|
+
if (process.platform === "win32") return collectWindowsProcesses(limit);
|
|
2255
|
+
try {
|
|
2256
|
+
const isMac = process.platform === "darwin";
|
|
2257
|
+
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
2258
|
+
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
|
|
2259
|
+
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm ${sortArg}${headClause}`)).trim();
|
|
2260
|
+
const lines = raw.split("\n").slice(1);
|
|
2261
|
+
return lines.map((line) => {
|
|
2262
|
+
const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
2263
|
+
if (!parts) return null;
|
|
2264
|
+
const runtimeSec = parts[7].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
2265
|
+
const command = parts[8];
|
|
2266
|
+
const isGpuOrMl = /gpu|metal|vulkan|cuda|ollama|llama|torch|mps|mlx|coreml|python/i.test(command);
|
|
2267
|
+
return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: 0, runtime: runtimeSec, command, isGpuOrMlAttributed: isGpuOrMl };
|
|
2268
|
+
}).filter((p) => p !== null && p.pid > 0);
|
|
2269
|
+
} catch {
|
|
2270
|
+
return [];
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
function parseBytes(str) {
|
|
2274
|
+
if (!str) return 0;
|
|
2275
|
+
const match = str.trim().match(/([\d.]+)\s*([KMGTPE]?)i?B?/i);
|
|
2276
|
+
if (!match) return 0;
|
|
2277
|
+
let val = parseFloat(match[1]);
|
|
2278
|
+
const unit = match[2].toUpperCase();
|
|
2279
|
+
if (unit === "K") val *= 1024;
|
|
2280
|
+
else if (unit === "M") val *= 1024 * 1024;
|
|
2281
|
+
else if (unit === "G") val *= 1024 * 1024 * 1024;
|
|
2282
|
+
else if (unit === "T") val *= 1024 * 1024 * 1024 * 1024;
|
|
2283
|
+
return Math.round(val);
|
|
2284
|
+
}
|
|
2285
|
+
async function collectPackets() {
|
|
2286
|
+
if (process.platform === "linux") return collectLinuxPackets();
|
|
2287
|
+
if (process.platform === "win32") return collectWindowsPackets();
|
|
2288
|
+
try {
|
|
2289
|
+
const netStat = await cachedNetstatIb();
|
|
2290
|
+
const lines = netStat.split("\n").slice(1);
|
|
2291
|
+
let totalRxPackets = 0;
|
|
2292
|
+
let totalTxPackets = 0;
|
|
2293
|
+
let totalRxBytes = 0;
|
|
2294
|
+
let totalTxBytes = 0;
|
|
2295
|
+
const ifaceStats = [];
|
|
2296
|
+
for (const line of lines) {
|
|
2297
|
+
const parts = line.split(/\s+/);
|
|
2298
|
+
if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
|
|
2299
|
+
const iface = parts[0];
|
|
2300
|
+
const rxPackets = parseInt(parts[4]) || 0;
|
|
2301
|
+
const rxBytes = parseInt(parts[5]) || 0;
|
|
2302
|
+
const txPackets = parseInt(parts[6]) || 0;
|
|
2303
|
+
const txBytes = parseInt(parts[7]) || 0;
|
|
2304
|
+
totalRxPackets += rxPackets;
|
|
2305
|
+
totalTxPackets += txPackets;
|
|
2306
|
+
totalRxBytes += rxBytes;
|
|
2307
|
+
totalTxBytes += txBytes;
|
|
2308
|
+
if (iface && !iface.includes("lo")) {
|
|
2309
|
+
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
let connections = 0;
|
|
2314
|
+
let allProcesses = [];
|
|
2315
|
+
try {
|
|
2316
|
+
const nettop = (await run("script -q /dev/null nettop -l 1 -J bytes_in,bytes_out,state 2>/dev/null", "", 2e3)).trim();
|
|
2317
|
+
const rows = nettop.split("\n");
|
|
2318
|
+
let currentProc = null;
|
|
2319
|
+
for (const row of rows) {
|
|
2320
|
+
if (!row || row.includes("bytes_in")) continue;
|
|
2321
|
+
const parts = row.trimStart().split(/\s{2,}/);
|
|
2322
|
+
const nameCol = parts[0];
|
|
2323
|
+
if (!nameCol.includes("<->")) {
|
|
2324
|
+
if (currentProc) {
|
|
2325
|
+
if (currentProc.connections && currentProc.connections.length > 0) {
|
|
2326
|
+
allProcesses.push(currentProc);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const lastDot = nameCol.lastIndexOf(".");
|
|
2330
|
+
let pid = 0;
|
|
2331
|
+
let command = nameCol;
|
|
2332
|
+
if (lastDot > 0) {
|
|
2333
|
+
const possiblePid = parseInt(nameCol.substring(lastDot + 1));
|
|
2334
|
+
if (!isNaN(possiblePid)) {
|
|
2335
|
+
pid = possiblePid;
|
|
2336
|
+
command = nameCol.substring(0, lastDot);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
const rx = parseBytes(parts[1]);
|
|
2340
|
+
const tx = parseBytes(parts[2]);
|
|
2341
|
+
currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
|
|
2342
|
+
} else {
|
|
2343
|
+
if (currentProc) {
|
|
2344
|
+
let state = "";
|
|
2345
|
+
let rx = 0;
|
|
2346
|
+
let tx = 0;
|
|
2347
|
+
if (parts.length >= 4) {
|
|
2348
|
+
state = parts[1];
|
|
2349
|
+
rx = parseBytes(parts[2]);
|
|
2350
|
+
tx = parseBytes(parts[3]);
|
|
2351
|
+
} else if (parts.length === 3) {
|
|
2352
|
+
rx = parseBytes(parts[1]);
|
|
2353
|
+
tx = parseBytes(parts[2]);
|
|
2354
|
+
}
|
|
2355
|
+
if (state === "Established") connections++;
|
|
2356
|
+
const spaceIdx = nameCol.indexOf(" ");
|
|
2357
|
+
const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : "";
|
|
2358
|
+
const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
|
|
2359
|
+
const arrowIdx = endpoints.indexOf("<->");
|
|
2360
|
+
let local = "";
|
|
2361
|
+
let remote = "";
|
|
2362
|
+
if (arrowIdx > 0) {
|
|
2363
|
+
local = endpoints.substring(0, arrowIdx);
|
|
2364
|
+
remote = endpoints.substring(arrowIdx + 3);
|
|
2365
|
+
remote = resolveIp(remote);
|
|
2366
|
+
} else {
|
|
2367
|
+
local = endpoints;
|
|
2368
|
+
}
|
|
2369
|
+
currentProc.connections.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
|
|
2374
|
+
allProcesses.push(currentProc);
|
|
2375
|
+
}
|
|
2376
|
+
} catch {
|
|
2377
|
+
allProcesses = [];
|
|
2378
|
+
}
|
|
2379
|
+
const topProcesses = [...allProcesses].sort((a, b) => b.rxBytes + b.txBytes - (a.rxBytes + a.txBytes));
|
|
2380
|
+
const protocolStats = await getProtocolStats();
|
|
2381
|
+
const connectionStates = await getConnectionStates();
|
|
2382
|
+
const listeningPorts = await getListeningPorts();
|
|
2383
|
+
return {
|
|
2384
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
2385
|
+
rxPackets: totalRxPackets,
|
|
2386
|
+
txPackets: totalTxPackets,
|
|
2387
|
+
rxRate: 0,
|
|
2388
|
+
txRate: 0,
|
|
2389
|
+
connections,
|
|
2390
|
+
topProcesses,
|
|
2391
|
+
interfaces: ifaceStats,
|
|
2392
|
+
allProcesses,
|
|
2393
|
+
protocolStats,
|
|
2394
|
+
connectionStates,
|
|
2395
|
+
listeningPorts
|
|
2396
|
+
};
|
|
2397
|
+
} catch {
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
async function collectTasks(limit = 12) {
|
|
2402
|
+
if (process.platform === "linux") return collectLinuxTasks(limit);
|
|
2403
|
+
if (process.platform === "win32") return collectWindowsTasks(limit);
|
|
2404
|
+
try {
|
|
2405
|
+
const isMac = process.platform === "darwin";
|
|
2406
|
+
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
2407
|
+
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
2408
|
+
const lines = raw.split("\n").slice(1);
|
|
2409
|
+
return lines.map((line) => {
|
|
2410
|
+
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
2411
|
+
if (!parts) return null;
|
|
2412
|
+
const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
2413
|
+
return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
|
|
2414
|
+
}).filter((t) => t !== null && t.pid > 0);
|
|
2415
|
+
} catch {
|
|
2416
|
+
return [];
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
async function getDisplayInfo() {
|
|
2420
|
+
if (process.platform === "linux") return getLinuxDisplayInfo();
|
|
2421
|
+
if (process.platform === "win32") return getWindowsDisplayInfo();
|
|
2422
|
+
try {
|
|
2423
|
+
const raw = await run("system_profiler SPDisplaysDataType 2>&1", "");
|
|
2424
|
+
const displays = [];
|
|
2425
|
+
const lines = raw.split("\n");
|
|
2426
|
+
let currentName = "";
|
|
2427
|
+
let currentRes = "";
|
|
2428
|
+
let isMain = false;
|
|
2429
|
+
let connType = "";
|
|
2430
|
+
for (const line of lines) {
|
|
2431
|
+
const trimmed = line.trim();
|
|
2432
|
+
if (trimmed.endsWith(":") && !trimmed.startsWith("Chipset") && !trimmed.startsWith("Displays") && !trimmed.startsWith("Graphics")) {
|
|
2433
|
+
if (currentName && currentRes) {
|
|
2434
|
+
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
|
|
2435
|
+
currentRes = "";
|
|
2436
|
+
isMain = false;
|
|
2437
|
+
connType = "";
|
|
2438
|
+
}
|
|
2439
|
+
currentName = trimmed.slice(0, -1);
|
|
2440
|
+
} else if (trimmed.startsWith("Resolution:")) {
|
|
2441
|
+
currentRes = trimmed.replace("Resolution:", "").trim();
|
|
2442
|
+
} else if (trimmed.startsWith("Main Display: Yes")) {
|
|
2443
|
+
isMain = true;
|
|
2444
|
+
} else if (trimmed.startsWith("Connection Type:")) {
|
|
2445
|
+
connType = trimmed.replace("Connection Type:", "").trim();
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
if (currentName && currentRes) {
|
|
2449
|
+
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
|
|
2450
|
+
}
|
|
2451
|
+
return displays;
|
|
2452
|
+
} catch {
|
|
2453
|
+
return [];
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
async function getTimeMachineStatus() {
|
|
2457
|
+
if (process.platform !== "darwin") {
|
|
2458
|
+
return { configured: false, running: false };
|
|
2459
|
+
}
|
|
2460
|
+
try {
|
|
2461
|
+
const statusRaw = await run("tmutil status 2>&1", "");
|
|
2462
|
+
if (statusRaw.includes("No Time Machine destination") || statusRaw.includes("not configured")) {
|
|
2463
|
+
return { configured: false, running: false };
|
|
2464
|
+
}
|
|
2465
|
+
const running = statusRaw.includes("Running = 1;");
|
|
2466
|
+
let percent = void 0;
|
|
2467
|
+
const pctMatch = statusRaw.match(/Percent\s*=\s*"?(0\.\d+|\d+)"?/i);
|
|
2468
|
+
if (pctMatch) {
|
|
2469
|
+
percent = Math.round(parseFloat(pctMatch[1]) * 100);
|
|
2470
|
+
}
|
|
2471
|
+
let latestBackup = "Unknown";
|
|
2472
|
+
try {
|
|
2473
|
+
const latestRaw = await run("tmutil latestbackup 2>&1", "");
|
|
2474
|
+
if (latestRaw.trim() && !latestRaw.includes("error") && !latestRaw.includes("No backups")) {
|
|
2475
|
+
const basename = latestRaw.trim().split("/").pop() || latestRaw.trim();
|
|
2476
|
+
latestBackup = basename.replace(".previous", "").replace(".backup", "");
|
|
2477
|
+
} else {
|
|
2478
|
+
latestBackup = "No backup found / Unmounted";
|
|
2479
|
+
}
|
|
2480
|
+
} catch {
|
|
2481
|
+
latestBackup = "Not mounted";
|
|
2482
|
+
}
|
|
2483
|
+
return { configured: true, running, percent, latestBackup };
|
|
2484
|
+
} catch {
|
|
2485
|
+
return { configured: false, running: false };
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
export {
|
|
2490
|
+
collectAll,
|
|
2491
|
+
collectContainers,
|
|
2492
|
+
collectPower,
|
|
2493
|
+
collectGpu,
|
|
2494
|
+
collectSystem,
|
|
2495
|
+
collectCpu,
|
|
2496
|
+
collectMemory,
|
|
2497
|
+
collectDisk,
|
|
2498
|
+
collectBattery,
|
|
2499
|
+
collectThermal,
|
|
2500
|
+
collectNetwork,
|
|
2501
|
+
collectProcesses,
|
|
2502
|
+
collectPackets,
|
|
2503
|
+
collectTasks,
|
|
2504
|
+
getDisplayInfo,
|
|
2505
|
+
getTimeMachineStatus
|
|
2506
|
+
};
|