mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Process management utilities for CLI commands

use std::collections::HashMap;
use std::process::{Child, Command};

/// Information about a running process
#[derive(Debug)]
pub struct ProcessInfo {
    pub name: String,
    pub pid: u32,
    pub status: ProcessStatus,
}

/// Process status
#[derive(Debug, Clone, PartialEq)]
pub enum ProcessStatus {
    Running,
    Exited(i32),
    Error,
}

/// Tracks and manages multiple child processes
///
/// This is used primarily by the `dev` and `run` commands to manage
/// node processes and infrastructure services.
pub struct ProcessTracker {
    processes: HashMap<String, Child>,
}

impl ProcessTracker {
    /// Create a new process tracker
    pub fn new() -> Self {
        Self {
            processes: HashMap::new(),
        }
    }

    /// Add a process to track
    pub fn add(&mut self, name: String, child: Child) {
        self.processes.insert(name, child);
    }

    /// Remove a process by name
    pub fn remove(&mut self, name: &str) -> Option<Child> {
        self.processes.remove(name)
    }

    /// Get the number of tracked processes
    pub fn len(&self) -> usize {
        self.processes.len()
    }

    /// Check if any processes are being tracked
    pub fn is_empty(&self) -> bool {
        self.processes.is_empty()
    }

    /// Get list of all tracked process names
    pub fn list(&self) -> Vec<String> {
        self.processes.keys().cloned().collect()
    }

    /// Get status of all processes
    pub fn get_status_map(&mut self) -> HashMap<String, String> {
        let mut status_map = HashMap::new();
        let mut to_remove = Vec::new();

        for (name, child) in self.processes.iter_mut() {
            let pid = child.id();

            match child.try_wait() {
                Ok(Some(status)) => {
                    // Process has exited
                    status_map.insert(
                        name.clone(),
                        format!("✗ exited (code: {}, was PID: {})", status.code().unwrap_or(-1), pid),
                    );
                    to_remove.push(name.clone());
                }
                Ok(None) => {
                    // Process is still running
                    status_map.insert(name.clone(), format!("running (PID: {})", pid));
                }
                Err(_) => {
                    // Error checking status
                    status_map.insert(name.clone(), format!("✗ error (PID: {})", pid));
                    to_remove.push(name.clone());
                }
            }
        }

        // Remove exited processes
        for name in to_remove {
            self.processes.remove(&name);
        }

        status_map
    }

    /// Get information about all tracked processes
    pub fn get_processes(&mut self) -> Vec<ProcessInfo> {
        let mut processes = Vec::new();
        let mut to_remove = Vec::new();

        for (name, child) in self.processes.iter_mut() {
            let pid = child.id();
            let status = match child.try_wait() {
                Ok(Some(exit_status)) => {
                    to_remove.push(name.clone());
                    ProcessStatus::Exited(exit_status.code().unwrap_or(-1))
                }
                Ok(None) => ProcessStatus::Running,
                Err(_) => {
                    to_remove.push(name.clone());
                    ProcessStatus::Error
                }
            };

            processes.push(ProcessInfo {
                name: name.clone(),
                pid,
                status,
            });
        }

        // Remove exited/errored processes
        for name in to_remove {
            self.processes.remove(&name);
        }

        processes
    }

    /// Stop all tracked processes gracefully
    pub fn cleanup(&mut self) {
        if self.processes.is_empty() {
            return;
        }

        println!("\nCleaning up processes...");

        for (name, mut child) in self.processes.drain() {
            let pid = child.id();
            println!("  ⏹  Stopping {} (PID: {})", name, pid);

            // Try graceful shutdown first (SIGTERM on Unix)
            #[cfg(unix)]
            {
                let _ = Command::new("kill").arg(pid.to_string()).output();

                // Wait briefly for graceful shutdown
                std::thread::sleep(std::time::Duration::from_millis(200));

                // Check if still running
                match child.try_wait() {
                    Ok(Some(_)) => {
                        // Process exited gracefully
                    }
                    Ok(None) => {
                        // Still running, force kill
                        println!("    Force killing {} (PID: {})", name, pid);
                        let _ = child.kill();
                        let _ = child.wait();
                    }
                    Err(_) => {
                        // Error checking status, try to kill anyway
                        let _ = child.kill();
                        let _ = child.wait();
                    }
                }
            }

            #[cfg(not(unix))]
            {
                let _ = child.kill();
                let _ = child.wait();
            }
        }

        println!("  ✓ All processes stopped\n");
    }

    /// Stop a specific process by name
    pub fn stop(&mut self, name: &str) -> Result<(), String> {
        if let Some(mut child) = self.processes.remove(name) {
            let pid = child.id();

            #[cfg(unix)]
            {
                let _ = Command::new("kill").arg(pid.to_string()).output();
                std::thread::sleep(std::time::Duration::from_millis(200));

                match child.try_wait() {
                    Ok(Some(_)) => return Ok(()),
                    Ok(None) => {
                        let _ = child.kill();
                        let _ = child.wait();
                        return Ok(());
                    }
                    Err(_) => {
                        let _ = child.kill();
                        let _ = child.wait();
                        return Ok(());
                    }
                }
            }

            #[cfg(not(unix))]
            {
                let _ = child.kill();
                let _ = child.wait();
                return Ok(());
            }
        }
        Err(format!("Process '{}' not found", name))
    }
}

impl Default for ProcessTracker {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for ProcessTracker {
    fn drop(&mut self) {
        self.cleanup();
    }
}