agent_tui_infra/infra/ipc/
process.rs1use std::process::Command;
4
5use sysinfo::Pid;
6use sysinfo::ProcessRefreshKind;
7use sysinfo::ProcessesToUpdate;
8use sysinfo::System;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Signal {
12 Term,
13 Kill,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ProcessStatus {
18 Running,
19 NotFound,
20 NoPermission,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ProcessIdentity {
25 pub pid: u32,
26 pub started_at: Option<u64>,
27}
28
29pub fn current_process_identity() -> ProcessIdentity {
30 let pid = std::process::id();
31 let controller = UnixProcessController;
32 match controller.process_identity(pid) {
33 Ok(Some(identity)) => identity,
34 Ok(None) | Err(_) => ProcessIdentity {
35 pid,
36 started_at: None,
37 },
38 }
39}
40
41pub fn check_expected_process<C: ProcessController>(
42 controller: &C,
43 expected: ProcessIdentity,
44) -> std::io::Result<ProcessStatus> {
45 let status = controller.check_process(expected.pid)?;
46 if !matches!(status, ProcessStatus::Running | ProcessStatus::NoPermission) {
47 return Ok(status);
48 }
49
50 let Some(expected_started_at) = expected.started_at else {
51 return Ok(status);
52 };
53
54 let actual = controller.process_identity(expected.pid)?;
55 match actual {
56 Some(actual) if actual.started_at == Some(expected_started_at) => Ok(status),
57 Some(_) | None => Ok(ProcessStatus::NotFound),
58 }
59}
60
61pub trait ProcessController: Send + Sync {
62 fn check_process(&self, pid: u32) -> std::io::Result<ProcessStatus>;
63
64 fn send_signal(&self, pid: u32, signal: Signal) -> std::io::Result<()>;
65
66 fn process_identity(&self, pid: u32) -> std::io::Result<Option<ProcessIdentity>>;
67}
68
69pub struct UnixProcessController;
70
71impl ProcessController for UnixProcessController {
72 fn check_process(&self, pid: u32) -> std::io::Result<ProcessStatus> {
73 let pid_t: libc::pid_t = pid.try_into().map_err(|_| {
74 std::io::Error::new(std::io::ErrorKind::InvalidInput, "PID out of range")
75 })?;
76
77 let result = unsafe { libc::kill(pid_t, 0) };
80 if result == 0 {
81 if is_defunct_process(pid) {
82 return Ok(ProcessStatus::NotFound);
83 }
84 return Ok(ProcessStatus::Running);
85 }
86
87 let err = std::io::Error::last_os_error();
88 match err.raw_os_error() {
89 Some(libc::ESRCH) => Ok(ProcessStatus::NotFound),
90 Some(libc::EPERM) => Ok(ProcessStatus::NoPermission),
91 _ => Err(err),
92 }
93 }
94
95 fn send_signal(&self, pid: u32, signal: Signal) -> std::io::Result<()> {
96 let pid_t: libc::pid_t = pid.try_into().map_err(|_| {
97 std::io::Error::new(std::io::ErrorKind::InvalidInput, "PID out of range")
98 })?;
99
100 let sig = match signal {
101 Signal::Term => libc::SIGTERM,
102 Signal::Kill => libc::SIGKILL,
103 };
104
105 let result = unsafe { libc::kill(pid_t, sig) };
107 if result == 0 {
108 Ok(())
109 } else {
110 Err(std::io::Error::last_os_error())
111 }
112 }
113
114 fn process_identity(&self, pid: u32) -> std::io::Result<Option<ProcessIdentity>> {
115 match self.check_process(pid)? {
116 ProcessStatus::NotFound => Ok(None),
117 ProcessStatus::Running | ProcessStatus::NoPermission => Ok(Some(ProcessIdentity {
118 pid,
119 started_at: process_started_at_secs(pid),
120 })),
121 }
122 }
123}
124
125fn is_defunct_process(pid: u32) -> bool {
126 matches!(process_status_code(pid), Some('Z' | 'X'))
127}
128
129fn process_status_code(pid: u32) -> Option<char> {
130 let output = Command::new("ps")
131 .args(["-o", "stat=", "-p", &pid.to_string()])
132 .output()
133 .ok()?;
134 if !output.status.success() {
135 return None;
136 }
137
138 String::from_utf8(output.stdout).ok()?.trim().chars().next()
139}
140
141fn process_started_at_secs(pid: u32) -> Option<u64> {
142 let pid = Pid::from_u32(pid);
143 let mut system = System::new();
144 system.refresh_processes_specifics(
145 ProcessesToUpdate::Some(&[pid]),
146 true,
147 ProcessRefreshKind::nothing(),
148 );
149 let process = system.process(pid)?;
150 let started_at = process.start_time();
151 (started_at > 0).then(|| System::boot_time().saturating_add(started_at))
152}
153
154#[allow(clippy::expect_used)]
155pub mod mock {
156 use super::*;
157 use crate::common::mutex_lock_or_recover;
158 use std::collections::HashMap;
159 use std::sync::Mutex;
160
161 #[derive(Debug, Clone, Copy)]
162 struct MockProcessRecord {
163 status: ProcessStatus,
164 started_at: Option<u64>,
165 }
166
167 pub struct MockProcessController {
168 process_states: Mutex<HashMap<u32, MockProcessRecord>>,
169 signals_sent: Mutex<Vec<(u32, Signal)>>,
170 check_error: Mutex<Option<std::io::Error>>,
171 signal_error: Mutex<Option<std::io::Error>>,
172 signal_kills_process_on: Option<Signal>,
173 }
174
175 impl Default for MockProcessController {
176 fn default() -> Self {
177 Self::new()
178 }
179 }
180
181 impl MockProcessController {
182 pub fn new() -> Self {
183 Self {
184 process_states: Mutex::new(HashMap::new()),
185 signals_sent: Mutex::new(Vec::new()),
186 check_error: Mutex::new(None),
187 signal_error: Mutex::new(None),
188 signal_kills_process_on: None,
189 }
190 }
191
192 pub fn with_signal_kills_process(mut self) -> Self {
193 self.signal_kills_process_on = Some(Signal::Term);
194 self
195 }
196
197 pub fn with_signal_kills_process_on(mut self, signal: Signal) -> Self {
198 self.signal_kills_process_on = Some(signal);
199 self
200 }
201
202 pub fn with_process(self, pid: u32, status: ProcessStatus) -> Self {
203 mutex_lock_or_recover(&self.process_states).insert(
204 pid,
205 MockProcessRecord {
206 status,
207 started_at: None,
208 },
209 );
210 self
211 }
212
213 pub fn with_process_identity(
214 self,
215 pid: u32,
216 status: ProcessStatus,
217 started_at: Option<u64>,
218 ) -> Self {
219 mutex_lock_or_recover(&self.process_states)
220 .insert(pid, MockProcessRecord { status, started_at });
221 self
222 }
223
224 pub fn with_check_error(self, error: std::io::Error) -> Self {
225 *mutex_lock_or_recover(&self.check_error) = Some(error);
226 self
227 }
228
229 pub fn with_signal_error(self, error: std::io::Error) -> Self {
230 *mutex_lock_or_recover(&self.signal_error) = Some(error);
231 self
232 }
233
234 pub fn signals_sent(&self) -> Vec<(u32, Signal)> {
235 mutex_lock_or_recover(&self.signals_sent).clone()
236 }
237 }
238
239 impl ProcessController for MockProcessController {
240 fn check_process(&self, pid: u32) -> std::io::Result<ProcessStatus> {
241 if let Some(err) = mutex_lock_or_recover(&self.check_error).take() {
242 return Err(err);
243 }
244 Ok(self
245 .process_states
246 .lock()
247 .unwrap_or_else(std::sync::PoisonError::into_inner)
248 .get(&pid)
249 .map(|record| record.status)
250 .unwrap_or(ProcessStatus::NotFound))
251 }
252
253 fn send_signal(&self, pid: u32, signal: Signal) -> std::io::Result<()> {
254 if let Some(err) = mutex_lock_or_recover(&self.signal_error).take() {
255 return Err(err);
256 }
257 mutex_lock_or_recover(&self.signals_sent).push((pid, signal));
258 if self.signal_kills_process_on == Some(signal) {
259 mutex_lock_or_recover(&self.process_states).remove(&pid);
260 }
261 Ok(())
262 }
263
264 fn process_identity(&self, pid: u32) -> std::io::Result<Option<ProcessIdentity>> {
265 let record = self
266 .process_states
267 .lock()
268 .unwrap_or_else(std::sync::PoisonError::into_inner)
269 .get(&pid)
270 .copied();
271 Ok(record.map(|record| ProcessIdentity {
272 pid,
273 started_at: record.started_at,
274 }))
275 }
276 }
277}
278
279#[cfg(test)]
280#[path = "process_tests.rs"]
281mod tests;