robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use super::*;

pub(super) fn load_runtime_state_or_default(
    state_file: &Path,
    config_file: &Path,
    profile: Option<String>,
) -> Result<RuntimeState, String> {
    if state_file.exists() {
        load_runtime_state(state_file)
    } else {
        Ok(RuntimeState {
            api_version: ORCHESTRATION_RUNTIME_STATE_API_VERSION.to_string(),
            config_file: config_file.to_path_buf(),
            profile,
            updated_at_unix_ms: now_unix_ms(),
            processes: Vec::new(),
        })
    }
}

pub(super) fn load_runtime_state(state_file: &Path) -> Result<RuntimeState, String> {
    let content = std::fs::read_to_string(state_file)
        .map_err(|err| format!("read runtime state {} failed: {err}", state_file.display()))?;
    serde_json::from_str::<RuntimeState>(&content)
        .map_err(|err| format!("parse runtime state {} failed: {err}", state_file.display()))
}

pub(super) fn save_runtime_state(state_file: &Path, state: &RuntimeState) -> Result<(), String> {
    if let Some(parent) = state_file.parent()
        && !parent.as_os_str().is_empty()
    {
        std::fs::create_dir_all(parent).map_err(|err| {
            format!(
                "create runtime state directory {} failed: {err}",
                parent.display()
            )
        })?;
    }

    let payload = serde_json::to_string_pretty(state)
        .map_err(|err| format!("serialize runtime state failed: {err}"))?;
    std::fs::write(state_file, format!("{payload}\n"))
        .map_err(|err| format!("write runtime state {} failed: {err}", state_file.display()))
}

pub(super) fn sanitize_name(raw: &str) -> String {
    raw.chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
                ch
            } else {
                '_'
            }
        })
        .collect()
}

pub(super) fn now_unix_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|dur| dur.as_millis() as u64)
        .unwrap_or(0)
}

pub(super) fn process_alive(pid: u32) -> bool {
    #[cfg(unix)]
    {
        Command::new("kill")
            .args(["-0", &pid.to_string()])
            .status()
            .map(|status| status.success())
            .unwrap_or(false)
    }

    #[cfg(windows)]
    {
        Command::new("cmd")
            .args([
                "/C",
                &format!("tasklist /FI \"PID eq {pid}\" | findstr {pid}"),
            ])
            .status()
            .map(|status| status.success())
            .unwrap_or(false)
    }
}

pub(super) fn terminate_process(pid: u32) -> Result<(), String> {
    #[cfg(unix)]
    {
        Command::new("kill")
            .args(["-TERM", &pid.to_string()])
            .status()
            .map_err(|err| format!("send TERM to pid {pid} failed: {err}"))?;
        for _ in 0..20 {
            if !process_alive(pid) {
                return Ok(());
            }
            std::thread::sleep(Duration::from_millis(50));
        }
        Command::new("kill")
            .args(["-KILL", &pid.to_string()])
            .status()
            .map_err(|err| format!("send KILL to pid {pid} failed: {err}"))?;
        Ok(())
    }

    #[cfg(windows)]
    {
        Command::new("cmd")
            .args(["/C", &format!("taskkill /PID {pid} /T /F")])
            .status()
            .map_err(|err| format!("taskkill pid {pid} failed: {err}"))?;
        Ok(())
    }
}