Files
zesdex/src/app/bgbash/control.rs
T
asepharyana 29a9fae3f6 ci: add GitHub Actions workflows with semantic-release auto-versioning
chore: fix all 702 clippy warnings across codebase
- auto-fix 475 via cargo clippy --fix
- fix remaining 227 manually: uninlined_format_args, redundant_closure, match_same_arms,
  underscore_binding, format_push_string, items_after_statements, needless_pass_by_value,
  clone_on_copy, case_sensitive_extension, single_match/let-else, write_with_newline,
  and other clippy lints
2026-07-13 08:12:12 +07:00

77 lines
3.0 KiB
Rust

#![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<HashMap<String, BashJob>>` (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<HashMap<...>>`, created on
/// first access.
pub(crate) fn bash_jobs_map() -> &'static Mutex<HashMap<String, BashJob>> {
static JOBS: OnceLock<Mutex<HashMap<String, BashJob>>> = 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<Vec<String>> {
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"),
}
}