process_state/
lib.rs

1//! # ProcState
2//!
3//! `procstate` is a lightweight, cross-platform library for tracking, saving, and restoring
4//! process metadata in Rust applications. Works on both Unix and Windows.
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::process::{Child};
9use std::time::SystemTime;
10use serde::{Serialize, Deserialize};
11
12/// Status of a tracked process.
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub enum ProcessStatus {
15    Running,
16    Stopped,
17    Error(String),
18}
19
20/// Metadata for a tracked process.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ProcessInfo {
23    pub pid: u32,
24    pub label: String,
25    pub command: String,
26    pub start_time: SystemTime,
27    pub status: ProcessStatus,
28}
29
30/// Core structure managing tracked processes.
31#[derive(Debug)]
32pub struct ProcessState {
33    processes: HashMap<u32, ProcessInfo>,
34    state_file: PathBuf,
35}
36
37impl ProcessState {
38    /// Create a new ProcessState instance with a namespace.
39    pub fn new<S: AsRef<str>>(namespace: S) -> Result<Self, Box<dyn std::error::Error>> {
40        let state_dir = dirs::data_dir()
41            .unwrap_or_else(|| PathBuf::from("."))
42            .join(namespace.as_ref());
43
44        std::fs::create_dir_all(&state_dir)?;
45        let state_file = state_dir.join("processes.json");
46
47        let mut state = Self {
48            processes: HashMap::new(),
49            state_file,
50        };
51        state.load_state()?;
52        Ok(state)
53    }
54
55    /// Add a process to tracking.
56    pub fn add_process(&mut self, child: &Child, label: &str, command: &str) -> Result<(), Box<dyn std::error::Error>> {
57        let pid = child.id();
58        let info = ProcessInfo {
59            pid,
60            label: label.to_string(),
61            command: command.to_string(),
62            start_time: SystemTime::now(),
63            status: ProcessStatus::Running,
64        };
65        self.processes.insert(pid, info);
66        self.save_state()?;
67        Ok(())
68    }
69
70    /// Remove a process from tracking.
71    pub fn remove_process(&mut self, pid: u32) -> Result<(), Box<dyn std::error::Error>> {
72        self.processes.remove(&pid);
73        self.save_state()?;
74        Ok(())
75    }
76
77    /// Update a process status manually.
78    pub fn update_status(&mut self, pid: u32, status: ProcessStatus) -> Result<(), Box<dyn std::error::Error>> {
79        if let Some(p) = self.processes.get_mut(&pid) {
80            p.status = status;
81            self.save_state()?;
82        }
83        Ok(())
84    }
85
86    /// Refresh process statuses.
87    /// On Unix, uses `kill -0 PID`.
88    /// On Windows, uses `try_wait()` if you have a Child handle stored externally.
89    pub fn refresh(&mut self) -> Result<(), Box<dyn std::error::Error>> {
90        let mut changed = false;
91
92        for info in self.processes.values_mut() {
93            #[cfg(unix)]
94            let is_running = std::process::Command::new("kill")
95                .arg("-0")
96                .arg(info.pid.to_string())
97                .status()
98                .map(|status| status.success())
99                .unwrap_or(false);
100
101            #[cfg(windows)]
102            let is_running = true; // Without Child handle, assume running
103
104            if !is_running && info.status != ProcessStatus::Stopped {
105                info.status = ProcessStatus::Stopped;
106                changed = true;
107            }
108        }
109
110        if changed {
111            self.save_state()?;
112        }
113
114        Ok(())
115    }
116
117    /// Get all running processes.
118    pub fn get_running(&self) -> Vec<&ProcessInfo> {
119        self.processes
120            .values()
121            .filter(|p| p.status == ProcessStatus::Running)
122            .collect()
123    }
124
125    /// Get all tracked processes.
126    pub fn get_all(&self) -> Vec<&ProcessInfo> {
127        self.processes.values().collect()
128    }
129
130    /// Save state to disk.
131    fn save_state(&self) -> Result<(), Box<dyn std::error::Error>> {
132        let serialized = serde_json::to_string_pretty(&self.processes)?;
133        std::fs::write(&self.state_file, serialized)?;
134        Ok(())
135    }
136
137    /// Load state from disk.
138    fn load_state(&mut self) -> Result<(), Box<dyn std::error::Error>> {
139        if self.state_file.exists() {
140            let content = std::fs::read_to_string(&self.state_file)?;
141            self.processes = serde_json::from_str(&content)?;
142        }
143        Ok(())
144    }
145}