use crate::record::ServiceRecord;
use rightkit_child_process::AdoptedIdentity;
use std::io;
use std::num::NonZeroU32;
use std::time::Duration;
const START_TOLERANCE: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum IncumbentStop {
AlreadyGone,
Asked,
Signalled,
Forced,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum IncumbentError {
NotIncumbent { pid: u32 },
NoIdentity { pid: u32 },
OwnProcess,
Survived { pid: u32 },
Io(io::Error),
}
impl std::fmt::Display for IncumbentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotIncumbent { pid } => {
write!(
f,
"pid {pid} is not the recorded host (recycled); not signalled"
)
}
Self::NoIdentity { pid } => write!(
f,
"the record of pid {pid} has no start time, so it was asked but never signalled"
),
Self::OwnProcess => write!(f, "the record names this process"),
Self::Survived { pid } => write!(f, "pid {pid} survived the forced stop"),
Self::Io(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for IncumbentError {}
impl From<io::Error> for IncumbentError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
pub fn stop_incumbent(
record: &ServiceRecord,
grace: Duration,
ask: impl FnOnce() -> io::Result<()>,
) -> Result<IncumbentStop, IncumbentError> {
let pid = record.pid;
if pid == std::process::id() {
return Err(IncumbentError::OwnProcess);
}
let Some(nz) = NonZeroU32::new(pid) else {
return Ok(IncumbentStop::AlreadyGone);
};
if !crate::transport::pid_alive(pid) {
return Ok(IncumbentStop::AlreadyGone);
}
if record.process_started_ms == 0 {
let _ = ask();
let deadline = std::time::Instant::now() + grace;
while std::time::Instant::now() < deadline {
if !crate::transport::pid_alive(pid) {
return Ok(IncumbentStop::Asked);
}
std::thread::sleep(Duration::from_millis(25));
}
return Err(IncumbentError::NoIdentity { pid });
}
let identity =
AdoptedIdentity::from_recorded_unix_ms(nz, record.process_started_ms, START_TOLERANCE)?;
let Some(identity) = identity else {
return if crate::transport::pid_alive(pid) {
Err(IncumbentError::NotIncumbent { pid })
} else {
Ok(IncumbentStop::AlreadyGone)
};
};
let _ = ask();
if identity.wait_confirmed_gone(grace)? {
return Ok(IncumbentStop::Asked);
}
if identity.quit_gracefully(grace)? {
return Ok(IncumbentStop::Signalled);
}
let report = identity.terminate_tree(grace)?;
if report.survivors.is_empty() && report.failed.is_empty() && identity.confirmed_gone()? {
Ok(IncumbentStop::Forced)
} else {
Err(IncumbentError::Survived { pid })
}
}