#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Global registry of running background bash jobs, and control operations //! (output polling, kill) exposed to the rest of the app. //! //! Flow: a process-wide `Mutex>` (lazily built via //! `OnceLock`) holds every job spawned via `bgbash::job::spawn_bash_job` → //! `bash_output` drains new lines for a given job id → `bash_kill` removes //! a job from the map and signals its child process. //! //! Why: a single static map (rather than storing jobs in `AppStateRest`) //! lets background jobs outlive the borrow of any particular state mutation //! and be looked up by id from tool calls issued at arbitrary points. use std::collections::HashMap; use std::sync::Mutex; use std::sync::OnceLock; use super::job::BashJob; /// Lazily-initialised, process-wide registry of background bash jobs keyed /// by job id. /// /// Return: a reference to the static `Mutex>`, created on /// first access. pub(crate) fn bash_jobs_map() -> &'static Mutex> { static JOBS: OnceLock>> = OnceLock::new(); JOBS.get_or_init(|| Mutex::new(HashMap::new())) } /// Drain any newly available output lines from a background bash job. /// /// Flow: look up the job by id → repeatedly call `try_read_line()` until it /// returns `None` → collect into a Vec. /// /// Why: non-blocking; a job that hasn't produced new output yields no lines /// rather than blocking the caller. /// /// Return: `Some(lines)` if at least one new line was read, `None` if the /// job doesn't exist, the lock is poisoned, or there was nothing new to read. pub fn bash_output(id: &str) -> Option> { let mut map = bash_jobs_map().lock().ok()?; let job = map.get_mut(id)?; let mut lines = Vec::new(); while let Some(line) = job.try_read_line() { lines.push(line); } if lines.is_empty() { None } else { Some(lines) } } /// Terminate a running background bash job and remove it from the registry. /// /// Flow: remove the job from the map → if it has a valid child PID, send /// `SIGTERM` to it (unix only) → return. /// /// Why: removing from the map first means a concurrent lookup can no longer /// see the job even if the signal delivery is delayed. /// /// Return: `Ok(())` on success, `Err` if the lock is poisoned or no job /// with that id exists. pub fn bash_kill(id: &str) -> anyhow::Result<()> { let mut map = bash_jobs_map().lock().map_err(|e| anyhow::anyhow!("lock error: {e}"))?; let job = map.remove(id); match job { Some(job) => { // Actually terminate the child process via its PID if job.child_pid > 0 { #[cfg(unix)] unsafe { libc::kill(job.child_pid as i32, libc::SIGTERM); } } Ok(()) } None => anyhow::bail!("bash job '{id}' not found"), } }