metwatch 0.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/LICENSE +21 -0
- package/README.md +336 -0
- package/bin/mw.ts +20 -0
- package/index.ts +174 -0
- package/metwatch.config.json +9 -0
- package/package.json +50 -0
- package/src/cli/args.ts +115 -0
- package/src/cli/commands/list.ts +77 -0
- package/src/cli/commands/logs.ts +69 -0
- package/src/cli/commands/monitor.ts +12 -0
- package/src/cli/commands/start.ts +124 -0
- package/src/cli/commands/stop.ts +33 -0
- package/src/cli/help.ts +96 -0
- package/src/core/event-bus.ts +113 -0
- package/src/core/launcher.ts +280 -0
- package/src/core/log-manager.ts +116 -0
- package/src/core/metrics-manager.ts +115 -0
- package/src/core/process-manager.ts +79 -0
- package/src/core/runtime-manager.ts +299 -0
- package/src/core/state-manager.ts +88 -0
- package/src/services/disk.service.ts +76 -0
- package/src/services/network.service.ts +131 -0
- package/src/services/process.service.ts +49 -0
- package/src/services/system.service.ts +63 -0
- package/src/types/blessed.d.ts +332 -0
- package/src/types/config.types.ts +77 -0
- package/src/types/managed-process.types.ts +55 -0
- package/src/types/metrics.types.ts +182 -0
- package/src/types/process.types.ts +49 -0
- package/src/ui/layout.ts +318 -0
- package/src/ui/screen.ts +45 -0
- package/src/ui/widgets/cpu.widget.ts +98 -0
- package/src/ui/widgets/disk.widget.ts +134 -0
- package/src/ui/widgets/logs.widget.ts +168 -0
- package/src/ui/widgets/memory.widget.ts +94 -0
- package/src/ui/widgets/network.widget.ts +185 -0
- package/src/ui/widgets/process-table.widget.ts +293 -0
- package/src/ui/widgets/runtime.widget.ts +119 -0
- package/src/utils/formatters.ts +74 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Runtime Manager
|
|
3
|
+
//
|
|
4
|
+
// Collects Node/Bun runtime metrics from managed processes via the Chrome
|
|
5
|
+
// DevTools Protocol (inspector).
|
|
6
|
+
//
|
|
7
|
+
// How it works:
|
|
8
|
+
// 1. The launcher passes --inspect=0 to each Node/Bun child process,
|
|
9
|
+
// which picks a random available port and prints:
|
|
10
|
+
// "Debugger listening on ws://127.0.0.1:<port>/..."
|
|
11
|
+
// 2. RuntimeManager reads that port (supplied via managedPorts map),
|
|
12
|
+
// opens a WebSocket to the inspector endpoint, and polls CDP APIs.
|
|
13
|
+
// 3. Metrics are emitted as 'metrics:runtime:updated' events on the bus.
|
|
14
|
+
//
|
|
15
|
+
// CDP methods used:
|
|
16
|
+
// Runtime.getHeapUsage() → heapUsed, heapTotal
|
|
17
|
+
// Runtime.evaluate(expression) → RSS, external, uptime, handles etc.
|
|
18
|
+
// (Uses process.memoryUsage() and process._getActiveHandles() evaluated
|
|
19
|
+
// inside the target process via the inspector)
|
|
20
|
+
//
|
|
21
|
+
// Event loop lag measurement:
|
|
22
|
+
// A lightweight probe is evaluated in the target via inspector:
|
|
23
|
+
// const start = Date.now(); setImmediate(() => resolve(Date.now() - start));
|
|
24
|
+
// Lag = actual delay - expected 0ms.
|
|
25
|
+
//
|
|
26
|
+
// GC metrics are not available via stable CDP; they require the
|
|
27
|
+
// --expose-gc flag and custom instrumentation. We track cumulative
|
|
28
|
+
// estimate via heap growth heuristic (Phase 2). Native GC events
|
|
29
|
+
// will be added in Phase 3 via v8.GCProfiler or perf_hooks.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
import { bus } from './event-bus.ts';
|
|
33
|
+
import { updateRuntimeMetrics } from './state-manager.ts';
|
|
34
|
+
import type { RuntimeMetrics, GcMetrics } from '../types/metrics.types.ts';
|
|
35
|
+
|
|
36
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
interface WsLike {
|
|
39
|
+
send: (data: string) => void;
|
|
40
|
+
close: () => void;
|
|
41
|
+
on: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface InspectorSession {
|
|
45
|
+
ws: WsLike;
|
|
46
|
+
id: number;
|
|
47
|
+
pending: Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RuntimeManagerHandle {
|
|
51
|
+
/** Register a managed process inspector port. Called by the launcher after spawn. */
|
|
52
|
+
register: (id: string, pid: number, wsUrl: string) => void;
|
|
53
|
+
/** Unregister a managed process. Called when the process exits. */
|
|
54
|
+
unregister: (id: string) => void;
|
|
55
|
+
/** Stop all inspector sessions and timers. */
|
|
56
|
+
stop: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Internal session state ────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
interface ProcessSession {
|
|
62
|
+
id: string;
|
|
63
|
+
pid: number;
|
|
64
|
+
wsUrl: string;
|
|
65
|
+
session: InspectorSession | null;
|
|
66
|
+
gcState: { lastHeap: number; count: number; totalMs: number; lastPauseMs: number | null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── WebSocket helper ──────────────────────────────────────────────────────────
|
|
70
|
+
// Use the global WebSocket available in Bun 1+ and Node 22+.
|
|
71
|
+
// Falls back gracefully if the runtime doesn't support it.
|
|
72
|
+
|
|
73
|
+
async function openSession(wsUrl: string): Promise<InspectorSession> {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
const WS = (globalThis as Record<string, unknown>)['WebSocket'] as (new (url: string) => WsLike) | undefined;
|
|
76
|
+
if (!WS) throw new Error('WebSocket not available in this runtime (need Bun 1+ or Node 22+)');
|
|
77
|
+
|
|
78
|
+
const ws = new WS(wsUrl);
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const session: InspectorSession = {
|
|
82
|
+
ws,
|
|
83
|
+
id: 1,
|
|
84
|
+
pending: new Map(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
ws.on('open', () => resolve(session));
|
|
88
|
+
ws.on('error', (err: unknown) => reject(err instanceof Error ? err : new Error(String(err))));
|
|
89
|
+
ws.on('message', (raw: unknown) => {
|
|
90
|
+
try {
|
|
91
|
+
const str = typeof raw === 'string' ? raw : String(raw);
|
|
92
|
+
const msg = JSON.parse(str) as {
|
|
93
|
+
id?: number;
|
|
94
|
+
result?: unknown;
|
|
95
|
+
error?: { message: string };
|
|
96
|
+
};
|
|
97
|
+
if (msg.id !== undefined) {
|
|
98
|
+
const cb = session.pending.get(msg.id);
|
|
99
|
+
if (cb) {
|
|
100
|
+
session.pending.delete(msg.id);
|
|
101
|
+
if (msg.error) {
|
|
102
|
+
cb.reject(new Error(msg.error.message));
|
|
103
|
+
} else {
|
|
104
|
+
cb.resolve(msg.result);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore malformed messages
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cdpCall<T>(
|
|
116
|
+
session: InspectorSession,
|
|
117
|
+
method: string,
|
|
118
|
+
params: Record<string, unknown> = {}
|
|
119
|
+
): Promise<T> {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const id = session.id++;
|
|
122
|
+
session.pending.set(id, {
|
|
123
|
+
resolve: resolve as (v: unknown) => void,
|
|
124
|
+
reject,
|
|
125
|
+
});
|
|
126
|
+
session.ws.send(JSON.stringify({ id, method, params }));
|
|
127
|
+
// Timeout individual calls to avoid hanging
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
if (session.pending.has(id)) {
|
|
130
|
+
session.pending.delete(id);
|
|
131
|
+
reject(new Error(`CDP timeout: ${method}`));
|
|
132
|
+
}
|
|
133
|
+
}, 3000);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// JS expression evaluated inside the target process
|
|
138
|
+
const METRICS_EXPR = `(function() {
|
|
139
|
+
const m = process.memoryUsage();
|
|
140
|
+
const handles = (process._getActiveHandles ? process._getActiveHandles().length : 0);
|
|
141
|
+
const requests = (process._getActiveRequests ? process._getActiveRequests().length : 0);
|
|
142
|
+
const uptime = process.uptime();
|
|
143
|
+
return JSON.stringify({ rss: m.rss, external: m.external, arrayBuffers: m.arrayBuffers, handles, requests, uptime });
|
|
144
|
+
})()`;
|
|
145
|
+
|
|
146
|
+
const LAG_EXPR = `(function() {
|
|
147
|
+
return new Promise(resolve => {
|
|
148
|
+
const start = Date.now();
|
|
149
|
+
setImmediate(() => resolve(Date.now() - start));
|
|
150
|
+
});
|
|
151
|
+
})()`;
|
|
152
|
+
|
|
153
|
+
// ── Poll a single process ──────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async function pollProcess(
|
|
156
|
+
ps: ProcessSession,
|
|
157
|
+
intervalMs: number
|
|
158
|
+
): Promise<RuntimeMetrics | null> {
|
|
159
|
+
if (!ps.session) return null;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const [heapResult, evalResult, lagResult] = await Promise.allSettled([
|
|
163
|
+
cdpCall<{ usedSize: number; totalSize: number }>(
|
|
164
|
+
ps.session, 'Runtime.getHeapUsage'
|
|
165
|
+
),
|
|
166
|
+
cdpCall<{ result: { value: string } }>(
|
|
167
|
+
ps.session, 'Runtime.evaluate',
|
|
168
|
+
{ expression: METRICS_EXPR, returnByValue: true, awaitPromise: false }
|
|
169
|
+
),
|
|
170
|
+
cdpCall<{ result: { value: number } }>(
|
|
171
|
+
ps.session, 'Runtime.evaluate',
|
|
172
|
+
{ expression: LAG_EXPR, returnByValue: true, awaitPromise: true,
|
|
173
|
+
timeout: Math.min(intervalMs, 2000) }
|
|
174
|
+
),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
const heap = heapResult.status === 'fulfilled' ? heapResult.value : null;
|
|
178
|
+
const evalData = evalResult.status === 'fulfilled'
|
|
179
|
+
? JSON.parse(evalResult.value.result.value) as {
|
|
180
|
+
rss: number; external: number; arrayBuffers: number;
|
|
181
|
+
handles: number; requests: number; uptime: number;
|
|
182
|
+
}
|
|
183
|
+
: null;
|
|
184
|
+
const lag = lagResult.status === 'fulfilled'
|
|
185
|
+
? lagResult.value.result.value
|
|
186
|
+
: 0;
|
|
187
|
+
|
|
188
|
+
// GC heuristic: if heap used dropped significantly, a GC likely ran
|
|
189
|
+
const heapUsed = heap?.usedSize ?? 0;
|
|
190
|
+
const heapTotal = heap?.totalSize ?? 0;
|
|
191
|
+
const prevHeap = ps.gcState.lastHeap;
|
|
192
|
+
if (prevHeap > 0 && heapUsed < prevHeap * 0.85) {
|
|
193
|
+
const freedMs = Math.round((prevHeap - heapUsed) / (1024 * 1024)); // crude proxy
|
|
194
|
+
ps.gcState.count++;
|
|
195
|
+
ps.gcState.totalMs += freedMs;
|
|
196
|
+
ps.gcState.lastPauseMs = freedMs;
|
|
197
|
+
}
|
|
198
|
+
ps.gcState.lastHeap = heapUsed;
|
|
199
|
+
|
|
200
|
+
const gc: GcMetrics = {
|
|
201
|
+
count: ps.gcState.count,
|
|
202
|
+
totalPauseMs: ps.gcState.totalMs,
|
|
203
|
+
lastPauseMs: ps.gcState.lastPauseMs,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
managedId: ps.id,
|
|
208
|
+
pid: ps.pid,
|
|
209
|
+
heapUsed,
|
|
210
|
+
heapTotal,
|
|
211
|
+
rss: evalData?.rss ?? 0,
|
|
212
|
+
external: evalData?.external ?? 0,
|
|
213
|
+
arrayBuffers: evalData?.arrayBuffers ?? 0,
|
|
214
|
+
eventLoopLag: typeof lag === 'number' ? lag : 0,
|
|
215
|
+
activeHandles: evalData?.handles ?? 0,
|
|
216
|
+
activeRequests: evalData?.requests ?? 0,
|
|
217
|
+
gc,
|
|
218
|
+
uptime: evalData?.uptime ?? 0,
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Factory ───────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
export function createRuntimeManager(intervalMs: number): RuntimeManagerHandle {
|
|
229
|
+
const sessions = new Map<string, ProcessSession>();
|
|
230
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
231
|
+
|
|
232
|
+
async function tick(): Promise<void> {
|
|
233
|
+
for (const ps of sessions.values()) {
|
|
234
|
+
if (!ps.session) continue;
|
|
235
|
+
const metrics = await pollProcess(ps, intervalMs);
|
|
236
|
+
if (metrics) {
|
|
237
|
+
updateRuntimeMetrics(metrics);
|
|
238
|
+
bus.emit('metrics:runtime:updated', metrics);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function startTimer(): void {
|
|
244
|
+
if (timer !== null) return;
|
|
245
|
+
// Slight delay on first poll to give the inspector time to warm up
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
void tick();
|
|
248
|
+
timer = setInterval(() => void tick(), intervalMs);
|
|
249
|
+
}, 1500);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function register(id: string, pid: number, wsUrl: string): Promise<void> {
|
|
253
|
+
const ps: ProcessSession = {
|
|
254
|
+
id, pid, wsUrl,
|
|
255
|
+
session: null,
|
|
256
|
+
gcState: { lastHeap: 0, count: 0, totalMs: 0, lastPauseMs: null },
|
|
257
|
+
};
|
|
258
|
+
sessions.set(id, ps);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
ps.session = await openSession(wsUrl);
|
|
262
|
+
// Enable Runtime domain
|
|
263
|
+
await cdpCall(ps.session, 'Runtime.enable');
|
|
264
|
+
} catch (err) {
|
|
265
|
+
bus.emit('app:error', {
|
|
266
|
+
source: `runtime-manager:${id}`,
|
|
267
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
268
|
+
});
|
|
269
|
+
ps.session = null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
startTimer();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function unregister(id: string): void {
|
|
276
|
+
const ps = sessions.get(id);
|
|
277
|
+
if (ps?.session) {
|
|
278
|
+
try { ps.session.ws.close(); } catch { /* ignore */ }
|
|
279
|
+
}
|
|
280
|
+
sessions.delete(id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function stop(): void {
|
|
284
|
+
if (timer !== null) { clearInterval(timer); timer = null; }
|
|
285
|
+
for (const ps of sessions.values()) {
|
|
286
|
+
try { ps.session?.ws.close(); } catch { /* ignore */ }
|
|
287
|
+
}
|
|
288
|
+
sessions.clear();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Expose register as async but return the synchronous-compatible handle
|
|
292
|
+
const handle: RuntimeManagerHandle = {
|
|
293
|
+
register: (id, pid, wsUrl) => { void register(id, pid, wsUrl); },
|
|
294
|
+
unregister,
|
|
295
|
+
stop,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return handle;
|
|
299
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// State Manager
|
|
3
|
+
//
|
|
4
|
+
// Single source of truth for the latest system snapshot.
|
|
5
|
+
// Widgets NEVER fetch data — they read from state and react to bus events.
|
|
6
|
+
//
|
|
7
|
+
// This is intentionally NOT a reactive store (no proxies, no signals).
|
|
8
|
+
// The event bus handles reactivity. State is just a plain in-memory cache
|
|
9
|
+
// so widgets can read the last known value synchronously on first render
|
|
10
|
+
// without waiting for the next poll cycle.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CpuMetrics,
|
|
15
|
+
MemoryMetrics,
|
|
16
|
+
SystemMetrics,
|
|
17
|
+
DiskMetrics,
|
|
18
|
+
NetworkMetrics,
|
|
19
|
+
RuntimeMetrics,
|
|
20
|
+
} from '../types/metrics.types.ts';
|
|
21
|
+
import type { ProcessList } from '../types/process.types.ts';
|
|
22
|
+
|
|
23
|
+
// ── Internal state (module-scoped, not exported) ──────────────────────────────
|
|
24
|
+
|
|
25
|
+
let _cpu: CpuMetrics | null = null;
|
|
26
|
+
let _memory: MemoryMetrics | null = null;
|
|
27
|
+
let _disk: DiskMetrics | null = null;
|
|
28
|
+
let _network: NetworkMetrics | null = null;
|
|
29
|
+
let _processes: ProcessList = [];
|
|
30
|
+
// Runtime metrics keyed by managed process id
|
|
31
|
+
const _runtime = new Map<string, RuntimeMetrics>();
|
|
32
|
+
|
|
33
|
+
// ── CPU ───────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export function getCpuMetrics(): CpuMetrics | null { return _cpu; }
|
|
36
|
+
export function updateCpuMetrics(m: CpuMetrics): void { _cpu = m; }
|
|
37
|
+
|
|
38
|
+
// ── Memory ────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function getMemoryMetrics(): MemoryMetrics | null { return _memory; }
|
|
41
|
+
export function updateMemoryMetrics(m: MemoryMetrics): void { _memory = m; }
|
|
42
|
+
|
|
43
|
+
// ── System (convenience) ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function getSystemMetrics(): SystemMetrics | null {
|
|
46
|
+
if (!_cpu || !_memory) return null;
|
|
47
|
+
return { cpu: _cpu, memory: _memory };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Disk ──────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export function getDiskMetrics(): DiskMetrics | null { return _disk; }
|
|
53
|
+
export function updateDiskMetrics(m: DiskMetrics): void { _disk = m; }
|
|
54
|
+
|
|
55
|
+
// ── Network ───────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export function getNetworkMetrics(): NetworkMetrics | null { return _network; }
|
|
58
|
+
export function updateNetworkMetrics(m: NetworkMetrics): void { _network = m; }
|
|
59
|
+
|
|
60
|
+
// ── Processes ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export function getProcesses(): ProcessList { return _processes; }
|
|
63
|
+
export function updateProcesses(list: ProcessList): void { _processes = list; }
|
|
64
|
+
|
|
65
|
+
// ── Runtime (per managed process) ─────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export function getRuntimeMetrics(id: string): RuntimeMetrics | null {
|
|
68
|
+
return _runtime.get(id) ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getAllRuntimeMetrics(): RuntimeMetrics[] {
|
|
72
|
+
return [..._runtime.values()];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function updateRuntimeMetrics(m: RuntimeMetrics): void {
|
|
76
|
+
_runtime.set(m.managedId, m);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Reset ─────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export function resetState(): void {
|
|
82
|
+
_cpu = null;
|
|
83
|
+
_memory = null;
|
|
84
|
+
_disk = null;
|
|
85
|
+
_network = null;
|
|
86
|
+
_processes = [];
|
|
87
|
+
_runtime.clear();
|
|
88
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Disk Service
|
|
3
|
+
//
|
|
4
|
+
// Wraps systeminformation filesystem APIs to produce normalized DiskMetrics.
|
|
5
|
+
//
|
|
6
|
+
// Two si calls per tick:
|
|
7
|
+
// si.fsSize() → per-mount space usage (total/used/free/%)
|
|
8
|
+
// si.fsStats() → aggregate read/write bytes since last call (delta-based)
|
|
9
|
+
//
|
|
10
|
+
// IO rates are calculated as deltas between successive calls. The first call
|
|
11
|
+
// returns zero rates (no baseline yet). Subsequent calls divide the byte delta
|
|
12
|
+
// by the elapsed seconds to get bytes/sec.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
import si from 'systeminformation';
|
|
16
|
+
import type { DiskMetrics, DiskMount, DiskIO } from '../types/metrics.types.ts';
|
|
17
|
+
|
|
18
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export async function fetchDiskMetrics(): Promise<DiskMetrics> {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
|
|
23
|
+
const [fsSizes, disksIO] = await Promise.all([
|
|
24
|
+
si.fsSize(),
|
|
25
|
+
si.disksIO().catch(() => null),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// ── IO delta calculation ────────────────────────────────────────────────────
|
|
29
|
+
// disksIO may return null on some platforms (e.g. Windows without admin).
|
|
30
|
+
|
|
31
|
+
let diskIo: DiskIO | null = null;
|
|
32
|
+
|
|
33
|
+
// disksIO returns an object with rIO/wIO/tIO/rIO_sec/wIO_sec/tIO_sec/rIO_ms/wIO_ms...
|
|
34
|
+
// The *_sec fields are already rates calculated by systeminformation.
|
|
35
|
+
if (disksIO !== null && typeof disksIO === 'object') {
|
|
36
|
+
const d = disksIO as unknown as Record<string, number>;
|
|
37
|
+
const readBPS = (d['rIO_sec'] ?? 0) * 512; // sectors → bytes (approx)
|
|
38
|
+
const writeBPS = (d['wIO_sec'] ?? 0) * 512;
|
|
39
|
+
const readIops = d['rIO_sec'] ?? 0;
|
|
40
|
+
const writeIops = d['wIO_sec'] ?? 0;
|
|
41
|
+
const totalBPS = readBPS + writeBPS;
|
|
42
|
+
const utilization = Math.min(100, (totalBPS / (500 * 1024 * 1024)) * 100);
|
|
43
|
+
|
|
44
|
+
diskIo = {
|
|
45
|
+
readBytesPerSec: Math.round(readBPS),
|
|
46
|
+
writeBytesPerSec: Math.round(writeBPS),
|
|
47
|
+
readIOPS: Math.round(readIops),
|
|
48
|
+
writeIOPS: Math.round(writeIops),
|
|
49
|
+
utilization: Math.round(utilization * 10) / 10,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void 0; // placeholder
|
|
54
|
+
// ── Mount entries ──────────────────────────────────────────────────────────
|
|
55
|
+
// Filter out pseudo-filesystems (tmpfs, devfs, etc.) that have 0 total size.
|
|
56
|
+
|
|
57
|
+
const mounts: DiskMount[] = fsSizes
|
|
58
|
+
.filter(fs => fs.size > 0)
|
|
59
|
+
.map(fs => ({
|
|
60
|
+
fs: fs.fs ?? 'unknown',
|
|
61
|
+
mount: fs.mount ?? '/',
|
|
62
|
+
type: fs.type ?? 'unknown',
|
|
63
|
+
total: fs.size ?? 0,
|
|
64
|
+
used: fs.used ?? 0,
|
|
65
|
+
free: (fs.size ?? 0) - (fs.used ?? 0),
|
|
66
|
+
percent: fs.use ?? 0,
|
|
67
|
+
io: diskIo, // same IO object for all mounts (aggregate only for now)
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
mounts,
|
|
72
|
+
totalReadBytesPerSec: diskIo?.readBytesPerSec ?? 0,
|
|
73
|
+
totalWriteBytesPerSec: diskIo?.writeBytesPerSec ?? 0,
|
|
74
|
+
timestamp: now,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Network Service
|
|
3
|
+
//
|
|
4
|
+
// Wraps systeminformation network APIs to produce normalized NetworkMetrics.
|
|
5
|
+
//
|
|
6
|
+
// Two si calls per tick:
|
|
7
|
+
// si.networkInterfaces() → static info (IP addresses, operstate)
|
|
8
|
+
// si.networkStats() → cumulative byte/packet counters (delta-based)
|
|
9
|
+
//
|
|
10
|
+
// Rates are calculated as deltas between successive calls — same approach as
|
|
11
|
+
// disk.service.ts. First call returns zero rates.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import si from 'systeminformation';
|
|
15
|
+
import type { NetworkMetrics, NetworkInterface } from '../types/metrics.types.ts';
|
|
16
|
+
|
|
17
|
+
// ── Delta tracking ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface IfaceSnapshot {
|
|
20
|
+
rx_bytes: number;
|
|
21
|
+
tx_bytes: number;
|
|
22
|
+
rx_sec: number; // packets
|
|
23
|
+
tx_sec: number;
|
|
24
|
+
rx_errors: number;
|
|
25
|
+
tx_errors: number;
|
|
26
|
+
rx_dropped: number;
|
|
27
|
+
tx_dropped: number;
|
|
28
|
+
ts: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const _prev = new Map<string, IfaceSnapshot>();
|
|
32
|
+
|
|
33
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function operstate(s: string | undefined): 'up' | 'down' | 'unknown' {
|
|
36
|
+
if (s === 'up') return 'up';
|
|
37
|
+
if (s === 'down') return 'down';
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Fetch ─────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export async function fetchNetworkMetrics(): Promise<NetworkMetrics> {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
|
|
46
|
+
const [ifaces, stats] = await Promise.all([
|
|
47
|
+
si.networkInterfaces(),
|
|
48
|
+
si.networkStats('*'),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Build a map of static info keyed by iface name
|
|
52
|
+
const ifaceArray = Array.isArray(ifaces) ? ifaces : [ifaces];
|
|
53
|
+
const staticInfo = new Map(ifaceArray.map(i => [i.iface, i]));
|
|
54
|
+
|
|
55
|
+
const statsArray = Array.isArray(stats) ? stats : [stats];
|
|
56
|
+
|
|
57
|
+
const interfaces: NetworkInterface[] = [];
|
|
58
|
+
let totalRx = 0;
|
|
59
|
+
let totalTx = 0;
|
|
60
|
+
|
|
61
|
+
for (const s of statsArray) {
|
|
62
|
+
const iface = s.iface ?? '';
|
|
63
|
+
if (!iface) continue;
|
|
64
|
+
|
|
65
|
+
const prev = _prev.get(iface);
|
|
66
|
+
const dtSec = prev ? Math.max(0.001, (now - prev.ts) / 1000) : 1;
|
|
67
|
+
|
|
68
|
+
const rxBPS = prev
|
|
69
|
+
? Math.max(0, ((s.rx_bytes ?? 0) - prev.rx_bytes) / dtSec)
|
|
70
|
+
: 0;
|
|
71
|
+
const txBPS = prev
|
|
72
|
+
? Math.max(0, ((s.tx_bytes ?? 0) - prev.tx_bytes) / dtSec)
|
|
73
|
+
: 0;
|
|
74
|
+
const rxPPS = prev
|
|
75
|
+
? Math.max(0, ((s.rx_sec ?? 0) - prev.rx_sec) / dtSec)
|
|
76
|
+
: 0;
|
|
77
|
+
const txPPS = prev
|
|
78
|
+
? Math.max(0, ((s.tx_sec ?? 0) - prev.tx_sec) / dtSec)
|
|
79
|
+
: 0;
|
|
80
|
+
|
|
81
|
+
_prev.set(iface, {
|
|
82
|
+
rx_bytes: s.rx_bytes ?? 0,
|
|
83
|
+
tx_bytes: s.tx_bytes ?? 0,
|
|
84
|
+
rx_sec: s.rx_sec ?? 0,
|
|
85
|
+
tx_sec: s.tx_sec ?? 0,
|
|
86
|
+
rx_errors: s.rx_errors ?? 0,
|
|
87
|
+
tx_errors: s.tx_errors ?? 0,
|
|
88
|
+
rx_dropped: s.rx_dropped ?? 0,
|
|
89
|
+
tx_dropped: s.tx_dropped ?? 0,
|
|
90
|
+
ts: now,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const info = staticInfo.get(iface);
|
|
94
|
+
|
|
95
|
+
totalRx += rxBPS;
|
|
96
|
+
totalTx += txBPS;
|
|
97
|
+
|
|
98
|
+
// Skip loopback in displayed interfaces but keep it in totals
|
|
99
|
+
if (iface === 'lo' || iface === 'loopback') continue;
|
|
100
|
+
|
|
101
|
+
interfaces.push({
|
|
102
|
+
iface,
|
|
103
|
+
ip4: info?.ip4 ?? '',
|
|
104
|
+
ip6: info?.ip6 ?? '',
|
|
105
|
+
operstate: operstate(info?.operstate),
|
|
106
|
+
rxBytesPerSec: Math.round(rxBPS),
|
|
107
|
+
txBytesPerSec: Math.round(txBPS),
|
|
108
|
+
rxPacketsPerSec: Math.round(rxPPS),
|
|
109
|
+
txPacketsPerSec: Math.round(txPPS),
|
|
110
|
+
rxErrors: s.rx_errors ?? 0,
|
|
111
|
+
txErrors: s.tx_errors ?? 0,
|
|
112
|
+
rxDrops: s.rx_dropped ?? 0,
|
|
113
|
+
txDrops: s.tx_dropped ?? 0,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Sort: up interfaces first, then by name
|
|
118
|
+
interfaces.sort((a, b) => {
|
|
119
|
+
if (a.operstate !== b.operstate) {
|
|
120
|
+
return a.operstate === 'up' ? -1 : 1;
|
|
121
|
+
}
|
|
122
|
+
return a.iface.localeCompare(b.iface);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
interfaces,
|
|
127
|
+
totalRxBytesPerSec: Math.round(totalRx),
|
|
128
|
+
totalTxBytesPerSec: Math.round(totalTx),
|
|
129
|
+
timestamp: now,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Process Service
|
|
3
|
+
//
|
|
4
|
+
// Fetches and normalizes the process list using systeminformation.
|
|
5
|
+
// Maps raw si data to the canonical ProcessInfo shape — including the
|
|
6
|
+
// Phase 2 extended fields: user, threads, handles.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import si from 'systeminformation';
|
|
10
|
+
import type { ProcessInfo, ProcessList, ProcessStatus } from '../types/process.types.ts';
|
|
11
|
+
|
|
12
|
+
function mapStatus(raw: string): ProcessStatus {
|
|
13
|
+
switch (raw.toLowerCase()) {
|
|
14
|
+
case 'running': return 'running';
|
|
15
|
+
case 'sleeping':
|
|
16
|
+
case 'sleep': return 'sleeping';
|
|
17
|
+
case 'stopped': return 'stopped';
|
|
18
|
+
case 'zombie': return 'zombie';
|
|
19
|
+
default: return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchProcessList(limit = 100): Promise<ProcessList> {
|
|
24
|
+
const { list } = await si.processes();
|
|
25
|
+
|
|
26
|
+
// Sort by CPU descending before slicing so we surface the most active
|
|
27
|
+
const sorted = [...list].sort((a, b) => b.cpu - a.cpu);
|
|
28
|
+
const top = sorted.slice(0, limit);
|
|
29
|
+
|
|
30
|
+
const totalMem = (await si.mem()).total;
|
|
31
|
+
|
|
32
|
+
return top.map((p): ProcessInfo => ({
|
|
33
|
+
pid: p.pid,
|
|
34
|
+
name: p.name || 'unknown',
|
|
35
|
+
command: p.command || p.name || '',
|
|
36
|
+
cpu: Math.round((p.cpu ?? 0) * 10) / 10,
|
|
37
|
+
memory: p.memRss ?? 0,
|
|
38
|
+
memoryPercent: totalMem > 0
|
|
39
|
+
? Math.round(((p.memRss ?? 0) / totalMem) * 1000) / 10
|
|
40
|
+
: 0,
|
|
41
|
+
status: mapStatus(p.state ?? ''),
|
|
42
|
+
startedAt: p.started ? new Date(p.started).getTime() : null,
|
|
43
|
+
ppid: p.parentPid ?? null,
|
|
44
|
+
// ── Extended (Phase 2) ──────────────────────────────────────────────────
|
|
45
|
+
user: (p as unknown as { user?: string }).user ?? '',
|
|
46
|
+
threads: (p as unknown as { threads?: number }).threads ?? 1,
|
|
47
|
+
handles: (p as unknown as { fd?: number }).fd ?? 0,
|
|
48
|
+
}));
|
|
49
|
+
}
|