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
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux platform telemetry collectors.
|
|
3
|
+
*
|
|
4
|
+
* Reads system metrics directly from standard Linux interfaces
|
|
5
|
+
* (/proc, /sys, standard POSIX tools) with zero native compilation requirements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { run } from '../run.js';
|
|
12
|
+
import type {
|
|
13
|
+
CpuData,
|
|
14
|
+
MemoryData,
|
|
15
|
+
DiskData,
|
|
16
|
+
BatteryData,
|
|
17
|
+
ThermalData,
|
|
18
|
+
NetworkData,
|
|
19
|
+
ProcessData,
|
|
20
|
+
GpuData,
|
|
21
|
+
PacketData,
|
|
22
|
+
TaskData,
|
|
23
|
+
FanData,
|
|
24
|
+
DisplayInfo,
|
|
25
|
+
PowerData,
|
|
26
|
+
ProtocolStats,
|
|
27
|
+
ConnectionStateStats,
|
|
28
|
+
RemoteHostInfo,
|
|
29
|
+
} from '../types.js';
|
|
30
|
+
|
|
31
|
+
let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
|
|
32
|
+
let prevDiskIoSample: { totalBytes: number; ts: number } | null = null;
|
|
33
|
+
let cachedDiskIo: { readBytesSec: number; writeBytesSec: number; ts: number } | null = null;
|
|
34
|
+
|
|
35
|
+
function safeReadFile(filePath: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
} catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function collectLinuxSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
44
|
+
const hostname = os.hostname();
|
|
45
|
+
let osName = 'Linux';
|
|
46
|
+
|
|
47
|
+
const osRelease = safeReadFile('/etc/os-release');
|
|
48
|
+
if (osRelease) {
|
|
49
|
+
const prettyMatch = osRelease.match(/PRETTY_NAME="?([^"\n]+)"?/);
|
|
50
|
+
if (prettyMatch) {
|
|
51
|
+
osName = prettyMatch[1];
|
|
52
|
+
} else {
|
|
53
|
+
const nameMatch = osRelease.match(/NAME="?([^"\n]+)"?/);
|
|
54
|
+
const verMatch = osRelease.match(/VERSION="?([^"\n]+)"?/);
|
|
55
|
+
if (nameMatch) {
|
|
56
|
+
osName = verMatch ? `${nameMatch[1]} ${verMatch[1]}` : nameMatch[1];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
osName = `Linux ${os.release()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uptimeSec = Math.floor(os.uptime());
|
|
64
|
+
const days = Math.floor(uptimeSec / 86400);
|
|
65
|
+
const hours = Math.floor((uptimeSec % 86400) / 3600);
|
|
66
|
+
const minutes = Math.floor((uptimeSec % 3600) / 60);
|
|
67
|
+
let uptime = '';
|
|
68
|
+
if (days > 0) uptime += `${days} ${days === 1 ? 'day' : 'days'}, `;
|
|
69
|
+
uptime += `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
70
|
+
|
|
71
|
+
return { hostname, os: osName, uptime };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function collectLinuxCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
|
|
75
|
+
const cpus = os.cpus();
|
|
76
|
+
const cores = cpus.length || 1;
|
|
77
|
+
let brand = cpus[0]?.model || 'Linux CPU';
|
|
78
|
+
let frequency = cpus[0]?.speed || 0;
|
|
79
|
+
|
|
80
|
+
// Try /proc/cpuinfo for accurate brand and frequency
|
|
81
|
+
const cpuInfo = safeReadFile('/proc/cpuinfo');
|
|
82
|
+
if (cpuInfo) {
|
|
83
|
+
const brandMatch = cpuInfo.match(/model name\s*:\s*(.+)/i);
|
|
84
|
+
if (brandMatch) brand = brandMatch[1].trim();
|
|
85
|
+
|
|
86
|
+
const mhzMatch = cpuInfo.match(/cpu MHz\s*:\s*([\d.]+)/i);
|
|
87
|
+
if (mhzMatch) frequency = Math.round(parseFloat(mhzMatch[1]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Scaling freq fallback from sysfs
|
|
91
|
+
if (frequency === 0) {
|
|
92
|
+
const curFreq = safeReadFile('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq').trim();
|
|
93
|
+
if (curFreq) {
|
|
94
|
+
frequency = Math.round(parseInt(curFreq, 10) / 1000);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Count physical cores from /proc/cpuinfo
|
|
99
|
+
let physicalCores = cores;
|
|
100
|
+
if (cpuInfo) {
|
|
101
|
+
const coreIds = new Set<string>();
|
|
102
|
+
const blocks = cpuInfo.split(/\n\s*\n/);
|
|
103
|
+
for (const b of blocks) {
|
|
104
|
+
const physMatch = b.match(/physical id\s*:\s*(\d+)/);
|
|
105
|
+
const coreMatch = b.match(/core id\s*:\s*(\d+)/);
|
|
106
|
+
if (physMatch && coreMatch) {
|
|
107
|
+
coreIds.add(`${physMatch[1]}:${coreMatch[1]}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (coreIds.size > 0) physicalCores = coreIds.size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Core usage calculation
|
|
114
|
+
const current = cpus.map(cpu => ({
|
|
115
|
+
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
116
|
+
idle: cpu.times.idle,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
let coreUsage: number[] = [];
|
|
120
|
+
let usage = 0;
|
|
121
|
+
if (prevCpuTimesRef.current && prevCpuTimesRef.current.length === current.length) {
|
|
122
|
+
coreUsage = current.map((c, i) => {
|
|
123
|
+
const prev = prevCpuTimesRef.current![i];
|
|
124
|
+
const totalDelta = c.total - prev.total;
|
|
125
|
+
const idleDelta = c.idle - prev.idle;
|
|
126
|
+
if (totalDelta <= 0) return 0;
|
|
127
|
+
return Math.min(100, Math.max(0, Math.round(((totalDelta - idleDelta) / totalDelta) * 100)));
|
|
128
|
+
});
|
|
129
|
+
if (coreUsage.length > 0) {
|
|
130
|
+
usage = Math.round(coreUsage.reduce((a, b) => a + b, 0) / coreUsage.length);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
prevCpuTimesRef.current = current;
|
|
134
|
+
|
|
135
|
+
// Temperature
|
|
136
|
+
let temperature: number | undefined;
|
|
137
|
+
const thermZone = safeReadFile('/sys/class/thermal/thermal_zone0/temp').trim();
|
|
138
|
+
if (thermZone) {
|
|
139
|
+
const rawVal = parseFloat(thermZone);
|
|
140
|
+
const val = rawVal > 1000 ? rawVal / 1000 : rawVal;
|
|
141
|
+
if (val >= 10 && val <= 125) temperature = Math.round(val * 10) / 10;
|
|
142
|
+
}
|
|
143
|
+
if (temperature === undefined) {
|
|
144
|
+
try {
|
|
145
|
+
const hwmonDirs = fs.readdirSync('/sys/class/hwmon');
|
|
146
|
+
for (const d of hwmonDirs) {
|
|
147
|
+
const tempFile = path.join('/sys/class/hwmon', d, 'temp1_input');
|
|
148
|
+
if (fs.existsSync(tempFile)) {
|
|
149
|
+
const raw = parseFloat(safeReadFile(tempFile).trim());
|
|
150
|
+
const val = raw > 1000 ? raw / 1000 : raw;
|
|
151
|
+
if (val >= 10 && val <= 125) {
|
|
152
|
+
temperature = Math.round(val * 10) / 10;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const loadAvg = os.loadavg();
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
brand,
|
|
166
|
+
cores,
|
|
167
|
+
physicalCores,
|
|
168
|
+
frequency,
|
|
169
|
+
usage,
|
|
170
|
+
loadAvg,
|
|
171
|
+
temperature,
|
|
172
|
+
coreUsage,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function collectLinuxMemory(): Promise<MemoryData> {
|
|
177
|
+
const meminfo = safeReadFile('/proc/meminfo');
|
|
178
|
+
let total = os.totalmem();
|
|
179
|
+
let free = os.freemem();
|
|
180
|
+
let available = free;
|
|
181
|
+
let swapTotal = 0;
|
|
182
|
+
let swapFree = 0;
|
|
183
|
+
let buffers = 0;
|
|
184
|
+
let cached = 0;
|
|
185
|
+
let shmem = 0;
|
|
186
|
+
let sreclaimable = 0;
|
|
187
|
+
|
|
188
|
+
if (meminfo) {
|
|
189
|
+
const parseKb = (key: string): number => {
|
|
190
|
+
const m = meminfo.match(new RegExp(`^${key}:\\s+(\\d+)\\s+kB`, 'm'));
|
|
191
|
+
return m ? parseInt(m[1], 10) * 1024 : 0;
|
|
192
|
+
};
|
|
193
|
+
const t = parseKb('MemTotal');
|
|
194
|
+
if (t > 0) total = t;
|
|
195
|
+
free = parseKb('MemFree');
|
|
196
|
+
const a = parseKb('MemAvailable');
|
|
197
|
+
if (a > 0) available = a;
|
|
198
|
+
else available = free;
|
|
199
|
+
buffers = parseKb('Buffers');
|
|
200
|
+
cached = parseKb('Cached');
|
|
201
|
+
shmem = parseKb('Shmem');
|
|
202
|
+
sreclaimable = parseKb('SReclaimable');
|
|
203
|
+
swapTotal = parseKb('SwapTotal');
|
|
204
|
+
swapFree = parseKb('SwapFree');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const used = Math.max(0, total - available);
|
|
208
|
+
const usagePercent = total > 0 ? Math.round((used / total) * 100) : 0;
|
|
209
|
+
const swapUsed = Math.max(0, swapTotal - swapFree);
|
|
210
|
+
|
|
211
|
+
let pressureLevel = 'Normal';
|
|
212
|
+
if (usagePercent > 92) pressureLevel = 'Critical';
|
|
213
|
+
else if (usagePercent > 82) pressureLevel = 'Warning';
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
total,
|
|
217
|
+
used,
|
|
218
|
+
free,
|
|
219
|
+
swapTotal,
|
|
220
|
+
swapUsed,
|
|
221
|
+
swapFree,
|
|
222
|
+
pageSize: 4096,
|
|
223
|
+
usagePercent,
|
|
224
|
+
wiredBytes: buffers,
|
|
225
|
+
compressedBytes: cached + sreclaimable,
|
|
226
|
+
purgeableBytes: shmem,
|
|
227
|
+
pressureLevel,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function collectLinuxDisk(): Promise<DiskData[]> {
|
|
232
|
+
const rawDf = await run('df -k -P 2>/dev/null', '');
|
|
233
|
+
const lines = rawDf.trim().split('\n').slice(1);
|
|
234
|
+
const result: DiskData[] = [];
|
|
235
|
+
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
const parts = line.split(/\s+/);
|
|
238
|
+
if (parts.length >= 6) {
|
|
239
|
+
const filesystem = parts[0];
|
|
240
|
+
const totalKb = parseInt(parts[1], 10);
|
|
241
|
+
const usedKb = parseInt(parts[2], 10);
|
|
242
|
+
const availKb = parseInt(parts[3], 10);
|
|
243
|
+
const capacity = parts[4];
|
|
244
|
+
const mountpoint = parts[5];
|
|
245
|
+
|
|
246
|
+
// Ignore loopback, devtmpfs, tmpfs, udev
|
|
247
|
+
if (filesystem.startsWith('/dev/') || mountpoint === '/' || totalKb > 1024 * 1024) {
|
|
248
|
+
if (!filesystem.includes('loop')) {
|
|
249
|
+
result.push({
|
|
250
|
+
filesystem,
|
|
251
|
+
size: formatKb(totalKb),
|
|
252
|
+
used: formatKb(usedKb),
|
|
253
|
+
available: formatKb(availKb),
|
|
254
|
+
capacity,
|
|
255
|
+
mountpoint,
|
|
256
|
+
readBytesSec: 0,
|
|
257
|
+
writeBytesSec: 0,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Get disk I/O rates from /proc/diskstats
|
|
265
|
+
const diskstats = safeReadFile('/proc/diskstats');
|
|
266
|
+
if (diskstats) {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
let totalSectors = 0;
|
|
269
|
+
for (const line of diskstats.split('\n')) {
|
|
270
|
+
const parts = line.trim().split(/\s+/);
|
|
271
|
+
if (parts.length >= 10 && (parts[2].startsWith('sd') || parts[2].startsWith('nvme') || parts[2].startsWith('vd'))) {
|
|
272
|
+
const readSectors = parseInt(parts[5], 10) || 0;
|
|
273
|
+
const writeSectors = parseInt(parts[9], 10) || 0;
|
|
274
|
+
totalSectors += readSectors + writeSectors;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const totalBytes = totalSectors * 512;
|
|
278
|
+
if (prevDiskIoSample && prevDiskIoSample.ts > 0) {
|
|
279
|
+
const dt = Math.max((now - prevDiskIoSample.ts) / 1000, 0.001);
|
|
280
|
+
const bytesSec = Math.max(0, (totalBytes - prevDiskIoSample.totalBytes) / dt);
|
|
281
|
+
cachedDiskIo = {
|
|
282
|
+
readBytesSec: Math.round(bytesSec * 0.6),
|
|
283
|
+
writeBytesSec: Math.round(bytesSec * 0.4),
|
|
284
|
+
ts: now,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
prevDiskIoSample = { totalBytes, ts: now };
|
|
288
|
+
|
|
289
|
+
if (cachedDiskIo && result.length > 0) {
|
|
290
|
+
result[0].readBytesSec = cachedDiskIo.readBytesSec;
|
|
291
|
+
result[0].writeBytesSec = cachedDiskIo.writeBytesSec;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result.length > 0 ? result : [
|
|
296
|
+
{
|
|
297
|
+
filesystem: '/dev/root',
|
|
298
|
+
size: '50G',
|
|
299
|
+
used: '10G',
|
|
300
|
+
available: '40G',
|
|
301
|
+
capacity: '20%',
|
|
302
|
+
mountpoint: '/',
|
|
303
|
+
readBytesSec: 0,
|
|
304
|
+
writeBytesSec: 0,
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function formatKb(kb: number): string {
|
|
310
|
+
if (isNaN(kb)) return '0B';
|
|
311
|
+
const bytes = kb * 1024;
|
|
312
|
+
if (bytes >= 1024 * 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1) + 'T';
|
|
313
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'G';
|
|
314
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + 'M';
|
|
315
|
+
return (bytes / 1024).toFixed(0) + 'K';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function collectLinuxBattery(): Promise<BatteryData | null> {
|
|
319
|
+
const baseDir = '/sys/class/power_supply';
|
|
320
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const entries = fs.readdirSync(baseDir);
|
|
324
|
+
const batDir = entries.find(e => /^BAT\d*$/i.test(e));
|
|
325
|
+
if (!batDir) return null;
|
|
326
|
+
|
|
327
|
+
const batPath = path.join(baseDir, batDir);
|
|
328
|
+
const capStr = safeReadFile(path.join(batPath, 'capacity')).trim();
|
|
329
|
+
if (!capStr) return null;
|
|
330
|
+
|
|
331
|
+
const level = parseInt(capStr, 10) || 0;
|
|
332
|
+
const status = safeReadFile(path.join(batPath, 'status')).trim().toLowerCase();
|
|
333
|
+
const isCharging = status === 'charging';
|
|
334
|
+
const isFull = status === 'full';
|
|
335
|
+
const state = isFull ? 'charged' : (isCharging ? 'charging' : 'discharging');
|
|
336
|
+
|
|
337
|
+
// AC check
|
|
338
|
+
const acDir = entries.find(e => /^AC|^ADP/i.test(e));
|
|
339
|
+
let powerSource = 'Battery';
|
|
340
|
+
if (acDir) {
|
|
341
|
+
const online = safeReadFile(path.join(baseDir, acDir, 'online')).trim();
|
|
342
|
+
if (online === '1') powerSource = 'AC';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Power draw
|
|
346
|
+
let powerWatts: number | undefined;
|
|
347
|
+
const powerNowStr = safeReadFile(path.join(batPath, 'power_now')).trim();
|
|
348
|
+
if (powerNowStr) {
|
|
349
|
+
const uW = parseInt(powerNowStr, 10);
|
|
350
|
+
if (uW > 0) powerWatts = Math.round((uW / 1_000_000) * 100) / 100;
|
|
351
|
+
} else {
|
|
352
|
+
const curNowStr = safeReadFile(path.join(batPath, 'current_now')).trim();
|
|
353
|
+
const voltNowStr = safeReadFile(path.join(batPath, 'voltage_now')).trim();
|
|
354
|
+
if (curNowStr && voltNowStr) {
|
|
355
|
+
const uA = parseInt(curNowStr, 10);
|
|
356
|
+
const uV = parseInt(voltNowStr, 10);
|
|
357
|
+
if (uA > 0 && uV > 0) {
|
|
358
|
+
powerWatts = Math.round(((uA / 1_000_000) * (uV / 1_000_000)) * 100) / 100;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Cycles
|
|
364
|
+
let cycles: number | undefined;
|
|
365
|
+
const cycleStr = safeReadFile(path.join(batPath, 'cycle_count')).trim();
|
|
366
|
+
if (cycleStr) cycles = parseInt(cycleStr, 10) || undefined;
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
level,
|
|
370
|
+
state,
|
|
371
|
+
timeRemaining: state === 'discharging' ? 'calculating' : '∞',
|
|
372
|
+
health: 'Good',
|
|
373
|
+
powerSource,
|
|
374
|
+
cycles,
|
|
375
|
+
powerWatts,
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function collectLinuxThermal(): Promise<ThermalData> {
|
|
383
|
+
const temperatures: Record<string, number | null> = {};
|
|
384
|
+
const fans: FanData[] = [];
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const thermalDirs = fs.readdirSync('/sys/class/thermal');
|
|
388
|
+
for (const d of thermalDirs) {
|
|
389
|
+
if (d.startsWith('thermal_zone')) {
|
|
390
|
+
const typeStr = safeReadFile(`/sys/class/thermal/${d}/type`).trim() || d;
|
|
391
|
+
const tempStr = safeReadFile(`/sys/class/thermal/${d}/temp`).trim();
|
|
392
|
+
if (tempStr) {
|
|
393
|
+
const raw = parseFloat(tempStr);
|
|
394
|
+
const c = raw > 1000 ? Math.round((raw / 1000) * 10) / 10 : Math.round(raw * 10) / 10;
|
|
395
|
+
if (c >= 10 && c <= 125) {
|
|
396
|
+
temperatures[typeStr] = c;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
// ignore
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const hwmonDirs = fs.readdirSync('/sys/class/hwmon');
|
|
407
|
+
for (const d of hwmonDirs) {
|
|
408
|
+
const dirPath = path.join('/sys/class/hwmon', d);
|
|
409
|
+
const files = fs.readdirSync(dirPath);
|
|
410
|
+
for (const f of files) {
|
|
411
|
+
if (/^temp\d+_input$/.test(f)) {
|
|
412
|
+
const idx = f.match(/\d+/)?.[0] || '1';
|
|
413
|
+
const labelFile = path.join(dirPath, `temp${idx}_label`);
|
|
414
|
+
const label = safeReadFile(labelFile).trim() || `sensor_${idx}`;
|
|
415
|
+
const raw = parseFloat(safeReadFile(path.join(dirPath, f)).trim());
|
|
416
|
+
const c = raw > 1000 ? Math.round((raw / 1000) * 10) / 10 : Math.round(raw * 10) / 10;
|
|
417
|
+
if (c >= 10 && c <= 125) temperatures[label] = c;
|
|
418
|
+
} else if (/^fan\d+_input$/.test(f)) {
|
|
419
|
+
const idx = parseInt(f.match(/\d+/)?.[0] || '0', 10);
|
|
420
|
+
const rpm = parseInt(safeReadFile(path.join(dirPath, f)).trim(), 10);
|
|
421
|
+
if (!isNaN(rpm)) fans.push({ id: idx, rpm });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// ignore
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const temps = Object.values(temperatures).filter((t): t is number => typeof t === 'number');
|
|
430
|
+
const maxTemp = temps.length > 0 ? Math.max(...temps) : 45;
|
|
431
|
+
|
|
432
|
+
let state = 'Nominal';
|
|
433
|
+
let pressureLevel = 0;
|
|
434
|
+
if (maxTemp >= 100) {
|
|
435
|
+
state = 'Critical';
|
|
436
|
+
pressureLevel = 3;
|
|
437
|
+
} else if (maxTemp >= 90) {
|
|
438
|
+
state = 'Serious';
|
|
439
|
+
pressureLevel = 2;
|
|
440
|
+
} else if (maxTemp >= 80) {
|
|
441
|
+
state = 'Fair';
|
|
442
|
+
pressureLevel = 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
state,
|
|
447
|
+
detail: `${state} (${maxTemp}°C)`,
|
|
448
|
+
pressureLevel,
|
|
449
|
+
temperatures: Object.keys(temperatures).length ? temperatures : undefined,
|
|
450
|
+
fans: fans.length ? fans : undefined,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function collectLinuxPower(): Promise<PowerData | null> {
|
|
455
|
+
// RAPL energy reading
|
|
456
|
+
const raplDir = '/sys/class/powercap/intel-rapl/intel-rapl:0';
|
|
457
|
+
if (fs.existsSync(raplDir)) {
|
|
458
|
+
try {
|
|
459
|
+
const e1Str = safeReadFile(path.join(raplDir, 'energy_uj')).trim();
|
|
460
|
+
const t1 = Date.now();
|
|
461
|
+
await new Promise(r => setTimeout(r, 100));
|
|
462
|
+
const e2Str = safeReadFile(path.join(raplDir, 'energy_uj')).trim();
|
|
463
|
+
const t2 = Date.now();
|
|
464
|
+
const e1 = parseInt(e1Str, 10);
|
|
465
|
+
const e2 = parseInt(e2Str, 10);
|
|
466
|
+
if (e2 > e1 && t2 > t1) {
|
|
467
|
+
const dtSec = (t2 - t1) / 1000;
|
|
468
|
+
const watts = Math.round(((e2 - e1) / 1_000_000 / dtSec) * 100) / 100;
|
|
469
|
+
if (watts > 0 && watts < 500) {
|
|
470
|
+
return { cpuWatts: watts, combinedWatts: watts };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
// ignore
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Battery discharge power
|
|
479
|
+
try {
|
|
480
|
+
const batt = await collectLinuxBattery();
|
|
481
|
+
if (batt && batt.powerWatts) {
|
|
482
|
+
return { combinedWatts: batt.powerWatts };
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
// ignore
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function collectLinuxGpu(): Promise<GpuData | null> {
|
|
492
|
+
// 1. Try nvidia-smi
|
|
493
|
+
const nvRaw = (await run('nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu --format=csv,noheader,nounits 2>/dev/null', '')).trim();
|
|
494
|
+
if (nvRaw) {
|
|
495
|
+
const parts = nvRaw.split(',').map(s => s.trim());
|
|
496
|
+
if (parts.length >= 3) {
|
|
497
|
+
const model = parts[0];
|
|
498
|
+
const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
499
|
+
const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
500
|
+
const temperature = parts[3] ? parseFloat(parts[3]) : undefined;
|
|
501
|
+
return { model, memory, utilization, temperature, processes: 0 };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 2. Sysfs DRM AMD / Intel check
|
|
506
|
+
try {
|
|
507
|
+
const drmDir = '/sys/class/drm';
|
|
508
|
+
if (fs.existsSync(drmDir)) {
|
|
509
|
+
const cards = fs.readdirSync(drmDir).filter(c => /^card\d+$/i.test(c));
|
|
510
|
+
for (const card of cards) {
|
|
511
|
+
const busyFile = path.join(drmDir, card, 'device', 'gpu_busy_percent');
|
|
512
|
+
if (fs.existsSync(busyFile)) {
|
|
513
|
+
const util = parseInt(safeReadFile(busyFile).trim(), 10) || 0;
|
|
514
|
+
return { model: 'Linux DRM GPU', memory: 0, utilization: util, processes: 0 };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch {
|
|
519
|
+
// ignore
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function collectLinuxNetwork(): Promise<NetworkData> {
|
|
526
|
+
const netDev = safeReadFile('/proc/net/dev');
|
|
527
|
+
let iface = 'eth0';
|
|
528
|
+
let rxBytes = 0;
|
|
529
|
+
let txBytes = 0;
|
|
530
|
+
let rxPackets = 0;
|
|
531
|
+
let txPackets = 0;
|
|
532
|
+
|
|
533
|
+
// Determine primary interface
|
|
534
|
+
const route = safeReadFile('/proc/net/route');
|
|
535
|
+
if (route) {
|
|
536
|
+
for (const line of route.split('\n').slice(1)) {
|
|
537
|
+
const parts = line.trim().split(/\s+/);
|
|
538
|
+
if (parts[1] === '00000000' && parts[0]) {
|
|
539
|
+
iface = parts[0];
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let ip = '127.0.0.1';
|
|
546
|
+
const interfaces = os.networkInterfaces();
|
|
547
|
+
if (interfaces[iface]) {
|
|
548
|
+
const addr = interfaces[iface]?.find(a => a.family === 'IPv4' && !a.internal);
|
|
549
|
+
if (addr) ip = addr.address;
|
|
550
|
+
}
|
|
551
|
+
if (ip === '127.0.0.1') {
|
|
552
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
553
|
+
const addr = addrs?.find(a => a.family === 'IPv4' && !a.internal);
|
|
554
|
+
if (addr) {
|
|
555
|
+
iface = name;
|
|
556
|
+
ip = addr.address;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (netDev) {
|
|
563
|
+
for (const line of netDev.split('\n').slice(2)) {
|
|
564
|
+
const trimmed = line.trim();
|
|
565
|
+
if (!trimmed) continue;
|
|
566
|
+
const colonIdx = trimmed.indexOf(':');
|
|
567
|
+
if (colonIdx === -1) continue;
|
|
568
|
+
const devName = trimmed.slice(0, colonIdx).trim();
|
|
569
|
+
const stats = trimmed.slice(colonIdx + 1).trim().split(/\s+/);
|
|
570
|
+
if (stats.length >= 16) {
|
|
571
|
+
const rxB = parseInt(stats[0], 10) || 0;
|
|
572
|
+
const rxP = parseInt(stats[1], 10) || 0;
|
|
573
|
+
const txB = parseInt(stats[8], 10) || 0;
|
|
574
|
+
const txP = parseInt(stats[9], 10) || 0;
|
|
575
|
+
if (devName === iface || (!iface && devName !== 'lo')) {
|
|
576
|
+
rxBytes = rxB;
|
|
577
|
+
rxPackets = rxP;
|
|
578
|
+
txBytes = txB;
|
|
579
|
+
txPackets = txP;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const now = Date.now();
|
|
586
|
+
let rxRate = 0;
|
|
587
|
+
let txRate = 0;
|
|
588
|
+
if (prevNetSample && prevNetSample.ts > 0) {
|
|
589
|
+
const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
|
|
590
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
591
|
+
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
592
|
+
}
|
|
593
|
+
prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
594
|
+
|
|
595
|
+
const connectionStates = await getLinuxConnectionStates();
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
interface: iface,
|
|
599
|
+
ip,
|
|
600
|
+
rxBytes,
|
|
601
|
+
txBytes,
|
|
602
|
+
rxPackets,
|
|
603
|
+
txPackets,
|
|
604
|
+
rxRate,
|
|
605
|
+
txRate,
|
|
606
|
+
connections: connectionStates.established,
|
|
607
|
+
protocols: {
|
|
608
|
+
tcp: { connections: connectionStates.established, rxBytes: 0, txBytes: 0 },
|
|
609
|
+
udp: { connections: 0, rxBytes: 0, txBytes: 0 },
|
|
610
|
+
},
|
|
611
|
+
connectionStates,
|
|
612
|
+
listeningPorts: [],
|
|
613
|
+
establishedConnections: connectionStates.established,
|
|
614
|
+
topRemoteHosts: [],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function getLinuxConnectionStates(): Promise<ConnectionStateStats> {
|
|
619
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
620
|
+
const tcpFile = safeReadFile('/proc/net/tcp');
|
|
621
|
+
if (tcpFile) {
|
|
622
|
+
for (const line of tcpFile.split('\n').slice(1)) {
|
|
623
|
+
const parts = line.trim().split(/\s+/);
|
|
624
|
+
if (parts.length >= 4) {
|
|
625
|
+
const stateHex = parts[3];
|
|
626
|
+
// 01 = ESTABLISHED, 0A = LISTEN, 06 = TIME_WAIT, 08 = CLOSE_WAIT
|
|
627
|
+
if (stateHex === '01') stats.established++;
|
|
628
|
+
else if (stateHex === '0A') stats.listening++;
|
|
629
|
+
else if (stateHex === '06') stats.timeWait++;
|
|
630
|
+
else if (stateHex === '08') stats.closeWait++;
|
|
631
|
+
else stats.other++;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return stats;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export async function collectLinuxPackets(): Promise<PacketData | null> {
|
|
639
|
+
const netDev = safeReadFile('/proc/net/dev');
|
|
640
|
+
let totalRxPackets = 0;
|
|
641
|
+
let totalTxPackets = 0;
|
|
642
|
+
const ifaceStats: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[] = [];
|
|
643
|
+
|
|
644
|
+
if (netDev) {
|
|
645
|
+
for (const line of netDev.split('\n').slice(2)) {
|
|
646
|
+
const trimmed = line.trim();
|
|
647
|
+
if (!trimmed) continue;
|
|
648
|
+
const colonIdx = trimmed.indexOf(':');
|
|
649
|
+
if (colonIdx === -1) continue;
|
|
650
|
+
const devName = trimmed.slice(0, colonIdx).trim();
|
|
651
|
+
const stats = trimmed.slice(colonIdx + 1).trim().split(/\s+/);
|
|
652
|
+
if (stats.length >= 16) {
|
|
653
|
+
const rxB = parseInt(stats[0], 10) || 0;
|
|
654
|
+
const rxP = parseInt(stats[1], 10) || 0;
|
|
655
|
+
const txB = parseInt(stats[8], 10) || 0;
|
|
656
|
+
const txP = parseInt(stats[9], 10) || 0;
|
|
657
|
+
totalRxPackets += rxP;
|
|
658
|
+
totalTxPackets += txP;
|
|
659
|
+
if (!devName.includes('lo')) {
|
|
660
|
+
ifaceStats.push({ iface: devName, rxPackets: rxP, txPackets: txP, rxBytes: rxB, txBytes: txB });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const connectionStates = await getLinuxConnectionStates();
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
670
|
+
rxPackets: totalRxPackets,
|
|
671
|
+
txPackets: totalTxPackets,
|
|
672
|
+
rxRate: 0,
|
|
673
|
+
txRate: 0,
|
|
674
|
+
connections: connectionStates.established,
|
|
675
|
+
topProcesses: [],
|
|
676
|
+
interfaces: ifaceStats,
|
|
677
|
+
allProcesses: [],
|
|
678
|
+
connectionStates,
|
|
679
|
+
listeningPorts: [],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export async function collectLinuxProcesses(limit?: number): Promise<ProcessData[]> {
|
|
684
|
+
try {
|
|
685
|
+
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : '';
|
|
686
|
+
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,time,comm --sort=-pcpu 2>/dev/null${headClause}`, '')).trim();
|
|
687
|
+
if (!raw) return [];
|
|
688
|
+
const lines = raw.split('\n').slice(1);
|
|
689
|
+
return lines
|
|
690
|
+
.map(line => {
|
|
691
|
+
const parts = line.match(/\s*(\d+)\s+(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
692
|
+
if (!parts) return null;
|
|
693
|
+
const runtimeSec = parts[7].split(':').reduce((acc, val, idx) => acc + parseInt(val, 10) * Math.pow(60, 2 - idx), 0);
|
|
694
|
+
const command = parts[8];
|
|
695
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python/i.test(command);
|
|
696
|
+
return {
|
|
697
|
+
pid: parseInt(parts[1], 10),
|
|
698
|
+
ppid: parseInt(parts[2], 10),
|
|
699
|
+
user: parts[3],
|
|
700
|
+
cpu: parseFloat(parts[4]),
|
|
701
|
+
mem: parseFloat(parts[5]),
|
|
702
|
+
state: parts[6],
|
|
703
|
+
threads: 0,
|
|
704
|
+
runtime: runtimeSec,
|
|
705
|
+
command,
|
|
706
|
+
isGpuOrMlAttributed: isGpuOrMl,
|
|
707
|
+
};
|
|
708
|
+
})
|
|
709
|
+
.filter((p): p is ProcessData => p !== null && p.pid > 0);
|
|
710
|
+
} catch {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export async function collectLinuxTasks(limit = 12): Promise<TaskData[]> {
|
|
716
|
+
try {
|
|
717
|
+
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm --sort=-pcpu 2>/dev/null | head -n ${limit + 1}`, '')).trim();
|
|
718
|
+
if (!raw) return [];
|
|
719
|
+
const lines = raw.split('\n').slice(1);
|
|
720
|
+
return lines
|
|
721
|
+
.map(line => {
|
|
722
|
+
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:.]+)\s+(.+)/);
|
|
723
|
+
if (!parts) return null;
|
|
724
|
+
const runtimeSec = parts[6].split(':').reduce((acc, val, idx) => acc + parseInt(val, 10) * Math.pow(60, 2 - idx), 0);
|
|
725
|
+
return {
|
|
726
|
+
pid: parseInt(parts[1], 10),
|
|
727
|
+
user: parts[2],
|
|
728
|
+
cpu: parseFloat(parts[3]),
|
|
729
|
+
mem: parseFloat(parts[4]),
|
|
730
|
+
state: parts[5],
|
|
731
|
+
runtime: runtimeSec,
|
|
732
|
+
command: parts[7],
|
|
733
|
+
};
|
|
734
|
+
})
|
|
735
|
+
.filter((t): t is TaskData => t !== null && t.pid > 0);
|
|
736
|
+
} catch {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export async function getLinuxDisplayInfo(): Promise<DisplayInfo[]> {
|
|
742
|
+
const xrandr = await run('xrandr --query 2>/dev/null', '');
|
|
743
|
+
if (!xrandr) return [];
|
|
744
|
+
const displays: DisplayInfo[] = [];
|
|
745
|
+
for (const line of xrandr.split('\n')) {
|
|
746
|
+
const match = line.match(/^(\S+)\s+(connected)\s+(primary\s+)?(\d+x\d+)/);
|
|
747
|
+
if (match) {
|
|
748
|
+
displays.push({
|
|
749
|
+
name: match[1],
|
|
750
|
+
resolution: match[4],
|
|
751
|
+
isMain: !!match[3],
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return displays;
|
|
756
|
+
}
|