Skip to main content

tmai_core/tmux/
process.rs

1use parking_lot::RwLock;
2use std::collections::HashMap;
3use std::time::{Duration, Instant};
4
5/// Cache key offset for child process cmdline lookups.
6/// We store direct PID cmdline at `pid`, child cmdline at `pid + CHILD_CACHE_OFFSET`,
7/// to avoid separate HashMaps while keeping O(1) lookup.
8const CHILD_CACHE_OFFSET: u32 = 1_000_000_000;
9
10/// Cache key offset for environment variable lookups.
11const ENV_CACHE_OFFSET: u32 = 2_000_000_000;
12
13/// Cached process information
14#[derive(Debug, Clone)]
15pub struct ProcessInfo {
16    /// Process command line
17    pub cmdline: String,
18    /// When this entry was last updated
19    pub last_update: Instant,
20}
21
22/// Cache for process information to reduce /proc reads
23pub struct ProcessCache {
24    /// Cached process info by PID
25    cache: RwLock<HashMap<u32, ProcessInfo>>,
26    /// How long entries remain valid
27    ttl: Duration,
28}
29
30impl ProcessCache {
31    /// Create a new process cache with default TTL (5 seconds)
32    pub fn new() -> Self {
33        Self {
34            cache: RwLock::new(HashMap::new()),
35            ttl: Duration::from_secs(5),
36        }
37    }
38
39    /// Create a new process cache with custom TTL
40    pub fn with_ttl(ttl: Duration) -> Self {
41        Self {
42            cache: RwLock::new(HashMap::new()),
43            ttl,
44        }
45    }
46
47    /// Get the command line for a process, using cache if available
48    pub fn get_cmdline(&self, pid: u32) -> Option<String> {
49        // Check cache first
50        {
51            let cache = self.cache.read();
52            if let Some(info) = cache.get(&pid) {
53                if info.last_update.elapsed() < self.ttl {
54                    return Some(info.cmdline.clone());
55                }
56            }
57        }
58
59        // Read from /proc
60        let cmdline = self.read_cmdline(pid)?;
61
62        // Update cache
63        {
64            let mut cache = self.cache.write();
65            cache.insert(
66                pid,
67                ProcessInfo {
68                    cmdline: cmdline.clone(),
69                    last_update: Instant::now(),
70                },
71            );
72        }
73
74        Some(cmdline)
75    }
76
77    /// Read command line directly from /proc
78    fn read_cmdline(&self, pid: u32) -> Option<String> {
79        let path = format!("/proc/{}/cmdline", pid);
80        std::fs::read_to_string(&path)
81            .ok()
82            .map(|s| s.replace('\0', " ").trim().to_string())
83    }
84
85    /// Get cmdline of first child process (for detecting agents running under shell)
86    pub fn get_child_cmdline(&self, pid: u32) -> Option<String> {
87        // Check cache first with child_ prefix
88        let cache_key = pid + CHILD_CACHE_OFFSET;
89        {
90            let cache = self.cache.read();
91            if let Some(info) = cache.get(&cache_key) {
92                if info.last_update.elapsed() < self.ttl {
93                    return Some(info.cmdline.clone());
94                }
95            }
96        }
97
98        // Find child processes
99        let children_path = format!("/proc/{}/task/{}/children", pid, pid);
100        let children = std::fs::read_to_string(&children_path).ok()?;
101
102        // Get first child's cmdline
103        let child_pid: u32 = children.split_whitespace().next()?.parse().ok()?;
104        let cmdline = self.read_cmdline(child_pid)?;
105
106        // Update cache
107        {
108            let mut cache = self.cache.write();
109            cache.insert(
110                cache_key,
111                ProcessInfo {
112                    cmdline: cmdline.clone(),
113                    last_update: Instant::now(),
114                },
115            );
116        }
117
118        Some(cmdline)
119    }
120
121    /// Clear expired entries from the cache
122    pub fn cleanup(&self) {
123        let mut cache = self.cache.write();
124        cache.retain(|_, info| info.last_update.elapsed() < self.ttl);
125    }
126
127    /// Read a specific environment variable from a process
128    ///
129    /// Reads `/proc/{pid}/environ` and extracts the value of the given variable.
130    /// Returns None on any error (permission denied, process gone, etc.)
131    pub fn get_env_var(&self, pid: u32, var_name: &str) -> Option<String> {
132        let _cache_key = pid + ENV_CACHE_OFFSET;
133
134        // Read from /proc directly since env reads are infrequent
135        let environ_path = format!("/proc/{}/environ", pid);
136        let content = std::fs::read(&environ_path).ok()?;
137
138        let prefix = format!("{}=", var_name);
139
140        // environ is null-byte separated
141        for entry in content.split(|&b| b == 0) {
142            if let Ok(entry_str) = std::str::from_utf8(entry) {
143                if let Some(value) = entry_str.strip_prefix(&prefix) {
144                    return Some(value.to_string());
145                }
146            }
147        }
148
149        None
150    }
151
152    /// Clear all entries from the cache
153    pub fn clear(&self) {
154        let mut cache = self.cache.write();
155        cache.clear();
156    }
157
158    /// Get the number of cached entries
159    pub fn len(&self) -> usize {
160        self.cache.read().len()
161    }
162
163    /// Check if the cache is empty
164    pub fn is_empty(&self) -> bool {
165        self.cache.read().is_empty()
166    }
167}
168
169impl Default for ProcessCache {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_cache_creation() {
181        let cache = ProcessCache::new();
182        assert!(cache.is_empty());
183    }
184
185    #[test]
186    fn test_cache_with_ttl() {
187        let cache = ProcessCache::with_ttl(Duration::from_secs(10));
188        assert!(cache.is_empty());
189    }
190
191    #[test]
192    fn test_cache_clear() {
193        let cache = ProcessCache::new();
194        // Can't easily test with real PIDs in unit tests
195        cache.clear();
196        assert!(cache.is_empty());
197    }
198}