@covibes/zeroshot 1.0.2 → 1.1.4
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/CHANGELOG.md +54 -0
- package/README.md +2 -0
- package/cli/index.js +152 -209
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +214 -0
- package/cli/message-formatters-watch.js +181 -0
- package/cluster-templates/base-templates/debug-workflow.json +0 -1
- package/cluster-templates/base-templates/full-workflow.json +10 -6
- package/cluster-templates/base-templates/single-worker.json +0 -1
- package/cluster-templates/base-templates/worker-validator.json +0 -1
- package/docker/zeroshot-cluster/Dockerfile +6 -0
- package/lib/settings.js +1 -1
- package/package.json +4 -2
- package/src/agent/agent-task-executor.js +237 -112
- package/src/isolation-manager.js +94 -51
- package/src/orchestrator.js +45 -10
- package/src/preflight.js +383 -0
- package/src/process-metrics.js +554 -0
- package/src/status-footer.js +543 -0
- package/src/tui/formatters.js +6 -1
- package/cluster-templates/conductor-junior-bootstrap.json +0 -69
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProcessMetrics - Cross-platform real-time process monitoring
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - CPU usage (percent)
|
|
6
|
+
* - Memory usage (MB)
|
|
7
|
+
* - Network I/O (bytes/sec)
|
|
8
|
+
* - Process state (running, sleeping, etc.)
|
|
9
|
+
* - Child process aggregation
|
|
10
|
+
*
|
|
11
|
+
* Supports:
|
|
12
|
+
* - Linux: /proc filesystem + ss
|
|
13
|
+
* - macOS: ps + lsof
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
const PLATFORM = process.platform;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ProcessMetrics
|
|
23
|
+
* @property {number} pid - Process ID
|
|
24
|
+
* @property {boolean} exists - Whether process exists
|
|
25
|
+
* @property {number} cpuPercent - CPU usage (0-100)
|
|
26
|
+
* @property {number} memoryMB - Memory usage in MB
|
|
27
|
+
* @property {string} state - Process state (R=running, S=sleeping, etc.)
|
|
28
|
+
* @property {number} threads - Thread count
|
|
29
|
+
* @property {Object} network - Network activity
|
|
30
|
+
* @property {number} network.established - Established connections
|
|
31
|
+
* @property {boolean} network.hasActivity - Has data in flight
|
|
32
|
+
* @property {number} network.sendQueueBytes - Bytes in send queue
|
|
33
|
+
* @property {number} network.recvQueueBytes - Bytes in receive queue
|
|
34
|
+
* @property {number} childCount - Number of child processes
|
|
35
|
+
* @property {number} timestamp - Measurement timestamp
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all child PIDs for a process (recursive)
|
|
40
|
+
* @param {number} pid - Parent process ID
|
|
41
|
+
* @returns {number[]} Array of child PIDs
|
|
42
|
+
*/
|
|
43
|
+
function getChildPids(pid) {
|
|
44
|
+
const children = [];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (PLATFORM === 'darwin') {
|
|
48
|
+
// macOS: Use pgrep with -P flag
|
|
49
|
+
const output = execSync(`pgrep -P ${pid} 2>/dev/null`, {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
timeout: 2000,
|
|
52
|
+
});
|
|
53
|
+
const pids = output.trim().split('\n').filter(Boolean).map(Number);
|
|
54
|
+
children.push(...pids);
|
|
55
|
+
|
|
56
|
+
// Recursively get grandchildren
|
|
57
|
+
for (const childPid of pids) {
|
|
58
|
+
children.push(...getChildPids(childPid));
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Linux: Read /proc/{pid}/task/{tid}/children
|
|
62
|
+
const taskPath = `/proc/${pid}/task`;
|
|
63
|
+
if (fs.existsSync(taskPath)) {
|
|
64
|
+
const tids = fs.readdirSync(taskPath);
|
|
65
|
+
for (const tid of tids) {
|
|
66
|
+
const childrenPath = `/proc/${pid}/task/${tid}/children`;
|
|
67
|
+
if (fs.existsSync(childrenPath)) {
|
|
68
|
+
const childPids = fs.readFileSync(childrenPath, 'utf8')
|
|
69
|
+
.trim()
|
|
70
|
+
.split(/\s+/)
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.map(Number);
|
|
73
|
+
children.push(...childPids);
|
|
74
|
+
|
|
75
|
+
// Recursively get grandchildren
|
|
76
|
+
for (const childPid of childPids) {
|
|
77
|
+
children.push(...getChildPids(childPid));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore errors (process may have exited)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...new Set(children)]; // Dedupe
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get metrics for a single process (Linux)
|
|
92
|
+
* @param {number} pid - Process ID
|
|
93
|
+
* @returns {Object|null} Metrics or null if process doesn't exist
|
|
94
|
+
*/
|
|
95
|
+
function getProcessMetricsLinux(pid) {
|
|
96
|
+
try {
|
|
97
|
+
const statPath = `/proc/${pid}/stat`;
|
|
98
|
+
if (!fs.existsSync(statPath)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const stat = fs.readFileSync(statPath, 'utf8');
|
|
103
|
+
const parts = stat.split(' ');
|
|
104
|
+
|
|
105
|
+
// state is field 3 (index 2)
|
|
106
|
+
const state = parts[2];
|
|
107
|
+
|
|
108
|
+
// utime (14) + stime (15) = CPU ticks
|
|
109
|
+
const utime = parseInt(parts[13], 10);
|
|
110
|
+
const stime = parseInt(parts[14], 10);
|
|
111
|
+
const cpuTicks = utime + stime;
|
|
112
|
+
|
|
113
|
+
// Read status for memory and threads
|
|
114
|
+
const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
|
|
115
|
+
const vmRss = status.match(/VmRSS:\s+(\d+)/)?.[1] || '0';
|
|
116
|
+
const threads = status.match(/Threads:\s+(\d+)/)?.[1] || '1';
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
pid,
|
|
120
|
+
exists: true,
|
|
121
|
+
state,
|
|
122
|
+
cpuTicks,
|
|
123
|
+
memoryKB: parseInt(vmRss, 10),
|
|
124
|
+
threads: parseInt(threads, 10),
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get metrics for a single process (macOS)
|
|
133
|
+
* @param {number} pid - Process ID
|
|
134
|
+
* @returns {Object|null} Metrics or null if process doesn't exist
|
|
135
|
+
*/
|
|
136
|
+
function getProcessMetricsDarwin(pid) {
|
|
137
|
+
try {
|
|
138
|
+
// ps -p PID -o %cpu=,rss=,state=
|
|
139
|
+
const output = execSync(`ps -p ${pid} -o %cpu=,rss=,state= 2>/dev/null`, {
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
timeout: 2000,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!output.trim()) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parts = output.trim().split(/\s+/);
|
|
149
|
+
const cpuPercent = parseFloat(parts[0]) || 0;
|
|
150
|
+
const rssKB = parseInt(parts[1], 10) || 0;
|
|
151
|
+
const state = parts[2]?.[0] || 'S'; // First char (R, S, etc.)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
pid,
|
|
155
|
+
exists: true,
|
|
156
|
+
state,
|
|
157
|
+
cpuPercent, // macOS ps gives us percent directly
|
|
158
|
+
memoryKB: rssKB,
|
|
159
|
+
threads: 1, // ps doesn't give thread count easily
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get network state for a process (Linux)
|
|
168
|
+
* Uses ss -tip to get extended TCP info including cumulative bytes sent/received
|
|
169
|
+
* @param {number} pid - Process ID
|
|
170
|
+
* @returns {Object} Network state
|
|
171
|
+
*/
|
|
172
|
+
function getNetworkStateLinux(pid) {
|
|
173
|
+
const result = {
|
|
174
|
+
established: 0,
|
|
175
|
+
hasActivity: false,
|
|
176
|
+
sendQueueBytes: 0,
|
|
177
|
+
recvQueueBytes: 0,
|
|
178
|
+
bytesSent: 0, // Cumulative bytes sent across all sockets
|
|
179
|
+
bytesReceived: 0, // Cumulative bytes received across all sockets
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Use ss -tip to get extended TCP info with bytes_sent/bytes_received
|
|
184
|
+
// -t = TCP only, -i = show internal TCP info, -p = show process
|
|
185
|
+
const output = execSync(`ss -tip 2>/dev/null | grep -A1 "pid=${pid}," || true`, {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
timeout: 3000,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!output.trim()) {
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const lines = output.trim().split('\n');
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
197
|
+
const line = lines[i];
|
|
198
|
+
|
|
199
|
+
// Parse socket line: State Recv-Q Send-Q Local:Port Peer:Port Process
|
|
200
|
+
const match = line.match(/^(\S+)\s+(\d+)\s+(\d+)\s+/);
|
|
201
|
+
if (match) {
|
|
202
|
+
const state = match[1];
|
|
203
|
+
const recvQ = parseInt(match[2], 10);
|
|
204
|
+
const sendQ = parseInt(match[3], 10);
|
|
205
|
+
|
|
206
|
+
if (state === 'ESTAB') {
|
|
207
|
+
result.established++;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
result.recvQueueBytes += recvQ;
|
|
211
|
+
result.sendQueueBytes += sendQ;
|
|
212
|
+
|
|
213
|
+
if (recvQ > 0 || sendQ > 0) {
|
|
214
|
+
result.hasActivity = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Parse extended TCP info line (follows socket line)
|
|
219
|
+
// Contains: bytes_sent:N bytes_received:N (and other metrics)
|
|
220
|
+
const bytesSentMatch = line.match(/bytes_sent:(\d+)/);
|
|
221
|
+
const bytesReceivedMatch = line.match(/bytes_received:(\d+)/);
|
|
222
|
+
|
|
223
|
+
if (bytesSentMatch) {
|
|
224
|
+
result.bytesSent += parseInt(bytesSentMatch[1], 10);
|
|
225
|
+
result.hasActivity = true;
|
|
226
|
+
}
|
|
227
|
+
if (bytesReceivedMatch) {
|
|
228
|
+
result.bytesReceived += parseInt(bytesReceivedMatch[1], 10);
|
|
229
|
+
result.hasActivity = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Ignore errors
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get network state for a process (macOS)
|
|
241
|
+
* Note: macOS doesn't expose per-socket byte counts like Linux's ss -tip
|
|
242
|
+
* We return 0 for bytesSent/bytesReceived (not available without dtrace/nettop)
|
|
243
|
+
* @param {number} pid - Process ID
|
|
244
|
+
* @returns {Object} Network state
|
|
245
|
+
*/
|
|
246
|
+
function getNetworkStateDarwin(pid) {
|
|
247
|
+
const result = {
|
|
248
|
+
established: 0,
|
|
249
|
+
hasActivity: false,
|
|
250
|
+
sendQueueBytes: 0,
|
|
251
|
+
recvQueueBytes: 0,
|
|
252
|
+
bytesSent: 0, // Not available on macOS without root/dtrace
|
|
253
|
+
bytesReceived: 0, // Not available on macOS without root/dtrace
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// lsof -i -n -P for network connections
|
|
258
|
+
const output = execSync(`lsof -i -n -P -a -p ${pid} 2>/dev/null || true`, {
|
|
259
|
+
encoding: 'utf8',
|
|
260
|
+
timeout: 3000,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!output.trim()) {
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
268
|
+
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const parts = line.split(/\s+/);
|
|
271
|
+
// Look for ESTABLISHED connections
|
|
272
|
+
if (parts.includes('ESTABLISHED') || parts.some(p => p.includes('->'))) {
|
|
273
|
+
result.established++;
|
|
274
|
+
result.hasActivity = true; // lsof doesn't show queue sizes, assume activity
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// Ignore errors
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get real-time metrics for a process and its children
|
|
286
|
+
* @param {number} pid - Process ID
|
|
287
|
+
* @param {Object} [options] - Options
|
|
288
|
+
* @param {number} [options.samplePeriodMs=1000] - Sample period for rate calculations (Linux only)
|
|
289
|
+
* @returns {Promise<ProcessMetrics>}
|
|
290
|
+
*/
|
|
291
|
+
function getProcessMetrics(pid, options = {}) {
|
|
292
|
+
const samplePeriodMs = options.samplePeriodMs || 1000;
|
|
293
|
+
|
|
294
|
+
if (PLATFORM === 'darwin') {
|
|
295
|
+
return getProcessMetricsDarwinAggregated(pid);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return getProcessMetricsLinuxAggregated(pid, samplePeriodMs);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get aggregated metrics for process tree (Linux)
|
|
303
|
+
* @param {number} pid - Root process ID
|
|
304
|
+
* @param {number} samplePeriodMs - Sample period for CPU calculation
|
|
305
|
+
* @returns {Promise<ProcessMetrics>}
|
|
306
|
+
*/
|
|
307
|
+
async function getProcessMetricsLinuxAggregated(pid, samplePeriodMs) {
|
|
308
|
+
// Get initial CPU sample
|
|
309
|
+
const allPids = [pid, ...getChildPids(pid)];
|
|
310
|
+
const t0Metrics = {};
|
|
311
|
+
|
|
312
|
+
for (const p of allPids) {
|
|
313
|
+
const m = getProcessMetricsLinux(p);
|
|
314
|
+
if (m) t0Metrics[p] = m;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (Object.keys(t0Metrics).length === 0) {
|
|
318
|
+
return {
|
|
319
|
+
pid,
|
|
320
|
+
exists: false,
|
|
321
|
+
cpuPercent: 0,
|
|
322
|
+
memoryMB: 0,
|
|
323
|
+
state: 'X',
|
|
324
|
+
threads: 0,
|
|
325
|
+
network: { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 },
|
|
326
|
+
childCount: 0,
|
|
327
|
+
timestamp: Date.now(),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Wait for sample period
|
|
332
|
+
await new Promise(r => setTimeout(r, samplePeriodMs));
|
|
333
|
+
|
|
334
|
+
// Get second CPU sample
|
|
335
|
+
const t1Metrics = {};
|
|
336
|
+
const currentPids = [pid, ...getChildPids(pid)];
|
|
337
|
+
|
|
338
|
+
for (const p of currentPids) {
|
|
339
|
+
const m = getProcessMetricsLinux(p);
|
|
340
|
+
if (m) t1Metrics[p] = m;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Calculate aggregated metrics
|
|
344
|
+
let totalCpuTicksDelta = 0;
|
|
345
|
+
let totalMemoryKB = 0;
|
|
346
|
+
let totalThreads = 0;
|
|
347
|
+
let rootState = 'S';
|
|
348
|
+
|
|
349
|
+
for (const p of Object.keys(t1Metrics)) {
|
|
350
|
+
const t1 = t1Metrics[p];
|
|
351
|
+
const t0 = t0Metrics[p];
|
|
352
|
+
|
|
353
|
+
if (t0 && t1) {
|
|
354
|
+
totalCpuTicksDelta += t1.cpuTicks - t0.cpuTicks;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
totalMemoryKB += t1.memoryKB;
|
|
358
|
+
totalThreads += t1.threads;
|
|
359
|
+
|
|
360
|
+
if (p === String(pid)) {
|
|
361
|
+
rootState = t1.state;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Calculate CPU percent
|
|
366
|
+
const clockTicks = 100; // Usually 100 on Linux
|
|
367
|
+
const cpuSeconds = totalCpuTicksDelta / clockTicks;
|
|
368
|
+
const sampleSeconds = samplePeriodMs / 1000;
|
|
369
|
+
const rawCpuPercent = (cpuSeconds / sampleSeconds) * 100;
|
|
370
|
+
|
|
371
|
+
// Normalize to per-core average (0-100% range)
|
|
372
|
+
const cpuCores = require('os').cpus().length;
|
|
373
|
+
const cpuPercent = Math.min(100, rawCpuPercent / cpuCores);
|
|
374
|
+
|
|
375
|
+
// Get network state for all processes
|
|
376
|
+
let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
|
|
377
|
+
for (const p of Object.keys(t1Metrics)) {
|
|
378
|
+
const netState = getNetworkStateLinux(parseInt(p, 10));
|
|
379
|
+
network.established += netState.established;
|
|
380
|
+
network.sendQueueBytes += netState.sendQueueBytes;
|
|
381
|
+
network.recvQueueBytes += netState.recvQueueBytes;
|
|
382
|
+
network.bytesSent += netState.bytesSent;
|
|
383
|
+
network.bytesReceived += netState.bytesReceived;
|
|
384
|
+
if (netState.hasActivity) network.hasActivity = true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
pid,
|
|
389
|
+
exists: true,
|
|
390
|
+
cpuPercent: parseFloat(cpuPercent.toFixed(1)),
|
|
391
|
+
memoryMB: parseFloat((totalMemoryKB / 1024).toFixed(1)),
|
|
392
|
+
state: rootState,
|
|
393
|
+
threads: totalThreads,
|
|
394
|
+
network,
|
|
395
|
+
childCount: Object.keys(t1Metrics).length - 1,
|
|
396
|
+
timestamp: Date.now(),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get aggregated metrics for process tree (macOS)
|
|
402
|
+
* @param {number} pid - Root process ID
|
|
403
|
+
* @returns {Promise<ProcessMetrics>}
|
|
404
|
+
*/
|
|
405
|
+
function getProcessMetricsDarwinAggregated(pid) {
|
|
406
|
+
const allPids = [pid, ...getChildPids(pid)];
|
|
407
|
+
let totalCpuPercent = 0;
|
|
408
|
+
let totalMemoryKB = 0;
|
|
409
|
+
let totalThreads = 0;
|
|
410
|
+
let rootState = 'S';
|
|
411
|
+
let existsCount = 0;
|
|
412
|
+
|
|
413
|
+
for (const p of allPids) {
|
|
414
|
+
const m = getProcessMetricsDarwin(p);
|
|
415
|
+
if (m) {
|
|
416
|
+
existsCount++;
|
|
417
|
+
totalCpuPercent += m.cpuPercent;
|
|
418
|
+
totalMemoryKB += m.memoryKB;
|
|
419
|
+
totalThreads += m.threads;
|
|
420
|
+
|
|
421
|
+
if (p === pid) {
|
|
422
|
+
rootState = m.state;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (existsCount === 0) {
|
|
428
|
+
return {
|
|
429
|
+
pid,
|
|
430
|
+
exists: false,
|
|
431
|
+
cpuPercent: 0,
|
|
432
|
+
memoryMB: 0,
|
|
433
|
+
state: 'X',
|
|
434
|
+
threads: 0,
|
|
435
|
+
network: { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0 },
|
|
436
|
+
childCount: 0,
|
|
437
|
+
timestamp: Date.now(),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Get network state
|
|
442
|
+
let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
|
|
443
|
+
for (const p of allPids) {
|
|
444
|
+
const netState = getNetworkStateDarwin(p);
|
|
445
|
+
network.established += netState.established;
|
|
446
|
+
network.bytesSent += netState.bytesSent;
|
|
447
|
+
network.bytesReceived += netState.bytesReceived;
|
|
448
|
+
if (netState.hasActivity) network.hasActivity = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Normalize to per-core average (0-100% range)
|
|
452
|
+
const cpuCores = require('os').cpus().length;
|
|
453
|
+
const normalizedCpu = Math.min(100, totalCpuPercent / cpuCores);
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
pid,
|
|
457
|
+
exists: true,
|
|
458
|
+
cpuPercent: parseFloat(normalizedCpu.toFixed(1)),
|
|
459
|
+
memoryMB: parseFloat((totalMemoryKB / 1024).toFixed(1)),
|
|
460
|
+
state: rootState,
|
|
461
|
+
threads: totalThreads,
|
|
462
|
+
network,
|
|
463
|
+
childCount: existsCount - 1,
|
|
464
|
+
timestamp: Date.now(),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Format metrics for display
|
|
470
|
+
* @param {ProcessMetrics} metrics
|
|
471
|
+
* @returns {string} Formatted string
|
|
472
|
+
*/
|
|
473
|
+
function formatMetrics(metrics) {
|
|
474
|
+
if (!metrics.exists) {
|
|
475
|
+
return '(process exited)';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const parts = [];
|
|
479
|
+
|
|
480
|
+
// CPU
|
|
481
|
+
parts.push(`CPU: ${metrics.cpuPercent}%`);
|
|
482
|
+
|
|
483
|
+
// Memory
|
|
484
|
+
parts.push(`Mem: ${metrics.memoryMB}MB`);
|
|
485
|
+
|
|
486
|
+
// Network
|
|
487
|
+
if (metrics.network.established > 0) {
|
|
488
|
+
parts.push(`Net: ${metrics.network.established} conn`);
|
|
489
|
+
if (metrics.network.hasActivity) {
|
|
490
|
+
parts.push('↕');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Children
|
|
495
|
+
if (metrics.childCount > 0) {
|
|
496
|
+
parts.push(`+${metrics.childCount} child`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return parts.join(' │ ');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get state icon for process state
|
|
504
|
+
* @param {string} state - Process state char
|
|
505
|
+
* @returns {string} Icon
|
|
506
|
+
*/
|
|
507
|
+
function getStateIcon(state) {
|
|
508
|
+
switch (state) {
|
|
509
|
+
case 'R': return '🟢'; // Running
|
|
510
|
+
case 'S': return '🔵'; // Sleeping
|
|
511
|
+
case 'D': return '🟡'; // Disk wait
|
|
512
|
+
case 'Z': return '💀'; // Zombie
|
|
513
|
+
case 'T': return '⏸️'; // Stopped
|
|
514
|
+
case 'X': return '❌'; // Dead
|
|
515
|
+
default: return '⚪';
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check if platform is supported for full metrics
|
|
521
|
+
* @returns {boolean}
|
|
522
|
+
*/
|
|
523
|
+
function isPlatformSupported() {
|
|
524
|
+
return PLATFORM === 'linux' || PLATFORM === 'darwin';
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Get platform-specific metrics provider info
|
|
529
|
+
* @returns {Object}
|
|
530
|
+
*/
|
|
531
|
+
function getPlatformInfo() {
|
|
532
|
+
return {
|
|
533
|
+
platform: PLATFORM,
|
|
534
|
+
supported: isPlatformSupported(),
|
|
535
|
+
cpuSource: PLATFORM === 'linux' ? '/proc/stat' : 'ps',
|
|
536
|
+
memorySource: PLATFORM === 'linux' ? '/proc/status' : 'ps',
|
|
537
|
+
networkSource: PLATFORM === 'linux' ? 'ss' : 'lsof',
|
|
538
|
+
ioSupported: PLATFORM === 'linux', // I/O only on Linux
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
module.exports = {
|
|
543
|
+
getProcessMetrics,
|
|
544
|
+
getChildPids,
|
|
545
|
+
formatMetrics,
|
|
546
|
+
getStateIcon,
|
|
547
|
+
isPlatformSupported,
|
|
548
|
+
getPlatformInfo,
|
|
549
|
+
// Export internal functions for testing
|
|
550
|
+
getProcessMetricsLinux,
|
|
551
|
+
getProcessMetricsDarwin,
|
|
552
|
+
getNetworkStateLinux,
|
|
553
|
+
getNetworkStateDarwin,
|
|
554
|
+
};
|