use std::io;
use std::path::PathBuf;
use crate::broker::backend_lifecycle::identity::{self, DaemonProcess};
use crate::broker::host_identity;
use crate::platform::process::{self, ProcessInspectError, ProcessInspectErrorKind};
pub fn verify_daemon_process(expected: &DaemonProcess) -> Result<ProcessHandle, VerifyPidError> {
verify_daemon_with_opener(expected, open_handle)
}
pub fn verify_daemon_process_for_control(
expected: &DaemonProcess,
) -> Result<ProcessHandle, VerifyPidError> {
verify_daemon_with_opener(expected, |pid| {
ProcessHandle::open_for_control(pid).map_err(|error| VerifyPidError::Handle {
pid,
source: error.source,
})
})
}
fn verify_daemon_with_opener(
expected: &DaemonProcess,
open: impl FnOnce(u32) -> Result<ProcessHandle, VerifyPidError>,
) -> Result<ProcessHandle, VerifyPidError> {
if expected.pid == 0 {
return Err(VerifyPidError::InvalidPid(expected.pid));
}
let current_boot_id = host_identity::current().boot_id;
if !expected.boot_id.is_empty()
&& !current_boot_id.is_empty()
&& expected.boot_id != current_boot_id
{
return Err(VerifyPidError::BootIdMismatch {
expected: expected.boot_id.clone(),
actual: current_boot_id,
});
}
let handle = open(expected.pid)?;
let exe_path =
process::executable_path(expected.pid).map_err(|source| VerifyPidError::ExePath {
pid: expected.pid,
source,
})?;
if !process::same_executable_path(&exe_path, &expected.exe_path) {
return Err(VerifyPidError::ExePathMismatch {
pid: expected.pid,
expected: expected.exe_path.clone(),
actual: exe_path,
});
}
let actual_hash =
identity::executable_hash_file(&exe_path).map_err(|source| VerifyPidError::ExeHash {
pid: expected.pid,
path: exe_path.clone(),
source,
})?;
if actual_hash != expected.exe_hash {
return Err(VerifyPidError::ExecutableHashMismatch { pid: expected.pid });
}
if !handle.is_alive() {
return Err(VerifyPidError::NotFound { pid: expected.pid });
}
Ok(handle)
}
pub fn process_is_alive(pid: u32) -> bool {
ProcessHandle::open(pid).is_ok_and(|handle| handle.is_alive())
}
pub fn signal_terminate(pid: u32) -> Result<(), VerifyPidError> {
process::signal_terminate(pid).map_err(|error| translate(pid, error))
}
pub fn force_kill_pid(pid: u32) -> Result<(), VerifyPidError> {
process::force_kill(pid).map_err(|error| translate(pid, error))
}
pub fn force_kill_handle(handle: &ProcessHandle) -> Result<(), VerifyPidError> {
handle
.force_kill()
.map_err(|source| VerifyPidError::Handle {
pid: handle.pid(),
source,
})
}
#[derive(Debug, thiserror::Error)]
pub enum VerifyPidError {
#[error("invalid daemon pid: {0}")]
InvalidPid(u32),
#[error("process not found: {pid}")]
NotFound {
pid: u32,
},
#[error("daemon boot id mismatch: expected {expected}, current {actual}")]
BootIdMismatch {
expected: String,
actual: String,
},
#[error("failed to hash executable for pid {pid} at {path:?}: {source}")]
ExeHash {
pid: u32,
path: PathBuf,
source: io::Error,
},
#[error("failed to resolve executable path for pid {pid}: {source}")]
ExePath {
pid: u32,
source: io::Error,
},
#[error(
"daemon executable path mismatch for pid {pid}: expected {expected:?}, actual {actual:?}"
)]
ExePathMismatch {
pid: u32,
expected: PathBuf,
actual: PathBuf,
},
#[error("daemon executable blake3 hash mismatch for pid {pid}")]
ExecutableHashMismatch {
pid: u32,
},
#[error("process handle operation failed for pid {pid}: {source}")]
Handle {
pid: u32,
source: io::Error,
},
#[error("graceful terminate is unsupported on this platform")]
GracefulTerminateUnsupported,
}
pub use crate::platform::process::ProcessLiveness as ProcessHandle;
fn open_handle(pid: u32) -> Result<ProcessHandle, VerifyPidError> {
ProcessHandle::open(pid).map_err(|error| translate(pid, error))
}
fn translate(pid: u32, error: ProcessInspectError) -> VerifyPidError {
match error.kind {
ProcessInspectErrorKind::InvalidPid => VerifyPidError::InvalidPid(pid),
ProcessInspectErrorKind::NotFound => VerifyPidError::NotFound { pid },
ProcessInspectErrorKind::Unsupported => VerifyPidError::GracefulTerminateUnsupported,
ProcessInspectErrorKind::Host => VerifyPidError::Handle {
pid,
source: error.source,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_host_kind_has_a_name_here() {
let staged = |kind| ProcessInspectError {
kind,
source: io::Error::from_raw_os_error(1),
};
assert!(matches!(
translate(7, staged(ProcessInspectErrorKind::InvalidPid)),
VerifyPidError::InvalidPid(7)
));
assert!(matches!(
translate(7, staged(ProcessInspectErrorKind::NotFound)),
VerifyPidError::NotFound { pid: 7 }
));
assert!(matches!(
translate(7, staged(ProcessInspectErrorKind::Unsupported)),
VerifyPidError::GracefulTerminateUnsupported
));
assert!(matches!(
translate(7, staged(ProcessInspectErrorKind::Host)),
VerifyPidError::Handle { pid: 7, .. }
));
}
#[test]
fn a_host_error_is_carried_through_whole() {
let error = translate(
7,
ProcessInspectError {
kind: ProcessInspectErrorKind::Host,
source: io::Error::from_raw_os_error(13),
},
);
let VerifyPidError::Handle { source, .. } = error else {
panic!("expected a handle error");
};
assert_eq!(source.raw_os_error(), Some(13));
}
#[test]
fn liveness_answers_for_this_process() {
assert!(process_is_alive(std::process::id()));
assert!(!process_is_alive(0));
}
}