aegis_tools/
background.rs1use std::collections::HashMap;
4use std::process::ExitStatus;
5
6pub struct BackgroundTaskManager {
7 tasks: HashMap<String, tokio::process::Child>,
8}
9
10impl BackgroundTaskManager {
11 pub fn new() -> Self {
13 Self {
14 tasks: HashMap::new(),
15 }
16 }
17
18 pub async fn spawn(&mut self, id: String, cmd: &str) -> anyhow::Result<()> {
21 let child = tokio::process::Command::new("sh")
22 .arg("-c")
23 .arg(cmd)
24 .stdout(std::process::Stdio::piped())
25 .stderr(std::process::Stdio::piped())
26 .spawn()?;
27 self.tasks.insert(id, child);
28 Ok(())
29 }
30
31 pub fn status(&self, id: &str) -> &'static str {
33 if self.tasks.contains_key(id) {
34 "running"
35 } else {
36 "not_found"
37 }
38 }
39
40 pub async fn cancel(&mut self, id: &str) {
45 if let Some(mut child) = self.tasks.remove(id) {
46 let _ = child.kill().await;
51 }
52 }
53
54 pub async fn wait_all(&mut self, ids: &[&str]) -> Vec<Option<ExitStatus>> {
56 let mut results = vec![];
57 for id in ids {
58 if let Some(mut child) = self.tasks.remove(*id) {
59 results.push(child.wait().await.ok());
60 } else {
61 results.push(None);
62 }
63 }
64 results
65 }
66
67 pub async fn wait_any(&mut self, ids: &[&str]) -> Option<(String, ExitStatus)> {
71 loop {
73 for id in ids {
74 let id_str = id.to_string();
75 if let Some(child) = self.tasks.get_mut(id_str.as_str()) {
76 match child.try_wait() {
77 Ok(Some(status)) => {
78 self.tasks.remove(id_str.as_str());
79 return Some((id_str, status));
80 }
81 Ok(None) => {} Err(_) => {
83 self.tasks.remove(id_str.as_str());
84 }
85 }
86 }
87 }
88 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
90 }
91 }
92}
93
94impl Default for BackgroundTaskManager {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100impl Drop for BackgroundTaskManager {
101 fn drop(&mut self) {
102 for (_, mut child) in self.tasks.drain() {
104 let _ = child.start_kill();
105 }
106 }
107}
108
109pub fn parse_progress_line(line: &str) {
112 const PREFIX: &str = "AEGIS_PROGRESS:";
113 if let Some(rest) = line.strip_prefix(PREFIX) {
114 match serde_json::from_str::<serde_json::Value>(rest.trim()) {
115 Ok(val) => eprintln!("[progress] {}", val),
116 Err(_) => eprintln!("[progress] {}", rest.trim()),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_background_task_manager_new() {
127 let mgr = BackgroundTaskManager::new();
128 assert_eq!(mgr.status("any"), "not_found");
129 }
130
131 #[test]
132 fn test_background_task_manager_default() {
133 let mgr = BackgroundTaskManager::default();
134 assert_eq!(mgr.status("any"), "not_found");
135 }
136
137 #[tokio::test]
138 async fn test_spawn_and_status() {
139 let mut mgr = BackgroundTaskManager::new();
140 mgr.spawn("task1".into(), "echo hello").await.unwrap();
141 assert_eq!(mgr.status("task1"), "running");
142 assert_eq!(mgr.status("task2"), "not_found");
143 mgr.wait_all(&["task1"]).await;
144 }
145
146 #[tokio::test]
147 async fn test_cancel_task() {
148 let mut mgr = BackgroundTaskManager::new();
149 mgr.spawn("task1".into(), "sleep 60").await.unwrap();
150 assert_eq!(mgr.status("task1"), "running");
151 mgr.cancel("task1").await;
152 assert_eq!(mgr.status("task1"), "not_found");
153 }
154
155 #[tokio::test]
156 async fn test_cancel_nonexistent_noop() {
157 let mut mgr = BackgroundTaskManager::new();
158 mgr.cancel("nonexistent").await; }
160
161 #[tokio::test]
162 async fn test_wait_all() {
163 let mut mgr = BackgroundTaskManager::new();
164 mgr.spawn("a".into(), "echo a").await.unwrap();
165 mgr.spawn("b".into(), "echo b").await.unwrap();
166 let results = mgr.wait_all(&["a", "b", "c"]).await;
167 assert_eq!(results.len(), 3);
168 assert!(results[0].is_some()); assert!(results[1].is_some()); assert!(results[2].is_none()); }
172
173 #[tokio::test]
174 async fn test_wait_any() {
175 let mut mgr = BackgroundTaskManager::new();
176 mgr.spawn("fast".into(), "echo done").await.unwrap();
177 mgr.spawn("slow".into(), "sleep 60").await.unwrap();
178 let result = mgr.wait_any(&["fast", "slow"]).await;
179 assert!(result.is_some());
180 let (id, status) = result.unwrap();
181 assert_eq!(id, "fast");
182 assert!(status.success());
183 mgr.cancel("slow").await;
184 }
185
186 #[test]
187 fn test_parse_progress_line_json() {
188 parse_progress_line("AEGIS_PROGRESS:{\"percent\":50}");
190 }
191
192 #[test]
193 fn test_parse_progress_line_plain() {
194 parse_progress_line("AEGIS_PROGRESS:halfway there");
196 }
197
198 #[test]
199 fn test_parse_progress_line_ignored() {
200 parse_progress_line("normal log output");
202 }
203
204 #[test]
205 fn test_drop_cleans_up() {
206 let mut mgr = BackgroundTaskManager::new();
207 let rt = tokio::runtime::Runtime::new().unwrap();
208 rt.block_on(async {
209 mgr.spawn("t1".into(), "sleep 60").await.unwrap();
210 });
211 drop(mgr);
213 }
215}