pyre-cli 9.5.2 → 10.1.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
package/dist/chunk-N4WJVF5M.js
DELETED
|
@@ -1,1184 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectBlenderRenders
|
|
3
|
-
} from "./chunk-25I533OF.js";
|
|
4
|
-
import {
|
|
5
|
-
run
|
|
6
|
-
} from "./chunk-FH5WDJBF.js";
|
|
7
|
-
|
|
8
|
-
// src/monitors/collectors.ts
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import os from "os";
|
|
11
|
-
|
|
12
|
-
// src/monitors/smc.ts
|
|
13
|
-
var smcCache = null;
|
|
14
|
-
async function getSmcMetrics() {
|
|
15
|
-
const now = Date.now();
|
|
16
|
-
if (smcCache && now - smcCache.ts < 4e3) return smcCache.data;
|
|
17
|
-
const result = { temps: {}, power: {}, freq: {} };
|
|
18
|
-
try {
|
|
19
|
-
const pm = (await run(
|
|
20
|
-
"sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null",
|
|
21
|
-
"",
|
|
22
|
-
2e3
|
|
23
|
-
)).trim();
|
|
24
|
-
if (pm) {
|
|
25
|
-
const cpuTemp = pm.match(/CPU die temperature:\s*([\d.]+)/i);
|
|
26
|
-
if (cpuTemp) result.temps["cpu_die"] = parseFloat(cpuTemp[1]);
|
|
27
|
-
const gpuTemp = pm.match(/GPU die temperature:\s*([\d.]+)/i);
|
|
28
|
-
if (gpuTemp) result.temps["gpu_die"] = parseFloat(gpuTemp[1]);
|
|
29
|
-
const smcTemp = pm.match(/SMC die temperature:\s*([\d.]+)/i);
|
|
30
|
-
if (smcTemp) result.temps["smc_die"] = parseFloat(smcTemp[1]);
|
|
31
|
-
const cpuPower = pm.match(/CPU Power:\s*([\d.]+)\s*mW/i);
|
|
32
|
-
if (cpuPower) result.power.cpu = parseFloat(cpuPower[1]) / 1e3;
|
|
33
|
-
const gpuPower = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
34
|
-
if (gpuPower) result.power.gpu = parseFloat(gpuPower[1]) / 1e3;
|
|
35
|
-
const combinedPower = pm.match(/Combined Power \(CPU \+ GPU.*?\):\s*([\d.]+)\s*mW/i);
|
|
36
|
-
if (combinedPower) result.power.combined = parseFloat(combinedPower[1]) / 1e3;
|
|
37
|
-
const eFreq = pm.match(/E-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
38
|
-
if (eFreq) result.freq["e_cluster"] = parseFloat(eFreq[1]);
|
|
39
|
-
const pFreq = pm.match(/P-Cluster HW active frequency:\s*([\d.]+)/i);
|
|
40
|
-
if (pFreq) result.freq["p_cluster"] = parseFloat(pFreq[1]);
|
|
41
|
-
const cpuFreq = pm.match(/CPU HW active frequency:\s*([\d.]+)/i);
|
|
42
|
-
if (cpuFreq) result.freq["cpu"] = parseFloat(cpuFreq[1]);
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
if (result.temps["cpu_die"] === void 0) {
|
|
47
|
-
try {
|
|
48
|
-
const ioregRaw = await run("ioreg -r -c AppleDeviceManagementHIDEventService 2>/dev/null", "");
|
|
49
|
-
if (ioregRaw) {
|
|
50
|
-
const matches = ioregRaw.matchAll(/"(?:PrimaryTemperature|Temperature)"\s*=\s*(\d+)/g);
|
|
51
|
-
for (const match of matches) {
|
|
52
|
-
let val = parseFloat(match[1]);
|
|
53
|
-
if (val > 1e3) val = val / 100;
|
|
54
|
-
if (val >= 15 && val <= 115) {
|
|
55
|
-
if (result.temps["cpu_die"] === void 0) result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
56
|
-
else if (result.temps["gpu_die"] === void 0) result.temps["gpu_die"] = Math.round(val * 10) / 10;
|
|
57
|
-
else if (result.temps["smc_die"] === void 0) result.temps["smc_die"] = Math.round(val * 10) / 10;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (result.temps["cpu_die"] === void 0) {
|
|
65
|
-
try {
|
|
66
|
-
const battRaw = await run("ioreg -r -n AppleSmartBattery 2>/dev/null", "");
|
|
67
|
-
if (battRaw) {
|
|
68
|
-
const virtMatch = battRaw.match(/"VirtualTemperature"\s*=\s*(\d+)/);
|
|
69
|
-
const cellMatch = battRaw.match(/"Temperature"\s*=\s*(\d+)/);
|
|
70
|
-
const rawVirt = virtMatch ? parseFloat(virtMatch[1]) : void 0;
|
|
71
|
-
const rawCell = cellMatch ? parseFloat(cellMatch[1]) : void 0;
|
|
72
|
-
const virtC = rawVirt !== void 0 ? Math.round(rawVirt / 10) / 10 : void 0;
|
|
73
|
-
const cellC = rawCell !== void 0 ? Math.round(rawCell / 10) / 10 : void 0;
|
|
74
|
-
if (virtC !== void 0 && virtC >= 15 && virtC <= 115) {
|
|
75
|
-
result.temps["cpu_die"] = virtC;
|
|
76
|
-
} else if (cellC !== void 0 && cellC >= 15 && cellC <= 115) {
|
|
77
|
-
result.temps["cpu_die"] = cellC;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (result.temps["cpu_die"] === void 0) {
|
|
84
|
-
try {
|
|
85
|
-
const tz0 = (await run("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", "")).trim();
|
|
86
|
-
if (tz0) {
|
|
87
|
-
const rawVal = parseFloat(tz0);
|
|
88
|
-
const val = rawVal > 1e3 ? rawVal / 1e3 : rawVal;
|
|
89
|
-
if (val >= 10 && val <= 120) {
|
|
90
|
-
result.temps["cpu_die"] = Math.round(val * 10) / 10;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (result.temps["cpu_die"] === void 0) {
|
|
97
|
-
const loadAvg = (await import("os")).loadavg();
|
|
98
|
-
const ncpus = (await import("os")).cpus().length || 1;
|
|
99
|
-
const loadUsage = Math.min(100, loadAvg[0] / ncpus * 100);
|
|
100
|
-
const estCpu = Math.round((38 + loadUsage * 0.42) * 10) / 10;
|
|
101
|
-
result.temps["cpu_die"] = estCpu;
|
|
102
|
-
if (result.temps["gpu_die"] === void 0) {
|
|
103
|
-
result.temps["gpu_die"] = Math.max(32, Math.round((estCpu - 3) * 10) / 10);
|
|
104
|
-
}
|
|
105
|
-
if (result.temps["smc_die"] === void 0) {
|
|
106
|
-
result.temps["smc_die"] = Math.max(30, Math.round((estCpu - 6) * 10) / 10);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
smcCache = { data: result, ts: now };
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
function parseSuffix(s) {
|
|
113
|
-
return s.toUpperCase() === "K" ? 1024 : s.toUpperCase() === "M" ? 1024 * 1024 : s.toUpperCase() === "G" ? 1024 * 1024 * 1024 : 1;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/monitors/dns_cache.ts
|
|
117
|
-
import dns from "dns";
|
|
118
|
-
var cache = /* @__PURE__ */ new Map();
|
|
119
|
-
var pending = /* @__PURE__ */ new Set();
|
|
120
|
-
function resolveIp(ip) {
|
|
121
|
-
let cleanIp = ip;
|
|
122
|
-
if (cleanIp.includes("]:")) {
|
|
123
|
-
cleanIp = cleanIp.substring(1, cleanIp.indexOf("]"));
|
|
124
|
-
} else if (cleanIp.includes(":") && cleanIp.split(":").length === 2) {
|
|
125
|
-
cleanIp = cleanIp.split(":")[0];
|
|
126
|
-
} else if (cleanIp.includes(":") && !cleanIp.includes("]")) {
|
|
127
|
-
const lastColon = cleanIp.lastIndexOf(":");
|
|
128
|
-
const isPort = !cleanIp.includes(":", cleanIp.indexOf(":") + 1);
|
|
129
|
-
}
|
|
130
|
-
if (cleanIp.includes(":") && cleanIp.includes(".")) {
|
|
131
|
-
const lastDot = cleanIp.lastIndexOf(".");
|
|
132
|
-
if (lastDot > cleanIp.lastIndexOf(":")) {
|
|
133
|
-
cleanIp = cleanIp.substring(0, lastDot);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (cache.has(cleanIp)) return cache.get(cleanIp);
|
|
137
|
-
if (pending.has(cleanIp)) return ip;
|
|
138
|
-
if (cleanIp.startsWith("127.") || cleanIp.startsWith("192.168.") || cleanIp.startsWith("10.") || cleanIp.startsWith("172.16.") || cleanIp.startsWith("ff02:") || cleanIp === "*:*" || cleanIp === "*" || cleanIp.startsWith("fe80:")) {
|
|
139
|
-
return ip;
|
|
140
|
-
}
|
|
141
|
-
pending.add(cleanIp);
|
|
142
|
-
dns.reverse(cleanIp, (err, hostnames) => {
|
|
143
|
-
if (!err && hostnames && hostnames.length > 0) {
|
|
144
|
-
cache.set(cleanIp, hostnames[0]);
|
|
145
|
-
} else {
|
|
146
|
-
cache.set(cleanIp, cleanIp);
|
|
147
|
-
}
|
|
148
|
-
pending.delete(cleanIp);
|
|
149
|
-
});
|
|
150
|
-
return ip;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/monitors/collectors.ts
|
|
154
|
-
var SP_TTL_MS = 1e4;
|
|
155
|
-
var NETSTAT_TTL_MS = 1e3;
|
|
156
|
-
var prevCpuTimes = null;
|
|
157
|
-
var prevCpuTotal = { total: 0, idle: 0, ts: 0 };
|
|
158
|
-
var ROUTE_TTL_MS = 5e3;
|
|
159
|
-
var SYSCTL_TTL_MS = 6e4;
|
|
160
|
-
var spGpuCache = null;
|
|
161
|
-
var spBatteryCache = null;
|
|
162
|
-
var netstatIbCache = null;
|
|
163
|
-
var routeCache = null;
|
|
164
|
-
var sysctlCache = /* @__PURE__ */ new Map();
|
|
165
|
-
var prevNetSample = null;
|
|
166
|
-
async function cachedSysctl(key, fallback) {
|
|
167
|
-
const now = Date.now();
|
|
168
|
-
const hit = sysctlCache.get(key);
|
|
169
|
-
if (hit && now - hit.ts < SYSCTL_TTL_MS) return hit.value;
|
|
170
|
-
const value = await run(`sysctl -n ${key}`, fallback);
|
|
171
|
-
sysctlCache.set(key, { value, ts: now });
|
|
172
|
-
return value;
|
|
173
|
-
}
|
|
174
|
-
async function cachedNetstatIb() {
|
|
175
|
-
const now = Date.now();
|
|
176
|
-
if (netstatIbCache && now - netstatIbCache.ts < NETSTAT_TTL_MS) return netstatIbCache.raw;
|
|
177
|
-
const raw = await run("netstat -ib");
|
|
178
|
-
netstatIbCache = { raw, ts: now };
|
|
179
|
-
return raw;
|
|
180
|
-
}
|
|
181
|
-
async function cachedRouteIface() {
|
|
182
|
-
const now = Date.now();
|
|
183
|
-
if (routeCache && now - routeCache.ts < ROUTE_TTL_MS) return routeCache;
|
|
184
|
-
const primary = (await run("route -n get default")).trim();
|
|
185
|
-
const ifaceMatch = primary.match(/interface:\s+(\S+)/);
|
|
186
|
-
const iface = ifaceMatch?.[1] || "en0";
|
|
187
|
-
let ip = "0.0.0.0";
|
|
188
|
-
try {
|
|
189
|
-
const ifconfig = (await run(`ifconfig ${iface}`)).trim();
|
|
190
|
-
const ipMatch = ifconfig.match(/inet\s+([\d.]+)\s+netmask/);
|
|
191
|
-
if (ipMatch) ip = ipMatch[1];
|
|
192
|
-
} catch {
|
|
193
|
-
}
|
|
194
|
-
routeCache = { iface, ip, ts: now };
|
|
195
|
-
return routeCache;
|
|
196
|
-
}
|
|
197
|
-
var BATTERY_WINDOW_MS = 15 * 60 * 1e3;
|
|
198
|
-
var MIN_ESTIMATE_WINDOW_MS = 30 * 1e3;
|
|
199
|
-
var prevBattery = { samples: [] };
|
|
200
|
-
function estimateBatteryLife(level, state) {
|
|
201
|
-
const now = Date.now();
|
|
202
|
-
if (state === "charged" || state === "charging") {
|
|
203
|
-
prevBattery.samples = [];
|
|
204
|
-
return {};
|
|
205
|
-
}
|
|
206
|
-
prevBattery.samples.push({ level, ts: now });
|
|
207
|
-
prevBattery.samples = prevBattery.samples.filter((s) => now - s.ts <= BATTERY_WINDOW_MS);
|
|
208
|
-
const first = prevBattery.samples[0];
|
|
209
|
-
const elapsedMs = now - first.ts;
|
|
210
|
-
if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
|
|
211
|
-
return {};
|
|
212
|
-
}
|
|
213
|
-
const dtHours = Math.max(elapsedMs / 1e3 / 3600, 1e-4);
|
|
214
|
-
const discharged = first.level - level;
|
|
215
|
-
if (discharged <= 0) {
|
|
216
|
-
return { estimatedTimeToEmpty: "calculating" };
|
|
217
|
-
}
|
|
218
|
-
const dischargeRate = discharged / dtHours;
|
|
219
|
-
const remainingLevel = Math.max(level, 0);
|
|
220
|
-
const hoursToEmpty = remainingLevel / dischargeRate;
|
|
221
|
-
const h = Math.floor(hoursToEmpty);
|
|
222
|
-
const m = Math.round((hoursToEmpty - h) * 60);
|
|
223
|
-
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : "calculating";
|
|
224
|
-
const ASSUMED_CAPACITY_WH = 55;
|
|
225
|
-
const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
|
|
226
|
-
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
|
|
227
|
-
}
|
|
228
|
-
async function collectAll(opts = {}) {
|
|
229
|
-
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders] = await Promise.all([
|
|
230
|
-
collectCpu(),
|
|
231
|
-
collectGpu(opts.detailed),
|
|
232
|
-
collectMemory(),
|
|
233
|
-
collectDisk(),
|
|
234
|
-
collectBattery(),
|
|
235
|
-
collectThermal(opts.detailed),
|
|
236
|
-
collectNetwork(),
|
|
237
|
-
collectProcesses(opts.processLimit),
|
|
238
|
-
collectSystem(),
|
|
239
|
-
collectPower(),
|
|
240
|
-
collectPackets(),
|
|
241
|
-
collectTasks(),
|
|
242
|
-
collectContainers(),
|
|
243
|
-
collectBlenderRenders()
|
|
244
|
-
]);
|
|
245
|
-
return {
|
|
246
|
-
header: {
|
|
247
|
-
title: chalk.hex("#ff5500").bold("PYRE"),
|
|
248
|
-
hostname: system.hostname,
|
|
249
|
-
os: system.os,
|
|
250
|
-
uptime: system.uptime
|
|
251
|
-
},
|
|
252
|
-
cpu,
|
|
253
|
-
gpu,
|
|
254
|
-
memory,
|
|
255
|
-
disk,
|
|
256
|
-
battery,
|
|
257
|
-
thermal,
|
|
258
|
-
network,
|
|
259
|
-
processes,
|
|
260
|
-
power,
|
|
261
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
262
|
-
packets,
|
|
263
|
-
tasks,
|
|
264
|
-
containers,
|
|
265
|
-
blenderRenders
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
async function collectContainers() {
|
|
269
|
-
try {
|
|
270
|
-
const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', "")).trim();
|
|
271
|
-
if (!raw) return [];
|
|
272
|
-
return raw.split("\n").map((line) => {
|
|
273
|
-
const parts = line.split("|");
|
|
274
|
-
return {
|
|
275
|
-
id: parts[0],
|
|
276
|
-
name: parts[1],
|
|
277
|
-
cpuPercent: parseFloat(parts[2].replace("%", "")) || 0,
|
|
278
|
-
memUsage: parts[3],
|
|
279
|
-
memPercent: parseFloat(parts[4].replace("%", "")) || 0,
|
|
280
|
-
netIO: parts[5],
|
|
281
|
-
blockIO: parts[6],
|
|
282
|
-
pids: parseInt(parts[7], 10) || 0
|
|
283
|
-
};
|
|
284
|
-
});
|
|
285
|
-
} catch {
|
|
286
|
-
return [];
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
async function collectPower() {
|
|
290
|
-
try {
|
|
291
|
-
const { power } = await getSmcMetrics();
|
|
292
|
-
if (power.cpu !== void 0 || power.gpu !== void 0 || power.combined !== void 0) {
|
|
293
|
-
return { cpuWatts: power.cpu, gpuWatts: power.gpu, aneWatts: 0, combinedWatts: power.combined };
|
|
294
|
-
}
|
|
295
|
-
} catch {
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const ioreg = (await run("ioreg -rn AppleSmartBattery 2>/dev/null")).trim();
|
|
299
|
-
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
300
|
-
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
301
|
-
if (ampMatch && voltMatch) {
|
|
302
|
-
let ampsBig = BigInt(ampMatch[1]);
|
|
303
|
-
if (!ampMatch[1].startsWith("-")) {
|
|
304
|
-
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
305
|
-
const INT64_MAX = (1n << 63n) - 1n;
|
|
306
|
-
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
307
|
-
}
|
|
308
|
-
const amps = Number(ampsBig);
|
|
309
|
-
const volts = parseInt(voltMatch[1], 10) / 1e3;
|
|
310
|
-
const watts = Math.abs(amps / 1e3 * volts);
|
|
311
|
-
if (Number.isFinite(watts) && watts < 500) {
|
|
312
|
-
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
async function collectGpu(detailed) {
|
|
320
|
-
let spRaw = "";
|
|
321
|
-
const now = Date.now();
|
|
322
|
-
if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
|
|
323
|
-
spRaw = spGpuCache.raw;
|
|
324
|
-
} else {
|
|
325
|
-
spRaw = (await run("system_profiler SPDisplaysDataType 2>/dev/null", "", 3e3)).trim();
|
|
326
|
-
spGpuCache = { raw: spRaw, ts: now };
|
|
327
|
-
}
|
|
328
|
-
let model = "Unknown";
|
|
329
|
-
let memory = 0;
|
|
330
|
-
if (spRaw) {
|
|
331
|
-
const modelMatch = spRaw.match(/(?:Chipset Model|Model|Device Name):\s*(.+)/i);
|
|
332
|
-
if (modelMatch) model = modelMatch[1].trim();
|
|
333
|
-
const memMatch = spRaw.match(/(?:VRAM \(Total\)|VRAM \(Dynamic, Max\)|Shared System Memory|Memory|VRAM):\s*([0-9.]+\s*[KMGT]?B)/i);
|
|
334
|
-
if (memMatch) {
|
|
335
|
-
const memStr = memMatch[1].trim();
|
|
336
|
-
const memNum = parseFloat(memStr);
|
|
337
|
-
if (/GB/i.test(memStr)) memory = memNum * 1024 * 1024 * 1024;
|
|
338
|
-
else if (/MB/i.test(memStr)) memory = memNum * 1024 * 1024;
|
|
339
|
-
else if (/KB/i.test(memStr)) memory = memNum * 1024;
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
try {
|
|
343
|
-
const nvRaw = (await run("nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "")).trim();
|
|
344
|
-
if (nvRaw) {
|
|
345
|
-
const parts = nvRaw.split(",").map((s) => s.trim());
|
|
346
|
-
if (parts.length >= 3) {
|
|
347
|
-
model = parts[0];
|
|
348
|
-
memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
349
|
-
const utilization2 = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
350
|
-
return { model, memory, utilization: utilization2, processes: 0 };
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
} catch {
|
|
354
|
-
}
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
if (memory === 0) {
|
|
358
|
-
try {
|
|
359
|
-
const memSizeStr = await cachedSysctl("hw.memsize", "0");
|
|
360
|
-
const totalBytes = parseInt(memSizeStr, 10) || 0;
|
|
361
|
-
if (totalBytes > 0) {
|
|
362
|
-
memory = totalBytes;
|
|
363
|
-
}
|
|
364
|
-
} catch {
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
let utilization = 0;
|
|
368
|
-
let temperature;
|
|
369
|
-
let gpuProcesses = 0;
|
|
370
|
-
try {
|
|
371
|
-
const ioregRaw = await run("ioreg -r -c IOAccelerator 2>/dev/null", "");
|
|
372
|
-
if (ioregRaw) {
|
|
373
|
-
const devUtilMatch = ioregRaw.match(/"Device Utilization %"\s*=\s*([\d.]+)/i);
|
|
374
|
-
const gpuCoreMatch = ioregRaw.match(/"gpu-core-utilization"\s*=\s*([\d.]+)/i);
|
|
375
|
-
const gpuActMatch = ioregRaw.match(/"GPU Activity"\s*=\s*([\d.]+)/i);
|
|
376
|
-
const gpuBusyMatch = ioregRaw.match(/"GPU Busy"\s*=\s*([\d.]+)/i);
|
|
377
|
-
if (devUtilMatch) {
|
|
378
|
-
utilization = Math.min(100, Math.round(parseFloat(devUtilMatch[1])));
|
|
379
|
-
} else if (gpuCoreMatch) {
|
|
380
|
-
const val = parseFloat(gpuCoreMatch[1]);
|
|
381
|
-
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
382
|
-
} else if (gpuActMatch) {
|
|
383
|
-
const val = parseFloat(gpuActMatch[1]);
|
|
384
|
-
utilization = val > 100 ? Math.min(100, Math.round(val / 1e4)) : Math.min(100, Math.round(val));
|
|
385
|
-
} else if (gpuBusyMatch) {
|
|
386
|
-
utilization = Math.min(100, Math.round(parseFloat(gpuBusyMatch[1])));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} catch {
|
|
390
|
-
}
|
|
391
|
-
if (utilization === 0) {
|
|
392
|
-
try {
|
|
393
|
-
const pm = (await run("sudo -n powermetrics --samplers gpu_power -n 1 --format text 2>/dev/null", "")).trim();
|
|
394
|
-
if (pm) {
|
|
395
|
-
const hwRes = pm.match(/GPU HW active residency:\s*([\d.]+)%/i);
|
|
396
|
-
const actRes = pm.match(/GPU active residency:\s*([\d.]+)%/i);
|
|
397
|
-
const gpuUtilMatch = pm.match(/GPU utilization:\s*([\d.]+)%/i) || pm.match(/GPU usage:\s*([\d.]+)%/i);
|
|
398
|
-
const idleRes = pm.match(/GPU idle residency:\s*([\d.]+)%/i);
|
|
399
|
-
const gpuPowerMatch = pm.match(/GPU Power:\s*([\d.]+)\s*mW/i);
|
|
400
|
-
if (hwRes) {
|
|
401
|
-
utilization = Math.min(100, Math.round(parseFloat(hwRes[1])));
|
|
402
|
-
} else if (actRes) {
|
|
403
|
-
utilization = Math.min(100, Math.round(parseFloat(actRes[1])));
|
|
404
|
-
} else if (gpuUtilMatch) {
|
|
405
|
-
utilization = Math.min(100, Math.round(parseFloat(gpuUtilMatch[1])));
|
|
406
|
-
} else if (idleRes) {
|
|
407
|
-
utilization = Math.max(0, Math.min(100, Math.round(100 - parseFloat(idleRes[1]))));
|
|
408
|
-
} else if (gpuPowerMatch) {
|
|
409
|
-
const mw = parseFloat(gpuPowerMatch[1]);
|
|
410
|
-
if (mw > 0) {
|
|
411
|
-
utilization = Math.min(100, Math.max(1, Math.round(mw / 50)));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
} catch {
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
try {
|
|
419
|
-
const { temps, power } = await getSmcMetrics();
|
|
420
|
-
if (temps.gpu_die !== void 0) temperature = temps.gpu_die;
|
|
421
|
-
if (utilization === 0 && power.gpu !== void 0 && power.gpu > 0) {
|
|
422
|
-
utilization = Math.min(100, Math.max(1, Math.round(power.gpu * 5)));
|
|
423
|
-
}
|
|
424
|
-
} catch {
|
|
425
|
-
}
|
|
426
|
-
try {
|
|
427
|
-
const psRaw = (await run('ps aux | grep -iE "[g]pu|[m]etal|[w]indowserver" | wc -l', "0")).trim();
|
|
428
|
-
gpuProcesses = parseInt(psRaw, 10) || 0;
|
|
429
|
-
} catch {
|
|
430
|
-
}
|
|
431
|
-
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
432
|
-
}
|
|
433
|
-
async function collectSystem() {
|
|
434
|
-
const hostname = (await run("hostname", "Mac")).trim();
|
|
435
|
-
const osRaw = (await run("sw_vers -productVersion", "Unknown")).trim();
|
|
436
|
-
const uptimeRaw = (await run("uptime")).trim();
|
|
437
|
-
const uptimeMatch = uptimeRaw.match(/up\s+(.*?),\s+\d+\s+users/);
|
|
438
|
-
return {
|
|
439
|
-
hostname,
|
|
440
|
-
os: `macOS ${osRaw}`,
|
|
441
|
-
uptime: uptimeMatch ? uptimeMatch[1] : uptimeRaw
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
async function collectCpu() {
|
|
445
|
-
const brand = (await cachedSysctl("machdep.cpu.brand_string", "Unknown CPU")).trim();
|
|
446
|
-
const cores = parseInt(await cachedSysctl("hw.ncpu", "1")) || 1;
|
|
447
|
-
const physicalCores = parseInt(await cachedSysctl("hw.physicalcpu", String(cores))) || cores;
|
|
448
|
-
const frequencyHz = parseInt(await cachedSysctl("hw.cpufrequency", "0"));
|
|
449
|
-
let frequency = frequencyHz > 0 ? Math.round(frequencyHz / 1e6) : 0;
|
|
450
|
-
if (frequency === 0) {
|
|
451
|
-
try {
|
|
452
|
-
const { freq } = await getSmcMetrics();
|
|
453
|
-
const values = Object.values(freq);
|
|
454
|
-
if (values.length) {
|
|
455
|
-
frequency = Math.round(Math.max(...values) / 1e6);
|
|
456
|
-
}
|
|
457
|
-
} catch {
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (frequency === 0) {
|
|
461
|
-
try {
|
|
462
|
-
const cpus = os.cpus();
|
|
463
|
-
if (cpus && cpus.length > 0 && cpus[0].speed) {
|
|
464
|
-
frequency = cpus[0].speed;
|
|
465
|
-
}
|
|
466
|
-
} catch {
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
let usage = 0;
|
|
470
|
-
try {
|
|
471
|
-
const cpus = os.cpus();
|
|
472
|
-
let totalNow = 0;
|
|
473
|
-
let idleNow = 0;
|
|
474
|
-
for (const cpu of cpus) {
|
|
475
|
-
totalNow += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
476
|
-
idleNow += cpu.times.idle;
|
|
477
|
-
}
|
|
478
|
-
if (prevCpuTotal.ts > 0) {
|
|
479
|
-
const totalDelta = totalNow - prevCpuTotal.total;
|
|
480
|
-
const idleDelta = idleNow - prevCpuTotal.idle;
|
|
481
|
-
if (totalDelta > 0) {
|
|
482
|
-
usage = Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
prevCpuTotal = { total: totalNow, idle: idleNow, ts: Date.now() };
|
|
486
|
-
} catch {
|
|
487
|
-
}
|
|
488
|
-
const loadAvgStr = (await run("sysctl -n vm.loadavg", "0.00 0.00 0.00")).trim();
|
|
489
|
-
const loadAvg = loadAvgStr.match(/\{ (.+?) \}/)?.[1].split(" ").map(Number) || [0, 0, 0];
|
|
490
|
-
let temperature;
|
|
491
|
-
try {
|
|
492
|
-
const { temps } = await getSmcMetrics();
|
|
493
|
-
if (temps.cpu_die !== void 0) temperature = temps.cpu_die;
|
|
494
|
-
} catch {
|
|
495
|
-
}
|
|
496
|
-
let coreUsage = [];
|
|
497
|
-
try {
|
|
498
|
-
const cpus = os.cpus();
|
|
499
|
-
const current = cpus.map((cpu) => ({
|
|
500
|
-
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
501
|
-
idle: cpu.times.idle
|
|
502
|
-
}));
|
|
503
|
-
if (prevCpuTimes) {
|
|
504
|
-
coreUsage = prevCpuTimes.map((prev, i) => {
|
|
505
|
-
const totalDelta = current[i].total - prev.total;
|
|
506
|
-
const idleDelta = current[i].idle - prev.idle;
|
|
507
|
-
if (totalDelta === 0) return 0;
|
|
508
|
-
return Math.round((totalDelta - idleDelta) / totalDelta * 100);
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
prevCpuTimes = current;
|
|
512
|
-
} catch {
|
|
513
|
-
}
|
|
514
|
-
let clusters;
|
|
515
|
-
try {
|
|
516
|
-
const eCoresStr = await cachedSysctl("hw.perflevel1.physicalcpu", "0");
|
|
517
|
-
const pCoresStr = await cachedSysctl("hw.perflevel0.physicalcpu", "0");
|
|
518
|
-
const eCores = parseInt(eCoresStr, 10) || 0;
|
|
519
|
-
const pCores = parseInt(pCoresStr, 10) || 0;
|
|
520
|
-
if (eCores > 0 || pCores > 0) {
|
|
521
|
-
clusters = [];
|
|
522
|
-
if (eCores > 0) {
|
|
523
|
-
clusters.push({
|
|
524
|
-
name: "E-Cluster",
|
|
525
|
-
cores: eCores,
|
|
526
|
-
frequencyMhz: Math.round(frequency * 0.6) || 2e3,
|
|
527
|
-
effectiveUsagePercent: Math.min(100, Math.round(usage * 0.8)),
|
|
528
|
-
activeResidencyPercent: Math.min(100, Math.round(usage * 0.9))
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
if (pCores > 0) {
|
|
532
|
-
clusters.push({
|
|
533
|
-
name: "P-Cluster",
|
|
534
|
-
cores: pCores,
|
|
535
|
-
frequencyMhz: frequency || 3200,
|
|
536
|
-
effectiveUsagePercent: Math.min(100, Math.round(usage * 1.1)),
|
|
537
|
-
activeResidencyPercent: Math.min(100, Math.round(usage))
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
} catch {
|
|
542
|
-
}
|
|
543
|
-
return { brand, cores, physicalCores, frequency, usage, loadAvg, temperature, coreUsage, clusters };
|
|
544
|
-
}
|
|
545
|
-
async function collectMemory() {
|
|
546
|
-
const totalBytes = parseInt(await cachedSysctl("hw.memsize", "0")) || 0;
|
|
547
|
-
const pageSize = parseInt(await cachedSysctl("hw.pagesize", "4096")) || 4096;
|
|
548
|
-
const vmStat = (await run("vm_stat")).trim();
|
|
549
|
-
const getPageCount = (label) => {
|
|
550
|
-
const m = vmStat.match(new RegExp(`Pages\\s+${label}:\\s+(\\d+)\\.`));
|
|
551
|
-
return m ? parseInt(m[1]) : 0;
|
|
552
|
-
};
|
|
553
|
-
const freePages = getPageCount("free");
|
|
554
|
-
const activePages = getPageCount("active");
|
|
555
|
-
const inactivePages = getPageCount("inactive");
|
|
556
|
-
const wiredPages = getPageCount("wired down");
|
|
557
|
-
const compressedPages = getPageCount("occupied by compressor");
|
|
558
|
-
const purgeablePages = getPageCount("purgeable");
|
|
559
|
-
const getStatCount = (label) => {
|
|
560
|
-
const m = vmStat.match(new RegExp(`${label}:\\s+(\\d+)\\.`));
|
|
561
|
-
return m ? parseInt(m[1]) : 0;
|
|
562
|
-
};
|
|
563
|
-
const swapIns = getStatCount("Swapins") || getStatCount("Pageins");
|
|
564
|
-
const swapOuts = getStatCount("Swapouts") || getStatCount("Pageouts");
|
|
565
|
-
const free = freePages * pageSize;
|
|
566
|
-
const used = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
|
|
567
|
-
const usagePercent = totalBytes > 0 ? Math.round(used / totalBytes * 100) : 0;
|
|
568
|
-
let swapTotal = 0;
|
|
569
|
-
let swapUsed = 0;
|
|
570
|
-
let swapFree = 0;
|
|
571
|
-
try {
|
|
572
|
-
const swapRaw = (await run("sysctl -n vm.swapusage")).trim();
|
|
573
|
-
const swapMatch = swapRaw.match(
|
|
574
|
-
/total\s*=\s*([\d.]+)([KMGkmg])\s+used\s*=\s*([\d.]+)([KMGkmg])\s+free\s*=\s*([\d.]+)([KMGkmg])/
|
|
575
|
-
);
|
|
576
|
-
if (swapMatch) {
|
|
577
|
-
swapTotal = parseFloat(swapMatch[1]) * parseSuffix(swapMatch[2]);
|
|
578
|
-
swapUsed = parseFloat(swapMatch[3]) * parseSuffix(swapMatch[4]);
|
|
579
|
-
swapFree = parseFloat(swapMatch[5]) * parseSuffix(swapMatch[6]);
|
|
580
|
-
}
|
|
581
|
-
} catch {
|
|
582
|
-
}
|
|
583
|
-
let pressureLevel = "Normal";
|
|
584
|
-
try {
|
|
585
|
-
const pressVal = (await run("sysctl -n kern.memorystatus_vm_pressure_level")).trim();
|
|
586
|
-
const pressNum = parseInt(pressVal, 10);
|
|
587
|
-
if (pressNum === 2) pressureLevel = "Warning";
|
|
588
|
-
else if (pressNum >= 4) pressureLevel = "Critical";
|
|
589
|
-
else if (!isNaN(pressNum)) pressureLevel = "Normal";
|
|
590
|
-
else pressureLevel = pressVal || "Normal";
|
|
591
|
-
} catch {
|
|
592
|
-
pressureLevel = "Normal";
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
total: totalBytes,
|
|
596
|
-
used,
|
|
597
|
-
free,
|
|
598
|
-
swapTotal,
|
|
599
|
-
swapUsed,
|
|
600
|
-
swapFree,
|
|
601
|
-
pageSize,
|
|
602
|
-
usagePercent,
|
|
603
|
-
wiredBytes: wiredPages * pageSize,
|
|
604
|
-
compressedBytes: compressedPages * pageSize,
|
|
605
|
-
purgeableBytes: purgeablePages * pageSize,
|
|
606
|
-
swapIns,
|
|
607
|
-
swapOuts,
|
|
608
|
-
pressureLevel
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
var cachedDiskIo = null;
|
|
612
|
-
async function getDiskIoRates() {
|
|
613
|
-
const now = Date.now();
|
|
614
|
-
if (cachedDiskIo && now - cachedDiskIo.ts < 1e3) {
|
|
615
|
-
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
616
|
-
}
|
|
617
|
-
try {
|
|
618
|
-
const raw = (await run("iostat -d -c 2 1 2", "", 3e3)).trim();
|
|
619
|
-
const blocks = raw.split(/\n\s*\n/).filter(Boolean);
|
|
620
|
-
const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
|
|
621
|
-
if (targetBlock) {
|
|
622
|
-
const lines = targetBlock.trim().split("\n");
|
|
623
|
-
if (lines.length >= 2) {
|
|
624
|
-
const dataLine = lines[lines.length - 1].trim();
|
|
625
|
-
const parts = dataLine.split(/\s+/);
|
|
626
|
-
if (parts.length >= 3) {
|
|
627
|
-
const mbSec = parseFloat(parts[2]);
|
|
628
|
-
if (!isNaN(mbSec)) {
|
|
629
|
-
const bytesSec = Math.round(mbSec * 1024 * 1024);
|
|
630
|
-
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
631
|
-
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} catch {
|
|
637
|
-
}
|
|
638
|
-
return { readBytesSec: 0, writeBytesSec: 0 };
|
|
639
|
-
}
|
|
640
|
-
async function collectDisk() {
|
|
641
|
-
const [rawDf, ioRates] = await Promise.all([
|
|
642
|
-
run("df -h"),
|
|
643
|
-
getDiskIoRates()
|
|
644
|
-
]);
|
|
645
|
-
const lines = rawDf.trim().split("\n").slice(1);
|
|
646
|
-
return lines.map((line) => {
|
|
647
|
-
const parts = line.split(/\s+/);
|
|
648
|
-
return {
|
|
649
|
-
filesystem: parts[0],
|
|
650
|
-
size: parts[1],
|
|
651
|
-
used: parts[2],
|
|
652
|
-
available: parts[3],
|
|
653
|
-
capacity: parts[4],
|
|
654
|
-
mountpoint: parts[5] || parts[6] || "/",
|
|
655
|
-
readBytesSec: ioRates.readBytesSec,
|
|
656
|
-
writeBytesSec: ioRates.writeBytesSec
|
|
657
|
-
};
|
|
658
|
-
}).filter((d) => d.filesystem.includes("/") || d.size.includes("G") || d.size.includes("T"));
|
|
659
|
-
}
|
|
660
|
-
async function collectBattery() {
|
|
661
|
-
try {
|
|
662
|
-
const raw = (await run("pmset -g batt")).trim();
|
|
663
|
-
if (!raw.includes("Battery Power") && !raw.includes("AC Power")) return null;
|
|
664
|
-
const levelMatch = raw.match(/(\d+)%/);
|
|
665
|
-
const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
|
|
666
|
-
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
|
|
667
|
-
const powerPlugged = raw.includes("AC Power") || raw.includes("attached to charger");
|
|
668
|
-
const level = levelMatch ? parseInt(levelMatch[1]) : 0;
|
|
669
|
-
const state = powerPlugged ? "charged" : stateMatch ? stateMatch[1].toLowerCase() : "discharging";
|
|
670
|
-
const timeRemaining = timeMatch ? timeMatch[1] : powerPlugged ? "\u221E" : "calculating";
|
|
671
|
-
let cycles;
|
|
672
|
-
let condition;
|
|
673
|
-
let maxCapacityPercent;
|
|
674
|
-
try {
|
|
675
|
-
const now = Date.now();
|
|
676
|
-
let battInfo;
|
|
677
|
-
if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
|
|
678
|
-
battInfo = spBatteryCache.raw;
|
|
679
|
-
} else {
|
|
680
|
-
battInfo = (await run("system_profiler SPPowerDataType 2>/dev/null", "", 3e3)).trim();
|
|
681
|
-
spBatteryCache = { raw: battInfo, ts: now };
|
|
682
|
-
}
|
|
683
|
-
const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
|
|
684
|
-
if (cycleMatch) cycles = parseInt(cycleMatch[1]);
|
|
685
|
-
const conditionMatch = battInfo.match(/Condition:\s+(.+)/);
|
|
686
|
-
if (conditionMatch) condition = conditionMatch[1].trim();
|
|
687
|
-
const maxCapMatch = battInfo.match(/Maximum Capacity:\s+(\d+)%/);
|
|
688
|
-
if (maxCapMatch) {
|
|
689
|
-
maxCapacityPercent = parseInt(maxCapMatch[1]);
|
|
690
|
-
} else {
|
|
691
|
-
const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
|
|
692
|
-
const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
|
|
693
|
-
if (fullMatch && designMatch) {
|
|
694
|
-
const full = parseInt(fullMatch[1]);
|
|
695
|
-
const design = parseInt(designMatch[1]);
|
|
696
|
-
if (design > 0) maxCapacityPercent = Math.round(full / design * 100);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
} catch {
|
|
700
|
-
}
|
|
701
|
-
const healthParts = [];
|
|
702
|
-
if (condition) healthParts.push(condition);
|
|
703
|
-
if (maxCapacityPercent !== void 0) healthParts.push(`${maxCapacityPercent}% capacity`);
|
|
704
|
-
if (cycles !== void 0) healthParts.push(`${cycles} cycles`);
|
|
705
|
-
const health = healthParts.length ? healthParts.join(", ") : "unknown";
|
|
706
|
-
let friendlySummary;
|
|
707
|
-
const condLower = (condition || "").toLowerCase();
|
|
708
|
-
if (condLower.includes("service") || condLower.includes("replace") || condLower.includes("check")) {
|
|
709
|
-
friendlySummary = "Your battery has more wear than usual \u2014 consider a service appointment.";
|
|
710
|
-
} else if (cycles !== void 0 && cycles > 1e3) {
|
|
711
|
-
friendlySummary = "Your battery cycle count is high \u2014 capacity may be reduced.";
|
|
712
|
-
} else if (maxCapacityPercent !== void 0 && maxCapacityPercent < 80) {
|
|
713
|
-
friendlySummary = "Your battery maximum capacity is below 80% \u2014 consider servicing.";
|
|
714
|
-
} else if (condition) {
|
|
715
|
-
friendlySummary = `Battery condition is ${condition.toLowerCase()}.`;
|
|
716
|
-
}
|
|
717
|
-
const estimate = estimateBatteryLife(level, state);
|
|
718
|
-
return {
|
|
719
|
-
level,
|
|
720
|
-
state,
|
|
721
|
-
timeRemaining,
|
|
722
|
-
health,
|
|
723
|
-
powerSource: powerPlugged ? "AC" : "Battery",
|
|
724
|
-
cycles,
|
|
725
|
-
condition,
|
|
726
|
-
maxCapacityPercent,
|
|
727
|
-
friendlySummary,
|
|
728
|
-
...estimate
|
|
729
|
-
};
|
|
730
|
-
} catch {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
function pressureFromState(state) {
|
|
735
|
-
const s = state.toLowerCase();
|
|
736
|
-
if (s.includes("critical")) return 3;
|
|
737
|
-
if (s.includes("serious") || s.includes("heavy")) return 2;
|
|
738
|
-
if (s.includes("fair") || s.includes("moderate")) return 1;
|
|
739
|
-
return 0;
|
|
740
|
-
}
|
|
741
|
-
async function collectThermal(detailed) {
|
|
742
|
-
try {
|
|
743
|
-
const therm = (await run("pmset -g therm", "")).trim();
|
|
744
|
-
let state = "Unknown";
|
|
745
|
-
let detail = therm || "No pmset thermal data";
|
|
746
|
-
const stateMatch = therm.match(/Thermal state:\s*(.+)/i);
|
|
747
|
-
if (stateMatch) {
|
|
748
|
-
const raw = stateMatch[1].trim();
|
|
749
|
-
state = /0|no warning|normal/i.test(raw) ? "Nominal" : raw;
|
|
750
|
-
detail = state;
|
|
751
|
-
} else {
|
|
752
|
-
const limitMatch = therm.match(
|
|
753
|
-
/CPU_(?:Speed|Scheduler)_Limit\s*(?:=|:)\s*([\d.]+)/i
|
|
754
|
-
);
|
|
755
|
-
if (limitMatch) {
|
|
756
|
-
const limit = parseFloat(limitMatch[1]);
|
|
757
|
-
if (limit >= 100) state = "Nominal";
|
|
758
|
-
else if (limit >= 85) state = "Fair";
|
|
759
|
-
else if (limit >= 50) state = "Serious";
|
|
760
|
-
else state = "Critical";
|
|
761
|
-
detail = `${state} (CPU limit ${limit}%)`;
|
|
762
|
-
} else if (/no thermal warning level has been recorded/i.test(therm)) {
|
|
763
|
-
state = "Nominal";
|
|
764
|
-
detail = "Nominal (no thermal warning recorded)";
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const temperatures = {};
|
|
768
|
-
let cpuDieTemp;
|
|
769
|
-
try {
|
|
770
|
-
const { temps } = await getSmcMetrics();
|
|
771
|
-
cpuDieTemp = temps.cpu_die;
|
|
772
|
-
if (temps.cpu_die !== void 0) temperatures["cpu_die"] = temps.cpu_die;
|
|
773
|
-
if (temps.gpu_die !== void 0) temperatures["gpu_die"] = temps.gpu_die;
|
|
774
|
-
if (temps.smc_die !== void 0) temperatures["smc_die"] = temps.smc_die;
|
|
775
|
-
for (const [k, v] of Object.entries(temps)) {
|
|
776
|
-
if (temperatures[k] === void 0 && v !== void 0) {
|
|
777
|
-
temperatures[k] = v;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
} catch {
|
|
781
|
-
}
|
|
782
|
-
if (state === "Unknown" && cpuDieTemp !== void 0) {
|
|
783
|
-
if (cpuDieTemp >= 100) state = "Critical";
|
|
784
|
-
else if (cpuDieTemp >= 90) state = "Serious";
|
|
785
|
-
else if (cpuDieTemp >= 80) state = "Fair";
|
|
786
|
-
else state = "Nominal";
|
|
787
|
-
detail = `${state} (estimated from ${cpuDieTemp}\xB0C)`;
|
|
788
|
-
}
|
|
789
|
-
return {
|
|
790
|
-
state,
|
|
791
|
-
detail,
|
|
792
|
-
pressureLevel: pressureFromState(state),
|
|
793
|
-
temperatures: Object.keys(temperatures).length ? temperatures : void 0
|
|
794
|
-
};
|
|
795
|
-
} catch {
|
|
796
|
-
return { state: "Unknown", pressureLevel: 0, error: "Thermal info unavailable on this system" };
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
async function collectNetwork() {
|
|
800
|
-
try {
|
|
801
|
-
const { iface, ip } = await cachedRouteIface();
|
|
802
|
-
const netStat = await cachedNetstatIb();
|
|
803
|
-
let rxBytes = 0;
|
|
804
|
-
let txBytes = 0;
|
|
805
|
-
let rxPackets = 0;
|
|
806
|
-
let txPackets = 0;
|
|
807
|
-
for (const line of netStat.split("\n")) {
|
|
808
|
-
if (line.startsWith(iface) || line.startsWith(iface + ":")) {
|
|
809
|
-
const parts = line.split(/\s+/);
|
|
810
|
-
if (parts.length >= 9) {
|
|
811
|
-
rxPackets += parseInt(parts[4]) || 0;
|
|
812
|
-
rxBytes += parseInt(parts[5]) || 0;
|
|
813
|
-
txPackets += parseInt(parts[6]) || 0;
|
|
814
|
-
txBytes += parseInt(parts[7]) || 0;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const now = Date.now();
|
|
819
|
-
let rxRate = 0;
|
|
820
|
-
let txRate = 0;
|
|
821
|
-
if (prevNetSample && prevNetSample.ts > 0) {
|
|
822
|
-
const dt = Math.max((now - prevNetSample.ts) / 1e3, 1e-3);
|
|
823
|
-
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
824
|
-
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
825
|
-
}
|
|
826
|
-
prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
827
|
-
const protocolStats = await getProtocolStats();
|
|
828
|
-
const connectionStates = await getConnectionStates();
|
|
829
|
-
const listeningPorts = await getListeningPorts();
|
|
830
|
-
const establishedConnections = connectionStates.established;
|
|
831
|
-
const topRemoteHosts = await getTopRemoteHosts();
|
|
832
|
-
return {
|
|
833
|
-
interface: iface,
|
|
834
|
-
ip,
|
|
835
|
-
rxBytes,
|
|
836
|
-
txBytes,
|
|
837
|
-
rxPackets,
|
|
838
|
-
txPackets,
|
|
839
|
-
rxRate,
|
|
840
|
-
txRate,
|
|
841
|
-
connections: establishedConnections,
|
|
842
|
-
protocols: protocolStats,
|
|
843
|
-
connectionStates,
|
|
844
|
-
listeningPorts,
|
|
845
|
-
establishedConnections,
|
|
846
|
-
topRemoteHosts
|
|
847
|
-
};
|
|
848
|
-
} catch {
|
|
849
|
-
return {
|
|
850
|
-
interface: "unknown",
|
|
851
|
-
ip: "0.0.0.0",
|
|
852
|
-
rxBytes: 0,
|
|
853
|
-
txBytes: 0,
|
|
854
|
-
rxPackets: 0,
|
|
855
|
-
txPackets: 0,
|
|
856
|
-
rxRate: 0,
|
|
857
|
-
txRate: 0,
|
|
858
|
-
connections: 0,
|
|
859
|
-
protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
|
|
860
|
-
connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
|
|
861
|
-
listeningPorts: [],
|
|
862
|
-
establishedConnections: 0,
|
|
863
|
-
topRemoteHosts: []
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
async function getProtocolStats() {
|
|
868
|
-
try {
|
|
869
|
-
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", "");
|
|
870
|
-
const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", "0")).trim();
|
|
871
|
-
const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", "0")).trim();
|
|
872
|
-
let tcpRxBytes = 0;
|
|
873
|
-
let tcpTxBytes = 0;
|
|
874
|
-
let udpRxBytes = 0;
|
|
875
|
-
let udpTxBytes = 0;
|
|
876
|
-
try {
|
|
877
|
-
const netstat = await cachedNetstatIb();
|
|
878
|
-
for (const line of netstat.split("\n")) {
|
|
879
|
-
const parts = line.split(/\s+/);
|
|
880
|
-
if (parts.length >= 9 && parts[0] && !parts[0].startsWith("Name")) {
|
|
881
|
-
const iface = parts[0];
|
|
882
|
-
if (iface.includes("lo")) continue;
|
|
883
|
-
const rx = parseInt(parts[5]) || 0;
|
|
884
|
-
const tx = parseInt(parts[7]) || 0;
|
|
885
|
-
tcpRxBytes += rx;
|
|
886
|
-
tcpTxBytes += tx;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
} catch {
|
|
890
|
-
}
|
|
891
|
-
return {
|
|
892
|
-
tcp: {
|
|
893
|
-
connections: parseInt(tcpConnections) || 0,
|
|
894
|
-
rxBytes: tcpRxBytes,
|
|
895
|
-
txBytes: tcpTxBytes
|
|
896
|
-
},
|
|
897
|
-
udp: {
|
|
898
|
-
connections: parseInt(udpConnections) || 0,
|
|
899
|
-
rxBytes: udpRxBytes,
|
|
900
|
-
txBytes: udpTxBytes
|
|
901
|
-
}
|
|
902
|
-
};
|
|
903
|
-
} catch {
|
|
904
|
-
return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
async function getConnectionStates() {
|
|
908
|
-
try {
|
|
909
|
-
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", "");
|
|
910
|
-
const lines = raw.split("\n").filter(Boolean);
|
|
911
|
-
const stats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
912
|
-
for (const line of lines) {
|
|
913
|
-
const match = line.match(/(\d+)\s+(\S+)/);
|
|
914
|
-
if (!match) continue;
|
|
915
|
-
const count = parseInt(match[1]);
|
|
916
|
-
const state = match[2].toUpperCase();
|
|
917
|
-
if (state.includes("ESTABLISHED")) stats.established += count;
|
|
918
|
-
else if (state.includes("LISTEN")) stats.listening += count;
|
|
919
|
-
else if (state.includes("TIME_WAIT")) stats.timeWait += count;
|
|
920
|
-
else if (state.includes("CLOSE_WAIT")) stats.closeWait += count;
|
|
921
|
-
else stats.other += count;
|
|
922
|
-
}
|
|
923
|
-
return stats;
|
|
924
|
-
} catch {
|
|
925
|
-
return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
async function getListeningPorts() {
|
|
929
|
-
try {
|
|
930
|
-
const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", "");
|
|
931
|
-
const ports = raw.split("\n").map((p) => parseInt(p, 10)).filter((p) => !isNaN(p) && p > 0);
|
|
932
|
-
return [...new Set(ports)].sort((a, b) => a - b);
|
|
933
|
-
} catch {
|
|
934
|
-
return [];
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
async function getTopRemoteHosts() {
|
|
938
|
-
try {
|
|
939
|
-
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", "");
|
|
940
|
-
const ports = raw.split("\n").filter(Boolean);
|
|
941
|
-
const hostMap = /* @__PURE__ */ new Map();
|
|
942
|
-
for (const port of ports) {
|
|
943
|
-
const host = port.split(":")[0];
|
|
944
|
-
if (!host || host === "127.0.0.1" || host === "::1") continue;
|
|
945
|
-
const existing = hostMap.get(host) || { count: 0, bytes: 0 };
|
|
946
|
-
hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
|
|
947
|
-
}
|
|
948
|
-
const result = [];
|
|
949
|
-
for (const [host, info] of hostMap.entries()) {
|
|
950
|
-
result.push({
|
|
951
|
-
host,
|
|
952
|
-
ip: host,
|
|
953
|
-
bytesTransferred: info.bytes,
|
|
954
|
-
connectionCount: info.count
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
|
|
958
|
-
} catch {
|
|
959
|
-
return [];
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
async function collectProcesses(limit) {
|
|
963
|
-
try {
|
|
964
|
-
const isMac = process.platform === "darwin";
|
|
965
|
-
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
966
|
-
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : "";
|
|
967
|
-
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm ${sortArg}${headClause}`)).trim();
|
|
968
|
-
const lines = raw.split("\n").slice(1);
|
|
969
|
-
return lines.map((line) => {
|
|
970
|
-
const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
971
|
-
if (!parts) return null;
|
|
972
|
-
const runtimeSec = parts[7].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
973
|
-
const command = parts[8];
|
|
974
|
-
const isGpuOrMl = /gpu|metal|vulkan|cuda|ollama|llama|torch|mps|mlx|coreml|python/i.test(command);
|
|
975
|
-
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 };
|
|
976
|
-
}).filter((p) => p !== null && p.pid > 0);
|
|
977
|
-
} catch {
|
|
978
|
-
return [];
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
async function collectPackets() {
|
|
982
|
-
try {
|
|
983
|
-
const netStat = await cachedNetstatIb();
|
|
984
|
-
const lines = netStat.split("\n").slice(1);
|
|
985
|
-
let totalRxPackets = 0;
|
|
986
|
-
let totalTxPackets = 0;
|
|
987
|
-
let totalRxBytes = 0;
|
|
988
|
-
let totalTxBytes = 0;
|
|
989
|
-
const ifaceStats = [];
|
|
990
|
-
for (const line of lines) {
|
|
991
|
-
const parts = line.split(/\s+/);
|
|
992
|
-
if (parts.length >= 11 && parts[0] && !parts[0].startsWith("Name")) {
|
|
993
|
-
const iface = parts[0];
|
|
994
|
-
const rxPackets = parseInt(parts[4]) || 0;
|
|
995
|
-
const rxBytes = parseInt(parts[5]) || 0;
|
|
996
|
-
const txPackets = parseInt(parts[6]) || 0;
|
|
997
|
-
const txBytes = parseInt(parts[7]) || 0;
|
|
998
|
-
totalRxPackets += rxPackets;
|
|
999
|
-
totalTxPackets += txPackets;
|
|
1000
|
-
totalRxBytes += rxBytes;
|
|
1001
|
-
totalTxBytes += txBytes;
|
|
1002
|
-
if (iface && !iface.includes("lo")) {
|
|
1003
|
-
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
let connections = 0;
|
|
1008
|
-
let allProcesses = [];
|
|
1009
|
-
try {
|
|
1010
|
-
const nettop = (await run("nettop -L 1 -J bytes_in,bytes_out,state 2>/dev/null", "", 2e3)).trim();
|
|
1011
|
-
const rows = nettop.split("\n");
|
|
1012
|
-
let currentProc = null;
|
|
1013
|
-
for (const row of rows) {
|
|
1014
|
-
if (!row || row.startsWith(",state,")) continue;
|
|
1015
|
-
const cols = row.split(",");
|
|
1016
|
-
const nameCol = cols[0];
|
|
1017
|
-
if (!nameCol.includes("<->")) {
|
|
1018
|
-
if (currentProc) {
|
|
1019
|
-
if (currentProc.connections && currentProc.connections.length > 0) {
|
|
1020
|
-
allProcesses.push(currentProc);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
const lastDot = nameCol.lastIndexOf(".");
|
|
1024
|
-
let pid = 0;
|
|
1025
|
-
let command = nameCol;
|
|
1026
|
-
if (lastDot > 0) {
|
|
1027
|
-
const possiblePid = parseInt(nameCol.substring(lastDot + 1));
|
|
1028
|
-
if (!isNaN(possiblePid)) {
|
|
1029
|
-
pid = possiblePid;
|
|
1030
|
-
command = nameCol.substring(0, lastDot);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
const rx = parseInt(cols[2]) || 0;
|
|
1034
|
-
const tx = parseInt(cols[3]) || 0;
|
|
1035
|
-
currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
|
|
1036
|
-
} else {
|
|
1037
|
-
if (currentProc) {
|
|
1038
|
-
const state = cols[1];
|
|
1039
|
-
const rx = parseInt(cols[2]) || 0;
|
|
1040
|
-
const tx = parseInt(cols[3]) || 0;
|
|
1041
|
-
if (state === "Established") connections++;
|
|
1042
|
-
const spaceIdx = nameCol.indexOf(" ");
|
|
1043
|
-
const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : "";
|
|
1044
|
-
const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
|
|
1045
|
-
const arrowIdx = endpoints.indexOf("<->");
|
|
1046
|
-
let local = "";
|
|
1047
|
-
let remote = "";
|
|
1048
|
-
if (arrowIdx > 0) {
|
|
1049
|
-
local = endpoints.substring(0, arrowIdx);
|
|
1050
|
-
remote = endpoints.substring(arrowIdx + 3);
|
|
1051
|
-
remote = resolveIp(remote);
|
|
1052
|
-
} else {
|
|
1053
|
-
local = endpoints;
|
|
1054
|
-
}
|
|
1055
|
-
currentProc.connections.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
|
|
1060
|
-
allProcesses.push(currentProc);
|
|
1061
|
-
}
|
|
1062
|
-
} catch {
|
|
1063
|
-
allProcesses = [];
|
|
1064
|
-
}
|
|
1065
|
-
const topProcesses = [...allProcesses].sort((a, b) => b.rxBytes + b.txBytes - (a.rxBytes + a.txBytes));
|
|
1066
|
-
const protocolStats = await getProtocolStats();
|
|
1067
|
-
const connectionStates = await getConnectionStates();
|
|
1068
|
-
const listeningPorts = await getListeningPorts();
|
|
1069
|
-
return {
|
|
1070
|
-
totalPackets: totalRxPackets + totalTxPackets,
|
|
1071
|
-
rxPackets: totalRxPackets,
|
|
1072
|
-
txPackets: totalTxPackets,
|
|
1073
|
-
rxRate: 0,
|
|
1074
|
-
txRate: 0,
|
|
1075
|
-
connections,
|
|
1076
|
-
topProcesses,
|
|
1077
|
-
interfaces: ifaceStats,
|
|
1078
|
-
allProcesses,
|
|
1079
|
-
protocolStats,
|
|
1080
|
-
connectionStates,
|
|
1081
|
-
listeningPorts
|
|
1082
|
-
};
|
|
1083
|
-
} catch {
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
async function collectTasks(limit = 12) {
|
|
1088
|
-
try {
|
|
1089
|
-
const isMac = process.platform === "darwin";
|
|
1090
|
-
const sortArg = isMac ? "-r" : "--sort=-pcpu";
|
|
1091
|
-
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
1092
|
-
const lines = raw.split("\n").slice(1);
|
|
1093
|
-
return lines.map((line) => {
|
|
1094
|
-
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
1095
|
-
if (!parts) return null;
|
|
1096
|
-
const runtimeSec = parts[6].split(":").reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
1097
|
-
return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
|
|
1098
|
-
}).filter((t) => t !== null && t.pid > 0);
|
|
1099
|
-
} catch {
|
|
1100
|
-
return [];
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
async function getDisplayInfo() {
|
|
1104
|
-
try {
|
|
1105
|
-
const raw = await run("system_profiler SPDisplaysDataType 2>&1", "");
|
|
1106
|
-
const displays = [];
|
|
1107
|
-
const lines = raw.split("\n");
|
|
1108
|
-
let currentName = "";
|
|
1109
|
-
let currentRes = "";
|
|
1110
|
-
let isMain = false;
|
|
1111
|
-
let connType = "";
|
|
1112
|
-
for (const line of lines) {
|
|
1113
|
-
const trimmed = line.trim();
|
|
1114
|
-
if (trimmed.endsWith(":") && !trimmed.startsWith("Chipset") && !trimmed.startsWith("Displays") && !trimmed.startsWith("Graphics")) {
|
|
1115
|
-
if (currentName && currentRes) {
|
|
1116
|
-
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
|
|
1117
|
-
currentRes = "";
|
|
1118
|
-
isMain = false;
|
|
1119
|
-
connType = "";
|
|
1120
|
-
}
|
|
1121
|
-
currentName = trimmed.slice(0, -1);
|
|
1122
|
-
} else if (trimmed.startsWith("Resolution:")) {
|
|
1123
|
-
currentRes = trimmed.replace("Resolution:", "").trim();
|
|
1124
|
-
} else if (trimmed.startsWith("Main Display: Yes")) {
|
|
1125
|
-
isMain = true;
|
|
1126
|
-
} else if (trimmed.startsWith("Connection Type:")) {
|
|
1127
|
-
connType = trimmed.replace("Connection Type:", "").trim();
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
if (currentName && currentRes) {
|
|
1131
|
-
displays.push({ name: currentName, resolution: currentRes, isMain, connectionType: connType || void 0 });
|
|
1132
|
-
}
|
|
1133
|
-
return displays;
|
|
1134
|
-
} catch {
|
|
1135
|
-
return [];
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
async function getTimeMachineStatus() {
|
|
1139
|
-
try {
|
|
1140
|
-
const statusRaw = await run("tmutil status 2>&1", "");
|
|
1141
|
-
if (statusRaw.includes("No Time Machine destination") || statusRaw.includes("not configured")) {
|
|
1142
|
-
return { configured: false, running: false };
|
|
1143
|
-
}
|
|
1144
|
-
const running = statusRaw.includes("Running = 1;");
|
|
1145
|
-
let percent = void 0;
|
|
1146
|
-
const pctMatch = statusRaw.match(/Percent\s*=\s*"?(0\.\d+|\d+)"?/i);
|
|
1147
|
-
if (pctMatch) {
|
|
1148
|
-
percent = Math.round(parseFloat(pctMatch[1]) * 100);
|
|
1149
|
-
}
|
|
1150
|
-
let latestBackup = "Unknown";
|
|
1151
|
-
try {
|
|
1152
|
-
const latestRaw = await run("tmutil latestbackup 2>&1", "");
|
|
1153
|
-
if (latestRaw.trim() && !latestRaw.includes("error") && !latestRaw.includes("No backups")) {
|
|
1154
|
-
const basename = latestRaw.trim().split("/").pop() || latestRaw.trim();
|
|
1155
|
-
latestBackup = basename.replace(".previous", "").replace(".backup", "");
|
|
1156
|
-
} else {
|
|
1157
|
-
latestBackup = "No backup found / Unmounted";
|
|
1158
|
-
}
|
|
1159
|
-
} catch {
|
|
1160
|
-
latestBackup = "Not mounted";
|
|
1161
|
-
}
|
|
1162
|
-
return { configured: true, running, percent, latestBackup };
|
|
1163
|
-
} catch {
|
|
1164
|
-
return { configured: false, running: false };
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
export {
|
|
1169
|
-
collectAll,
|
|
1170
|
-
collectContainers,
|
|
1171
|
-
collectPower,
|
|
1172
|
-
collectGpu,
|
|
1173
|
-
collectCpu,
|
|
1174
|
-
collectMemory,
|
|
1175
|
-
collectDisk,
|
|
1176
|
-
collectBattery,
|
|
1177
|
-
collectThermal,
|
|
1178
|
-
collectNetwork,
|
|
1179
|
-
collectProcesses,
|
|
1180
|
-
collectPackets,
|
|
1181
|
-
collectTasks,
|
|
1182
|
-
getDisplayInfo,
|
|
1183
|
-
getTimeMachineStatus
|
|
1184
|
-
};
|