process_state 1.0.0

A Rust library that lets you track, cache, and manage commands like a process manager
Documentation
//! # ProcState
//!
//! `procstate` is a lightweight, cross-platform library for tracking, saving, and restoring
//! process metadata in Rust applications. Works on both Unix and Windows.

use std::collections::HashMap;
use std::path::PathBuf;
use std::process::{Child};
use std::time::SystemTime;
use serde::{Serialize, Deserialize};

/// Status of a tracked process.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProcessStatus {
    Running,
    Stopped,
    Error(String),
}

/// Metadata for a tracked process.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessInfo {
    pub pid: u32,
    pub label: String,
    pub command: String,
    pub start_time: SystemTime,
    pub status: ProcessStatus,
}

/// Core structure managing tracked processes.
#[derive(Debug)]
pub struct ProcessState {
    processes: HashMap<u32, ProcessInfo>,
    state_file: PathBuf,
}

impl ProcessState {
    /// Create a new ProcessState instance with a namespace.
    pub fn new<S: AsRef<str>>(namespace: S) -> Result<Self, Box<dyn std::error::Error>> {
        let state_dir = dirs::data_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(namespace.as_ref());

        std::fs::create_dir_all(&state_dir)?;
        let state_file = state_dir.join("processes.json");

        let mut state = Self {
            processes: HashMap::new(),
            state_file,
        };
        state.load_state()?;
        Ok(state)
    }

    /// Add a process to tracking.
    pub fn add_process(&mut self, child: &Child, label: &str, command: &str) -> Result<(), Box<dyn std::error::Error>> {
        let pid = child.id();
        let info = ProcessInfo {
            pid,
            label: label.to_string(),
            command: command.to_string(),
            start_time: SystemTime::now(),
            status: ProcessStatus::Running,
        };
        self.processes.insert(pid, info);
        self.save_state()?;
        Ok(())
    }

    /// Remove a process from tracking.
    pub fn remove_process(&mut self, pid: u32) -> Result<(), Box<dyn std::error::Error>> {
        self.processes.remove(&pid);
        self.save_state()?;
        Ok(())
    }

    /// Update a process status manually.
    pub fn update_status(&mut self, pid: u32, status: ProcessStatus) -> Result<(), Box<dyn std::error::Error>> {
        if let Some(p) = self.processes.get_mut(&pid) {
            p.status = status;
            self.save_state()?;
        }
        Ok(())
    }

    /// Refresh process statuses.
    /// On Unix, uses `kill -0 PID`.
    /// On Windows, uses `try_wait()` if you have a Child handle stored externally.
    pub fn refresh(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let mut changed = false;

        for info in self.processes.values_mut() {
            #[cfg(unix)]
            let is_running = std::process::Command::new("kill")
                .arg("-0")
                .arg(info.pid.to_string())
                .status()
                .map(|status| status.success())
                .unwrap_or(false);

            #[cfg(windows)]
            let is_running = true; // Without Child handle, assume running

            if !is_running && info.status != ProcessStatus::Stopped {
                info.status = ProcessStatus::Stopped;
                changed = true;
            }
        }

        if changed {
            self.save_state()?;
        }

        Ok(())
    }

    /// Get all running processes.
    pub fn get_running(&self) -> Vec<&ProcessInfo> {
        self.processes
            .values()
            .filter(|p| p.status == ProcessStatus::Running)
            .collect()
    }

    /// Get all tracked processes.
    pub fn get_all(&self) -> Vec<&ProcessInfo> {
        self.processes.values().collect()
    }

    /// Save state to disk.
    fn save_state(&self) -> Result<(), Box<dyn std::error::Error>> {
        let serialized = serde_json::to_string_pretty(&self.processes)?;
        std::fs::write(&self.state_file, serialized)?;
        Ok(())
    }

    /// Load state from disk.
    fn load_state(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        if self.state_file.exists() {
            let content = std::fs::read_to_string(&self.state_file)?;
            self.processes = serde_json::from_str(&content)?;
        }
        Ok(())
    }
}