//! 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 using try_wait() polling so the lock is never // held across a blocking wait, allowing cancel() to acquire the lock. let job_clone = Arc::clone(&job); std::thread::spawn(move || { loop { let mut exited = false; { 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 { match child.try_wait() { Ok(Some(_)) => exited = true, Ok(None) => {} // still running Err(e) => { error!("bgbash wait error: {e}"); exited = true; } } } else { exited = true; // no child process } } // lock is dropped here — cancel() can now acquire it if exited || job_clone.cancelled.load(Ordering::SeqCst) { break; } std::thread::sleep(std::time::Duration::from_millis(50)); } }); 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().is_some_and(|c| matches!(c.try_wait(), Ok(None))) } }