pyre-cli 9.5.1 → 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 +84 -14
- package/dist/alertChannels-TPSQIJ7S.js +8 -0
- package/dist/blender-FGH3ASEQ.js +7 -0
- package/dist/brewHealth-P2GW5Y5C.js +176 -0
- package/dist/buildCmd-CDPZ2ZFA.js +38 -0
- package/dist/{checkCmd-EWFNLBWE.js → checkCmd-RKW6QGDR.js} +10 -11
- package/dist/{chunk-RVGKEJXT.js → chunk-53O2RGSL.js} +47 -17
- package/dist/{chunk-MOAEGMFP.js → chunk-6UHZP4QP.js} +204 -46
- 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-VIACN2RU.js +133 -0
- package/dist/chunk-Z2YV5HKV.js +87 -0
- package/dist/chunk-ZLVIVFSO.js +110 -0
- package/dist/{collectors-WD6VKH6M.js → collectors-H43PN4PS.js} +8 -2
- package/dist/{completions-VQMHQCXE.js → completions-JKYEJNGR.js} +34 -1
- package/dist/{config-4ZLF4HTS.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 +841 -206
- package/dist/ioreport.py +72 -0
- package/dist/netUsageCmd-WKBTW2YG.js +90 -0
- package/dist/{pipeCmd-7THXEGDH.js → pipeCmd-7REDBLBU.js} +5 -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-GVIAHIVV.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/anomalies.ts +114 -60
- 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/index.ts +1 -1
- package/src/formatters/output.ts +100 -13
- package/src/formatters/render.ts +274 -38
- package/src/formatters/themes.ts +8 -0
- package/src/formatters/types.ts +6 -1
- package/src/index.ts +298 -56
- package/src/live/input.ts +122 -4
- package/src/live/render.ts +98 -0
- package/src/live/state.ts +70 -52
- package/src/live/types.ts +2 -2
- package/src/monitors/batteryPredictor.ts +143 -0
- package/src/monitors/blender.ts +144 -0
- package/src/monitors/buildTracker.ts +121 -0
- package/src/monitors/collectors.ts +558 -147
- package/src/monitors/dns_cache.ts +48 -0
- package/src/monitors/index.ts +3 -2
- 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 +112 -14
- 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 +64 -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-HGRBBPYX.js +0 -937
- package/dist/doctor-L6FZYA7F.js +0 -184
- package/dist/extensions-DVA5YLGH.js +0 -74
- package/dist/service-WTS3NBQU.js +0 -73
- /package/dist/{chunk-SG26Y3WF.js → chunk-IM3IQMMT.js} +0 -0
|
@@ -13,9 +13,44 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import fs from 'node:fs';
|
|
16
19
|
import { run } from './run.js';
|
|
17
20
|
import { getSmcMetrics, parseSuffix } from './smc.js';
|
|
18
|
-
import
|
|
21
|
+
import { resolveIp } from './dns_cache.js';
|
|
22
|
+
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, NetworkConnection, BlenderRenderData, ProtocolStats, ConnectionStateStats, RemoteHostInfo } from './types.js';
|
|
23
|
+
import { collectBlenderRenders } from './blender.js';
|
|
24
|
+
import {
|
|
25
|
+
collectLinuxSystem,
|
|
26
|
+
collectLinuxCpu,
|
|
27
|
+
collectLinuxMemory,
|
|
28
|
+
collectLinuxDisk,
|
|
29
|
+
collectLinuxBattery,
|
|
30
|
+
collectLinuxThermal,
|
|
31
|
+
collectLinuxPower,
|
|
32
|
+
collectLinuxGpu,
|
|
33
|
+
collectLinuxNetwork,
|
|
34
|
+
collectLinuxPackets,
|
|
35
|
+
collectLinuxProcesses,
|
|
36
|
+
collectLinuxTasks,
|
|
37
|
+
getLinuxDisplayInfo,
|
|
38
|
+
} from './platform/linux.js';
|
|
39
|
+
import {
|
|
40
|
+
collectWindowsSystem,
|
|
41
|
+
collectWindowsCpu,
|
|
42
|
+
collectWindowsMemory,
|
|
43
|
+
collectWindowsDisk,
|
|
44
|
+
collectWindowsBattery,
|
|
45
|
+
collectWindowsThermal,
|
|
46
|
+
collectWindowsPower,
|
|
47
|
+
collectWindowsGpu,
|
|
48
|
+
collectWindowsNetwork,
|
|
49
|
+
collectWindowsPackets,
|
|
50
|
+
collectWindowsProcesses,
|
|
51
|
+
collectWindowsTasks,
|
|
52
|
+
getWindowsDisplayInfo,
|
|
53
|
+
} from './platform/windows.js';
|
|
19
54
|
|
|
20
55
|
const SP_TTL_MS = 10_000;
|
|
21
56
|
const NETSTAT_TTL_MS = 1000;
|
|
@@ -30,6 +65,33 @@ let netstatIbCache: { raw: string; ts: number } | null = null;
|
|
|
30
65
|
let routeCache: { iface: string; ip: string; ts: number } | null = null;
|
|
31
66
|
const sysctlCache = new Map<string, { value: string; ts: number }>();
|
|
32
67
|
|
|
68
|
+
let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
|
|
69
|
+
|
|
70
|
+
let cachedIoReportPath: string | null | undefined = undefined;
|
|
71
|
+
function resolveIoReportScript(): string | null {
|
|
72
|
+
if (cachedIoReportPath !== undefined) return cachedIoReportPath;
|
|
73
|
+
try {
|
|
74
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
const candidates = [
|
|
76
|
+
path.join(dir, 'ioreport.py'),
|
|
77
|
+
path.join(dir, '..', 'src', 'monitors', 'ioreport.py'),
|
|
78
|
+
path.join(dir, 'monitors', 'ioreport.py'),
|
|
79
|
+
path.join(process.cwd(), 'src', 'monitors', 'ioreport.py'),
|
|
80
|
+
path.join(process.cwd(), 'dist', 'ioreport.py'),
|
|
81
|
+
];
|
|
82
|
+
for (const c of candidates) {
|
|
83
|
+
if (fs.existsSync(c)) {
|
|
84
|
+
cachedIoReportPath = c;
|
|
85
|
+
return c;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
cachedIoReportPath = null;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
33
95
|
async function cachedSysctl(key: string, fallback: string): Promise<string> {
|
|
34
96
|
const now = Date.now();
|
|
35
97
|
const hit = sysctlCache.get(key);
|
|
@@ -77,7 +139,7 @@ const MIN_ESTIMATE_WINDOW_MS = 30 * 1000;
|
|
|
77
139
|
|
|
78
140
|
let prevBattery: { samples: { level: number; ts: number }[] } = { samples: [] };
|
|
79
141
|
|
|
80
|
-
function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number
|
|
142
|
+
function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number } {
|
|
81
143
|
const now = Date.now();
|
|
82
144
|
|
|
83
145
|
if (state === 'charged' || state === 'charging') {
|
|
@@ -113,21 +175,18 @@ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmp
|
|
|
113
175
|
const m = Math.round((hoursToEmpty - h) * 60);
|
|
114
176
|
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : 'calculating';
|
|
115
177
|
|
|
116
|
-
|
|
117
|
-
// in Wh) / 100. This is only used as a last-resort estimate — collectPower()
|
|
118
|
-
// now prefers real ioreg-derived wattage when available (see below).
|
|
119
|
-
const ASSUMED_CAPACITY_WH = 55;
|
|
120
|
-
const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
|
|
121
|
-
|
|
122
|
-
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
|
|
178
|
+
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10 };
|
|
123
179
|
}
|
|
124
180
|
|
|
181
|
+
import { collectBuilds } from './buildTracker.js';
|
|
182
|
+
import { computeSmartBatteryPrediction } from './batteryPredictor.js';
|
|
183
|
+
|
|
125
184
|
/**
|
|
126
185
|
* Gather every metric category in parallel and return a
|
|
127
186
|
* complete {@link StatsData} snapshot.
|
|
128
187
|
*/
|
|
129
188
|
export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
|
|
130
|
-
const [cpu, gpu, memory, disk,
|
|
189
|
+
const [cpu, gpu, memory, disk, rawBattery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders, activeBuilds] = await Promise.all([
|
|
131
190
|
collectCpu(),
|
|
132
191
|
collectGpu(opts.detailed),
|
|
133
192
|
collectMemory(),
|
|
@@ -140,8 +199,16 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
140
199
|
collectPower(),
|
|
141
200
|
collectPackets(),
|
|
142
201
|
collectTasks(),
|
|
202
|
+
collectContainers(),
|
|
203
|
+
collectBlenderRenders(),
|
|
204
|
+
collectBuilds(),
|
|
143
205
|
]);
|
|
144
206
|
|
|
207
|
+
const battery = rawBattery ? {
|
|
208
|
+
...rawBattery,
|
|
209
|
+
smartPrediction: computeSmartBatteryPrediction(rawBattery, power) || undefined,
|
|
210
|
+
} : null;
|
|
211
|
+
|
|
145
212
|
return {
|
|
146
213
|
header: {
|
|
147
214
|
title: chalk.hex('#ff5500').bold('PYRE'),
|
|
@@ -161,66 +228,100 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
161
228
|
timestamp: new Date().toISOString(),
|
|
162
229
|
packets,
|
|
163
230
|
tasks,
|
|
231
|
+
containers,
|
|
232
|
+
blenderRenders,
|
|
233
|
+
activeBuilds,
|
|
164
234
|
};
|
|
165
235
|
}
|
|
166
236
|
|
|
237
|
+
export async function collectContainers(): Promise<ContainerData[]> {
|
|
238
|
+
try {
|
|
239
|
+
const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', '')).trim();
|
|
240
|
+
if (!raw) return [];
|
|
241
|
+
|
|
242
|
+
return raw.split('\n').map(line => {
|
|
243
|
+
const parts = line.split('|');
|
|
244
|
+
return {
|
|
245
|
+
id: parts[0],
|
|
246
|
+
name: parts[1],
|
|
247
|
+
cpuPercent: parseFloat(parts[2].replace('%', '')) || 0,
|
|
248
|
+
memUsage: parts[3],
|
|
249
|
+
memPercent: parseFloat(parts[4].replace('%', '')) || 0,
|
|
250
|
+
netIO: parts[5],
|
|
251
|
+
blockIO: parts[6],
|
|
252
|
+
pids: parseInt(parts[7], 10) || 0
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
} catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
167
260
|
/**
|
|
168
261
|
* Read power-draw metrics (CPU watts, GPU watts, combined)
|
|
169
262
|
* from the SMC via powermetrics. Returns `null` when no
|
|
170
263
|
* power data is available.
|
|
171
264
|
*/
|
|
172
265
|
export async function collectPower(): Promise<PowerData | null> {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
266
|
+
if (process.platform === 'linux') return collectLinuxPower();
|
|
267
|
+
if (process.platform === 'win32') return collectWindowsPower();
|
|
268
|
+
try {
|
|
269
|
+
const { power } = await getSmcMetrics();
|
|
270
|
+
if (power.cpu !== undefined || power.gpu !== undefined || power.combined !== undefined) {
|
|
271
|
+
return { cpuWatts: power.cpu, gpuWatts: power.gpu, aneWatts: 0, combinedWatts: power.combined };
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// ignore
|
|
275
|
+
}
|
|
181
276
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let ampsBig = BigInt(ampMatch[1]);
|
|
203
|
-
if (!ampMatch[1].startsWith('-')) {
|
|
204
|
-
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
205
|
-
const INT64_MAX = (1n << 63n) - 1n;
|
|
206
|
-
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
207
|
-
}
|
|
208
|
-
const amps = Number(ampsBig);
|
|
209
|
-
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
210
|
-
const watts = Math.abs((amps / 1000) * volts);
|
|
211
|
-
// Sanity clamp — a real Mac never draws anywhere near this much;
|
|
212
|
-
// treat an out-of-range reading as "no usable data" rather than
|
|
213
|
-
// display nonsense.
|
|
214
|
-
if (Number.isFinite(watts) && watts < 500) {
|
|
215
|
-
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// ignore
|
|
220
|
-
}
|
|
277
|
+
// Apple Silicon IOReport (reads Energy Model channels GPU Energy, CPU Energy, ANE without root)
|
|
278
|
+
const scriptPath = resolveIoReportScript();
|
|
279
|
+
if (scriptPath && process.platform === 'darwin') {
|
|
280
|
+
try {
|
|
281
|
+
const pyOut = (await run(`python3 "${scriptPath}" 2>/dev/null`, '')).trim();
|
|
282
|
+
if (pyOut.startsWith('{') && pyOut.endsWith('}')) {
|
|
283
|
+
const parsed = JSON.parse(pyOut);
|
|
284
|
+
if (typeof parsed.cpuWatts === 'number' || typeof parsed.gpuWatts === 'number') {
|
|
285
|
+
return {
|
|
286
|
+
cpuWatts: parsed.cpuWatts,
|
|
287
|
+
gpuWatts: parsed.gpuWatts,
|
|
288
|
+
aneWatts: parsed.aneWatts ?? 0,
|
|
289
|
+
combinedWatts: parsed.combinedWatts ?? (Math.round(((parsed.cpuWatts ?? 0) + (parsed.gpuWatts ?? 0) + (parsed.aneWatts ?? 0)) * 100) / 100),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// ignore
|
|
295
|
+
}
|
|
296
|
+
}
|
|
221
297
|
|
|
222
|
-
|
|
223
|
-
|
|
298
|
+
// Fallback: ioreg's AppleSmartBattery entry exposes instantaneous
|
|
299
|
+
// amperage (mA, signed: negative while discharging) and voltage (mV)
|
|
300
|
+
// without any privileges, so we can derive real wattage from that.
|
|
301
|
+
try {
|
|
302
|
+
const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
|
|
303
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
304
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
305
|
+
if (ampMatch && voltMatch) {
|
|
306
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
307
|
+
if (!ampMatch[1].startsWith('-')) {
|
|
308
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
309
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
310
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
311
|
+
}
|
|
312
|
+
const amps = Number(ampsBig);
|
|
313
|
+
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
314
|
+
const watts = Math.abs((amps / 1000) * volts);
|
|
315
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
316
|
+
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
224
325
|
|
|
225
326
|
/**
|
|
226
327
|
* Gather GPU information from system_profiler and powermetrics.
|
|
@@ -232,6 +333,8 @@ export async function collectPower(): Promise<PowerData | null> {
|
|
|
232
333
|
* Returns null when GPU hardware is unavailable.
|
|
233
334
|
*/
|
|
234
335
|
export async function collectGpu(detailed?: boolean): Promise<GpuData | null> {
|
|
336
|
+
if (process.platform === 'linux') return collectLinuxGpu();
|
|
337
|
+
if (process.platform === 'win32') return collectWindowsGpu();
|
|
235
338
|
let spRaw = '';
|
|
236
339
|
const now = Date.now();
|
|
237
340
|
if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
|
|
@@ -370,7 +473,9 @@ export async function collectPower(): Promise<PowerData | null> {
|
|
|
370
473
|
return { model, memory, utilization, temperature, processes: gpuProcesses };
|
|
371
474
|
}
|
|
372
475
|
|
|
373
|
-
async function collectSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
476
|
+
export async function collectSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
477
|
+
if (process.platform === 'linux') return collectLinuxSystem();
|
|
478
|
+
if (process.platform === 'win32') return collectWindowsSystem();
|
|
374
479
|
const hostname = (await run('hostname', 'Mac')).trim();
|
|
375
480
|
const osRaw = (await run('sw_vers -productVersion', 'Unknown')).trim();
|
|
376
481
|
const uptimeRaw = (await run('uptime')).trim();
|
|
@@ -383,6 +488,17 @@ async function collectSystem(): Promise<{ hostname: string; os: string; uptime:
|
|
|
383
488
|
}
|
|
384
489
|
|
|
385
490
|
export async function collectCpu(): Promise<CpuData> {
|
|
491
|
+
const prevCpuTimesRef = { current: prevCpuTimes };
|
|
492
|
+
if (process.platform === 'linux') {
|
|
493
|
+
const res = await collectLinuxCpu(prevCpuTimesRef);
|
|
494
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
495
|
+
return res;
|
|
496
|
+
}
|
|
497
|
+
if (process.platform === 'win32') {
|
|
498
|
+
const res = await collectWindowsCpu(prevCpuTimesRef);
|
|
499
|
+
prevCpuTimes = prevCpuTimesRef.current;
|
|
500
|
+
return res;
|
|
501
|
+
}
|
|
386
502
|
const brand = (await cachedSysctl('machdep.cpu.brand_string', 'Unknown CPU')).trim();
|
|
387
503
|
const cores = parseInt(await cachedSysctl('hw.ncpu', '1')) || 1;
|
|
388
504
|
const physicalCores = parseInt(await cachedSysctl('hw.physicalcpu', String(cores))) || cores;
|
|
@@ -502,6 +618,8 @@ export async function collectCpu(): Promise<CpuData> {
|
|
|
502
618
|
}
|
|
503
619
|
|
|
504
620
|
export async function collectMemory(): Promise<MemoryData> {
|
|
621
|
+
if (process.platform === 'linux') return collectLinuxMemory();
|
|
622
|
+
if (process.platform === 'win32') return collectWindowsMemory();
|
|
505
623
|
const totalBytes = parseInt(await cachedSysctl('hw.memsize', '0')) || 0;
|
|
506
624
|
const pageSize = parseInt(await cachedSysctl('hw.pagesize', '4096')) || 4096;
|
|
507
625
|
|
|
@@ -576,7 +694,7 @@ export async function collectMemory(): Promise<MemoryData> {
|
|
|
576
694
|
};
|
|
577
695
|
}
|
|
578
696
|
|
|
579
|
-
let prevDiskIoSample: { ts: number } | null = null;
|
|
697
|
+
let prevDiskIoSample: { totalBytes: number; ts: number } | null = null;
|
|
580
698
|
let cachedDiskIo: { readBytesSec: number; writeBytesSec: number; ts: number } | null = null;
|
|
581
699
|
|
|
582
700
|
async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec: number }> {
|
|
@@ -585,22 +703,23 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
|
|
|
585
703
|
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
586
704
|
}
|
|
587
705
|
try {
|
|
588
|
-
const raw = (await run('iostat -
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
if (
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
602
|
-
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
706
|
+
const raw = (await run('iostat -Id -c 1', '', 1000)).trim();
|
|
707
|
+
const lines = raw.split('\n');
|
|
708
|
+
if (lines.length >= 3) {
|
|
709
|
+
const dataLine = lines[lines.length - 1].trim();
|
|
710
|
+
const parts = dataLine.split(/\s+/);
|
|
711
|
+
if (parts.length >= 3) {
|
|
712
|
+
const totalMb = parseFloat(parts[2]);
|
|
713
|
+
if (!isNaN(totalMb)) {
|
|
714
|
+
const totalBytes = totalMb * 1024 * 1024;
|
|
715
|
+
let bytesSec = 0;
|
|
716
|
+
if (prevDiskIoSample && prevDiskIoSample.ts > 0) {
|
|
717
|
+
const dt = Math.max((now - prevDiskIoSample.ts) / 1000, 0.001);
|
|
718
|
+
bytesSec = Math.max(0, (totalBytes - prevDiskIoSample.totalBytes) / dt);
|
|
603
719
|
}
|
|
720
|
+
prevDiskIoSample = { totalBytes, ts: now };
|
|
721
|
+
cachedDiskIo = { readBytesSec: Math.round(bytesSec * 0.6), writeBytesSec: Math.round(bytesSec * 0.4), ts: now };
|
|
722
|
+
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
604
723
|
}
|
|
605
724
|
}
|
|
606
725
|
}
|
|
@@ -611,6 +730,8 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
|
|
|
611
730
|
}
|
|
612
731
|
|
|
613
732
|
export async function collectDisk(): Promise<DiskData[]> {
|
|
733
|
+
if (process.platform === 'linux') return collectLinuxDisk();
|
|
734
|
+
if (process.platform === 'win32') return collectWindowsDisk();
|
|
614
735
|
const [rawDf, ioRates] = await Promise.all([
|
|
615
736
|
run('df -h'),
|
|
616
737
|
getDiskIoRates(),
|
|
@@ -634,6 +755,8 @@ export async function collectDisk(): Promise<DiskData[]> {
|
|
|
634
755
|
}
|
|
635
756
|
|
|
636
757
|
export async function collectBattery(): Promise<BatteryData | null> {
|
|
758
|
+
if (process.platform === 'linux') return collectLinuxBattery();
|
|
759
|
+
if (process.platform === 'win32') return collectWindowsBattery();
|
|
637
760
|
try {
|
|
638
761
|
const raw = (await run('pmset -g batt')).trim();
|
|
639
762
|
if (!raw.includes('Battery Power') && !raw.includes('AC Power')) return null;
|
|
@@ -703,6 +826,31 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
703
826
|
|
|
704
827
|
const estimate = estimateBatteryLife(level, state);
|
|
705
828
|
|
|
829
|
+
let powerWatts: number | undefined;
|
|
830
|
+
if (!powerPlugged && state === 'discharging') {
|
|
831
|
+
try {
|
|
832
|
+
const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
|
|
833
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
834
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
835
|
+
if (ampMatch && voltMatch) {
|
|
836
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
837
|
+
if (!ampMatch[1].startsWith('-')) {
|
|
838
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
839
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
840
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
841
|
+
}
|
|
842
|
+
const amps = Number(ampsBig);
|
|
843
|
+
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
844
|
+
const watts = Math.abs((amps / 1000) * volts);
|
|
845
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
846
|
+
powerWatts = Math.round(watts * 100) / 100;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
// ignore
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
706
854
|
return {
|
|
707
855
|
level,
|
|
708
856
|
state,
|
|
@@ -713,6 +861,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
713
861
|
condition,
|
|
714
862
|
maxCapacityPercent,
|
|
715
863
|
friendlySummary,
|
|
864
|
+
powerWatts,
|
|
716
865
|
...estimate,
|
|
717
866
|
};
|
|
718
867
|
} catch {
|
|
@@ -729,6 +878,8 @@ function pressureFromState(state: string): number {
|
|
|
729
878
|
}
|
|
730
879
|
|
|
731
880
|
export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
881
|
+
if (process.platform === 'linux') return collectLinuxThermal();
|
|
882
|
+
if (process.platform === 'win32') return collectWindowsThermal();
|
|
732
883
|
try {
|
|
733
884
|
const therm = (await run('pmset -g therm', '')).trim();
|
|
734
885
|
|
|
@@ -797,11 +948,21 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
|
797
948
|
detail = `${state} (estimated from ${cpuDieTemp}°C)`;
|
|
798
949
|
}
|
|
799
950
|
|
|
951
|
+
// Collect fan speeds from SMC (same cached call as temps above)
|
|
952
|
+
let fans: import('./types.js').FanData[] = [];
|
|
953
|
+
try {
|
|
954
|
+
const smcData = await getSmcMetrics();
|
|
955
|
+
fans = smcData.fans;
|
|
956
|
+
} catch {
|
|
957
|
+
// ignore — fans unavailable without sudo
|
|
958
|
+
}
|
|
959
|
+
|
|
800
960
|
return {
|
|
801
961
|
state,
|
|
802
962
|
detail,
|
|
803
963
|
pressureLevel: pressureFromState(state),
|
|
804
964
|
temperatures: Object.keys(temperatures).length ? temperatures : undefined,
|
|
965
|
+
fans: fans.length > 0 ? fans : undefined,
|
|
805
966
|
};
|
|
806
967
|
} catch {
|
|
807
968
|
return { state: 'Unknown', pressureLevel: 0, error: 'Thermal info unavailable on this system' };
|
|
@@ -809,6 +970,8 @@ export async function collectThermal(detailed?: boolean): Promise<ThermalData> {
|
|
|
809
970
|
}
|
|
810
971
|
|
|
811
972
|
export async function collectNetwork(): Promise<NetworkData> {
|
|
973
|
+
if (process.platform === 'linux') return collectLinuxNetwork();
|
|
974
|
+
if (process.platform === 'win32') return collectWindowsNetwork();
|
|
812
975
|
try {
|
|
813
976
|
const { iface, ip } = await cachedRouteIface();
|
|
814
977
|
const netStat = await cachedNetstatIb();
|
|
@@ -829,13 +992,172 @@ export async function collectNetwork(): Promise<NetworkData> {
|
|
|
829
992
|
}
|
|
830
993
|
}
|
|
831
994
|
|
|
832
|
-
|
|
995
|
+
const now = Date.now();
|
|
996
|
+
let rxRate = 0;
|
|
997
|
+
let txRate = 0;
|
|
998
|
+
if (prevNetSample && prevNetSample.ts > 0) {
|
|
999
|
+
const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
|
|
1000
|
+
rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
|
|
1001
|
+
txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
|
|
1002
|
+
}
|
|
1003
|
+
prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
|
|
1004
|
+
|
|
1005
|
+
const protocolStats = await getProtocolStats();
|
|
1006
|
+
const connectionStates = await getConnectionStates();
|
|
1007
|
+
const listeningPorts = await getListeningPorts();
|
|
1008
|
+
const establishedConnections = connectionStates.established;
|
|
1009
|
+
const topRemoteHosts = await getTopRemoteHosts();
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
interface: iface,
|
|
1013
|
+
ip,
|
|
1014
|
+
rxBytes,
|
|
1015
|
+
txBytes,
|
|
1016
|
+
rxPackets,
|
|
1017
|
+
txPackets,
|
|
1018
|
+
rxRate,
|
|
1019
|
+
txRate,
|
|
1020
|
+
connections: establishedConnections,
|
|
1021
|
+
protocols: protocolStats,
|
|
1022
|
+
connectionStates,
|
|
1023
|
+
listeningPorts,
|
|
1024
|
+
establishedConnections,
|
|
1025
|
+
topRemoteHosts,
|
|
1026
|
+
};
|
|
833
1027
|
} catch {
|
|
834
|
-
return {
|
|
1028
|
+
return {
|
|
1029
|
+
interface: 'unknown',
|
|
1030
|
+
ip: '0.0.0.0',
|
|
1031
|
+
rxBytes: 0,
|
|
1032
|
+
txBytes: 0,
|
|
1033
|
+
rxPackets: 0,
|
|
1034
|
+
txPackets: 0,
|
|
1035
|
+
rxRate: 0,
|
|
1036
|
+
txRate: 0,
|
|
1037
|
+
connections: 0,
|
|
1038
|
+
protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
|
|
1039
|
+
connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
|
|
1040
|
+
listeningPorts: [],
|
|
1041
|
+
establishedConnections: 0,
|
|
1042
|
+
topRemoteHosts: [],
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function getProtocolStats(): Promise<ProtocolStats> {
|
|
1048
|
+
try {
|
|
1049
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", '');
|
|
1050
|
+
const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
|
|
1051
|
+
const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
|
|
1052
|
+
|
|
1053
|
+
let tcpRxBytes = 0;
|
|
1054
|
+
let tcpTxBytes = 0;
|
|
1055
|
+
let udpRxBytes = 0;
|
|
1056
|
+
let udpTxBytes = 0;
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const netstat = await cachedNetstatIb();
|
|
1060
|
+
for (const line of netstat.split('\n')) {
|
|
1061
|
+
const parts = line.split(/\s+/);
|
|
1062
|
+
if (parts.length >= 9 && parts[0] && !parts[0].startsWith('Name')) {
|
|
1063
|
+
const iface = parts[0];
|
|
1064
|
+
if (iface.includes('lo')) continue;
|
|
1065
|
+
const rx = parseInt(parts[5]) || 0;
|
|
1066
|
+
const tx = parseInt(parts[7]) || 0;
|
|
1067
|
+
tcpRxBytes += rx;
|
|
1068
|
+
tcpTxBytes += tx;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
// ignore
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
tcp: {
|
|
1077
|
+
connections: parseInt(tcpConnections) || 0,
|
|
1078
|
+
rxBytes: tcpRxBytes,
|
|
1079
|
+
txBytes: tcpTxBytes,
|
|
1080
|
+
},
|
|
1081
|
+
udp: {
|
|
1082
|
+
connections: parseInt(udpConnections) || 0,
|
|
1083
|
+
rxBytes: udpRxBytes,
|
|
1084
|
+
txBytes: udpTxBytes,
|
|
1085
|
+
},
|
|
1086
|
+
};
|
|
1087
|
+
} catch {
|
|
1088
|
+
return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function getConnectionStates(): Promise<ConnectionStateStats> {
|
|
1093
|
+
try {
|
|
1094
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", '');
|
|
1095
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
1096
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
1097
|
+
|
|
1098
|
+
for (const line of lines) {
|
|
1099
|
+
const match = line.match(/(\d+)\s+(\S+)/);
|
|
1100
|
+
if (!match) continue;
|
|
1101
|
+
const count = parseInt(match[1]);
|
|
1102
|
+
const state = match[2].toUpperCase();
|
|
1103
|
+
|
|
1104
|
+
if (state.includes('ESTABLISHED')) stats.established += count;
|
|
1105
|
+
else if (state.includes('LISTEN')) stats.listening += count;
|
|
1106
|
+
else if (state.includes('TIME_WAIT')) stats.timeWait += count;
|
|
1107
|
+
else if (state.includes('CLOSE_WAIT')) stats.closeWait += count;
|
|
1108
|
+
else stats.other += count;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return stats;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function getListeningPorts(): Promise<number[]> {
|
|
1118
|
+
try {
|
|
1119
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", '');
|
|
1120
|
+
const ports = raw.split('\n')
|
|
1121
|
+
.map(p => parseInt(p, 10))
|
|
1122
|
+
.filter(p => !isNaN(p) && p > 0);
|
|
1123
|
+
return [...new Set(ports)].sort((a, b) => a - b);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function getTopRemoteHosts(): Promise<RemoteHostInfo[]> {
|
|
1130
|
+
try {
|
|
1131
|
+
const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", '');
|
|
1132
|
+
const ports = raw.split('\n').filter(Boolean);
|
|
1133
|
+
const hostMap = new Map<string, { count: number; bytes: number }>();
|
|
1134
|
+
|
|
1135
|
+
for (const port of ports) {
|
|
1136
|
+
const host = port.split(':')[0];
|
|
1137
|
+
if (!host || host === '127.0.0.1' || host === '::1') continue;
|
|
1138
|
+
const existing = hostMap.get(host) || { count: 0, bytes: 0 };
|
|
1139
|
+
hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const result: RemoteHostInfo[] = [];
|
|
1143
|
+
for (const [host, info] of hostMap.entries()) {
|
|
1144
|
+
result.push({
|
|
1145
|
+
host,
|
|
1146
|
+
ip: host,
|
|
1147
|
+
bytesTransferred: info.bytes,
|
|
1148
|
+
connectionCount: info.count,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return [];
|
|
835
1155
|
}
|
|
836
1156
|
}
|
|
837
1157
|
|
|
838
1158
|
export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
|
|
1159
|
+
if (process.platform === 'linux') return collectLinuxProcesses(limit);
|
|
1160
|
+
if (process.platform === 'win32') return collectWindowsProcesses(limit);
|
|
839
1161
|
try {
|
|
840
1162
|
const isMac = process.platform === 'darwin';
|
|
841
1163
|
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
@@ -857,79 +1179,163 @@ export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
|
|
|
857
1179
|
}
|
|
858
1180
|
}
|
|
859
1181
|
|
|
1182
|
+
function parseBytes(str: string): number {
|
|
1183
|
+
if (!str) return 0;
|
|
1184
|
+
const match = str.trim().match(/([\d.]+)\s*([KMGTPE]?)i?B?/i);
|
|
1185
|
+
if (!match) return 0;
|
|
1186
|
+
let val = parseFloat(match[1]);
|
|
1187
|
+
const unit = match[2].toUpperCase();
|
|
1188
|
+
if (unit === 'K') val *= 1024;
|
|
1189
|
+
else if (unit === 'M') val *= 1024 * 1024;
|
|
1190
|
+
else if (unit === 'G') val *= 1024 * 1024 * 1024;
|
|
1191
|
+
else if (unit === 'T') val *= 1024 * 1024 * 1024 * 1024;
|
|
1192
|
+
return Math.round(val);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
860
1195
|
export async function collectPackets(): Promise<PacketData | null> {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const parts = line.split(/\s+/);
|
|
872
|
-
if (parts.length >= 11 && parts[0] && !parts[0].startsWith('Name')) {
|
|
873
|
-
const iface = parts[0];
|
|
874
|
-
const rxPackets = parseInt(parts[4]) || 0;
|
|
875
|
-
const rxBytes = parseInt(parts[5]) || 0;
|
|
876
|
-
const txPackets = parseInt(parts[6]) || 0;
|
|
877
|
-
const txBytes = parseInt(parts[7]) || 0;
|
|
878
|
-
totalRxPackets += rxPackets;
|
|
879
|
-
totalTxPackets += txPackets;
|
|
880
|
-
totalRxBytes += rxBytes;
|
|
881
|
-
totalTxBytes += txBytes;
|
|
882
|
-
if (iface && !iface.includes('lo')) {
|
|
883
|
-
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
let connections = 0;
|
|
889
|
-
try {
|
|
890
|
-
const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0', 2000)).trim();
|
|
891
|
-
connections = parseInt(tcpConns) || 0;
|
|
892
|
-
} catch {
|
|
893
|
-
connections = 0;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
|
|
897
|
-
let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
|
|
898
|
-
try {
|
|
899
|
-
const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '', 2000)).trim();
|
|
900
|
-
const lsofLines = lsof.split('\n');
|
|
901
|
-
topProcesses = lsofLines.map(line => {
|
|
902
|
-
const parts = line.trim().split(/\s+/);
|
|
903
|
-
if (parts.length >= 3) {
|
|
904
|
-
return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
|
|
905
|
-
}
|
|
906
|
-
return { pid: 0, command: '', rxBytes: 0, txBytes: 0 };
|
|
907
|
-
}).filter(p => p.pid > 0);
|
|
908
|
-
|
|
909
|
-
allProcesses = topProcesses;
|
|
910
|
-
} catch {
|
|
911
|
-
topProcesses = [];
|
|
912
|
-
allProcesses = [];
|
|
913
|
-
}
|
|
1196
|
+
if (process.platform === 'linux') return collectLinuxPackets();
|
|
1197
|
+
if (process.platform === 'win32') return collectWindowsPackets();
|
|
1198
|
+
try {
|
|
1199
|
+
const netStat = await cachedNetstatIb();
|
|
1200
|
+
const lines = netStat.split('\n').slice(1);
|
|
1201
|
+
let totalRxPackets = 0;
|
|
1202
|
+
let totalTxPackets = 0;
|
|
1203
|
+
let totalRxBytes = 0;
|
|
1204
|
+
let totalTxBytes = 0;
|
|
1205
|
+
const ifaceStats: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[] = [];
|
|
914
1206
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1207
|
+
for (const line of lines) {
|
|
1208
|
+
const parts = line.split(/\s+/);
|
|
1209
|
+
if (parts.length >= 11 && parts[0] && !parts[0].startsWith('Name')) {
|
|
1210
|
+
const iface = parts[0];
|
|
1211
|
+
const rxPackets = parseInt(parts[4]) || 0;
|
|
1212
|
+
const rxBytes = parseInt(parts[5]) || 0;
|
|
1213
|
+
const txPackets = parseInt(parts[6]) || 0;
|
|
1214
|
+
const txBytes = parseInt(parts[7]) || 0;
|
|
1215
|
+
totalRxPackets += rxPackets;
|
|
1216
|
+
totalTxPackets += txPackets;
|
|
1217
|
+
totalRxBytes += rxBytes;
|
|
1218
|
+
totalTxBytes += txBytes;
|
|
1219
|
+
if (iface && !iface.includes('lo')) {
|
|
1220
|
+
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let connections = 0;
|
|
1226
|
+
let allProcesses: NetworkProcess[] = [];
|
|
1227
|
+
|
|
1228
|
+
try {
|
|
1229
|
+
const nettop = (await run('script -q /dev/null nettop -l 1 -J bytes_in,bytes_out,state 2>/dev/null', '', 2000)).trim();
|
|
1230
|
+
const rows = nettop.split('\n');
|
|
1231
|
+
|
|
1232
|
+
let currentProc: NetworkProcess | null = null;
|
|
1233
|
+
|
|
1234
|
+
for (const row of rows) {
|
|
1235
|
+
if (!row || row.includes('bytes_in')) continue;
|
|
1236
|
+
|
|
1237
|
+
const parts = row.trimStart().split(/\s{2,}/);
|
|
1238
|
+
const nameCol = parts[0];
|
|
1239
|
+
|
|
1240
|
+
// If it doesn't contain <->, it's a process row
|
|
1241
|
+
if (!nameCol.includes('<->')) {
|
|
1242
|
+
if (currentProc) {
|
|
1243
|
+
if (currentProc.connections && currentProc.connections.length > 0) {
|
|
1244
|
+
allProcesses.push(currentProc);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Extract PID and Name (e.g., "syslogd.369")
|
|
1249
|
+
const lastDot = nameCol.lastIndexOf('.');
|
|
1250
|
+
let pid = 0;
|
|
1251
|
+
let command = nameCol;
|
|
1252
|
+
if (lastDot > 0) {
|
|
1253
|
+
const possiblePid = parseInt(nameCol.substring(lastDot + 1));
|
|
1254
|
+
if (!isNaN(possiblePid)) {
|
|
1255
|
+
pid = possiblePid;
|
|
1256
|
+
command = nameCol.substring(0, lastDot);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const rx = parseBytes(parts[1]);
|
|
1261
|
+
const tx = parseBytes(parts[2]);
|
|
1262
|
+
|
|
1263
|
+
currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
|
|
1264
|
+
} else {
|
|
1265
|
+
// Connection row
|
|
1266
|
+
if (currentProc) {
|
|
1267
|
+
let state = '';
|
|
1268
|
+
let rx = 0;
|
|
1269
|
+
let tx = 0;
|
|
1270
|
+
if (parts.length >= 4) {
|
|
1271
|
+
state = parts[1];
|
|
1272
|
+
rx = parseBytes(parts[2]);
|
|
1273
|
+
tx = parseBytes(parts[3]);
|
|
1274
|
+
} else if (parts.length === 3) {
|
|
1275
|
+
rx = parseBytes(parts[1]);
|
|
1276
|
+
tx = parseBytes(parts[2]);
|
|
1277
|
+
}
|
|
1278
|
+
if (state === 'Established') connections++;
|
|
1279
|
+
|
|
1280
|
+
// Parse endpoints (e.g. "tcp4 172.16.0.2:49844<->17.242.13.5:5223")
|
|
1281
|
+
const spaceIdx = nameCol.indexOf(' ');
|
|
1282
|
+
const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : '';
|
|
1283
|
+
const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
|
|
1284
|
+
const arrowIdx = endpoints.indexOf('<->');
|
|
1285
|
+
let local = '';
|
|
1286
|
+
let remote = '';
|
|
1287
|
+
if (arrowIdx > 0) {
|
|
1288
|
+
local = endpoints.substring(0, arrowIdx);
|
|
1289
|
+
remote = endpoints.substring(arrowIdx + 3);
|
|
1290
|
+
|
|
1291
|
+
// Attempt DNS resolution on remote IP
|
|
1292
|
+
remote = resolveIp(remote);
|
|
1293
|
+
} else {
|
|
1294
|
+
local = endpoints;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Ignore purely local communication if you want, but for now include all
|
|
1298
|
+
currentProc.connections!.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
|
|
1303
|
+
allProcesses.push(currentProc);
|
|
1304
|
+
}
|
|
1305
|
+
} catch {
|
|
1306
|
+
allProcesses = [];
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Sort processes by total bandwidth (tx + rx) to get top processes
|
|
1310
|
+
const topProcesses = [...allProcesses].sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
|
|
1311
|
+
|
|
1312
|
+
const protocolStats = await getProtocolStats();
|
|
1313
|
+
const connectionStates = await getConnectionStates();
|
|
1314
|
+
const listeningPorts = await getListeningPorts();
|
|
1315
|
+
|
|
1316
|
+
return {
|
|
1317
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
1318
|
+
rxPackets: totalRxPackets,
|
|
1319
|
+
txPackets: totalTxPackets,
|
|
1320
|
+
rxRate: 0,
|
|
1321
|
+
txRate: 0,
|
|
1322
|
+
connections,
|
|
1323
|
+
topProcesses,
|
|
1324
|
+
interfaces: ifaceStats,
|
|
1325
|
+
allProcesses,
|
|
1326
|
+
protocolStats,
|
|
1327
|
+
connectionStates,
|
|
1328
|
+
listeningPorts,
|
|
1329
|
+
};
|
|
1330
|
+
} catch {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
930
1334
|
|
|
931
1335
|
export async function collectTasks(limit = 12): Promise<TaskData[]> {
|
|
932
|
-
|
|
1336
|
+
if (process.platform === 'linux') return collectLinuxTasks(limit);
|
|
1337
|
+
if (process.platform === 'win32') return collectWindowsTasks(limit);
|
|
1338
|
+
try {
|
|
933
1339
|
const isMac = process.platform === 'darwin';
|
|
934
1340
|
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
935
1341
|
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
@@ -955,6 +1361,8 @@ export interface DisplayInfo {
|
|
|
955
1361
|
}
|
|
956
1362
|
|
|
957
1363
|
export async function getDisplayInfo(): Promise<DisplayInfo[]> {
|
|
1364
|
+
if (process.platform === 'linux') return getLinuxDisplayInfo();
|
|
1365
|
+
if (process.platform === 'win32') return getWindowsDisplayInfo();
|
|
958
1366
|
try {
|
|
959
1367
|
const raw = await run('system_profiler SPDisplaysDataType 2>&1', '');
|
|
960
1368
|
const displays: DisplayInfo[] = [];
|
|
@@ -999,6 +1407,9 @@ export interface TimeMachineStatus {
|
|
|
999
1407
|
}
|
|
1000
1408
|
|
|
1001
1409
|
export async function getTimeMachineStatus(): Promise<TimeMachineStatus> {
|
|
1410
|
+
if (process.platform !== 'darwin') {
|
|
1411
|
+
return { configured: false, running: false };
|
|
1412
|
+
}
|
|
1002
1413
|
try {
|
|
1003
1414
|
const statusRaw = await run('tmutil status 2>&1', '');
|
|
1004
1415
|
if (statusRaw.includes('No Time Machine destination') || statusRaw.includes('not configured')) {
|