204 lines
5 KiB
TypeScript
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;
|
|
}
|