sandbox_rs/monitoring/
monitor.rs

1//! Process monitoring via /proc
2//!
3//! Provides real-time monitoring of process resources using /proc filesystem.
4//! Tracks memory usage, CPU time, thread count, and process state.
5
6use std::fs;
7use std::path::Path;
8use std::time::{Duration, Instant};
9
10use nix::sys::signal::{Signal, kill};
11use nix::unistd::Pid;
12
13use crate::errors::{Result, SandboxError};
14
15/// Process state enumeration
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ProcessState {
18    /// Process is running
19    Running,
20    /// Process is sleeping
21    Sleeping,
22    /// Process is zombie
23    Zombie,
24    /// Process state is unknown
25    Unknown,
26}
27
28impl ProcessState {
29    /// Parse state from /proc stat first character
30    pub fn from_char(c: char) -> Self {
31        match c {
32            'R' => ProcessState::Running,
33            'S' => ProcessState::Sleeping,
34            'Z' => ProcessState::Zombie,
35            _ => ProcessState::Unknown,
36        }
37    }
38}
39
40/// Process statistics snapshot
41#[derive(Debug, Clone)]
42pub struct ProcessStats {
43    /// Process ID
44    pub pid: i32,
45    /// Virtual memory size in bytes
46    pub vsize: u64,
47    /// Resident set size in bytes (physical memory)
48    pub rss: u64,
49    /// RSS in MB (for convenience)
50    pub memory_usage_mb: u64,
51    /// CPU time in milliseconds
52    pub cpu_time_ms: u64,
53    /// Number of threads
54    pub num_threads: u32,
55    /// Current process state
56    pub state: ProcessState,
57    /// Timestamp of this snapshot
58    pub timestamp: Instant,
59}
60
61impl ProcessStats {
62    /// Create stats from /proc data
63    fn from_proc(pid: i32, timestamp: Instant) -> Result<Self> {
64        let stat_path = format!("/proc/{}/stat", pid);
65        let status_path = format!("/proc/{}/status", pid);
66
67        // Read /proc/{pid}/stat
68        let stat_content = fs::read_to_string(&stat_path).map_err(|e| {
69            SandboxError::ProcessMonitoring(format!("Failed to read {}: {}", stat_path, e))
70        })?;
71
72        // Parse stat: pid (comm) state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime priority nice num_threads ...
73        let parts: Vec<&str> = stat_content.split_whitespace().collect();
74        if parts.len() < 20 {
75            return Err(SandboxError::ProcessMonitoring(
76                "Invalid /proc/stat format".to_string(),
77            ));
78        }
79
80        let state = ProcessState::from_char(parts[2].chars().next().unwrap_or('?'));
81        let utime: u64 = parts[13]
82            .parse()
83            .map_err(|_| SandboxError::ProcessMonitoring("Invalid utime".to_string()))?;
84        let stime: u64 = parts[14]
85            .parse()
86            .map_err(|_| SandboxError::ProcessMonitoring("Invalid stime".to_string()))?;
87        let num_threads: u32 = parts[19]
88            .parse()
89            .map_err(|_| SandboxError::ProcessMonitoring("Invalid num_threads".to_string()))?;
90        let vsize: u64 = parts[22]
91            .parse()
92            .map_err(|_| SandboxError::ProcessMonitoring("Invalid vsize".to_string()))?;
93        let rss: u64 = parts[23]
94            .parse()
95            .map_err(|_| SandboxError::ProcessMonitoring("Invalid rss".to_string()))?;
96
97        // Read /proc/{pid}/status for additional info (placeholder for future enhancements)
98        let _status_content = fs::read_to_string(&status_path).unwrap_or_default();
99
100        // Calculate CPU time in milliseconds
101        // Kernel reports in clock ticks, get actual CLK_TCK from system
102        let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as u64;
103        let cpu_time_ms = if clk_tck > 0 {
104            ((utime + stime) * 1000) / clk_tck
105        } else {
106            0
107        };
108
109        // RSS is in pages, convert to bytes using actual page size
110        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
111        let rss_bytes = rss * page_size;
112        let memory_usage_mb = rss_bytes / (1024 * 1024);
113
114        Ok(ProcessStats {
115            pid,
116            vsize,
117            rss: rss_bytes,
118            memory_usage_mb,
119            cpu_time_ms,
120            num_threads,
121            state,
122            timestamp,
123        })
124    }
125}
126
127/// Process monitor for tracking sandbox resource usage
128pub struct ProcessMonitor {
129    pid: Pid,
130    creation_time: Instant,
131    peak_memory_mb: u64,
132    last_stats: Option<ProcessStats>,
133}
134
135impl ProcessMonitor {
136    /// Create new monitor for process
137    pub fn new(pid: Pid) -> Result<Self> {
138        // Verify process exists
139        let stat_path = format!("/proc/{}/stat", pid.as_raw());
140        if !Path::new(&stat_path).exists() {
141            return Err(SandboxError::ProcessMonitoring(format!(
142                "Process {} not found",
143                pid
144            )));
145        }
146
147        Ok(ProcessMonitor {
148            pid,
149            creation_time: Instant::now(),
150            peak_memory_mb: 0,
151            last_stats: None,
152        })
153    }
154
155    /// Collect current statistics
156    pub fn collect_stats(&mut self) -> Result<ProcessStats> {
157        let now = Instant::now();
158        let stats = ProcessStats::from_proc(self.pid.as_raw(), now)?;
159
160        // Track peak memory
161        if stats.memory_usage_mb > self.peak_memory_mb {
162            self.peak_memory_mb = stats.memory_usage_mb;
163        }
164
165        self.last_stats = Some(stats.clone());
166        Ok(stats)
167    }
168
169    /// Get peak memory usage since monitor creation (in MB)
170    pub fn peak_memory_mb(&self) -> u64 {
171        self.peak_memory_mb
172    }
173
174    /// Get elapsed time since monitor creation
175    pub fn elapsed(&self) -> Duration {
176        self.creation_time.elapsed()
177    }
178
179    /// Check if process is still alive
180    pub fn is_alive(&self) -> Result<bool> {
181        let stat_path = format!("/proc/{}/stat", self.pid.as_raw());
182        Ok(Path::new(&stat_path).exists())
183    }
184
185    /// Send SIGTERM (graceful shutdown)
186    pub fn send_sigterm(&self) -> Result<()> {
187        kill(self.pid, Signal::SIGTERM)
188            .map_err(|e| SandboxError::Syscall(format!("Failed to send SIGTERM: {}", e)))
189    }
190
191    /// Send SIGKILL (force termination)
192    pub fn send_sigkill(&self) -> Result<()> {
193        kill(self.pid, Signal::SIGKILL)
194            .map_err(|e| SandboxError::Syscall(format!("Failed to send SIGKILL: {}", e)))
195    }
196
197    /// Graceful shutdown: SIGTERM → wait → SIGKILL
198    pub fn graceful_shutdown(&self, wait_duration: Duration) -> Result<()> {
199        // First try SIGTERM
200        self.send_sigterm()?;
201
202        // Wait for process to exit
203        let start = Instant::now();
204        while start.elapsed() < wait_duration && self.is_alive()? {
205            std::thread::sleep(Duration::from_millis(10));
206        }
207
208        // If still alive, SIGKILL
209        if self.is_alive()? {
210            self.send_sigkill()?;
211        }
212
213        Ok(())
214    }
215
216    /// Get last collected stats
217    pub fn last_stats(&self) -> Option<&ProcessStats> {
218        self.last_stats.as_ref()
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_process_state_from_char() {
228        assert_eq!(ProcessState::from_char('R'), ProcessState::Running);
229        assert_eq!(ProcessState::from_char('S'), ProcessState::Sleeping);
230        assert_eq!(ProcessState::from_char('Z'), ProcessState::Zombie);
231        assert_eq!(ProcessState::from_char('X'), ProcessState::Unknown);
232    }
233
234    #[test]
235    fn test_process_stats_creation() {
236        // We can at least create stats for the test runner process itself
237        let pid = std::process::id() as i32;
238        let timestamp = Instant::now();
239        let result = ProcessStats::from_proc(pid, timestamp);
240        assert!(result.is_ok());
241
242        if let Ok(stats) = result {
243            assert_eq!(stats.pid, pid);
244            assert!(stats.memory_usage_mb > 0);
245        }
246    }
247
248    #[test]
249    fn test_process_monitor_new() {
250        let pid = Pid::from_raw(std::process::id() as i32);
251        let result = ProcessMonitor::new(pid);
252        assert!(result.is_ok());
253    }
254
255    #[test]
256    fn test_process_monitor_is_alive() {
257        let pid = Pid::from_raw(std::process::id() as i32);
258        let monitor = ProcessMonitor::new(pid).unwrap();
259        assert!(monitor.is_alive().unwrap());
260    }
261
262    #[test]
263    fn test_process_monitor_collect_stats() {
264        let pid = Pid::from_raw(std::process::id() as i32);
265        let mut monitor = ProcessMonitor::new(pid).unwrap();
266        let stats = monitor.collect_stats().unwrap();
267
268        assert_eq!(stats.pid, pid.as_raw());
269        assert!(stats.memory_usage_mb > 0);
270        assert_eq!(monitor.peak_memory_mb(), stats.memory_usage_mb);
271    }
272
273    #[test]
274    fn test_process_monitor_peak_memory() {
275        let pid = Pid::from_raw(std::process::id() as i32);
276        let mut monitor = ProcessMonitor::new(pid).unwrap();
277
278        monitor.collect_stats().unwrap();
279        let peak1 = monitor.peak_memory_mb();
280
281        monitor.collect_stats().unwrap();
282        let peak2 = monitor.peak_memory_mb();
283
284        assert!(peak1 > 0);
285        assert!(peak2 >= peak1);
286    }
287
288    #[test]
289    fn test_process_stats_from_proc_missing_file() {
290        let invalid_pid = 9_999_999i32;
291        let timestamp = Instant::now();
292        let result = ProcessStats::from_proc(invalid_pid, timestamp);
293        assert!(result.is_err());
294    }
295
296    #[test]
297    fn test_process_stats_from_proc_invalid_format() {
298        let pid = std::process::id() as i32;
299        let timestamp = Instant::now();
300        let result = ProcessStats::from_proc(pid, timestamp);
301        assert!(result.is_ok());
302    }
303}