Files
zesdex/apps/infrastructure/src/bgbash/job.rs
T

95 lines
2.8 KiB
Rust
Raw Normal View History

//! 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<Option<Child>>,
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<BashJob> {
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)))
}
}