use std::collections::HashMap;
use std::process::ExitStatus;
pub struct BackgroundTaskManager {
tasks: HashMap<String, tokio::process::Child>,
}
impl BackgroundTaskManager {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
}
}
pub async fn spawn(&mut self, id: String, cmd: &str) -> anyhow::Result<()> {
let child = tokio::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
self.tasks.insert(id, child);
Ok(())
}
pub fn status(&self, id: &str) -> &'static str {
if self.tasks.contains_key(id) {
"running"
} else {
"not_found"
}
}
pub async fn cancel(&mut self, id: &str) {
if let Some(mut child) = self.tasks.remove(id) {
let _ = child.kill().await;
}
}
pub async fn wait_all(&mut self, ids: &[&str]) -> Vec<Option<ExitStatus>> {
let mut results = vec![];
for id in ids {
if let Some(mut child) = self.tasks.remove(*id) {
results.push(child.wait().await.ok());
} else {
results.push(None);
}
}
results
}
pub async fn wait_any(&mut self, ids: &[&str]) -> Option<(String, ExitStatus)> {
loop {
for id in ids {
let id_str = id.to_string();
if let Some(child) = self.tasks.get_mut(id_str.as_str()) {
match child.try_wait() {
Ok(Some(status)) => {
self.tasks.remove(id_str.as_str());
return Some((id_str, status));
}
Ok(None) => {} Err(_) => {
self.tasks.remove(id_str.as_str());
}
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
}
}
impl Default for BackgroundTaskManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for BackgroundTaskManager {
fn drop(&mut self) {
for (_, mut child) in self.tasks.drain() {
let _ = child.start_kill();
}
}
}
pub fn parse_progress_line(line: &str) {
const PREFIX: &str = "AEGIS_PROGRESS:";
if let Some(rest) = line.strip_prefix(PREFIX) {
match serde_json::from_str::<serde_json::Value>(rest.trim()) {
Ok(val) => eprintln!("[progress] {}", val),
Err(_) => eprintln!("[progress] {}", rest.trim()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_background_task_manager_new() {
let mgr = BackgroundTaskManager::new();
assert_eq!(mgr.status("any"), "not_found");
}
#[test]
fn test_background_task_manager_default() {
let mgr = BackgroundTaskManager::default();
assert_eq!(mgr.status("any"), "not_found");
}
#[tokio::test]
async fn test_spawn_and_status() {
let mut mgr = BackgroundTaskManager::new();
mgr.spawn("task1".into(), "echo hello").await.unwrap();
assert_eq!(mgr.status("task1"), "running");
assert_eq!(mgr.status("task2"), "not_found");
mgr.wait_all(&["task1"]).await;
}
#[tokio::test]
async fn test_cancel_task() {
let mut mgr = BackgroundTaskManager::new();
mgr.spawn("task1".into(), "sleep 60").await.unwrap();
assert_eq!(mgr.status("task1"), "running");
mgr.cancel("task1").await;
assert_eq!(mgr.status("task1"), "not_found");
}
#[tokio::test]
async fn test_cancel_nonexistent_noop() {
let mut mgr = BackgroundTaskManager::new();
mgr.cancel("nonexistent").await; }
#[tokio::test]
async fn test_wait_all() {
let mut mgr = BackgroundTaskManager::new();
mgr.spawn("a".into(), "echo a").await.unwrap();
mgr.spawn("b".into(), "echo b").await.unwrap();
let results = mgr.wait_all(&["a", "b", "c"]).await;
assert_eq!(results.len(), 3);
assert!(results[0].is_some()); assert!(results[1].is_some()); assert!(results[2].is_none()); }
#[tokio::test]
async fn test_wait_any() {
let mut mgr = BackgroundTaskManager::new();
mgr.spawn("fast".into(), "echo done").await.unwrap();
mgr.spawn("slow".into(), "sleep 60").await.unwrap();
let result = mgr.wait_any(&["fast", "slow"]).await;
assert!(result.is_some());
let (id, status) = result.unwrap();
assert_eq!(id, "fast");
assert!(status.success());
mgr.cancel("slow").await;
}
#[test]
fn test_parse_progress_line_json() {
parse_progress_line("AEGIS_PROGRESS:{\"percent\":50}");
}
#[test]
fn test_parse_progress_line_plain() {
parse_progress_line("AEGIS_PROGRESS:halfway there");
}
#[test]
fn test_parse_progress_line_ignored() {
parse_progress_line("normal log output");
}
#[test]
fn test_drop_cleans_up() {
let mut mgr = BackgroundTaskManager::new();
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
mgr.spawn("t1".into(), "sleep 60").await.unwrap();
});
drop(mgr);
}
}