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,280 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Launcher
|
|
3
|
+
//
|
|
4
|
+
// Owns the lifecycle of every managed process: spawn, restart, stop, and
|
|
5
|
+
// exponential back-off on crash. This is the ONLY place in MetWatch that
|
|
6
|
+
// calls child_process.spawn().
|
|
7
|
+
//
|
|
8
|
+
// Back-off schedule (resets if process lives > STABLE_UPTIME_MS):
|
|
9
|
+
// attempt 1 → 1s
|
|
10
|
+
// attempt 2 → 2s
|
|
11
|
+
// attempt 3 → 4s
|
|
12
|
+
// attempt 4 → 8s
|
|
13
|
+
// attempt 5 → 16s
|
|
14
|
+
// attempt 6+ → 30s (cap)
|
|
15
|
+
//
|
|
16
|
+
// Events emitted on bus:
|
|
17
|
+
// managed:started — process came up
|
|
18
|
+
// managed:stopped — process was intentionally stopped
|
|
19
|
+
// managed:crashed — process exited unexpectedly
|
|
20
|
+
// managed:restarted — process was restarted (crash or manual)
|
|
21
|
+
// log:line — stdout / stderr line from the child
|
|
22
|
+
// log:stream:started — child stdin/stdout attached
|
|
23
|
+
// log:stream:stopped — child exited, streams closed
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
import { spawn } from 'child_process';
|
|
27
|
+
import type { ChildProcess } from 'child_process';
|
|
28
|
+
import { bus } from './event-bus.ts';
|
|
29
|
+
import type { ManagedProcess, ManagedProcessDef } from '../types/managed-process.types.ts';
|
|
30
|
+
|
|
31
|
+
const BACKOFF_STEPS_MS = [1000, 2000, 4000, 8000, 16000, 30000];
|
|
32
|
+
const STABLE_UPTIME_MS = 10_000; // reset back-off counter after this many ms uptime
|
|
33
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
// ── Internal per-process state ─────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
interface ProcessEntry {
|
|
38
|
+
state: ManagedProcess;
|
|
39
|
+
child: ChildProcess | null;
|
|
40
|
+
backoffTimer: ReturnType<typeof setTimeout> | null;
|
|
41
|
+
/** Whether stop() was explicitly called (suppresses auto-restart) */
|
|
42
|
+
intentional: boolean;
|
|
43
|
+
/** Timestamp the current run started (for stability check) */
|
|
44
|
+
runStart: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Launcher handle returned to callers ───────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface LauncherHandle {
|
|
50
|
+
/** Start all defined processes. Called once by bootstrap. */
|
|
51
|
+
startAll: () => void;
|
|
52
|
+
/** Restart a process by id. */
|
|
53
|
+
restart: (id: string) => void;
|
|
54
|
+
/** Gracefully stop a process by id (no auto-restart). */
|
|
55
|
+
stop: (id: string) => void;
|
|
56
|
+
/** Stop all managed processes. Called on TUI quit. */
|
|
57
|
+
stopAll: () => void;
|
|
58
|
+
/** Get a snapshot of all managed process states. */
|
|
59
|
+
getAll: () => ManagedProcess[];
|
|
60
|
+
/** Get a single managed process state by id. */
|
|
61
|
+
get: (id: string) => ManagedProcess | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Factory ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function createLauncher(defs: ManagedProcessDef[]): LauncherHandle {
|
|
67
|
+
const entries = new Map<string, ProcessEntry>();
|
|
68
|
+
|
|
69
|
+
// Build initial entries from defs
|
|
70
|
+
for (const def of defs) {
|
|
71
|
+
entries.set(def.name, {
|
|
72
|
+
state: {
|
|
73
|
+
...def,
|
|
74
|
+
pid: null,
|
|
75
|
+
status: 'stopped',
|
|
76
|
+
restarts: 0,
|
|
77
|
+
startedAt: null,
|
|
78
|
+
exitCode: null,
|
|
79
|
+
},
|
|
80
|
+
child: null,
|
|
81
|
+
backoffTimer: null,
|
|
82
|
+
intentional: false,
|
|
83
|
+
runStart: null,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Subscribe to bus requests so the TUI can trigger restart/stop
|
|
88
|
+
const unsubRestart = bus.on('managed:restart:requested', ({ id }) => restart(id));
|
|
89
|
+
const unsubStop = bus.on('managed:stop:requested', ({ id }) => stop(id));
|
|
90
|
+
|
|
91
|
+
// ── Spawn ──────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function spawnProcess(entry: ProcessEntry): void {
|
|
94
|
+
const { state } = entry;
|
|
95
|
+
|
|
96
|
+
const child = spawn(state.command, state.args, {
|
|
97
|
+
cwd: state.cwd ?? process.cwd(),
|
|
98
|
+
env: { ...process.env, ...(state.env ?? {}) },
|
|
99
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
entry.child = child;
|
|
103
|
+
entry.runStart = Date.now();
|
|
104
|
+
entry.intentional = false;
|
|
105
|
+
|
|
106
|
+
state.pid = child.pid ?? null;
|
|
107
|
+
state.status = 'running';
|
|
108
|
+
state.startedAt = entry.runStart;
|
|
109
|
+
state.exitCode = null;
|
|
110
|
+
|
|
111
|
+
bus.emit('managed:started', { id: state.name, pid: state.pid ?? 0 });
|
|
112
|
+
bus.emit('log:stream:started', { id: state.name, pid: state.pid ?? 0 });
|
|
113
|
+
|
|
114
|
+
// ── Stream stdout ──────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
child.stdout?.setEncoding('utf8');
|
|
117
|
+
child.stdout?.on('data', (chunk: string) => {
|
|
118
|
+
for (const line of chunk.split('\n')) {
|
|
119
|
+
if (line.trim()) {
|
|
120
|
+
bus.emit('log:line', {
|
|
121
|
+
id: state.name,
|
|
122
|
+
stream: 'stdout',
|
|
123
|
+
line,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Stream stderr ──────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
child.stderr?.setEncoding('utf8');
|
|
133
|
+
child.stderr?.on('data', (chunk: string) => {
|
|
134
|
+
for (const line of chunk.split('\n')) {
|
|
135
|
+
if (line.trim()) {
|
|
136
|
+
bus.emit('log:line', {
|
|
137
|
+
id: state.name,
|
|
138
|
+
stream: 'stderr',
|
|
139
|
+
line,
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── Exit handler ───────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
child.on('exit', (code, signal) => {
|
|
149
|
+
const exitCode = code ?? (signal ? 1 : 0);
|
|
150
|
+
state.exitCode = exitCode;
|
|
151
|
+
entry.child = null;
|
|
152
|
+
entry.runStart = null;
|
|
153
|
+
|
|
154
|
+
bus.emit('log:stream:stopped', { id: state.name, exitCode });
|
|
155
|
+
|
|
156
|
+
if (entry.intentional) {
|
|
157
|
+
state.status = 'stopped';
|
|
158
|
+
state.pid = null;
|
|
159
|
+
bus.emit('managed:stopped', { id: state.name });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Unexpected exit — treat as crash
|
|
164
|
+
state.status = 'crashed';
|
|
165
|
+
state.pid = null;
|
|
166
|
+
state.restarts++;
|
|
167
|
+
bus.emit('managed:crashed', {
|
|
168
|
+
id: state.name,
|
|
169
|
+
exitCode,
|
|
170
|
+
restarts: state.restarts,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (state.autoRestart) {
|
|
174
|
+
scheduleRestart(entry);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Back-off restart scheduler ─────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function scheduleRestart(entry: ProcessEntry): void {
|
|
182
|
+
const { state } = entry;
|
|
183
|
+
|
|
184
|
+
// Check if previous run was stable → reset back-off
|
|
185
|
+
const uptime = entry.runStart ? Date.now() - entry.runStart : 0;
|
|
186
|
+
if (uptime >= STABLE_UPTIME_MS) {
|
|
187
|
+
state.restarts = 1; // keep count but reset delay
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stepIndex = Math.min(state.restarts - 1, BACKOFF_STEPS_MS.length - 1);
|
|
191
|
+
const delayMs = Math.min(BACKOFF_STEPS_MS[stepIndex] ?? MAX_BACKOFF_MS, MAX_BACKOFF_MS);
|
|
192
|
+
|
|
193
|
+
state.status = 'restarting';
|
|
194
|
+
|
|
195
|
+
entry.backoffTimer = setTimeout(() => {
|
|
196
|
+
entry.backoffTimer = null;
|
|
197
|
+
spawnProcess(entry);
|
|
198
|
+
bus.emit('managed:restarted', {
|
|
199
|
+
id: state.name,
|
|
200
|
+
pid: state.pid ?? 0,
|
|
201
|
+
restarts: state.restarts,
|
|
202
|
+
});
|
|
203
|
+
}, delayMs);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function startAll(): void {
|
|
209
|
+
for (const entry of entries.values()) {
|
|
210
|
+
spawnProcess(entry);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function restart(id: string): void {
|
|
215
|
+
const entry = entries.get(id);
|
|
216
|
+
if (!entry) return;
|
|
217
|
+
|
|
218
|
+
// Cancel any pending back-off timer
|
|
219
|
+
if (entry.backoffTimer) {
|
|
220
|
+
clearTimeout(entry.backoffTimer);
|
|
221
|
+
entry.backoffTimer = null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Kill current child if running
|
|
225
|
+
if (entry.child) {
|
|
226
|
+
entry.intentional = true; // prevent crash handler from re-triggering
|
|
227
|
+
entry.child.kill('SIGTERM');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Brief settle then re-spawn
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
entry.state.restarts++;
|
|
233
|
+
spawnProcess(entry);
|
|
234
|
+
bus.emit('managed:restarted', {
|
|
235
|
+
id,
|
|
236
|
+
pid: entry.state.pid ?? 0,
|
|
237
|
+
restarts: entry.state.restarts,
|
|
238
|
+
});
|
|
239
|
+
}, 300);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function stop(id: string): void {
|
|
243
|
+
const entry = entries.get(id);
|
|
244
|
+
if (!entry) return;
|
|
245
|
+
|
|
246
|
+
// Cancel any pending back-off restart
|
|
247
|
+
if (entry.backoffTimer) {
|
|
248
|
+
clearTimeout(entry.backoffTimer);
|
|
249
|
+
entry.backoffTimer = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (entry.child) {
|
|
253
|
+
entry.intentional = true;
|
|
254
|
+
entry.child.kill('SIGTERM');
|
|
255
|
+
} else {
|
|
256
|
+
// Already dead — just mark stopped
|
|
257
|
+
entry.state.status = 'stopped';
|
|
258
|
+
bus.emit('managed:stopped', { id });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function stopAll(): void {
|
|
263
|
+
unsubRestart();
|
|
264
|
+
unsubStop();
|
|
265
|
+
for (const [id] of entries) {
|
|
266
|
+
stop(id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getAll(): ManagedProcess[] {
|
|
271
|
+
return [...entries.values()].map(e => ({ ...e.state }));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function get(id: string): ManagedProcess | undefined {
|
|
275
|
+
const entry = entries.get(id);
|
|
276
|
+
return entry ? { ...entry.state } : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { startAll, restart, stop, stopAll, getAll, get };
|
|
280
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Log Manager
|
|
3
|
+
//
|
|
4
|
+
// Maintains a per-process circular buffer of log lines captured from
|
|
5
|
+
// managed process stdout/stderr. Widgets read from this buffer on mount
|
|
6
|
+
// (so they show history immediately) and then subscribe to 'log:line'
|
|
7
|
+
// events for live updates.
|
|
8
|
+
//
|
|
9
|
+
// Design:
|
|
10
|
+
// - One circular buffer (fixed-size array + head pointer) per process ID
|
|
11
|
+
// - Lines are never sorted — insertion order is display order
|
|
12
|
+
// - Scrollback limit comes from ResolvedConfig.logScrollback
|
|
13
|
+
// - No UI awareness: log-manager only stores; widgets only render
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
import { bus } from './event-bus.ts';
|
|
17
|
+
|
|
18
|
+
interface LogLine {
|
|
19
|
+
id: string;
|
|
20
|
+
stream: 'stdout' | 'stderr';
|
|
21
|
+
line: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Circular buffer ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
class CircularBuffer<T> {
|
|
28
|
+
private readonly buf: (T | undefined)[];
|
|
29
|
+
private head = 0;
|
|
30
|
+
private size = 0;
|
|
31
|
+
|
|
32
|
+
constructor(private readonly capacity: number) {
|
|
33
|
+
this.buf = new Array<T | undefined>(capacity).fill(undefined);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
push(item: T): void {
|
|
37
|
+
this.buf[this.head] = item;
|
|
38
|
+
this.head = (this.head + 1) % this.capacity;
|
|
39
|
+
if (this.size < this.capacity) this.size++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toArray(): T[] {
|
|
43
|
+
if (this.size === 0) return [];
|
|
44
|
+
if (this.size < this.capacity) {
|
|
45
|
+
return this.buf.slice(0, this.size) as T[];
|
|
46
|
+
}
|
|
47
|
+
// Buffer is full — head points to oldest entry
|
|
48
|
+
return [
|
|
49
|
+
...this.buf.slice(this.head),
|
|
50
|
+
...this.buf.slice(0, this.head),
|
|
51
|
+
] as T[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear(): void {
|
|
55
|
+
this.buf.fill(undefined);
|
|
56
|
+
this.head = 0;
|
|
57
|
+
this.size = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get length(): number {
|
|
61
|
+
return this.size;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Log Manager ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface LogManagerHandle {
|
|
68
|
+
/** Get all buffered lines for a process id. */
|
|
69
|
+
getLines: (id: string) => LogLine[];
|
|
70
|
+
/** Get all lines across all processes, sorted by timestamp. */
|
|
71
|
+
getAllLines: () => LogLine[];
|
|
72
|
+
/** Clear the buffer for a specific process. */
|
|
73
|
+
clearLines: (id: string) => void;
|
|
74
|
+
/** Stop listening to bus events. Call on TUI destroy. */
|
|
75
|
+
destroy: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createLogManager(scrollback: number): LogManagerHandle {
|
|
79
|
+
const buffers = new Map<string, CircularBuffer<LogLine>>();
|
|
80
|
+
|
|
81
|
+
function getOrCreate(id: string): CircularBuffer<LogLine> {
|
|
82
|
+
let buf = buffers.get(id);
|
|
83
|
+
if (!buf) {
|
|
84
|
+
buf = new CircularBuffer<LogLine>(scrollback);
|
|
85
|
+
buffers.set(id, buf);
|
|
86
|
+
}
|
|
87
|
+
return buf;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const unsub = bus.on('log:line', (payload) => {
|
|
91
|
+
getOrCreate(payload.id).push(payload);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function getLines(id: string): LogLine[] {
|
|
95
|
+
return buffers.get(id)?.toArray() ?? [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getAllLines(): LogLine[] {
|
|
99
|
+
const all: LogLine[] = [];
|
|
100
|
+
for (const buf of buffers.values()) {
|
|
101
|
+
all.push(...buf.toArray());
|
|
102
|
+
}
|
|
103
|
+
return all.sort((a, b) => a.timestamp - b.timestamp);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearLines(id: string): void {
|
|
107
|
+
buffers.get(id)?.clear();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function destroy(): void {
|
|
111
|
+
unsub();
|
|
112
|
+
buffers.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { getLines, getAllLines, clearLines, destroy };
|
|
116
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Metrics Manager
|
|
3
|
+
//
|
|
4
|
+
// Orchestrates the CPU, memory, disk, and network polling loop.
|
|
5
|
+
// Responsibilities:
|
|
6
|
+
// 1. Call system services at the configured interval
|
|
7
|
+
// 2. Write results into state manager
|
|
8
|
+
// 3. Emit typed events on the bus
|
|
9
|
+
// 4. Handle errors without crashing (emit app:error instead)
|
|
10
|
+
//
|
|
11
|
+
// The manager knows nothing about the UI. It is a pure data pipeline.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { bus } from './event-bus.ts';
|
|
15
|
+
import {
|
|
16
|
+
updateCpuMetrics,
|
|
17
|
+
updateMemoryMetrics,
|
|
18
|
+
updateDiskMetrics,
|
|
19
|
+
updateNetworkMetrics,
|
|
20
|
+
} from './state-manager.ts';
|
|
21
|
+
import { fetchCpuMetrics, fetchMemoryMetrics } from '../services/system.service.ts';
|
|
22
|
+
import { fetchDiskMetrics } from '../services/disk.service.ts';
|
|
23
|
+
import { fetchNetworkMetrics } from '../services/network.service.ts';
|
|
24
|
+
|
|
25
|
+
interface MetricsManagerOptions {
|
|
26
|
+
intervalMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface MetricsManagerHandle {
|
|
30
|
+
start: () => void;
|
|
31
|
+
stop: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createMetricsManager(options: MetricsManagerOptions): MetricsManagerHandle {
|
|
35
|
+
const { intervalMs } = options;
|
|
36
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
37
|
+
let running = false;
|
|
38
|
+
|
|
39
|
+
async function tick(): Promise<void> {
|
|
40
|
+
// Run all fetches in parallel — they are independent OS calls
|
|
41
|
+
const [cpuResult, memResult, diskResult, netResult] = await Promise.allSettled([
|
|
42
|
+
fetchCpuMetrics(),
|
|
43
|
+
fetchMemoryMetrics(),
|
|
44
|
+
fetchDiskMetrics(),
|
|
45
|
+
fetchNetworkMetrics(),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (cpuResult.status === 'fulfilled') {
|
|
49
|
+
updateCpuMetrics(cpuResult.value);
|
|
50
|
+
bus.emit('metrics:cpu:updated', cpuResult.value);
|
|
51
|
+
} else {
|
|
52
|
+
bus.emit('app:error', {
|
|
53
|
+
source: 'metrics-manager:cpu',
|
|
54
|
+
error: cpuResult.reason instanceof Error
|
|
55
|
+
? cpuResult.reason
|
|
56
|
+
: new Error(String(cpuResult.reason)),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (memResult.status === 'fulfilled') {
|
|
61
|
+
updateMemoryMetrics(memResult.value);
|
|
62
|
+
bus.emit('metrics:memory:updated', memResult.value);
|
|
63
|
+
} else {
|
|
64
|
+
bus.emit('app:error', {
|
|
65
|
+
source: 'metrics-manager:memory',
|
|
66
|
+
error: memResult.reason instanceof Error
|
|
67
|
+
? memResult.reason
|
|
68
|
+
: new Error(String(memResult.reason)),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (diskResult.status === 'fulfilled') {
|
|
73
|
+
updateDiskMetrics(diskResult.value);
|
|
74
|
+
bus.emit('metrics:disk:updated', diskResult.value);
|
|
75
|
+
} else {
|
|
76
|
+
bus.emit('app:error', {
|
|
77
|
+
source: 'metrics-manager:disk',
|
|
78
|
+
error: diskResult.reason instanceof Error
|
|
79
|
+
? diskResult.reason
|
|
80
|
+
: new Error(String(diskResult.reason)),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (netResult.status === 'fulfilled') {
|
|
85
|
+
updateNetworkMetrics(netResult.value);
|
|
86
|
+
bus.emit('metrics:network:updated', netResult.value);
|
|
87
|
+
} else {
|
|
88
|
+
bus.emit('app:error', {
|
|
89
|
+
source: 'metrics-manager:network',
|
|
90
|
+
error: netResult.reason instanceof Error
|
|
91
|
+
? netResult.reason
|
|
92
|
+
: new Error(String(netResult.reason)),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function start(): void {
|
|
98
|
+
if (running) return;
|
|
99
|
+
running = true;
|
|
100
|
+
// Immediate first tick so the UI isn't blank on startup
|
|
101
|
+
void tick();
|
|
102
|
+
timer = setInterval(() => void tick(), intervalMs);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stop(): void {
|
|
106
|
+
if (!running) return;
|
|
107
|
+
running = false;
|
|
108
|
+
if (timer !== null) {
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
timer = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { start, stop };
|
|
115
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Process Manager
|
|
3
|
+
//
|
|
4
|
+
// Orchestrates the process list polling loop and handles process lifecycle
|
|
5
|
+
// actions (kill). Follows the same pattern as metrics-manager:
|
|
6
|
+
// poll → normalize → write state → emit events
|
|
7
|
+
//
|
|
8
|
+
// Kill flow:
|
|
9
|
+
// Widget emits 'process:kill:requested' → manager catches it → sends SIGTERM
|
|
10
|
+
// → emits 'process:kill:result' back → widget shows feedback
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import { bus } from './event-bus.ts';
|
|
14
|
+
import { updateProcesses } from './state-manager.ts';
|
|
15
|
+
import { fetchProcessList } from '../services/process.service.ts';
|
|
16
|
+
import type { ResolvedConfig } from '../types/config.types.ts';
|
|
17
|
+
|
|
18
|
+
interface ProcessManagerHandle {
|
|
19
|
+
start: () => void;
|
|
20
|
+
stop: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createProcessManager(config: ResolvedConfig): ProcessManagerHandle {
|
|
24
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
+
let running = false;
|
|
26
|
+
|
|
27
|
+
// Listen for kill requests from the UI
|
|
28
|
+
const unsubKill = bus.on('process:kill:requested', ({ pid }) => {
|
|
29
|
+
void handleKill(pid);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function tick(): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const list = await fetchProcessList(config.maxProcesses);
|
|
35
|
+
updateProcesses(list);
|
|
36
|
+
bus.emit('processes:updated', list);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
bus.emit('app:error', {
|
|
39
|
+
source: 'process-manager:poll',
|
|
40
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleKill(pid: number): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 'SIGTERM');
|
|
48
|
+
// Brief delay then trigger a fresh poll so the table updates
|
|
49
|
+
await new Promise<void>(resolve => setTimeout(resolve, 300));
|
|
50
|
+
await tick();
|
|
51
|
+
bus.emit('process:kill:result', { pid, success: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
bus.emit('process:kill:result', {
|
|
54
|
+
pid,
|
|
55
|
+
success: false,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function start(): void {
|
|
62
|
+
if (running) return;
|
|
63
|
+
running = true;
|
|
64
|
+
void tick();
|
|
65
|
+
timer = setInterval(() => void tick(), config.refreshInterval);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stop(): void {
|
|
69
|
+
if (!running) return;
|
|
70
|
+
running = false;
|
|
71
|
+
unsubKill();
|
|
72
|
+
if (timer !== null) {
|
|
73
|
+
clearInterval(timer);
|
|
74
|
+
timer = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { start, stop };
|
|
79
|
+
}
|