zinit 0.3.8

Process supervisor with dependency management
Documentation
//! Debug utilities for visibility into supervisor state.

use std::collections::HashMap;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

use super::graph::{ServiceGraph, ServiceId};

/// Information about a child process.
#[derive(Debug, Clone)]
pub struct ChildProcess {
    pub pid: u32,
    pub ppid: u32,
    pub comm: String,
    pub state: char,
}

/// Get all child processes of a given PID by scanning /proc.
pub fn get_child_processes(parent_pid: u32) -> Vec<ChildProcess> {
    let mut children = Vec::new();

    let proc_dir = match fs::read_dir("/proc") {
        Ok(dir) => dir,
        Err(_) => return children,
    };

    for entry in proc_dir.flatten() {
        let name = entry.file_name();
        let name_str = name.to_string_lossy();

        // Skip non-numeric entries
        let pid: u32 = match name_str.parse() {
            Ok(p) => p,
            Err(_) => continue,
        };

        // Read /proc/[pid]/stat
        let stat_path = format!("/proc/{}/stat", pid);
        let stat_content = match fs::read_to_string(&stat_path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        // Parse stat: pid (comm) state ppid ...
        // comm can contain spaces and parentheses, so find the last ')' first
        let comm_start = match stat_content.find('(') {
            Some(i) => i + 1,
            None => continue,
        };
        let comm_end = match stat_content.rfind(')') {
            Some(i) => i,
            None => continue,
        };

        let comm = stat_content[comm_start..comm_end].to_string();
        let after_comm = &stat_content[comm_end + 2..]; // skip ") "

        let parts: Vec<&str> = after_comm.split_whitespace().collect();
        if parts.len() < 2 {
            continue;
        }

        let state = parts[0].chars().next().unwrap_or('?');
        let ppid: u32 = match parts[1].parse() {
            Ok(p) => p,
            Err(_) => continue,
        };

        if ppid == parent_pid {
            children.push(ChildProcess {
                pid,
                ppid,
                comm,
                state,
            });
        }
    }

    children
}

/// Recursively get all descendant processes.
pub fn get_process_tree(pid: u32) -> Vec<ChildProcess> {
    let mut all_descendants = Vec::new();
    let mut to_visit = vec![pid];

    while let Some(current_pid) = to_visit.pop() {
        let children = get_child_processes(current_pid);
        for child in &children {
            to_visit.push(child.pid);
        }
        all_descendants.extend(children);
    }

    all_descendants
}

/// Format a process tree as ASCII.
pub fn format_process_tree(pid: u32, service_name: &str) -> String {
    let mut lines = Vec::new();
    lines.push(format!("{} (pid={})", service_name, pid));

    let children = get_process_tree(pid);
    for (i, child) in children.iter().enumerate() {
        let prefix = if i == children.len() - 1 {
            "└─"
        } else {
            "├─"
        };
        let state_desc = match child.state {
            'R' => "running",
            'S' => "sleeping",
            'D' => "disk sleep",
            'Z' => "zombie",
            'T' => "stopped",
            'X' => "dead",
            _ => "unknown",
        };
        lines.push(format!(
            "  {} {} (pid={}, ppid={}, {})",
            prefix, child.comm, child.pid, child.ppid, state_desc
        ));
    }

    if children.is_empty() {
        lines.push("  └─ (no child processes)".to_string());
    }

    lines.join("\n")
}

/// Format the full graph state for debugging.
pub fn format_graph_state(
    graph: &ServiceGraph,
    pending_timers: &HashMap<ServiceId, Vec<String>>,
) -> String {
    let mut lines = Vec::new();
    lines.push("=== Service Graph State ===".to_string());
    lines.push(String::new());

    let order = graph.start_order();
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64;

    for id in &order {
        let service = match graph.get(*id) {
            Some(s) => s,
            None => continue,
        };

        // Service header with state
        let state_info = match &service.state {
            crate::sdk::ServiceState::Running { pid } => {
                let uptime = service
                    .started_at
                    .map(|started| {
                        let secs = (now.saturating_sub(started)) / 1000;
                        format_duration(secs)
                    })
                    .unwrap_or_else(|| "?".to_string());
                format!("Running (pid={}, uptime={})", pid, uptime)
            }
            crate::sdk::ServiceState::Starting { pid } => {
                format!("Starting (pid={})", pid)
            }
            crate::sdk::ServiceState::Stopping { pid } => {
                format!("Stopping (pid={})", pid)
            }
            crate::sdk::ServiceState::Blocked { waiting_on } => {
                format!("Blocked (waiting on: {})", waiting_on.join(", "))
            }
            crate::sdk::ServiceState::Exited { exit_code } => {
                format!("Exited (code={:?})", exit_code)
            }
            crate::sdk::ServiceState::Failed { reason } => {
                format!("Failed ({})", reason)
            }
            crate::sdk::ServiceState::Inactive => "Inactive".to_string(),
        };

        lines.push(format!(
            "{} {} - {}",
            service.state.symbol(),
            service.name,
            state_info
        ));

        // Show restart count if > 0
        if service.restart_count > 0 {
            lines.push(format!(
                "    restart_count: {}, next_delay: {}ms",
                service.restart_count, service.current_restart_delay_ms
            ));
        }

        // Show dependencies (incoming edges)
        let deps = graph.dependencies(*id);
        if !deps.is_empty() {
            lines.push("    ← depends on:".to_string());
            for (dep_id, dep_type) in &deps {
                let dep = graph.get(*dep_id);
                if let Some(dep) = dep {
                    let satisfied = dep.state.is_satisfied();
                    let marker = if satisfied { "" } else { "" };
                    lines.push(format!(
                        "      {} {} {} ({})",
                        marker,
                        dep_type,
                        dep.name,
                        dep.state.name()
                    ));
                }
            }
        }

        // Show dependents (outgoing edges - who depends on us)
        let dependents = graph.dependents(*id);
        if !dependents.is_empty() {
            let names: Vec<String> = dependents
                .iter()
                .filter_map(|did| graph.get(*did))
                .map(|s| s.name.clone())
                .collect();
            lines.push(format!("    → required by: {}", names.join(", ")));
        }

        // Show pending timers
        if let Some(timers) = pending_timers.get(id)
            && !timers.is_empty()
        {
            lines.push(format!("    ⏱ pending: {}", timers.join(", ")));
        }

        // Show process tree if running
        if let Some(pid) = service.state.pid()
            && pid > 0
        {
            let children = get_child_processes(pid);
            if !children.is_empty() {
                lines.push("    └─ child processes:".to_string());
                for child in &children {
                    lines.push(format!(
                        "       └─ {} (pid={}, state={})",
                        child.comm, child.pid, child.state
                    ));
                }
            }
        }

        lines.push(String::new());
    }

    lines.join("\n")
}

/// Format a duration in human-readable form.
fn format_duration(secs: u64) -> String {
    if secs < 60 {
        format!("{}s", secs)
    } else if secs < 3600 {
        format!("{}m{}s", secs / 60, secs % 60)
    } else {
        format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
    }
}

/// State transition log entry.
#[derive(Debug, Clone)]
pub struct StateTransition {
    pub timestamp_ms: u64,
    pub service: String,
    pub from_state: String,
    pub to_state: String,
    pub trigger: String,
}

impl StateTransition {
    pub fn new(service: &str, from: &str, to: &str, trigger: &str) -> Self {
        let timestamp_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as u64;
        Self {
            timestamp_ms,
            service: service.to_string(),
            from_state: from.to_string(),
            to_state: to.to_string(),
            trigger: trigger.to_string(),
        }
    }
}

/// Format a state transition for logging.
pub fn format_state_transition(service: &str, from: &str, to: &str, trigger: &str) -> String {
    format!("state: {} {}{} ({})", service, from, to, trigger)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_duration() {
        assert_eq!(format_duration(0), "0s");
        assert_eq!(format_duration(45), "45s");
        assert_eq!(format_duration(60), "1m0s");
        assert_eq!(format_duration(90), "1m30s");
        assert_eq!(format_duration(3600), "1h0m");
        assert_eq!(format_duration(3661), "1h1m");
    }

    #[test]
    fn test_get_child_processes_self() {
        // This test just ensures the function doesn't panic
        let children = get_child_processes(std::process::id());
        // We might or might not have children, just check it runs
        let _ = children;
    }
}