agent_tui/ipc/
process.rs

1//! Process control for daemon lifecycle management.
2//!
3//! This module provides an abstraction over process control operations,
4//! enabling testability through the `ProcessController` trait.
5
6/// Signal types for process control.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Signal {
9    /// Graceful shutdown (SIGTERM)
10    Term,
11    /// Force kill (SIGKILL)
12    Kill,
13}
14
15/// Result of checking if a process exists.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ProcessStatus {
18    /// Process is running
19    Running,
20    /// Process not found (ESRCH)
21    NotFound,
22    /// Process exists but no permission to signal (EPERM)
23    NoPermission,
24}
25
26/// Trait for process control operations.
27///
28/// This abstraction enables testing of daemon lifecycle logic
29/// without actually sending signals to processes.
30pub trait ProcessController: Send + Sync {
31    /// Check if a process exists.
32    fn check_process(&self, pid: u32) -> Result<ProcessStatus, std::io::Error>;
33
34    /// Send a signal to a process.
35    fn send_signal(&self, pid: u32, signal: Signal) -> Result<(), std::io::Error>;
36}
37
38/// Unix implementation using libc.
39pub struct UnixProcessController;
40
41impl ProcessController for UnixProcessController {
42    fn check_process(&self, pid: u32) -> Result<ProcessStatus, std::io::Error> {
43        let pid_t: libc::pid_t = pid.try_into().map_err(|_| {
44            std::io::Error::new(std::io::ErrorKind::InvalidInput, "PID out of range")
45        })?;
46
47        // SAFETY: kill(pid, 0) checks process existence without sending a signal.
48        // The pid is validated above.
49        let result = unsafe { libc::kill(pid_t, 0) };
50        if result == 0 {
51            return Ok(ProcessStatus::Running);
52        }
53
54        let err = std::io::Error::last_os_error();
55        match err.raw_os_error() {
56            Some(libc::ESRCH) => Ok(ProcessStatus::NotFound),
57            Some(libc::EPERM) => Ok(ProcessStatus::NoPermission),
58            _ => Err(err),
59        }
60    }
61
62    fn send_signal(&self, pid: u32, signal: Signal) -> Result<(), std::io::Error> {
63        let pid_t: libc::pid_t = pid.try_into().map_err(|_| {
64            std::io::Error::new(std::io::ErrorKind::InvalidInput, "PID out of range")
65        })?;
66
67        let sig = match signal {
68            Signal::Term => libc::SIGTERM,
69            Signal::Kill => libc::SIGKILL,
70        };
71
72        // SAFETY: Sending signal to validated PID.
73        let result = unsafe { libc::kill(pid_t, sig) };
74        if result == 0 {
75            Ok(())
76        } else {
77            Err(std::io::Error::last_os_error())
78        }
79    }
80}
81
82#[cfg(test)]
83pub mod mock {
84    use super::*;
85    use std::collections::HashMap;
86    use std::sync::Mutex;
87
88    /// Mock process controller for testing.
89    pub struct MockProcessController {
90        process_states: Mutex<HashMap<u32, ProcessStatus>>,
91        signals_sent: Mutex<Vec<(u32, Signal)>>,
92        check_error: Mutex<Option<std::io::Error>>,
93        signal_error: Mutex<Option<std::io::Error>>,
94    }
95
96    impl Default for MockProcessController {
97        fn default() -> Self {
98            Self::new()
99        }
100    }
101
102    impl MockProcessController {
103        pub fn new() -> Self {
104            Self {
105                process_states: Mutex::new(HashMap::new()),
106                signals_sent: Mutex::new(Vec::new()),
107                check_error: Mutex::new(None),
108                signal_error: Mutex::new(None),
109            }
110        }
111
112        pub fn with_process(self, pid: u32, status: ProcessStatus) -> Self {
113            self.process_states.lock().unwrap().insert(pid, status);
114            self
115        }
116
117        pub fn with_check_error(self, error: std::io::Error) -> Self {
118            *self.check_error.lock().unwrap() = Some(error);
119            self
120        }
121
122        pub fn with_signal_error(self, error: std::io::Error) -> Self {
123            *self.signal_error.lock().unwrap() = Some(error);
124            self
125        }
126
127        pub fn signals_sent(&self) -> Vec<(u32, Signal)> {
128            self.signals_sent.lock().unwrap().clone()
129        }
130    }
131
132    impl ProcessController for MockProcessController {
133        fn check_process(&self, pid: u32) -> Result<ProcessStatus, std::io::Error> {
134            if let Some(err) = self.check_error.lock().unwrap().take() {
135                return Err(err);
136            }
137            Ok(self
138                .process_states
139                .lock()
140                .unwrap()
141                .get(&pid)
142                .copied()
143                .unwrap_or(ProcessStatus::NotFound))
144        }
145
146        fn send_signal(&self, pid: u32, signal: Signal) -> Result<(), std::io::Error> {
147            if let Some(err) = self.signal_error.lock().unwrap().take() {
148                return Err(err);
149            }
150            self.signals_sent.lock().unwrap().push((pid, signal));
151            Ok(())
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use mock::MockProcessController;
160
161    #[test]
162    fn test_signal_variants() {
163        assert_ne!(Signal::Term, Signal::Kill);
164    }
165
166    #[test]
167    fn test_process_status_variants() {
168        assert_ne!(ProcessStatus::Running, ProcessStatus::NotFound);
169        assert_ne!(ProcessStatus::Running, ProcessStatus::NoPermission);
170        assert_ne!(ProcessStatus::NotFound, ProcessStatus::NoPermission);
171    }
172
173    #[test]
174    fn test_mock_check_process_not_found() {
175        let mock = MockProcessController::new();
176        assert_eq!(mock.check_process(1234).unwrap(), ProcessStatus::NotFound);
177    }
178
179    #[test]
180    fn test_mock_check_process_running() {
181        let mock = MockProcessController::new().with_process(1234, ProcessStatus::Running);
182        assert_eq!(mock.check_process(1234).unwrap(), ProcessStatus::Running);
183    }
184
185    #[test]
186    fn test_mock_send_signal() {
187        let mock = MockProcessController::new().with_process(1234, ProcessStatus::Running);
188        mock.send_signal(1234, Signal::Term).unwrap();
189        assert_eq!(mock.signals_sent(), vec![(1234, Signal::Term)]);
190    }
191
192    #[test]
193    fn test_mock_send_multiple_signals() {
194        let mock = MockProcessController::new().with_process(1234, ProcessStatus::Running);
195        mock.send_signal(1234, Signal::Term).unwrap();
196        mock.send_signal(1234, Signal::Kill).unwrap();
197        assert_eq!(
198            mock.signals_sent(),
199            vec![(1234, Signal::Term), (1234, Signal::Kill)]
200        );
201    }
202
203    #[test]
204    fn test_mock_check_error() {
205        let mock =
206            MockProcessController::new().with_check_error(std::io::Error::other("test error"));
207        assert!(mock.check_process(1234).is_err());
208    }
209
210    #[test]
211    fn test_mock_signal_error() {
212        let mock = MockProcessController::new()
213            .with_process(1234, ProcessStatus::Running)
214            .with_signal_error(std::io::Error::other("test error"));
215        assert!(mock.send_signal(1234, Signal::Term).is_err());
216    }
217}