use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
const RUN_TIMEOUT: Duration = Duration::from_secs(60);
const WAIT_TICK: Duration = Duration::from_millis(100);
pub(crate) struct UpdateRunner {
command: Option<String>,
done: Option<Arc<AtomicBool>>,
}
impl Default for UpdateRunner {
fn default() -> Self {
Self::new()
}
}
impl UpdateRunner {
pub(crate) fn new() -> Self {
Self {
command: None,
done: None,
}
}
pub(crate) fn configure(&mut self, command: Option<String>) {
self.command = command;
}
pub(crate) fn trigger_if_idle(&mut self) -> bool {
if let Some(done) = &self.done
&& done.load(Ordering::Relaxed)
{
self.done = None;
}
let Some(command) = self.command.clone() else {
return false;
};
if self.done.is_some() {
return false; }
let done = Arc::new(AtomicBool::new(false));
let worker_done = Arc::clone(&done);
self.done = Some(done);
std::thread::spawn(move || {
run_command(&command);
worker_done.store(true, Ordering::Relaxed);
});
true
}
}
fn run_command(command: &str) {
let spawned = std::process::Command::new("sh")
.arg("-c")
.arg(command)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn();
let mut child = match spawned {
Ok(child) => child,
Err(e) => {
log::warn!("agent-usage update command failed to spawn: {e}");
return;
}
};
let started = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
if status.success() {
log::info!("agent-usage update command finished");
} else {
use std::io::Read;
let stderr = child
.stderr
.take()
.and_then(|mut s| {
let mut buf = String::new();
s.read_to_string(&mut buf).ok().map(|_| buf)
})
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "none".to_string());
log::warn!("agent-usage update command exited {status} (stderr: {stderr})");
}
return;
}
Ok(None) if started.elapsed() >= RUN_TIMEOUT => {
let _ = child.kill();
let _ = child.wait();
log::warn!(
"agent-usage update command killed after {}s: {command}",
RUN_TIMEOUT.as_secs()
);
return;
}
Ok(None) => std::thread::sleep(WAIT_TICK),
Err(e) => {
log::warn!("agent-usage update command wait failed: {e}");
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_command_configured_never_spawns() {
let mut runner = UpdateRunner::new();
assert!(!runner.trigger_if_idle());
}
#[test]
fn fast_command_completes_and_runner_reaps() {
let mut runner = UpdateRunner::new();
runner.configure(Some("echo updated".to_string()));
assert!(runner.trigger_if_idle(), "first run spawns");
let done = runner.done.clone().expect("run in flight");
let deadline = Instant::now() + Duration::from_secs(5);
while !done.load(Ordering::Relaxed) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(done.load(Ordering::Relaxed), "echo run finishes");
assert!(
runner.trigger_if_idle(),
"finished run is reaped and retriggerable"
);
}
#[test]
fn concurrent_trigger_while_busy_is_refused() {
let mut runner = UpdateRunner::new();
runner.configure(Some("sleep 1".to_string()));
assert!(runner.trigger_if_idle(), "first run spawns");
assert!(!runner.trigger_if_idle(), "second run refused while busy");
let done = runner.done.clone().expect("run in flight");
let deadline = Instant::now() + Duration::from_secs(5);
while !done.load(Ordering::Relaxed) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
}
}