herolib-virt 0.3.13

Virtualization and container management for herolib (buildah, nerdctl, kubernetes)
Documentation
// Process Discovery for Cloud Hypervisor VMs
//
// This module provides functionality to discover running Cloud Hypervisor
// processes by VM name, similar to zosbase's Find() function.

use crate::cloudhv::errors::{CloudHypervisorError, Result};
use std::fs;
use std::path::PathBuf;

/// Information about a discovered Cloud Hypervisor process
#[derive(Debug, Clone)]
pub struct ProcessInfo {
    pub pid: u32,
    pub vm_name: String,
    pub api_socket: Option<PathBuf>,
    pub cmdline: String,
}

/// Find a Cloud Hypervisor process by VM name
///
/// Searches through /proc to find a cloud-hypervisor process with the given VM name.
/// The VM name is typically passed as part of the command line or can be inferred
/// from the API socket path.
pub fn find_vm_process(vm_name: &str) -> Result<ProcessInfo> {
    let proc_dir = fs::read_dir("/proc").map_err(|e| {
        CloudHypervisorError::Process(format!("Failed to read /proc directory: {}", e))
    })?;

    for entry in proc_dir {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        // Only process numeric directories (PIDs)
        let file_name = entry.file_name();
        let pid_str = match file_name.to_str() {
            Some(s) => s,
            None => continue,
        };

        let pid: u32 = match pid_str.parse() {
            Ok(p) => p,
            Err(_) => continue,
        };

        // Read cmdline
        let cmdline_path = entry.path().join("cmdline");
        let cmdline = match fs::read_to_string(&cmdline_path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        // Check if this is a cloud-hypervisor process
        if !cmdline.contains("cloud-hypervisor") {
            continue;
        }

        // Parse command line arguments (null-separated)
        let args: Vec<&str> = cmdline.split('\0').collect();

        // Look for API socket path
        let mut api_socket = None;
        for i in 0..args.len() {
            if args[i] == "--api-socket" && i + 1 < args.len() {
                api_socket = Some(PathBuf::from(args[i + 1]));
                break;
            }
        }

        // Check if this process matches the VM name
        // VM name can be in the API socket path (e.g., /var/run/cloud-hypervisor/vm-name)
        let matches = if let Some(ref socket) = api_socket {
            socket.to_string_lossy().contains(vm_name)
        } else {
            false
        };

        if matches {
            return Ok(ProcessInfo {
                pid,
                vm_name: vm_name.to_string(),
                api_socket,
                cmdline: cmdline.replace('\0', " "),
            });
        }
    }

    Err(CloudHypervisorError::VmNotFound(format!(
        "No Cloud Hypervisor process found for VM: {}",
        vm_name
    )))
}

/// Find all running Cloud Hypervisor processes
pub fn find_all_vm_processes() -> Result<Vec<ProcessInfo>> {
    let proc_dir = fs::read_dir("/proc").map_err(|e| {
        CloudHypervisorError::Process(format!("Failed to read /proc directory: {}", e))
    })?;

    let mut processes = Vec::new();

    for entry in proc_dir {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        let file_name = entry.file_name();
        let pid_str = match file_name.to_str() {
            Some(s) => s,
            None => continue,
        };

        let pid: u32 = match pid_str.parse() {
            Ok(p) => p,
            Err(_) => continue,
        };

        let cmdline_path = entry.path().join("cmdline");
        let cmdline = match fs::read_to_string(&cmdline_path) {
            Ok(c) => c,
            Err(_) => continue,
        };

        if !cmdline.contains("cloud-hypervisor") {
            continue;
        }

        let args: Vec<&str> = cmdline.split('\0').collect();

        let mut api_socket = None;
        for i in 0..args.len() {
            if args[i] == "--api-socket" && i + 1 < args.len() {
                api_socket = Some(PathBuf::from(args[i + 1]));
                break;
            }
        }

        // Extract VM name from socket path if available
        let vm_name = if let Some(ref socket) = api_socket {
            socket
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string()
        } else {
            format!("pid-{}", pid)
        };

        processes.push(ProcessInfo {
            pid,
            vm_name,
            api_socket,
            cmdline: cmdline.replace('\0', " "),
        });
    }

    Ok(processes)
}

/// Check if a Cloud Hypervisor process with the given PID is still running
pub fn is_process_running(pid: u32) -> bool {
    let proc_path = format!("/proc/{}", pid);
    PathBuf::from(proc_path).exists()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    #[test]
    fn test_is_process_running() {
        // PID 1 (init/systemd) should always be running
        assert!(is_process_running(1));

        // Very high PID unlikely to exist
        assert!(!is_process_running(9999999));
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_find_all_vm_processes() {
        // This test will only work if there are running CH processes
        // In most test environments, this will return an empty list
        let result = find_all_vm_processes();
        assert!(result.is_ok());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_find_vm_process_not_found() {
        // Looking for a VM that definitely doesn't exist
        let result = find_vm_process("nonexistent-vm-12345");
        assert!(result.is_err());

        if let Err(CloudHypervisorError::VmNotFound(msg)) = result {
            assert!(msg.contains("nonexistent-vm-12345"));
        } else {
            panic!("Expected VmNotFound error");
        }
    }
}