running_process/broker/backend_lifecycle/
verify_pid.rs1use std::io;
4use std::path::PathBuf;
5
6use crate::broker::backend_lifecycle::identity::{self, DaemonProcess};
7use crate::broker::host_identity;
8use crate::platform::process::{self, ProcessInspectError, ProcessInspectErrorKind};
9
10pub fn verify_daemon_process(expected: &DaemonProcess) -> Result<ProcessHandle, VerifyPidError> {
12 verify_daemon_with_opener(expected, open_handle)
13}
14
15pub fn verify_daemon_process_for_control(
17 expected: &DaemonProcess,
18) -> Result<ProcessHandle, VerifyPidError> {
19 verify_daemon_with_opener(expected, |pid| {
20 ProcessHandle::open_for_control(pid).map_err(|error| VerifyPidError::Handle {
21 pid,
22 source: error.source,
23 })
24 })
25}
26
27fn verify_daemon_with_opener(
28 expected: &DaemonProcess,
29 open: impl FnOnce(u32) -> Result<ProcessHandle, VerifyPidError>,
30) -> Result<ProcessHandle, VerifyPidError> {
31 if expected.pid == 0 {
32 return Err(VerifyPidError::InvalidPid(expected.pid));
33 }
34
35 let current_boot_id = host_identity::current().boot_id;
36 if !expected.boot_id.is_empty()
37 && !current_boot_id.is_empty()
38 && expected.boot_id != current_boot_id
39 {
40 return Err(VerifyPidError::BootIdMismatch {
41 expected: expected.boot_id.clone(),
42 actual: current_boot_id,
43 });
44 }
45
46 let handle = open(expected.pid)?;
47 let exe_path =
48 process::executable_path(expected.pid).map_err(|source| VerifyPidError::ExePath {
49 pid: expected.pid,
50 source,
51 })?;
52 if !process::same_executable_path(&exe_path, &expected.exe_path) {
53 return Err(VerifyPidError::ExePathMismatch {
54 pid: expected.pid,
55 expected: expected.exe_path.clone(),
56 actual: exe_path,
57 });
58 }
59
60 let actual_hash =
61 identity::executable_hash_file(&exe_path).map_err(|source| VerifyPidError::ExeHash {
62 pid: expected.pid,
63 path: exe_path.clone(),
64 source,
65 })?;
66 if actual_hash != expected.exe_hash {
67 return Err(VerifyPidError::ExecutableHashMismatch { pid: expected.pid });
68 }
69
70 if !handle.is_alive() {
71 return Err(VerifyPidError::NotFound { pid: expected.pid });
72 }
73 Ok(handle)
74}
75
76pub fn process_is_alive(pid: u32) -> bool {
78 ProcessHandle::open(pid).is_ok_and(|handle| handle.is_alive())
79}
80
81pub fn signal_terminate(pid: u32) -> Result<(), VerifyPidError> {
83 process::signal_terminate(pid).map_err(|error| translate(pid, error))
84}
85
86pub fn force_kill_pid(pid: u32) -> Result<(), VerifyPidError> {
88 process::force_kill(pid).map_err(|error| translate(pid, error))
89}
90
91pub fn force_kill_handle(handle: &ProcessHandle) -> Result<(), VerifyPidError> {
93 handle
94 .force_kill()
95 .map_err(|source| VerifyPidError::Handle {
96 pid: handle.pid(),
97 source,
98 })
99}
100
101#[derive(Debug, thiserror::Error)]
103pub enum VerifyPidError {
104 #[error("invalid daemon pid: {0}")]
106 InvalidPid(u32),
107 #[error("process not found: {pid}")]
109 NotFound {
110 pid: u32,
112 },
113 #[error("daemon boot id mismatch: expected {expected}, current {actual}")]
115 BootIdMismatch {
116 expected: String,
118 actual: String,
120 },
121 #[error("failed to hash executable for pid {pid} at {path:?}: {source}")]
123 ExeHash {
124 pid: u32,
126 path: PathBuf,
128 source: io::Error,
130 },
131 #[error("failed to resolve executable path for pid {pid}: {source}")]
133 ExePath {
134 pid: u32,
136 source: io::Error,
138 },
139 #[error(
141 "daemon executable path mismatch for pid {pid}: expected {expected:?}, actual {actual:?}"
142 )]
143 ExePathMismatch {
144 pid: u32,
146 expected: PathBuf,
148 actual: PathBuf,
150 },
151 #[error("daemon executable blake3 hash mismatch for pid {pid}")]
153 ExecutableHashMismatch {
154 pid: u32,
156 },
157 #[error("process handle operation failed for pid {pid}: {source}")]
159 Handle {
160 pid: u32,
162 source: io::Error,
164 },
165 #[error("graceful terminate is unsupported on this platform")]
167 GracefulTerminateUnsupported,
168}
169
170pub use crate::platform::process::ProcessLiveness as ProcessHandle;
176
177fn open_handle(pid: u32) -> Result<ProcessHandle, VerifyPidError> {
178 ProcessHandle::open(pid).map_err(|error| translate(pid, error))
179}
180
181fn translate(pid: u32, error: ProcessInspectError) -> VerifyPidError {
188 match error.kind {
189 ProcessInspectErrorKind::InvalidPid => VerifyPidError::InvalidPid(pid),
190 ProcessInspectErrorKind::NotFound => VerifyPidError::NotFound { pid },
191 ProcessInspectErrorKind::Unsupported => VerifyPidError::GracefulTerminateUnsupported,
192 ProcessInspectErrorKind::Host => VerifyPidError::Handle {
193 pid,
194 source: error.source,
195 },
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
209 fn every_host_kind_has_a_name_here() {
210 let staged = |kind| ProcessInspectError {
211 kind,
212 source: io::Error::from_raw_os_error(1),
213 };
214 assert!(matches!(
215 translate(7, staged(ProcessInspectErrorKind::InvalidPid)),
216 VerifyPidError::InvalidPid(7)
217 ));
218 assert!(matches!(
219 translate(7, staged(ProcessInspectErrorKind::NotFound)),
220 VerifyPidError::NotFound { pid: 7 }
221 ));
222 assert!(matches!(
223 translate(7, staged(ProcessInspectErrorKind::Unsupported)),
224 VerifyPidError::GracefulTerminateUnsupported
225 ));
226 assert!(matches!(
227 translate(7, staged(ProcessInspectErrorKind::Host)),
228 VerifyPidError::Handle { pid: 7, .. }
229 ));
230 }
231
232 #[test]
237 fn a_host_error_is_carried_through_whole() {
238 let error = translate(
239 7,
240 ProcessInspectError {
241 kind: ProcessInspectErrorKind::Host,
242 source: io::Error::from_raw_os_error(13),
243 },
244 );
245 let VerifyPidError::Handle { source, .. } = error else {
246 panic!("expected a handle error");
247 };
248 assert_eq!(source.raw_os_error(), Some(13));
249 }
250
251 #[test]
253 fn liveness_answers_for_this_process() {
254 assert!(process_is_alive(std::process::id()));
255 assert!(!process_is_alive(0));
256 }
257}