malvin 0.2.5

Non-interactive research and coding agent
use std::collections::HashSet;

#[path = "unix_process_group_teardown_timing.rs"]
mod unix_process_group_teardown_timing;

use super::unix_process_group_kill_targets::kill_targets_for_teardown;
use super::unix_process_group_ps::{signal_pid, signal_process_group};
use unix_process_group_teardown_timing::{
    teardown_kill_after_polls, teardown_poll_interval, teardown_total_cap,
    test_fast_acp_teardown_enabled,
};

#[allow(unused_imports)]
pub(crate) use unix_process_group_teardown_timing::{
    shutdown_cancel_timeout, shutdown_child_wait_timeout,
};

#[derive(Default)]
struct TeardownPollState {
    sigterm_pids: HashSet<u32>,
    sigkill_pids: HashSet<u32>,
    pg_sigterm: bool,
    pg_sigkill: bool,
    polls: u32,
}

fn escalate_pid(pid: u32, state: &mut TeardownPollState, force_kill: bool) {
    if state.sigkill_pids.contains(&pid) {
        return;
    }
    if state.sigterm_pids.insert(pid) {
        signal_pid(pid, 15);
    } else if (force_kill || state.polls >= teardown_kill_after_polls())
        && state.sigkill_pids.insert(pid)
    {
        signal_pid(pid, 9);
    }
}

fn teardown_poll_tick(
    process_group_id: Option<u32>,
    spawn_baseline: Option<&HashSet<u32>>,
    state: &mut TeardownPollState,
    force_kill: bool,
) {
    let targets = kill_targets_for_teardown(process_group_id, spawn_baseline);
    for pid in targets {
        escalate_pid(pid, state, force_kill);
    }
    let Some(pgid) = process_group_id else {
        return;
    };
    if !state.pg_sigterm {
        signal_pid(pgid, 15);
        signal_process_group(pgid, 15);
        state.pg_sigterm = true;
        return;
    }
    if (force_kill || state.polls >= teardown_kill_after_polls()) && !state.pg_sigkill {
        signal_pid(pgid, 9);
        signal_process_group(pgid, 9);
        state.pg_sigkill = true;
    }
}

fn teardown_agent_sandbox_fast_tick(
    process_group_id: Option<u32>,
    baseline_opt: Option<&HashSet<u32>>,
) {
    if baseline_opt.is_none() {
        if let Some(pgid) = process_group_id {
            signal_pid(pgid, 9);
            signal_process_group(pgid, 9);
        }
        return;
    }
    let mut state = TeardownPollState::default();
    teardown_poll_tick(process_group_id, baseline_opt, &mut state, true);
}

fn teardown_agent_sandbox_slow_blocking(
    process_group_id: Option<u32>,
    baseline_opt: Option<&HashSet<u32>>,
    baseline_for_alive: &HashSet<u32>,
) {
    let mut state = TeardownPollState::default();
    let start = std::time::Instant::now();
    while crate::malvin_sandbox::sandbox_still_alive(process_group_id, baseline_for_alive)
        && start.elapsed() < teardown_total_cap()
    {
        teardown_poll_tick(process_group_id, baseline_opt, &mut state, false);
        if !crate::malvin_sandbox::sandbox_still_alive(process_group_id, baseline_for_alive) {
            break;
        }
        std::thread::sleep(teardown_poll_interval());
        state.polls = state.polls.saturating_add(1);
    }
    if crate::malvin_sandbox::sandbox_still_alive(process_group_id, baseline_for_alive) {
        teardown_poll_tick(process_group_id, baseline_opt, &mut state, true);
    }
}

pub(crate) fn teardown_agent_sandbox_blocking(
    process_group_id: Option<u32>,
    spawn_baseline: &HashSet<u32>,
) {
    let orphan_scan = !spawn_baseline.is_empty();
    let affiliated_scan = process_group_id.is_none()
        && super::unix_process_group_kill_targets::has_noted_session_affiliated_pids();
    if process_group_id.is_none() && !orphan_scan && !affiliated_scan {
        return;
    }
    let baseline_opt = (orphan_scan || affiliated_scan).then_some(spawn_baseline);
    if test_fast_acp_teardown_enabled() {
        teardown_agent_sandbox_fast_tick(process_group_id, baseline_opt);
        return;
    }
    teardown_agent_sandbox_slow_blocking(process_group_id, baseline_opt, spawn_baseline);
}

pub(crate) fn teardown_agent_sandbox_for_interrupt(
    process_group_id: Option<u32>,
    spawn_baseline: &HashSet<u32>,
) {
    let orphan_scan = !spawn_baseline.is_empty();
    let affiliated_scan = process_group_id.is_none()
        && super::unix_process_group_kill_targets::has_noted_session_affiliated_pids();
    if process_group_id.is_none() && !orphan_scan && !affiliated_scan {
        return;
    }
    if orphan_scan || affiliated_scan {
        let targets = kill_targets_for_teardown(process_group_id, Some(spawn_baseline));
        for pid in targets {
            signal_pid(pid, 9);
        }
    }
    if let Some(pgid) = process_group_id {
        signal_process_group(pgid, 9);
    }
}

async fn teardown_agent_sandbox_slow_async(
    process_group_id: Option<u32>,
    baseline_opt: Option<&HashSet<u32>>,
    baseline_for_alive: &HashSet<u32>,
) {
    let baseline_for_alive = baseline_for_alive.clone();
    let baseline_owned = baseline_opt.cloned();
    tokio::task::spawn_blocking(move || {
        teardown_agent_sandbox_slow_blocking(
            process_group_id,
            baseline_owned.as_ref(),
            &baseline_for_alive,
        );
    })
    .await
    .ok();
}

pub(crate) async fn teardown_agent_sandbox_async(
    process_group_id: Option<u32>,
    spawn_baseline: Option<&HashSet<u32>>,
) {
    let orphan_scan = spawn_baseline.is_some_and(|b| !b.is_empty());
    let affiliated_scan = process_group_id.is_none()
        && super::unix_process_group_kill_targets::has_noted_session_affiliated_pids();
    if process_group_id.is_none() && !orphan_scan && !affiliated_scan {
        return;
    }
    let empty_baseline = HashSet::new();
    let baseline_for_alive = spawn_baseline.unwrap_or(&empty_baseline);
    let baseline_opt = (orphan_scan || affiliated_scan).then_some(baseline_for_alive);
    if test_fast_acp_teardown_enabled() {
        teardown_agent_sandbox_fast_tick(process_group_id, baseline_opt);
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
        return;
    }
    teardown_agent_sandbox_slow_async(process_group_id, baseline_opt, baseline_for_alive).await;
}

pub(crate) fn reap_fixed_pid_targets_blocking(targets: &HashSet<u32>) {
    if targets.is_empty() {
        return;
    }
    let mut state = TeardownPollState::default();
    let start = std::time::Instant::now();
    let any_alive = || targets.iter().any(|pid| crate::acp::pid_alive(*pid));
    while any_alive() && start.elapsed() < teardown_total_cap() {
        for pid in targets {
            escalate_pid(*pid, &mut state, false);
        }
        if !any_alive() {
            break;
        }
        std::thread::sleep(teardown_poll_interval());
        state.polls = state.polls.saturating_add(1);
    }
    for pid in targets {
        if crate::acp::pid_alive(*pid) {
            signal_pid(*pid, 9);
        }
    }
}

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

    #[test]
    fn kiss_cov_teardown_agent_sandbox_blocking() {
        let _ = teardown_agent_sandbox_blocking;
    }
    #[test]
    fn kiss_cov_teardown_agent_sandbox_for_interrupt() {
        let _ = teardown_agent_sandbox_for_interrupt;
    }
    #[test]
    fn kiss_cov_teardown_agent_sandbox_async() {
        let _ = teardown_agent_sandbox_async;
    }
    #[test]
    fn kiss_cov_teardown_poll_tick() {
        let _ = teardown_poll_tick;
    }
    #[test]
    fn kiss_cov_teardown_poll_state() {
        let _ = std::mem::size_of::<TeardownPollState>();
    }
    #[test]
    fn kiss_cov_reap_fixed_pid_targets_blocking() {
        let _ = reap_fixed_pid_targets_blocking;
    }
    #[test]
    fn kiss_cov_escalate_pid() {
        let _ = escalate_pid;
    }
}