platform-tooling/run/utils/process-analyzer.ts
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

204 lines
5 KiB
TypeScript

/**
* Process tree inspection utilities
*
* Provides:
* - Process info extraction (PID, state, CPU, memory)
* - Process tree traversal
* - Zombie process detection
* - State analysis (running, sleeping, blocked, zombie)
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
// =============================================================================
// Types
// =============================================================================
export interface ProcessInfo {
pid: number;
ppid: number;
state: string; // R (running), S (sleeping), D (uninterruptible), Z (zombie), T (stopped)
cpuPercent: number;
memoryMB: number;
command: string;
}
// =============================================================================
// Process Query Functions
// =============================================================================
/**
* Get information about a specific process
*/
export async function getProcessInfo(pid: number): Promise<ProcessInfo | null> {
try {
// Use ps to get process info
// Format: PID PPID STATE %CPU %MEM COMMAND
const { stdout } = await execAsync(`ps -p ${pid} -o pid=,ppid=,state=,pcpu=,pmem=,comm= 2>/dev/null || true`);
if (!stdout.trim()) {
return null; // Process not found
}
const line = stdout.trim();
const parts = line.split(/\s+/);
if (parts.length < 6) {
return null; // Unexpected format
}
return {
pid: parseInt(parts[0], 10),
ppid: parseInt(parts[1], 10),
state: parts[2],
cpuPercent: parseFloat(parts[3]),
memoryMB: calculateMemoryMB(parseFloat(parts[4])),
command: parts.slice(5).join(' '),
};
} catch (error) {
// Process might have exited
return null;
}
}
/**
* Get all child processes of a given PID (recursively)
*/
export async function getProcessTree(rootPid: number): Promise<ProcessInfo[]> {
const processes: ProcessInfo[] = [];
// Get root process info
const rootInfo = await getProcessInfo(rootPid);
if (rootInfo) {
processes.push(rootInfo);
}
// Get all child PIDs
const children = await getChildPids(rootPid);
// Recursively get info for each child
for (const childPid of children) {
const childTree = await getProcessTree(childPid);
processes.push(...childTree);
}
return processes;
}
/**
* Find all zombie processes on the system
*/
export async function findZombieProcesses(): Promise<ProcessInfo[]> {
try {
// Find all processes in zombie state (Z)
const { stdout } = await execAsync(`ps aux | awk '$8 == "Z" {print $2}' || true`);
if (!stdout.trim()) {
return [];
}
const zombiePids = stdout
.trim()
.split('\n')
.map(line => parseInt(line.trim(), 10))
.filter(pid => !isNaN(pid));
const zombies: ProcessInfo[] = [];
for (const pid of zombiePids) {
const info = await getProcessInfo(pid);
if (info && info.state.includes('Z')) {
zombies.push(info);
}
}
return zombies;
} catch (error) {
// If we can't detect zombies, return empty array
return [];
}
}
/**
* Check if a process is in uninterruptible sleep (blocked I/O)
*/
export function isProcessBlocked(processInfo: ProcessInfo): boolean {
return processInfo.state.includes('D');
}
/**
* Check if a process is running
*/
export function isProcessRunning(processInfo: ProcessInfo): boolean {
return processInfo.state.includes('R');
}
/**
* Check if a process is zombie
*/
export function isProcessZombie(processInfo: ProcessInfo): boolean {
return processInfo.state.includes('Z');
}
/**
* Format process state for display
*/
export function formatProcessState(state: string): string {
const stateMap: Record<string, string> = {
R: 'Running',
S: 'Sleeping',
D: 'Uninterruptible (I/O wait)',
Z: 'Zombie',
T: 'Stopped',
I: 'Idle',
};
const mainState = state[0];
const description = stateMap[mainState] ?? 'Unknown';
// Include additional flags if present
const flags = state.slice(1);
if (flags) {
return `${description} (${flags})`;
}
return description;
}
// =============================================================================
// Private Helper Functions
// =============================================================================
/**
* Get direct child PIDs of a given process
*/
async function getChildPids(ppid: number): Promise<number[]> {
try {
// Find all processes with this PPID
const { stdout } = await execAsync(`pgrep -P ${ppid} 2>/dev/null || true`);
if (!stdout.trim()) {
return [];
}
return stdout
.trim()
.split('\n')
.map(line => parseInt(line.trim(), 10))
.filter(pid => !isNaN(pid));
} catch (error) {
return [];
}
}
/**
* Calculate memory in MB from percentage
* Assumes 16GB total RAM for estimation
*/
function calculateMemoryMB(percentMem: number): number {
// Rough estimation: assume 16GB total memory
const totalMemoryMB = 16384;
return (percentMem / 100) * totalMemoryMB;
}