1use crate::shutdown::ShutdownHandle;
7use anyhow::Result;
8use std::collections::HashMap;
9use std::process::Child;
10use std::time::Duration;
11use tracing::{debug, error, info, warn};
12
13#[derive(Debug, Clone)]
15pub struct ProcessInfo {
16 pub name: String,
18 pub pid: u32,
20 pub status: ProcessStatus,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum ProcessStatus {
27 Running,
29 Exited(i32),
31 Error,
33}
34
35pub struct ProcessManager {
45 processes: HashMap<String, Child>,
47 dependency_graph: HashMap<String, Vec<String>>,
49 shutdown_handle: Option<ShutdownHandle>,
51 shutdown_timeout: Duration,
53}
54
55impl ProcessManager {
56 pub fn new() -> Self {
58 Self {
59 processes: HashMap::new(),
60 dependency_graph: HashMap::new(),
61 shutdown_handle: None,
62 shutdown_timeout: Duration::from_secs(10),
63 }
64 }
65
66 pub fn with_shutdown_handle(shutdown_handle: ShutdownHandle) -> Self {
68 Self {
69 processes: HashMap::new(),
70 dependency_graph: HashMap::new(),
71 shutdown_handle: Some(shutdown_handle),
72 shutdown_timeout: Duration::from_secs(10),
73 }
74 }
75
76 pub fn track(&mut self, name: String, child: Child) {
83 let pid = child.id();
84 debug!("Tracking process: {} (PID: {})", name, pid);
85 self.processes.insert(name, child);
86 }
87
88 pub fn remove(&mut self, name: &str) -> Option<Child> {
90 self.processes.remove(name)
91 }
92
93 pub fn add_dependency(&mut self, node: String, dependency: String) {
102 self.dependency_graph.entry(node).or_default().push(dependency);
103 }
104
105 pub fn list(&self) -> Vec<String> {
107 self.processes.keys().cloned().collect()
108 }
109
110 pub fn len(&self) -> usize {
112 self.processes.len()
113 }
114
115 pub fn is_empty(&self) -> bool {
117 self.processes.is_empty()
118 }
119
120 pub fn status_all(&mut self) -> HashMap<String, ProcessStatus> {
122 let mut status_map = HashMap::new();
123 let mut to_remove = Vec::new();
124
125 for (name, child) in self.processes.iter_mut() {
126 let status = match child.try_wait() {
127 Ok(Some(exit_status)) => {
128 to_remove.push(name.clone());
129 ProcessStatus::Exited(exit_status.code().unwrap_or(-1))
130 }
131 Ok(None) => ProcessStatus::Running,
132 Err(_) => {
133 to_remove.push(name.clone());
134 ProcessStatus::Error
135 }
136 };
137 status_map.insert(name.clone(), status);
138 }
139
140 for name in to_remove {
142 self.processes.remove(&name);
143 }
144
145 status_map
146 }
147
148 pub fn processes(&mut self) -> Vec<ProcessInfo> {
150 let mut processes = Vec::new();
151 let mut to_remove = Vec::new();
152
153 for (name, child) in self.processes.iter_mut() {
154 let pid = child.id();
155 let status = match child.try_wait() {
156 Ok(Some(exit_status)) => {
157 to_remove.push(name.clone());
158 ProcessStatus::Exited(exit_status.code().unwrap_or(-1))
159 }
160 Ok(None) => ProcessStatus::Running,
161 Err(_) => {
162 to_remove.push(name.clone());
163 ProcessStatus::Error
164 }
165 };
166
167 processes.push(ProcessInfo {
168 name: name.clone(),
169 pid,
170 status,
171 });
172 }
173
174 for name in to_remove {
176 self.processes.remove(&name);
177 }
178
179 processes
180 }
181
182 pub fn shutdown_order(&self) -> Vec<String> {
186 let mut order = Vec::new();
187 let mut visited = std::collections::HashSet::new();
188
189 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
191 for (node, deps) in &self.dependency_graph {
192 for dep in deps {
193 dependents.entry(dep.clone()).or_default().push(node.clone());
194 }
195 }
196
197 let all_nodes: Vec<String> = self.list();
199
200 for node in &all_nodes {
202 if !visited.contains(node) {
203 visit_for_shutdown(node, &dependents, &mut visited, &mut order);
204 }
205 }
206
207 order
208 }
209
210 pub fn stop_graceful(&mut self, name: &str, timeout: Duration) -> Result<()> {
221 use std::thread;
222 use std::time::Instant;
223
224 if let Some(mut child) = self.remove(name) {
225 let pid = child.id();
226 debug!("Stopping process: {} (PID: {})", name, pid);
227
228 #[cfg(unix)]
230 {
231 use std::process::Command;
232 let _ = Command::new("kill").arg("--").arg(format!("-{}", pid)).output();
235
236 let _ = Command::new("kill").arg(pid.to_string()).output();
238 }
239
240 #[cfg(not(unix))]
241 {
242 child.kill()?;
244 }
245
246 let start = Instant::now();
248 loop {
249 match child.try_wait() {
250 Ok(Some(_)) => {
251 return Ok(());
253 }
254 Ok(None) => {
255 if start.elapsed() >= timeout {
257 warn!("Process {} did not stop within {:?}, force killing", name, timeout);
259
260 #[cfg(unix)]
261 {
262 use std::process::Command;
263 let _ = Command::new("kill")
265 .arg("-9")
266 .arg("--")
267 .arg(format!("-{}", pid))
268 .output();
269 }
270
271 child.kill()?;
272 let _ = child.wait();
273 return Ok(());
274 }
275 thread::sleep(Duration::from_millis(100));
276 }
277 Err(e) => {
278 error!("Error checking process {} status: {}", name, e);
280
281 #[cfg(unix)]
282 {
283 use std::process::Command;
284 let _ = Command::new("kill")
285 .arg("-9")
286 .arg("--")
287 .arg(format!("-{}", pid))
288 .output();
289 }
290
291 child.kill()?;
292 let _ = child.wait();
293 return Ok(());
294 }
295 }
296 }
297 } else {
298 Err(anyhow::anyhow!("Process '{}' not found", name))
299 }
300 }
301
302 pub fn force_kill(&mut self, name: &str) -> Result<()> {
304 if let Some(mut child) = self.remove(name) {
305 let pid = child.id();
306
307 #[cfg(unix)]
308 {
309 use std::process::Command;
310 let _ = Command::new("kill")
312 .arg("-9")
313 .arg("--")
314 .arg(format!("-{}", pid))
315 .output();
316 }
317
318 child.kill()?;
319 let _ = child.wait();
320 info!("Process {} force killed", name);
321 Ok(())
322 } else {
323 Err(anyhow::anyhow!("Process '{}' not found", name))
324 }
325 }
326
327 pub fn shutdown_all(&mut self) {
329 if self.is_empty() {
330 debug!("No processes to shut down");
331 return;
332 }
333
334 info!("Shutting down all processes in dependency order");
335
336 if let Some(ref shutdown_handle) = self.shutdown_handle {
338 shutdown_handle.shutdown();
339 }
340
341 let shutdown_order = self.shutdown_order();
342
343 for node in shutdown_order {
344 info!("Stopping {} with {:?} timeout", node, self.shutdown_timeout);
345
346 if let Err(e) = self.stop_graceful(&node, self.shutdown_timeout) {
347 warn!("Failed to stop {}: {}, attempting force kill", node, e);
348 if let Err(kill_err) = self.force_kill(&node) {
349 error!("Failed to force kill {}: {}", node, kill_err);
350 }
351 }
352 }
353
354 info!("All processes stopped");
355 }
356
357 pub fn is_running(&mut self, name: &str) -> bool {
359 if let Some(child) = self.processes.get_mut(name) {
360 matches!(child.try_wait(), Ok(None))
361 } else {
362 false
363 }
364 }
365}
366
367impl Default for ProcessManager {
368 fn default() -> Self {
369 Self::new()
370 }
371}
372
373impl Drop for ProcessManager {
374 fn drop(&mut self) {
375 self.shutdown_all();
377 }
378}
379
380fn visit_for_shutdown(
384 node: &str,
385 dependents: &HashMap<String, Vec<String>>,
386 visited: &mut std::collections::HashSet<String>,
387 order: &mut Vec<String>,
388) {
389 visited.insert(node.to_string());
390
391 if let Some(deps) = dependents.get(node) {
393 for dep in deps {
394 if !visited.contains(dep) {
395 visit_for_shutdown(dep, dependents, visited, order);
396 }
397 }
398 }
399
400 order.push(node.to_string());
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_dependency_ordering() {
410 let mut pm = ProcessManager::new();
411
412 pm.add_dependency("planner".to_string(), "motor".to_string());
414 pm.add_dependency("camera".to_string(), "planner".to_string());
415
416 pm.processes.insert("motor".to_string(), mock_child());
418 pm.processes.insert("planner".to_string(), mock_child());
419 pm.processes.insert("camera".to_string(), mock_child());
420
421 let order = pm.shutdown_order();
422
423 let camera_idx = order.iter().position(|n| n == "camera").unwrap();
425 let planner_idx = order.iter().position(|n| n == "planner").unwrap();
426 let motor_idx = order.iter().position(|n| n == "motor").unwrap();
427
428 assert!(camera_idx < planner_idx);
429 assert!(planner_idx < motor_idx);
430 }
431
432 #[cfg(unix)]
433 fn mock_child() -> Child {
434 use std::process::{Command, Stdio};
435 Command::new("sleep")
436 .arg("1000")
437 .stdin(Stdio::null())
438 .stdout(Stdio::null())
439 .stderr(Stdio::null())
440 .spawn()
441 .unwrap()
442 }
443
444 #[cfg(not(unix))]
445 fn mock_child() -> Child {
446 use std::process::{Command, Stdio};
447 Command::new("timeout")
448 .arg("/t")
449 .arg("1000")
450 .stdin(Stdio::null())
451 .stdout(Stdio::null())
452 .stderr(Stdio::null())
453 .spawn()
454 .unwrap()
455 }
456}