//! Background bash job — spawns a `bash -c` subprocess and tracks its life. use std::process::{Child, Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use tracing::error; /// A handle to a spawned background bash job. pub struct BashJob { pub id: String, pub command: String, pub process: Mutex>, pub cancelled: AtomicBool, } /// Spawn a background bash job and return a handle. /// /// The job runs until completion or until `cancel()` is called. pub fn spawn_bash_job(cmd: String) -> Arc { let child = Command::new("bash") .arg("-c") .arg(&cmd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .ok(); let job = Arc::new(BashJob { id: uuid::Uuid::new_v4().to_string(), command: cmd, process: Mutex::new(child), cancelled: AtomicBool::new(false), }); // Spawn a monitor thread (in production this would use an async task) let job_clone = Arc::clone(&job); std::thread::spawn(move || { let mut guard = match job_clone.process.lock() { Ok(g) => g, Err(poisoned) => { error!("bgbash job mutex poisoned, recovering"); poisoned.into_inner() } }; if let Some(ref mut child) = *guard { let _ = child.wait(); } }); job } impl BashJob { pub fn cancel(&self) { self.cancelled.store(true, Ordering::SeqCst); if let Ok(mut guard) = self.process.lock() { if let Some(ref mut child) = *guard { let _ = child.kill(); let _ = child.wait(); } } } pub fn is_running(&self) -> bool { if self.cancelled.load(Ordering::SeqCst) { return false; } let Ok(mut guard) = self.process.lock() else { return false; }; guard.as_mut().map_or(false, |c| { matches!(c.try_wait(), Ok(None)) }) } }