ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
//! macOS-specific child process detection using libproc.

use super::ChildProcessInfo;
use std::collections::{HashSet, VecDeque};
use std::ffi::c_void;

const PROC_PIDT_SHORTBSDINFO: libc::c_int = 13;
const PROC_PIDTASKINFO: libc::c_int = 4;
const MAXCOMLEN: usize = 16;
const SIDL: u32 = 1;
const SRUN: u32 = 2;
const SSTOP: u32 = 4;
const SZOMB: u32 = 5;

#[repr(C)]
struct ProcBsdShortInfo {
    pid: u32,
    parent_pid: u32,
    process_group_id: u32,
    status: u32,
    command: [libc::c_char; MAXCOMLEN],
    flags: u32,
    uid: libc::uid_t,
    gid: libc::gid_t,
    real_uid: libc::uid_t,
    real_gid: libc::gid_t,
    saved_uid: libc::uid_t,
    saved_gid: libc::gid_t,
    reserved: u32,
}

#[repr(C)]
struct ProcTaskInfo {
    virtual_size: u64,
    resident_size: u64,
    total_user_time: u64,
    total_system_time: u64,
    threads_user_time: u64,
    threads_system_time: u64,
    policy: i32,
    faults: i32,
    pageins: i32,
    cow_faults: i32,
    messages_sent: i32,
    messages_received: i32,
    mach_syscalls: i32,
    unix_syscalls: i32,
    context_switches: i32,
    thread_count: i32,
    running_thread_count: i32,
    priority: i32,
}

#[link(name = "proc")]
unsafe extern "C" {
    fn proc_listchildpids(pid: libc::pid_t, buffer: *mut c_void, buffersize: i32) -> i32;
    fn proc_pidinfo(
        pid: libc::pid_t,
        flavor: libc::c_int,
        arg: u64,
        buffer: *mut c_void,
        buffersize: libc::c_int,
    ) -> libc::c_int;
}

pub fn child_pid_entry_count(bytes_written: i32) -> Option<usize> {
    let bytes = usize::try_from(bytes_written).ok()?;
    let pid_width = std::mem::size_of::<libc::pid_t>();
    Some(bytes / pid_width)
}

fn descendant_pid_signature(descendants: &[u32]) -> u64 {
    const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;

    descendants.iter().fold(FNV_OFFSET, |signature, &pid| {
        pid.to_le_bytes().iter().fold(signature, |sig, &byte| {
            (sig ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
        })
    })
}

const fn qualifies_libproc_status(status: u32) -> bool {
    !matches!(status, SIDL | SSTOP | SZOMB)
}

const fn libproc_state_indicates_current_activity(
    status: u32,
    cpu_time_ms: u64,
    num_running_threads: i32,
) -> bool {
    status == SRUN && cpu_time_ms > 0 && num_running_threads > 0
}

fn call_proc_listchildpids(pid: libc::pid_t, capacity: usize) -> Option<(i32, Vec<libc::pid_t>)> {
    let byte_len = capacity.checked_mul(std::mem::size_of::<libc::pid_t>())?;
    let buffer_size = i32::try_from(byte_len).ok()?;
    let mut buffer = vec![libc::pid_t::default(); capacity];
    let bytes_written =
        unsafe { proc_listchildpids(pid, buffer.as_mut_ptr().cast::<c_void>(), buffer_size) };
    Some((bytes_written, buffer))
}

fn collect_child_pids_from_buffer(buffer: Vec<libc::pid_t>, count: usize) -> Vec<u32> {
    buffer
        .into_iter()
        .take(count)
        .filter_map(|p| u32::try_from(p).ok())
        .collect()
}

fn try_read_child_pids(pid: libc::pid_t, capacity: usize) -> Option<Result<Vec<u32>, usize>> {
    let (bytes_written, buffer) = call_proc_listchildpids(pid, capacity)?;
    if bytes_written < 0 {
        return None;
    }
    if bytes_written == 0 {
        return Some(Ok(Vec::new()));
    }
    let count = child_pid_entry_count(bytes_written)?;
    if count < capacity {
        return Some(Ok(collect_child_pids_from_buffer(buffer, count)));
    }
    Some(Err(capacity.checked_mul(2)?))
}

fn list_child_pids(parent_pid: u32) -> Option<Vec<u32>> {
    let pid = libc::pid_t::try_from(parent_pid).ok()?;
    let mut capacity: usize = 32;

    loop {
        match try_read_child_pids(pid, capacity)? {
            Ok(pids) => return Some(pids),
            Err(new_capacity) => capacity = new_capacity,
        }
    }
}

fn fetch_bsd_short_info(pid: u32) -> Option<ProcBsdShortInfo> {
    let mut info = ProcBsdShortInfo {
        pid: 0,
        parent_pid: 0,
        process_group_id: 0,
        status: 0,
        command: [0; MAXCOMLEN],
        flags: 0,
        uid: 0,
        gid: 0,
        real_uid: 0,
        real_gid: 0,
        saved_uid: 0,
        saved_gid: 0,
        reserved: 0,
    };
    let pid = libc::pid_t::try_from(pid).ok()?;
    let expected = i32::try_from(std::mem::size_of::<ProcBsdShortInfo>()).ok()?;
    let bytes = unsafe {
        proc_pidinfo(
            pid,
            PROC_PIDT_SHORTBSDINFO,
            0,
            (&raw mut info).cast::<c_void>(),
            expected,
        )
    };
    (bytes == expected).then_some(info)
}

fn fetch_task_info(pid: u32) -> Option<ProcTaskInfo> {
    let mut info = ProcTaskInfo {
        virtual_size: 0,
        resident_size: 0,
        total_user_time: 0,
        total_system_time: 0,
        threads_user_time: 0,
        threads_system_time: 0,
        policy: 0,
        faults: 0,
        pageins: 0,
        cow_faults: 0,
        messages_sent: 0,
        messages_received: 0,
        mach_syscalls: 0,
        unix_syscalls: 0,
        context_switches: 0,
        thread_count: 0,
        running_thread_count: 0,
        priority: 0,
    };
    let pid = libc::pid_t::try_from(pid).ok()?;
    let expected = i32::try_from(std::mem::size_of::<ProcTaskInfo>()).ok()?;
    let bytes = unsafe {
        proc_pidinfo(
            pid,
            PROC_PIDTASKINFO,
            0,
            (&raw mut info).cast::<c_void>(),
            expected,
        )
    };
    (bytes == expected).then_some(info)
}

fn process_child_pids(
    child_pids: Vec<u32>,
    descendants: &mut Vec<u32>,
    visited: &mut HashSet<u32>,
    queue: &mut VecDeque<u32>,
) {
    child_pids
        .into_iter()
        .filter(|&p| visited.insert(p))
        .for_each(|p| {
            descendants.push(p);
            queue.push_back(p);
        });
}

fn drain_pid_queue(
    queue: &mut VecDeque<u32>,
    descendants: &mut Vec<u32>,
    visited: &mut HashSet<u32>,
) -> Option<()> {
    while let Some(pid) = queue.pop_front() {
        let child_pids = list_child_pids(pid)?;
        process_child_pids(child_pids, descendants, visited, queue);
    }
    Some(())
}

fn collect_descendant_pids(current_pid: u32) -> Option<Vec<u32>> {
    let mut descendants = Vec::new();
    let mut visited = HashSet::new();
    let mut queue = VecDeque::from([current_pid]);
    drain_pid_queue(&mut queue, &mut descendants, &mut visited)?;
    descendants.sort_unstable();
    Some(descendants)
}

struct DescendantTally {
    child_count: u32,
    active_child_count: u32,
    total_cpu_ms: u64,
    qualifying_pids: Vec<u32>,
}

fn pid_qualifies_for_parent(bsd_info: &ProcBsdShortInfo, parent_pid: u32) -> bool {
    bsd_info.process_group_id == parent_pid && qualifies_libproc_status(bsd_info.status)
}

fn cpu_time_ms_from_task(task_info: &Option<ProcTaskInfo>) -> u64 {
    task_info.as_ref().map_or(0, |info| {
        (info.total_user_time + info.total_system_time) / 1_000_000
    })
}

fn running_threads_from_task(task_info: &Option<ProcTaskInfo>) -> i32 {
    task_info
        .as_ref()
        .map_or(0, |info| info.running_thread_count)
}

fn tally_one_descendant(pid: u32, parent_pid: u32, tally: &mut DescendantTally) {
    let Some(bsd_info) = fetch_bsd_short_info(pid) else {
        return;
    };
    if !pid_qualifies_for_parent(&bsd_info, parent_pid) {
        return;
    }
    let task_info = fetch_task_info(pid);
    let cpu_time_ms = cpu_time_ms_from_task(&task_info);
    let num_running_threads = running_threads_from_task(&task_info);
    tally.child_count = tally.child_count.saturating_add(1);
    tally.total_cpu_ms += cpu_time_ms;
    if libproc_state_indicates_current_activity(bsd_info.status, cpu_time_ms, num_running_threads) {
        tally.active_child_count = tally.active_child_count.saturating_add(1);
    }
    tally.qualifying_pids.push(pid);
}

fn build_descendant_tally(parent_pid: u32, descendants: &[u32]) -> DescendantTally {
    let mut tally = DescendantTally {
        child_count: 0,
        active_child_count: 0,
        total_cpu_ms: 0,
        qualifying_pids: Vec::new(),
    };
    descendants
        .iter()
        .for_each(|&pid| tally_one_descendant(pid, parent_pid, &mut tally));
    tally
}

fn tally_to_child_info(tally: DescendantTally) -> ChildProcessInfo {
    ChildProcessInfo {
        child_count: tally.child_count,
        active_child_count: tally.active_child_count,
        cpu_time_ms: tally.total_cpu_ms,
        descendant_pid_signature: descendant_pid_signature(&tally.qualifying_pids),
    }
}

fn compute_child_info_from_descendants(
    parent_pid: u32,
    descendants: &[u32],
) -> Option<ChildProcessInfo> {
    if descendants.is_empty() {
        return None;
    }
    let tally = build_descendant_tally(parent_pid, descendants);
    if tally.child_count == 0 {
        return Some(ChildProcessInfo::NONE);
    }
    Some(tally_to_child_info(tally))
}

pub fn child_info_from_libproc(parent_pid: u32) -> Option<ChildProcessInfo> {
    let descendants = collect_descendant_pids(parent_pid)?;
    compute_child_info_from_descendants(parent_pid, &descendants)
}