pyre-cli 8.2.0 → 9.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/dist/{anomaliesCmd-TO52YY2B.js → anomaliesCmd-25OFKYPK.js} +1 -1
- package/dist/{brewHealth-6NE37NAU.js → brewHealth-F2BXF5M6.js} +1 -1
- package/dist/{chunk-GLH7EKAX.js → chunk-FH5WDJBF.js} +1 -1
- package/dist/{chunk-KBKXU2ZV.js → chunk-QOLZ7T32.js} +35 -19
- package/dist/{collectors-4VYRJVRO.js → collectors-VACLPHRP.js} +2 -2
- package/dist/{doctor-LJQAABZ4.js → doctor-L6FZYA7F.js} +1 -1
- package/dist/{extensions-QCFSVCL5.js → extensions-DVA5YLGH.js} +1 -1
- package/dist/index.js +435 -92
- package/package.json +1 -1
- package/src/anomaliesCmd.ts +1 -1
- package/src/formatters/render.ts +127 -38
- package/src/formatters/types.ts +6 -0
- package/src/live/input.ts +149 -5
- package/src/live/render.ts +161 -11
- package/src/live/state.ts +7 -0
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +33 -14
- package/src/monitors/run.ts +1 -1
- package/src/monitors/smc.ts +6 -9
- package/src/splash.ts +1 -1
package/src/live/render.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* `state.ts`.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile, exec } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
10
13
|
import chalk from 'chalk';
|
|
11
|
-
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible } from '../formatters/index.js';
|
|
14
|
+
import { formatTable, formatGraphs, gridColumns, THEMES, type ThemeName, panel, fitVisible, visLen } from '../formatters/index.js';
|
|
12
15
|
import { detectAnomalies } from '../anomalies.js';
|
|
13
|
-
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey } from './state.js';
|
|
16
|
+
import { state, setStatus, SIGNAL_OPTIONS, getToggleKey, MENU_OPTIONS } from './state.js';
|
|
14
17
|
import type { StatsData } from '../monitors/index.js';
|
|
15
18
|
|
|
16
19
|
function formatTempForUnit(c: number): string {
|
|
@@ -48,6 +51,9 @@ function tableCacheParams(): string {
|
|
|
48
51
|
return [
|
|
49
52
|
state.sortMode,
|
|
50
53
|
state.processFilter || '',
|
|
54
|
+
state.processSelectionIndex,
|
|
55
|
+
state.trackedPid || '',
|
|
56
|
+
state.inspectingProcess ? state.inspectingProcess.pid : '',
|
|
51
57
|
limit,
|
|
52
58
|
state.currentTheme,
|
|
53
59
|
state.activePanel,
|
|
@@ -68,8 +74,7 @@ function graphsCacheParams(): string {
|
|
|
68
74
|
|
|
69
75
|
function processRowBudget(): number {
|
|
70
76
|
let overlayRows = 0;
|
|
71
|
-
if (state.inputMode === '
|
|
72
|
-
else if (state.inputMode === 'signal') overlayRows = 6;
|
|
77
|
+
if (state.inputMode === 'signal') overlayRows = 6;
|
|
73
78
|
else if (state.inputMode === 'filter' || state.inputMode === 'kill' || state.inputMode === 'p2p') overlayRows = 3;
|
|
74
79
|
|
|
75
80
|
if (state.activePanel === 'process') {
|
|
@@ -198,13 +203,41 @@ function render() {
|
|
|
198
203
|
writeFrame(out);
|
|
199
204
|
return;
|
|
200
205
|
}
|
|
206
|
+
|
|
207
|
+
if (state.inputMode === 'menu') {
|
|
208
|
+
writeFrame(renderSplashMenu(cols, rows));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (state.inputMode === 'readme') {
|
|
212
|
+
writeFrame(renderReadme(cols, rows));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (state.inputMode === 'credits') {
|
|
216
|
+
writeFrame(renderCredits(cols, rows));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
201
219
|
const lines: string[] = [];
|
|
202
220
|
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
203
221
|
|
|
204
222
|
const params = tableCacheParams();
|
|
205
223
|
const anomalies = detectAnomalies(state.lastData, state.history);
|
|
224
|
+
if (anomalies.length > 0) {
|
|
225
|
+
for (const a of anomalies) {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const alertWithTime = { ...a, timestamp: now };
|
|
228
|
+
const exists = state.anomalyHistory.some(
|
|
229
|
+
h => h.metric === a.metric && Math.abs(h.zScore - a.zScore) < 0.1 && (now.getTime() - (h.timestamp?.getTime() || 0)) < 10000
|
|
230
|
+
);
|
|
231
|
+
if (!exists) {
|
|
232
|
+
state.anomalyHistory.unshift(alertWithTime);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (state.anomalyHistory.length > 100) {
|
|
236
|
+
state.anomalyHistory.length = 100;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
206
239
|
const anomalyCacheKey = anomalies.map(a => `${a.metric}:${a.zScore.toFixed(2)}`).join(',');
|
|
207
|
-
const cacheParams = params + '|' + anomalyCacheKey;
|
|
240
|
+
const cacheParams = params + '|' + anomalyCacheKey + '|' + state.anomalyHistory.length;
|
|
208
241
|
const tableStr = _tableDataDirty || state.lastData !== _cachedTableData || cacheParams !== _cachedTableParams
|
|
209
242
|
? (() => {
|
|
210
243
|
const out = formatTable(state.lastData, {
|
|
@@ -222,6 +255,9 @@ function render() {
|
|
|
222
255
|
history: state.history,
|
|
223
256
|
graphMode: state.graphMode,
|
|
224
257
|
panelLayout: state.panelLayout,
|
|
258
|
+
selectedProcessIndex: state.processSelectionIndex,
|
|
259
|
+
trackedPid: state.trackedPid,
|
|
260
|
+
inspectProcess: state.inspectingProcess,
|
|
225
261
|
});
|
|
226
262
|
_cachedTableData = state.lastData;
|
|
227
263
|
_cachedTableParams = cacheParams;
|
|
@@ -265,17 +301,26 @@ function render() {
|
|
|
265
301
|
const targetBodyHeight = Math.max(1, state.termHeight - footerHeight);
|
|
266
302
|
|
|
267
303
|
if (state.inputMode === 'customizer') {
|
|
268
|
-
const preOverlayCount = bodyLines.length;
|
|
269
|
-
const availableForOverlay = targetBodyHeight - preOverlayCount;
|
|
270
304
|
const totalItems = state.CUSTOMIZER_OPTIONS.length;
|
|
271
|
-
const headerLines = 1;
|
|
272
|
-
const blankLines =
|
|
273
|
-
|
|
305
|
+
const headerLines = 1; // the "UI CUSTOMIZER" header
|
|
306
|
+
const blankLines = 1; // blank separator line
|
|
307
|
+
// Reserve a fixed height for the customizer: show all items if they fit,
|
|
308
|
+
// otherwise guarantee at least 10 visible items (or half the screen).
|
|
309
|
+
const idealOverlayHeight = totalItems + headerLines + blankLines;
|
|
310
|
+
const minOverlayHeight = Math.min(10 + headerLines + blankLines, Math.floor(targetBodyHeight / 2));
|
|
311
|
+
const overlayHeight = Math.min(idealOverlayHeight, Math.max(minOverlayHeight, Math.floor(targetBodyHeight * 0.6)));
|
|
312
|
+
// Truncate table body to make room for the customizer overlay
|
|
313
|
+
const maxBodyForTable = Math.max(3, targetBodyHeight - overlayHeight);
|
|
314
|
+
if (bodyLines.length > maxBodyForTable) {
|
|
315
|
+
bodyLines.length = maxBodyForTable;
|
|
316
|
+
}
|
|
317
|
+
const availableForOverlay = targetBodyHeight - bodyLines.length;
|
|
318
|
+
const maxVisibleItems = Math.max(1, availableForOverlay - headerLines - blankLines);
|
|
274
319
|
const scrollOffset = totalItems > maxVisibleItems
|
|
275
320
|
? Math.max(0, Math.min(state.customizerIndex, totalItems - maxVisibleItems))
|
|
276
321
|
: 0;
|
|
277
322
|
|
|
278
|
-
|
|
323
|
+
bodyLines.push('');
|
|
279
324
|
bodyLines.push(...renderCustomizerOverlay(scrollOffset, maxVisibleItems).split('\n'));
|
|
280
325
|
} else if (state.inputMode === 'filter') {
|
|
281
326
|
bodyLines.push(chalk.cyan(` Filter processes: ${state.inputBuffer}_`));
|
|
@@ -414,4 +459,109 @@ function checkAlerts(data: StatsData) {
|
|
|
414
459
|
}
|
|
415
460
|
}
|
|
416
461
|
|
|
462
|
+
function renderSplashMenu(cols: number, rows: number): string[] {
|
|
463
|
+
const out: string[] = [];
|
|
464
|
+
const contentLines: string[] = [];
|
|
465
|
+
const logo = [
|
|
466
|
+
chalk.hex('#ff6004').bold(' ██████╗ ██╗ ██╗██████╗ ███████╗'),
|
|
467
|
+
chalk.hex('#ff8004').bold(' ██╔══██╗╚██╗ ██╔╝██╔══██╗██╔════╝'),
|
|
468
|
+
chalk.hex('#ffa004').bold(' ██████╔╝ ╚████╔╝ ██████╔╝█████╗ '),
|
|
469
|
+
chalk.hex('#ffc004').bold(' ██╔═══╝ ╚██╔╝ ██╔══██╗██╔══╝ '),
|
|
470
|
+
chalk.hex('#ffe004').bold(' ██║ ██║ ██║ ██║███████╗'),
|
|
471
|
+
chalk.hex('#ffff40').bold(' ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝')
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const menuTop = Math.max(0, Math.floor(rows / 2) - 8);
|
|
475
|
+
for (let i = 0; i < menuTop; i++) out.push('');
|
|
476
|
+
|
|
477
|
+
const strip = (s: string) => s.replace(/^(\x1b\[[0-9;]*m)+( +)/, '$1');
|
|
478
|
+
for (const line of logo) contentLines.push(strip(line));
|
|
479
|
+
contentLines.push('');
|
|
480
|
+
contentLines.push(strip(chalk.dim(' system monitor v9.0.0')));
|
|
481
|
+
contentLines.push('');
|
|
482
|
+
contentLines.push('');
|
|
483
|
+
|
|
484
|
+
for (let i = 0; i < MENU_OPTIONS.length; i++) {
|
|
485
|
+
const isSelected = i === state.menuSelectionIndex;
|
|
486
|
+
const opt = MENU_OPTIONS[i];
|
|
487
|
+
const prefix = isSelected ? chalk.cyan.bold(' ▶ ') : ' ';
|
|
488
|
+
const text = isSelected ? chalk.bgCyan.black(` ${opt} `) : chalk.dim(` ${opt} `);
|
|
489
|
+
contentLines.push(prefix + text);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
contentLines.push('');
|
|
493
|
+
contentLines.push('');
|
|
494
|
+
contentLines.push(strip(chalk.dim(' Use ↑/↓ to navigate, Enter to select')));
|
|
495
|
+
|
|
496
|
+
while (contentLines.length < rows) contentLines.push('');
|
|
497
|
+
const maxWidth = Math.max(...contentLines.map(visLen));
|
|
498
|
+
const padding = Math.max(0, Math.floor((cols - maxWidth) / 2));
|
|
499
|
+
const padStr = ' '.repeat(padding);
|
|
500
|
+
for (const line of contentLines) out.push(padStr + fitVisible(line, cols));
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let cachedReadmeLines: string[] | null = null;
|
|
505
|
+
|
|
506
|
+
function getReadmeLines(): string[] {
|
|
507
|
+
if (cachedReadmeLines) return cachedReadmeLines;
|
|
508
|
+
try {
|
|
509
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
510
|
+
const __dirname = path.dirname(__filename);
|
|
511
|
+
const rootDir = path.resolve(__dirname, '..', '..');
|
|
512
|
+
const readmePath = path.join(rootDir, 'README.md');
|
|
513
|
+
const content = fs.readFileSync(readmePath, 'utf8');
|
|
514
|
+
cachedReadmeLines = content.split('\n');
|
|
515
|
+
return cachedReadmeLines;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
return [chalk.red('Failed to load README.md')];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderReadme(cols: number, rows: number): string[] {
|
|
522
|
+
const lines = getReadmeLines();
|
|
523
|
+
const availableLines = Math.max(1, rows - 2); // Body height of the panel
|
|
524
|
+
|
|
525
|
+
const startIdx = Math.min(state.readmeScrollOffset, Math.max(0, lines.length - availableLines));
|
|
526
|
+
state.readmeScrollOffset = startIdx; // Clamp the state value
|
|
527
|
+
|
|
528
|
+
const content = lines.slice(startIdx, startIdx + availableLines).map(l => {
|
|
529
|
+
let out = l;
|
|
530
|
+
if (out.startsWith('#')) {
|
|
531
|
+
out = chalk.cyan.bold(out);
|
|
532
|
+
} else if (out.startsWith('```')) {
|
|
533
|
+
out = chalk.dim(out);
|
|
534
|
+
} else if (out.startsWith('- ') || out.startsWith('* ')) {
|
|
535
|
+
out = ' ' + chalk.yellow(out.substring(0, 2)) + out.substring(2);
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const out: string[] = [];
|
|
541
|
+
const pct = Math.round((startIdx / Math.max(1, lines.length - availableLines)) * 100);
|
|
542
|
+
const title = lines.length > availableLines
|
|
543
|
+
? `README (${pct}%)`
|
|
544
|
+
: 'README';
|
|
545
|
+
out.push(...panel(title, content, cols, chalk.cyan, chalk.dim, availableLines));
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderCredits(cols: number, rows: number): string[] {
|
|
550
|
+
const out: string[] = [];
|
|
551
|
+
const content = [
|
|
552
|
+
chalk.bold.hex('#ff6004')('Pyre - The Terminal System Monitor'),
|
|
553
|
+
'',
|
|
554
|
+
'Created with ❤️ using Node.js',
|
|
555
|
+
'',
|
|
556
|
+
chalk.cyan('Libraries used:'),
|
|
557
|
+
' - chalk (Terminal styling)',
|
|
558
|
+
' - systeminformation (System metrics)',
|
|
559
|
+
'',
|
|
560
|
+
chalk.dim('Press Esc or Enter to return to menu.')
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
out.push(...panel('CREDITS', content, cols, chalk.magenta, chalk.dim, rows - 2));
|
|
564
|
+
return out;
|
|
565
|
+
}
|
|
566
|
+
|
|
417
567
|
export { render, footerLine, renderCustomizerOverlay, checkAlerts, processRowBudget, invalidateTableCache, invalidateFrame };
|
package/src/live/state.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { type SplashColorScheme, type SplashAnimation } from '../splash.js';
|
|
|
19
19
|
|
|
20
20
|
export { type SplashColorScheme, type SplashAnimation };
|
|
21
21
|
|
|
22
|
+
export const MENU_OPTIONS = ['Resume Dashboard', 'Settings (Customizer)', 'Readme', 'Credits', 'Quit pyre'] as const;
|
|
23
|
+
|
|
22
24
|
const config = readConfig();
|
|
23
25
|
|
|
24
26
|
const state = {
|
|
@@ -60,6 +62,10 @@ const state = {
|
|
|
60
62
|
termHeight: process.stdout.rows || 24,
|
|
61
63
|
sortMode: config.sortMode as SortMode,
|
|
62
64
|
processFilter: '',
|
|
65
|
+
processSelectionIndex: 0,
|
|
66
|
+
processScrollOffset: 0,
|
|
67
|
+
trackedPid: null as number | null,
|
|
68
|
+
inspectingProcess: null as any | null,
|
|
63
69
|
inputMode: null as InputMode,
|
|
64
70
|
inputBuffer: '',
|
|
65
71
|
targetPid: '',
|
|
@@ -126,6 +132,7 @@ const state = {
|
|
|
126
132
|
splashEnabled: config.splashEnabled,
|
|
127
133
|
splashColorScheme: config.splashColorScheme as SplashColorScheme,
|
|
128
134
|
splashAnimation: config.splashAnimation as SplashAnimation,
|
|
135
|
+
readmeScrollOffset: 0,
|
|
129
136
|
};
|
|
130
137
|
|
|
131
138
|
function setStatus(msg: string, ms = 3000) {
|
package/src/live/types.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface LiveOptions {
|
|
|
20
20
|
|
|
21
21
|
export type ExportFormat = 'json' | 'csv' | 'tsv' | 'html' | 'md';
|
|
22
22
|
|
|
23
|
-
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p';
|
|
23
|
+
type InputMode = null | 'filter' | 'kill' | 'signal' | 'customizer' | 'p2p' | 'menu' | 'readme' | 'credits';
|
|
24
24
|
|
|
25
25
|
type SortMode = 'cpu' | 'mem' | 'pid' | 'user' | 'command' | 'state' | 'threads' | 'runtime';
|
|
26
26
|
|
|
@@ -20,6 +20,7 @@ import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerDat
|
|
|
20
20
|
const SP_TTL_MS = 10_000;
|
|
21
21
|
const NETSTAT_TTL_MS = 1000;
|
|
22
22
|
let prevCpuTimes: { total: number; idle: number }[] | null = null;
|
|
23
|
+
let prevCpuTotal = { total: 0, idle: 0, ts: 0 };
|
|
23
24
|
const ROUTE_TTL_MS = 5_000;
|
|
24
25
|
const SYSCTL_TTL_MS = 60_000;
|
|
25
26
|
|
|
@@ -236,7 +237,7 @@ export async function collectPower(): Promise<PowerData | null> {
|
|
|
236
237
|
if (spGpuCache && now - spGpuCache.ts < SP_TTL_MS) {
|
|
237
238
|
spRaw = spGpuCache.raw;
|
|
238
239
|
} else {
|
|
239
|
-
spRaw = (await run('system_profiler SPDisplaysDataType 2>/dev/null')).trim();
|
|
240
|
+
spRaw = (await run('system_profiler SPDisplaysDataType 2>/dev/null', '', 3000)).trim();
|
|
240
241
|
spGpuCache = { raw: spRaw, ts: now };
|
|
241
242
|
}
|
|
242
243
|
|
|
@@ -400,18 +401,36 @@ export async function collectCpu(): Promise<CpuData> {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
if (frequency === 0) {
|
|
405
|
+
try {
|
|
406
|
+
const cpus = os.cpus();
|
|
407
|
+
if (cpus && cpus.length > 0 && cpus[0].speed) {
|
|
408
|
+
frequency = cpus[0].speed;
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
// ignore
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Use os.cpus() delta instead of the very slow `top -l 1 -n 0` which
|
|
416
|
+
// blocks for ~1-2s waiting for a sampling window. os.cpus() is instant.
|
|
403
417
|
let usage = 0;
|
|
404
418
|
try {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
const cpus = os.cpus();
|
|
420
|
+
let totalNow = 0;
|
|
421
|
+
let idleNow = 0;
|
|
422
|
+
for (const cpu of cpus) {
|
|
423
|
+
totalNow += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
|
|
424
|
+
idleNow += cpu.times.idle;
|
|
425
|
+
}
|
|
426
|
+
if (prevCpuTotal.ts > 0) {
|
|
427
|
+
const totalDelta = totalNow - prevCpuTotal.total;
|
|
428
|
+
const idleDelta = idleNow - prevCpuTotal.idle;
|
|
429
|
+
if (totalDelta > 0) {
|
|
430
|
+
usage = Math.round(((totalDelta - idleDelta) / totalDelta) * 100);
|
|
431
|
+
}
|
|
414
432
|
}
|
|
433
|
+
prevCpuTotal = { total: totalNow, idle: idleNow, ts: Date.now() };
|
|
415
434
|
} catch {
|
|
416
435
|
// ignore
|
|
417
436
|
}
|
|
@@ -534,7 +553,7 @@ async function getDiskIoRates(): Promise<{ readBytesSec: number; writeBytesSec:
|
|
|
534
553
|
return { readBytesSec: cachedDiskIo.readBytesSec, writeBytesSec: cachedDiskIo.writeBytesSec };
|
|
535
554
|
}
|
|
536
555
|
try {
|
|
537
|
-
const raw = (await run('iostat -d -c 2 1 2', '')).trim();
|
|
556
|
+
const raw = (await run('iostat -d -c 2 1 2', '', 3000)).trim();
|
|
538
557
|
const blocks = raw.split(/\n\s*\n/).filter(Boolean);
|
|
539
558
|
const targetBlock = blocks.length > 1 ? blocks[1] : blocks[0];
|
|
540
559
|
if (targetBlock) {
|
|
@@ -606,7 +625,7 @@ export async function collectBattery(): Promise<BatteryData | null> {
|
|
|
606
625
|
if (spBatteryCache && now - spBatteryCache.ts < SP_TTL_MS) {
|
|
607
626
|
battInfo = spBatteryCache.raw;
|
|
608
627
|
} else {
|
|
609
|
-
battInfo = (await run('system_profiler SPPowerDataType 2>/dev/null')).trim();
|
|
628
|
+
battInfo = (await run('system_profiler SPPowerDataType 2>/dev/null', '', 3000)).trim();
|
|
610
629
|
spBatteryCache = { raw: battInfo, ts: now };
|
|
611
630
|
}
|
|
612
631
|
|
|
@@ -821,7 +840,7 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
821
840
|
|
|
822
841
|
let connections = 0;
|
|
823
842
|
try {
|
|
824
|
-
const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0')).trim();
|
|
843
|
+
const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0', 2000)).trim();
|
|
825
844
|
connections = parseInt(tcpConns) || 0;
|
|
826
845
|
} catch {
|
|
827
846
|
connections = 0;
|
|
@@ -830,7 +849,7 @@ export async function collectPackets(): Promise<PacketData | null> {
|
|
|
830
849
|
let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
|
|
831
850
|
let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
|
|
832
851
|
try {
|
|
833
|
-
const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '')).trim();
|
|
852
|
+
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();
|
|
834
853
|
const lsofLines = lsof.split('\n');
|
|
835
854
|
topProcesses = lsofLines.map(line => {
|
|
836
855
|
const parts = line.trim().split(/\s+/);
|
package/src/monitors/run.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { promisify } from 'node:util';
|
|
|
11
11
|
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
|
|
14
|
-
async function run(cmd: string, fallback: string = '', timeout =
|
|
14
|
+
async function run(cmd: string, fallback: string = '', timeout = 3000): Promise<string> {
|
|
15
15
|
try {
|
|
16
16
|
return (await execAsync(cmd, { encoding: 'utf8', timeout })).stdout.trim();
|
|
17
17
|
} catch {
|
package/src/monitors/smc.ts
CHANGED
|
@@ -41,7 +41,8 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
|
41
41
|
const pm = (
|
|
42
42
|
await run(
|
|
43
43
|
'sudo -n powermetrics --samplers smc,cpu_power -n 1 --format text 2>/dev/null',
|
|
44
|
-
''
|
|
44
|
+
'',
|
|
45
|
+
2000
|
|
45
46
|
)
|
|
46
47
|
).trim();
|
|
47
48
|
|
|
@@ -116,14 +117,10 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
|
|
|
116
117
|
|
|
117
118
|
// 4. Smart load-based thermal estimation if hardware sensors return nothing
|
|
118
119
|
if (result.temps['cpu_die'] === undefined) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (m) loadUsage = Math.max(0, 100 - parseFloat(m[1]));
|
|
124
|
-
} catch {
|
|
125
|
-
// ignore
|
|
126
|
-
}
|
|
120
|
+
// Use os.loadavg() (instant) instead of the slow `top -l 1` (~1-2s)
|
|
121
|
+
const loadAvg = (await import('node:os')).loadavg();
|
|
122
|
+
const ncpus = (await import('node:os')).cpus().length || 1;
|
|
123
|
+
const loadUsage = Math.min(100, (loadAvg[0] / ncpus) * 100);
|
|
127
124
|
|
|
128
125
|
const estCpu = Math.round((38 + (loadUsage * 0.42)) * 10) / 10;
|
|
129
126
|
result.temps['cpu_die'] = estCpu;
|