running_process_platform_internal/platform_linux/
process_inspect.rs1use std::io;
4use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
5use std::path::PathBuf;
6
7use crate::platform::process::{ProcessInspectError, ProcessInspectErrorKind};
8
9pub struct ProcessLiveness {
17 pid: u32,
18 pid_fd: Option<OwnedFd>,
19}
20
21impl std::fmt::Debug for ProcessLiveness {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("ProcessLiveness")
29 .field("pid", &self.pid)
30 .finish_non_exhaustive()
31 }
32}
33
34impl ProcessLiveness {
35 pub fn open_for_control(pid: u32) -> Result<Self, ProcessInspectError> {
37 Self::open_pinned(pid).map_err(|source| ProcessInspectError {
38 kind: ProcessInspectErrorKind::Host,
39 source,
40 })
41 }
42
43 pub fn force_kill(&self) -> io::Result<()> {
45 self.signal_pinned(libc::SIGKILL)
46 }
47
48 pub fn has_exited(&self) -> io::Result<bool> {
50 let fd = self.pid_fd.as_ref().ok_or_else(|| {
51 io::Error::new(
52 io::ErrorKind::Unsupported,
53 "exit confirmation requires a held pidfd",
54 )
55 })?;
56 let mut poll_fd = libc::pollfd {
57 fd: fd.as_raw_fd(),
58 events: libc::POLLIN,
59 revents: 0,
60 };
61 if unsafe { libc::poll(&mut poll_fd, 1, 0) } < 0 {
63 return Err(io::Error::last_os_error());
64 }
65 if poll_fd.revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
66 return Err(io::Error::other("held pidfd poll failed"));
67 }
68 Ok(poll_fd.revents & libc::POLLIN != 0)
69 }
70
71 pub(crate) fn open_pinned(pid: u32) -> io::Result<Self> {
74 if pid == 0 || pid > libc::pid_t::MAX as u32 {
75 return Err(io::Error::from(io::ErrorKind::InvalidInput));
76 }
77 let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
79 if raw < 0 {
80 let error = io::Error::last_os_error();
81 return Err(match error.raw_os_error() {
82 Some(libc::ENOSYS | libc::EINVAL) => io::Error::new(
83 io::ErrorKind::Unsupported,
84 "kernel-pinned process control is unavailable",
85 ),
86 _ => error,
87 });
88 }
89 let pid_fd = unsafe { OwnedFd::from_raw_fd(raw as i32) };
91 Ok(Self {
92 pid,
93 pid_fd: Some(pid_fd),
94 })
95 }
96
97 pub(crate) fn signal_pinned(&self, signal: i32) -> io::Result<()> {
99 let fd = self.pid_fd.as_ref().ok_or_else(|| {
100 io::Error::new(
101 io::ErrorKind::Unsupported,
102 "process identity is not kernel-pinned",
103 )
104 })?;
105 let result = unsafe {
108 libc::syscall(
109 libc::SYS_pidfd_send_signal,
110 fd.as_raw_fd(),
111 signal,
112 std::ptr::null::<libc::siginfo_t>(),
113 0_u32,
114 )
115 };
116 if result < 0 {
117 Err(io::Error::last_os_error())
118 } else {
119 Ok(())
120 }
121 }
122
123 pub fn open(pid: u32) -> Result<Self, ProcessInspectError> {
125 validate_pid(pid)?;
126 if !process_exists(pid) {
127 return Err(not_found());
128 }
129 Ok(Self {
130 pid,
131 pid_fd: try_pidfd_open(pid)?,
132 })
133 }
134
135 pub fn pid(&self) -> u32 {
137 self.pid
138 }
139
140 pub fn is_alive(&self) -> bool {
142 match self.pid_fd.as_ref() {
143 Some(pid_fd) => pidfd_is_alive(pid_fd),
144 None => process_exists(self.pid),
145 }
146 }
147}
148
149pub fn process_executable_path(pid: u32) -> Result<PathBuf, io::Error> {
151 std::fs::read_link(format!("/proc/{pid}/exe"))
152}
153
154pub fn process_signal_terminate(pid: u32) -> Result<(), ProcessInspectError> {
156 signal(pid, libc::SIGTERM)
157}
158
159pub fn process_force_kill(pid: u32) -> Result<(), ProcessInspectError> {
161 signal(pid, libc::SIGKILL)
162}
163
164fn signal(pid: u32, signal: libc::c_int) -> Result<(), ProcessInspectError> {
165 let native_pid = validate_pid(pid)?;
166 let rc = unsafe { libc::kill(native_pid, signal) };
168 if rc == 0 {
169 Ok(())
170 } else {
171 Err(ProcessInspectError::last_os_error(
172 ProcessInspectErrorKind::Host,
173 ))
174 }
175}
176
177fn process_exists(pid: u32) -> bool {
182 let Ok(native_pid) = validate_pid(pid) else {
183 return false;
184 };
185 let rc = unsafe { libc::kill(native_pid, 0) };
187 if rc == 0 {
188 return true;
189 }
190 matches!(io::Error::last_os_error().raw_os_error(), Some(libc::EPERM))
191}
192
193fn validate_pid(pid: u32) -> Result<libc::pid_t, ProcessInspectError> {
194 if pid == 0 || pid > libc::pid_t::MAX as u32 {
195 Err(ProcessInspectError::stated(
196 ProcessInspectErrorKind::InvalidPid,
197 "pid outside the range this host issues",
198 ))
199 } else {
200 Ok(pid as libc::pid_t)
201 }
202}
203
204fn try_pidfd_open(pid: u32) -> Result<Option<OwnedFd>, ProcessInspectError> {
211 let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid as libc::pid_t, 0_u32) };
213 if raw >= 0 {
214 return Ok(Some(unsafe { OwnedFd::from_raw_fd(raw as i32) }));
217 }
218
219 match io::Error::last_os_error().raw_os_error() {
220 Some(libc::ESRCH) => Err(not_found()),
221 _ => Ok(None),
222 }
223}
224
225fn pidfd_is_alive(pid_fd: &OwnedFd) -> bool {
227 let mut poll_fd = libc::pollfd {
228 fd: pid_fd.as_raw_fd(),
229 events: libc::POLLIN,
230 revents: 0,
231 };
232 let rc = unsafe { libc::poll(&mut poll_fd, 1, 0) };
235 rc == 0
236}
237
238fn not_found() -> ProcessInspectError {
239 ProcessInspectError::stated(ProcessInspectErrorKind::NotFound, "no such process")
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
249 fn pid_zero_is_never_valid() {
250 let error = ProcessLiveness::open(0).expect_err("pid 0");
251 assert_eq!(error.kind, ProcessInspectErrorKind::InvalidPid);
252 assert!(!process_exists(0));
253 }
254
255 #[test]
256 fn pinned_control_rejects_missing_handle_without_signalling_pid() {
257 let unpinned = ProcessLiveness {
258 pid: std::process::id(),
259 pid_fd: None,
260 };
261 assert_eq!(
262 unpinned.signal_pinned(libc::SIGKILL).unwrap_err().kind(),
263 io::ErrorKind::Unsupported
264 );
265 assert_eq!(
266 ProcessLiveness::open_pinned(0).unwrap_err().kind(),
267 io::ErrorKind::InvalidInput
268 );
269 }
270
271 #[test]
272 fn pinned_control_keeps_identity_after_exit() {
273 let mut command = std::process::Command::new("/bin/sh");
274 command.args(["-c", "exec sleep 60"]);
275 let mut child = crate::spawn_sync(
276 &mut command,
277 crate::platform::process::SpawnStdio::default(),
278 crate::platform::process::SyncEnvironment::Inherit,
279 )
280 .unwrap();
281 let pinned = ProcessLiveness::open_pinned(child.id()).unwrap();
282 pinned.signal_pinned(libc::SIGKILL).unwrap();
283 child.wait().unwrap();
284 assert!(!pinned.is_alive());
285 assert_eq!(
286 pinned.signal_pinned(0).unwrap_err().raw_os_error(),
287 Some(libc::ESRCH)
288 );
289 }
290
291 #[test]
293 fn this_process_is_alive_and_locatable() {
294 let me = std::process::id();
295 let handle = ProcessLiveness::open(me).expect("open self");
296 assert_eq!(handle.pid(), me);
297 assert!(handle.is_alive());
298 assert_eq!(
299 process_executable_path(me).expect("exe"),
300 std::env::current_exe().expect("current_exe")
301 );
302 }
303
304 #[test]
308 fn a_dead_process_reports_dead() {
309 let child = std::process::Command::new("/bin/sh")
310 .args(["-c", "exit 0"])
311 .spawn()
312 .expect("spawn");
313 let pid = child.id();
314 let handle = ProcessLiveness::open(pid).expect("open child");
315 let mut child = child;
316 child.wait().expect("reap");
317 assert!(!handle.is_alive(), "a reaped child must report dead");
318 }
319}
320
321pub fn process_same_executable_path(actual: &std::path::Path, expected: &std::path::Path) -> bool {
329 let resolve =
330 |path: &std::path::Path| std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
331 resolve(actual) == resolve(expected)
332}
333
334#[cfg(test)]
335mod path_tests {
336 use super::*;
337 use std::path::Path;
338
339 #[test]
341 fn case_distinguishes_two_images() {
342 assert!(!process_same_executable_path(
343 Path::new("/tmp/Daemon"),
344 Path::new("/tmp/daemon"),
345 ));
346 }
347
348 #[test]
350 fn a_path_matches_itself() {
351 assert!(process_same_executable_path(
352 Path::new("/tmp/rp-does-not-exist/daemon"),
353 Path::new("/tmp/rp-does-not-exist/daemon"),
354 ));
355 let me = std::env::current_exe().expect("current_exe");
356 assert!(process_same_executable_path(&me, &me));
357 }
358}