2026-07-20 09:04:57 +07:00
|
|
|
//! 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};
|
|
|
|
|
|
2026-07-20 10:55:09 +07:00
|
|
|
use tracing::error;
|
|
|
|
|
|
2026-07-20 09:04:57 +07:00
|
|
|
/// 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),
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-20 12:02:48 +07:00
|
|
|
// Spawn a monitor thread using try_wait() polling so the lock is never
|
|
|
|
|
// held across a blocking wait, allowing cancel() to acquire the lock.
|
2026-07-20 09:04:57 +07:00
|
|
|
let job_clone = Arc::clone(&job);
|
|
|
|
|
std::thread::spawn(move || {
|
2026-07-20 12:02:48 +07:00
|
|
|
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;
|
2026-07-20 10:55:09 +07:00
|
|
|
}
|
2026-07-20 12:02:48 +07:00
|
|
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
2026-07-20 12:02:48 +07:00
|
|
|
guard.as_mut().is_some_and(|c| matches!(c.try_wait(), Ok(None)))
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
|
|
|
|
}
|