pyre-cli 1.4.0 → 3.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 +78 -11
- package/dist/index.js +1325 -33
- package/package.json +2 -2
- package/src/formatters/index.ts +1 -1
- package/src/formatters/output.ts +47 -19
- package/src/formatters/render.ts +207 -16
- package/src/formatters/types.ts +4 -0
- package/src/history.ts +85 -47
- package/src/index.ts +200 -20
- package/src/live/export.ts +5 -2
- package/src/live/input.ts +296 -164
- package/src/live/render.ts +32 -28
- package/src/live/state.ts +41 -21
- package/src/monitors/collectors.ts +220 -17
- package/src/monitors/index.ts +2 -2
- package/src/monitors/types.ts +42 -1
- package/src/p2p/client.ts +271 -0
- package/src/p2p/index.ts +3 -0
- package/src/p2p/protocol.ts +112 -0
- package/src/p2p/server.ts +390 -0
- package/src/p2p/types.ts +97 -0
package/src/live/state.ts
CHANGED
|
@@ -27,16 +27,19 @@ const state = {
|
|
|
27
27
|
|
|
28
28
|
exportDir: './pyre-exports',
|
|
29
29
|
currentTheme: 'default' as ThemeName,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
visiblePanels: {
|
|
31
|
+
cpu: true,
|
|
32
|
+
mem: true,
|
|
33
|
+
gpu: true,
|
|
34
|
+
power: true,
|
|
35
|
+
battery: true,
|
|
36
|
+
thermal: true,
|
|
37
|
+
network: true,
|
|
38
|
+
packets: true,
|
|
39
|
+
tasks: true,
|
|
40
|
+
disk: true,
|
|
41
|
+
process: true,
|
|
42
|
+
} as VisibleItems,
|
|
40
43
|
logging: false,
|
|
41
44
|
logStream: null as fs.WriteStream | null,
|
|
42
45
|
statusMessage: '',
|
|
@@ -57,20 +60,37 @@ const state = {
|
|
|
57
60
|
'Theme',
|
|
58
61
|
'Graph Mode',
|
|
59
62
|
'Toggle CPU',
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
'Toggle Memory',
|
|
64
|
+
'Toggle GPU',
|
|
65
|
+
'Toggle Power',
|
|
66
|
+
'Toggle Battery',
|
|
67
|
+
'Toggle Thermal',
|
|
68
|
+
'Toggle Network',
|
|
69
|
+
'Toggle Packets',
|
|
70
|
+
'Toggle Tasks',
|
|
71
|
+
'Toggle Disk',
|
|
72
|
+
'Toggle Processes',
|
|
73
|
+
'Toggle Tree View',
|
|
74
|
+
],
|
|
70
75
|
CPU_ALERT_PCT: 90,
|
|
71
76
|
TEMP_ALERT_C: 95,
|
|
72
77
|
alerted: false,
|
|
73
|
-
|
|
78
|
+
activePanel: 'grid' as 'grid' | 'cpu' | 'mem' | 'gpu' | 'power' | 'battery' | 'thermal' | 'network' | 'packets' | 'tasks' | 'disk' | 'process',
|
|
79
|
+
mouseEnabled: true,
|
|
80
|
+
PANEL_TABS: [
|
|
81
|
+
{ id: 'cpu', label: 'CPU', key: '1' },
|
|
82
|
+
{ id: 'mem', label: 'Memory', key: '2' },
|
|
83
|
+
{ id: 'gpu', label: 'GPU', key: '3' },
|
|
84
|
+
{ id: 'power', label: 'Power', key: '4' },
|
|
85
|
+
{ id: 'battery', label: 'Battery', key: '5' },
|
|
86
|
+
{ id: 'thermal', label: 'Thermal', key: '6' },
|
|
87
|
+
{ id: 'network', label: 'Network', key: '7' },
|
|
88
|
+
{ id: 'packets', label: 'Packets', key: '8' },
|
|
89
|
+
{ id: 'tasks', label: 'Tasks', key: '9' },
|
|
90
|
+
{ id: 'disk', label: 'Disk', key: '0' },
|
|
91
|
+
{ id: 'process', label: 'Process', key: 'P' },
|
|
92
|
+
] as const,
|
|
93
|
+
};
|
|
74
94
|
|
|
75
95
|
function setStatus(msg: string, ms = 3000) {
|
|
76
96
|
state.statusMessage = msg;
|
|
@@ -14,14 +14,71 @@
|
|
|
14
14
|
import chalk from 'chalk';
|
|
15
15
|
import { run } from './run.js';
|
|
16
16
|
import { getSmcMetrics, parseSuffix } from './smc.js';
|
|
17
|
-
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData } from './types.js';
|
|
17
|
+
import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess } from './types.js';
|
|
18
|
+
|
|
19
|
+
// Retain samples over a *time* window rather than a fixed tick count. At the
|
|
20
|
+
// default 2s refresh interval the old 10-sample window only spanned ~20s,
|
|
21
|
+
// but battery level only moves in whole-percent steps that typically take
|
|
22
|
+
// several minutes to change — so `discharged` was almost always 0 and the
|
|
23
|
+
// estimate never populated. A 15-minute time window plus a minimum elapsed
|
|
24
|
+
// time before we trust the numbers gives enough resolution to see a real
|
|
25
|
+
// drop while still updating reasonably often.
|
|
26
|
+
const BATTERY_WINDOW_MS = 15 * 60 * 1000;
|
|
27
|
+
const MIN_ESTIMATE_WINDOW_MS = 30 * 1000;
|
|
28
|
+
|
|
29
|
+
let prevBattery: { samples: { level: number; ts: number }[] } = { samples: [] };
|
|
30
|
+
|
|
31
|
+
function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmpty?: string; dischargeRatePerHour?: number; powerWatts?: number } {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
|
|
34
|
+
if (state === 'charged' || state === 'charging') {
|
|
35
|
+
// Reset the window on a state change so a stale pre-charge sample
|
|
36
|
+
// doesn't get blended with post-charge readings once discharging
|
|
37
|
+
// resumes.
|
|
38
|
+
prevBattery.samples = [];
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
prevBattery.samples.push({ level, ts: now });
|
|
43
|
+
prevBattery.samples = prevBattery.samples.filter(s => now - s.ts <= BATTERY_WINDOW_MS);
|
|
44
|
+
|
|
45
|
+
const first = prevBattery.samples[0];
|
|
46
|
+
const elapsedMs = now - first.ts;
|
|
47
|
+
if (elapsedMs < MIN_ESTIMATE_WINDOW_MS) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const dtHours = Math.max(elapsedMs / 1000 / 3600, 0.0001);
|
|
52
|
+
const discharged = first.level - level;
|
|
53
|
+
if (discharged <= 0) {
|
|
54
|
+
// No measurable drop yet within the window — report "calculating"
|
|
55
|
+
// rather than silently omitting the field, so the UI shows progress
|
|
56
|
+
// instead of a blank card.
|
|
57
|
+
return { estimatedTimeToEmpty: 'calculating' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const dischargeRate = discharged / dtHours; // %/hour
|
|
61
|
+
const remainingLevel = Math.max(level, 0);
|
|
62
|
+
const hoursToEmpty = remainingLevel / dischargeRate;
|
|
63
|
+
const h = Math.floor(hoursToEmpty);
|
|
64
|
+
const m = Math.round((hoursToEmpty - h) * 60);
|
|
65
|
+
const estimatedTimeToEmpty = hoursToEmpty > 0 ? `${h}h ${m}m` : 'calculating';
|
|
66
|
+
|
|
67
|
+
// Rough wattage from discharge rate: %/hour * (typical battery capacity
|
|
68
|
+
// in Wh) / 100. This is only used as a last-resort estimate — collectPower()
|
|
69
|
+
// now prefers real ioreg-derived wattage when available (see below).
|
|
70
|
+
const ASSUMED_CAPACITY_WH = 55;
|
|
71
|
+
const powerWatts = Math.round(dischargeRate * (ASSUMED_CAPACITY_WH / 100) * 100) / 100;
|
|
72
|
+
|
|
73
|
+
return { estimatedTimeToEmpty, dischargeRatePerHour: Math.round(dischargeRate * 10) / 10, powerWatts };
|
|
74
|
+
}
|
|
18
75
|
|
|
19
76
|
/**
|
|
20
77
|
* Gather every metric category in parallel and return a
|
|
21
78
|
* complete {@link StatsData} snapshot.
|
|
22
79
|
*/
|
|
23
80
|
export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
|
|
24
|
-
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power] = await Promise.all([
|
|
81
|
+
const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
|
|
25
82
|
collectCpu(),
|
|
26
83
|
collectGpu(opts.detailed),
|
|
27
84
|
collectMemory(),
|
|
@@ -29,9 +86,11 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
29
86
|
collectBattery(),
|
|
30
87
|
collectThermal(opts.detailed),
|
|
31
88
|
collectNetwork(),
|
|
32
|
-
collectProcesses(opts.processLimit
|
|
89
|
+
collectProcesses(opts.processLimit),
|
|
33
90
|
collectSystem(),
|
|
34
91
|
collectPower(),
|
|
92
|
+
collectPackets(),
|
|
93
|
+
collectTasks(),
|
|
35
94
|
]);
|
|
36
95
|
|
|
37
96
|
return {
|
|
@@ -51,6 +110,8 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
51
110
|
processes,
|
|
52
111
|
power,
|
|
53
112
|
timestamp: new Date().toISOString(),
|
|
113
|
+
packets,
|
|
114
|
+
tasks,
|
|
54
115
|
};
|
|
55
116
|
}
|
|
56
117
|
|
|
@@ -62,11 +123,54 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
|
|
|
62
123
|
export async function collectPower(): Promise<PowerData | null> {
|
|
63
124
|
try {
|
|
64
125
|
const { power } = await getSmcMetrics();
|
|
65
|
-
if (power.cpu
|
|
66
|
-
|
|
126
|
+
if (power.cpu !== undefined || power.gpu !== undefined || power.combined !== undefined) {
|
|
127
|
+
return { cpuWatts: power.cpu, gpuWatts: power.gpu, combinedWatts: power.combined };
|
|
128
|
+
}
|
|
67
129
|
} catch {
|
|
68
|
-
|
|
130
|
+
// ignore
|
|
69
131
|
}
|
|
132
|
+
|
|
133
|
+
// `pmset -g batt` never actually prints a "mW" figure on any macOS
|
|
134
|
+
// version — that regex could never match, so this fallback was dead
|
|
135
|
+
// code and collectPower() silently returned null whenever powermetrics
|
|
136
|
+
// wasn't available (which is most machines without passwordless sudo
|
|
137
|
+
// configured). ioreg's AppleSmartBattery entry exposes instantaneous
|
|
138
|
+
// amperage (mA, signed: negative while discharging) and voltage (mV)
|
|
139
|
+
// without any privileges, so we can derive real wattage from that.
|
|
140
|
+
try {
|
|
141
|
+
const ioreg = (await run('ioreg -rn AppleSmartBattery 2>/dev/null')).trim();
|
|
142
|
+
const ampMatch = ioreg.match(/"InstantAmperage"\s*=\s*(-?\d+)/);
|
|
143
|
+
const voltMatch = ioreg.match(/"Voltage"\s*=\s*(\d+)/);
|
|
144
|
+
if (ampMatch && voltMatch) {
|
|
145
|
+
// Two representations show up in the wild: Intel Macs often print a
|
|
146
|
+
// literal minus sign ("-1200") — no unwrap needed. Apple Silicon
|
|
147
|
+
// instead wraps a negative reading into an *unsigned 64-bit* field
|
|
148
|
+
// (e.g. real -1200mA prints as 18446744073709550416). The previous
|
|
149
|
+
// 32-bit unwrap (`- 0x100000000`) didn't touch that at all, and
|
|
150
|
+
// `parseInt` can't even represent a number that large precisely, so
|
|
151
|
+
// it silently mangled into a huge garbage value → absurd wattage.
|
|
152
|
+
// Parse as BigInt and only unwrap when there's no explicit sign.
|
|
153
|
+
let ampsBig = BigInt(ampMatch[1]);
|
|
154
|
+
if (!ampMatch[1].startsWith('-')) {
|
|
155
|
+
const UINT64_MAX_PLUS_1 = 1n << 64n;
|
|
156
|
+
const INT64_MAX = (1n << 63n) - 1n;
|
|
157
|
+
if (ampsBig > INT64_MAX) ampsBig -= UINT64_MAX_PLUS_1;
|
|
158
|
+
}
|
|
159
|
+
const amps = Number(ampsBig);
|
|
160
|
+
const volts = parseInt(voltMatch[1], 10) / 1000;
|
|
161
|
+
const watts = Math.abs((amps / 1000) * volts);
|
|
162
|
+
// Sanity clamp — a real Mac never draws anywhere near this much;
|
|
163
|
+
// treat an out-of-range reading as "no usable data" rather than
|
|
164
|
+
// display nonsense.
|
|
165
|
+
if (Number.isFinite(watts) && watts < 500) {
|
|
166
|
+
return { combinedWatts: Math.round(watts * 100) / 100 };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// ignore
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
70
174
|
}
|
|
71
175
|
|
|
72
176
|
/**
|
|
@@ -274,7 +378,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
274
378
|
|
|
275
379
|
const levelMatch = raw.match(/(\d+)%/);
|
|
276
380
|
const stateMatch = raw.match(/;\s*(charging|discharging|charged|finishing charge|attached to charger)/i);
|
|
277
|
-
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i);
|
|
381
|
+
const timeMatch = raw.match(/:\s*([\d:]+)\s+remaining/i) || raw.match(/(\d+:\d+)\s+remaining/i);
|
|
278
382
|
const powerPlugged = raw.includes('AC Power') || raw.includes('attached to charger');
|
|
279
383
|
|
|
280
384
|
const level = levelMatch ? parseInt(levelMatch[1]) : 0;
|
|
@@ -286,10 +390,6 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
286
390
|
let maxCapacityPercent: number | undefined;
|
|
287
391
|
|
|
288
392
|
try {
|
|
289
|
-
// Plain text output (NOT -json) — its field names ("Cycle Count:", "Condition:")
|
|
290
|
-
// are what the regexes below expect. Using -json here was the original bug:
|
|
291
|
-
// it silently returned JSON that these text regexes could never match, so
|
|
292
|
-
// health always fell back to 'unknown'.
|
|
293
393
|
const battInfo = (await run('system_profiler SPPowerDataType 2>/dev/null')).trim();
|
|
294
394
|
|
|
295
395
|
const cycleMatch = battInfo.match(/Cycle Count:\s+(\d+)/);
|
|
@@ -302,8 +402,6 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
302
402
|
if (maxCapMatch) {
|
|
303
403
|
maxCapacityPercent = parseInt(maxCapMatch[1]);
|
|
304
404
|
} else {
|
|
305
|
-
// Older/alternate output reports Full Charge Capacity vs Design Capacity
|
|
306
|
-
// instead of a direct percentage — derive it if both are present.
|
|
307
405
|
const fullMatch = battInfo.match(/Full Charge Capacity[^\d]*(\d+)/i);
|
|
308
406
|
const designMatch = battInfo.match(/Design Capacity[^\d]*(\d+)/i);
|
|
309
407
|
if (fullMatch && designMatch) {
|
|
@@ -322,6 +420,8 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
322
420
|
if (cycles !== undefined) healthParts.push(`${cycles} cycles`);
|
|
323
421
|
const health = healthParts.length ? healthParts.join(', ') : 'unknown';
|
|
324
422
|
|
|
423
|
+
const estimate = estimateBatteryLife(level, state);
|
|
424
|
+
|
|
325
425
|
return {
|
|
326
426
|
level,
|
|
327
427
|
state,
|
|
@@ -331,6 +431,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
331
431
|
cycles,
|
|
332
432
|
condition,
|
|
333
433
|
maxCapacityPercent,
|
|
434
|
+
...estimate,
|
|
334
435
|
};
|
|
335
436
|
} catch {
|
|
336
437
|
return null;
|
|
@@ -466,9 +567,13 @@ export async function collectNetwork(): Promise<NetworkData> {
|
|
|
466
567
|
}
|
|
467
568
|
}
|
|
468
569
|
|
|
469
|
-
export async function collectProcesses(limit
|
|
570
|
+
export async function collectProcesses(limit?: number): Promise<ProcessData[]> {
|
|
470
571
|
try {
|
|
471
|
-
const
|
|
572
|
+
const isMac = process.platform === 'darwin';
|
|
573
|
+
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
574
|
+
const threadsArg = isMac ? 'threads' : 'nlwp';
|
|
575
|
+
const headClause = limit && limit > 0 ? ` | head -n ${limit + 1}` : '';
|
|
576
|
+
const raw = (await run(`ps -eo pid,ppid,user,pcpu,pmem,state,${threadsArg},time,comm ${sortArg}${headClause}`)).trim();
|
|
472
577
|
const lines = raw.split('\n').slice(1);
|
|
473
578
|
return lines
|
|
474
579
|
.map(line => {
|
|
@@ -478,7 +583,105 @@ export async function collectProcesses(limit = 10): Promise<ProcessData[]> {
|
|
|
478
583
|
return { pid: parseInt(parts[1]), ppid: parseInt(parts[2]), user: parts[3], cpu: parseFloat(parts[4]), mem: parseFloat(parts[5]), state: parts[6], threads: parseInt(parts[7]), runtime: runtimeSec, command: parts[9] };
|
|
479
584
|
})
|
|
480
585
|
.filter((p): p is ProcessData => p !== null && p.pid > 0);
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export async function collectPackets(): Promise<PacketData | null> {
|
|
592
|
+
try {
|
|
593
|
+
const netStat = (await run('netstat -ib')).trim();
|
|
594
|
+
const lines = netStat.split('\n').slice(1);
|
|
595
|
+
let totalRxPackets = 0;
|
|
596
|
+
let totalTxPackets = 0;
|
|
597
|
+
let totalRxBytes = 0;
|
|
598
|
+
let totalTxBytes = 0;
|
|
599
|
+
const ifaceStats: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[] = [];
|
|
600
|
+
|
|
601
|
+
for (const line of lines) {
|
|
602
|
+
const parts = line.split(/\s+/);
|
|
603
|
+
if (parts.length >= 11 && parts[0] && !parts[0].startsWith('Name')) {
|
|
604
|
+
const iface = parts[0];
|
|
605
|
+
const rxPackets = parseInt(parts[4]) || 0;
|
|
606
|
+
const rxBytes = parseInt(parts[5]) || 0;
|
|
607
|
+
const txPackets = parseInt(parts[6]) || 0;
|
|
608
|
+
const txBytes = parseInt(parts[7]) || 0;
|
|
609
|
+
totalRxPackets += rxPackets;
|
|
610
|
+
totalTxPackets += txPackets;
|
|
611
|
+
totalRxBytes += rxBytes;
|
|
612
|
+
totalTxBytes += txBytes;
|
|
613
|
+
if (iface && !iface.includes('lo')) {
|
|
614
|
+
ifaceStats.push({ iface, rxPackets, txPackets, rxBytes, txBytes });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
let connections = 0;
|
|
620
|
+
try {
|
|
621
|
+
const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0')).trim();
|
|
622
|
+
connections = parseInt(tcpConns) || 0;
|
|
623
|
+
} catch {
|
|
624
|
+
connections = 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
|
|
628
|
+
let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
|
|
629
|
+
try {
|
|
630
|
+
const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
|
|
631
|
+
const lsofLines = lsof.split('\n');
|
|
632
|
+
topProcesses = lsofLines.map(line => {
|
|
633
|
+
const parts = line.trim().split(/\s+/);
|
|
634
|
+
if (parts.length >= 3) {
|
|
635
|
+
return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
|
|
636
|
+
}
|
|
637
|
+
return { pid: 0, command: '', rxBytes: 0, txBytes: 0 };
|
|
638
|
+
}).filter(p => p.pid > 0);
|
|
639
|
+
|
|
640
|
+
const allLsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
|
|
641
|
+
const allLsofLines = allLsof.split('\n');
|
|
642
|
+
allProcesses = allLsofLines.map(line => {
|
|
643
|
+
const parts = line.trim().split(/\s+/);
|
|
644
|
+
if (parts.length >= 3) {
|
|
645
|
+
return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
|
|
646
|
+
}
|
|
647
|
+
return { pid: 0, command: '', rxBytes: 0, txBytes: 0 };
|
|
648
|
+
}).filter(p => p.pid > 0);
|
|
649
|
+
} catch {
|
|
650
|
+
topProcesses = [];
|
|
651
|
+
allProcesses = [];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
totalPackets: totalRxPackets + totalTxPackets,
|
|
656
|
+
rxPackets: totalRxPackets,
|
|
657
|
+
txPackets: totalTxPackets,
|
|
658
|
+
rxRate: 0,
|
|
659
|
+
txRate: 0,
|
|
660
|
+
connections,
|
|
661
|
+
topProcesses,
|
|
662
|
+
interfaces: ifaceStats,
|
|
663
|
+
allProcesses,
|
|
664
|
+
};
|
|
481
665
|
} catch {
|
|
482
|
-
return
|
|
666
|
+
return null;
|
|
483
667
|
}
|
|
484
|
-
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export async function collectTasks(limit = 12): Promise<TaskData[]> {
|
|
671
|
+
try {
|
|
672
|
+
const isMac = process.platform === 'darwin';
|
|
673
|
+
const sortArg = isMac ? '-r' : '--sort=-pcpu';
|
|
674
|
+
const raw = (await run(`ps -eo pid,user,pcpu,pmem,state,time,comm ${sortArg} | head -n ${limit + 1}`)).trim();
|
|
675
|
+
const lines = raw.split('\n').slice(1);
|
|
676
|
+
return lines
|
|
677
|
+
.map(line => {
|
|
678
|
+
const parts = line.match(/\s*(\d+)\s+(\S+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+([\d:]+)\s+(.+)/);
|
|
679
|
+
if (!parts) return null;
|
|
680
|
+
const runtimeSec = parts[6].split(':').reduce((acc, val, idx) => acc + parseInt(val) * Math.pow(60, 2 - idx), 0);
|
|
681
|
+
return { pid: parseInt(parts[1]), user: parts[2], cpu: parseFloat(parts[3]), mem: parseFloat(parts[4]), state: parts[5], runtime: runtimeSec, command: parts[7] };
|
|
682
|
+
})
|
|
683
|
+
.filter((t): t is TaskData => t !== null && t.pid > 0);
|
|
684
|
+
} catch {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
}
|
package/src/monitors/index.ts
CHANGED
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
* - `collectors.ts` — individual metric collectors and the orchestrator
|
|
11
11
|
* - `run.ts` — shared shell-execution helper
|
|
12
12
|
*/
|
|
13
|
-
export { collectAll, collectPower } from './collectors.js';
|
|
14
|
-
export type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData } from './types.js';
|
|
13
|
+
export { collectAll, collectPower, collectPackets, collectTasks } from './collectors.js';
|
|
14
|
+
export type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData } from './types.js';
|
package/src/monitors/types.ts
CHANGED
|
@@ -19,7 +19,9 @@ export interface StatsData {
|
|
|
19
19
|
processes: ProcessData[];
|
|
20
20
|
power: PowerData | null;
|
|
21
21
|
timestamp: string;
|
|
22
|
-
|
|
22
|
+
packets: PacketData | null;
|
|
23
|
+
tasks: TaskData[];
|
|
24
|
+
}
|
|
23
25
|
|
|
24
26
|
export interface GpuData {
|
|
25
27
|
model: string;
|
|
@@ -35,6 +37,29 @@ export interface GpuData {
|
|
|
35
37
|
combinedWatts?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export interface PacketData {
|
|
41
|
+
totalPackets: number;
|
|
42
|
+
rxPackets: number;
|
|
43
|
+
txPackets: number;
|
|
44
|
+
rxRate: number;
|
|
45
|
+
txRate: number;
|
|
46
|
+
connections: number;
|
|
47
|
+
topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[];
|
|
48
|
+
/** Every process with an open network connection (unlike topProcesses, not truncated). */
|
|
49
|
+
allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[];
|
|
50
|
+
interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TaskData {
|
|
54
|
+
pid: number;
|
|
55
|
+
user: string;
|
|
56
|
+
cpu: number;
|
|
57
|
+
mem: number;
|
|
58
|
+
command: string;
|
|
59
|
+
state: string;
|
|
60
|
+
runtime: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export interface CpuData {
|
|
39
64
|
brand: string;
|
|
40
65
|
cores: number;
|
|
@@ -74,6 +99,9 @@ export interface BatteryData {
|
|
|
74
99
|
cycles?: number;
|
|
75
100
|
condition?: string;
|
|
76
101
|
maxCapacityPercent?: number;
|
|
102
|
+
estimatedTimeToEmpty?: string;
|
|
103
|
+
dischargeRatePerHour?: number;
|
|
104
|
+
powerWatts?: number;
|
|
77
105
|
}
|
|
78
106
|
|
|
79
107
|
export interface DiskData {
|
|
@@ -92,6 +120,19 @@ export interface NetworkData {
|
|
|
92
120
|
txBytes: number;
|
|
93
121
|
rxPackets: number;
|
|
94
122
|
txPackets: number;
|
|
123
|
+
rxPacketsPerSec?: number;
|
|
124
|
+
txPacketsPerSec?: number;
|
|
125
|
+
connections?: number;
|
|
126
|
+
topProcesses?: NetworkProcess[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface NetworkProcess {
|
|
130
|
+
pid: number;
|
|
131
|
+
command: string;
|
|
132
|
+
rxBytes: number;
|
|
133
|
+
txBytes: number;
|
|
134
|
+
rxPackets: number;
|
|
135
|
+
txPackets: number;
|
|
95
136
|
}
|
|
96
137
|
|
|
97
138
|
export interface ProcessData {
|