use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use super::incarnation::{self, IncarnationProbe};
use super::outcome::{self, NoteFate};
use super::pid_file::{self, PidRecord};
use crate::error::ServerError;
#[derive(Clone, Debug)]
pub enum StopOutcome {
Stopped {
record: PidRecord,
fate: Result<NoteFate, String>,
waited: Duration,
pid_file_reconciled: Result<bool, String>,
},
AlreadyGone {
record: PidRecord,
fate: Result<NoteFate, String>,
pid_file_reconciled: Result<bool, String>,
},
StillDraining {
record: PidRecord,
waited: Duration,
},
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum StopRefusal {
#[error(
"no pid file at `{path}`: no server has claimed this home. If a server is \
running, it predates `aion server stop` — find it with `ps` and signal it \
by hand this one time; its next start writes the pid file"
)]
NoPidFile {
path: PathBuf,
},
#[error(
"refusing to signal pid {pid}: the pid file records a server started at unix \
second {recorded_started_at}, but the live process wearing that pid started \
at {live_started_at}{live_exe} — the pid was recycled onto a different \
process after the recorded server died. Nothing was signalled and nothing \
was removed; verify with `ps -p {pid}` and remove `{path}` once satisfied"
)]
StaleIncarnation {
pid: u32,
recorded_started_at: u64,
live_started_at: u64,
live_exe: String,
path: PathBuf,
},
#[error(
"refusing to signal pid {pid}: the server is RUNNING, but no wait patience \
could be resolved from the configuration to govern the stop ({unresolved}). \
Pass `--patience <seconds>` to rule it at invocation, or repair the \
configuration"
)]
UnresolvedPatience {
pid: u32,
unresolved: String,
},
#[error("`aion server stop` needs POSIX signals, which this platform does not have")]
UnsupportedPlatform,
#[error("could not signal pid {pid}: {message}")]
SignalFailed {
pid: u32,
message: String,
},
}
#[derive(Clone, Debug)]
pub enum StopVerdict {
Outcome(Box<StopOutcome>),
Refusal(StopRefusal),
}
pub fn stop(home: &Path, patience: Result<Duration, String>) -> Result<StopVerdict, ServerError> {
let Some(record) = pid_file::read(home)? else {
return Ok(StopVerdict::Refusal(StopRefusal::NoPidFile {
path: pid_file::pid_file_path(home),
}));
};
match incarnation::probe(&record) {
IncarnationProbe::ProcessGone => {
let (fate, pid_file_reconciled) = exit_bookkeeping(home, &record);
Ok(StopVerdict::Outcome(Box::new(StopOutcome::AlreadyGone {
record,
fate,
pid_file_reconciled,
})))
}
IncarnationProbe::DifferentIncarnation {
live_started_at_unix_secs,
exe,
} => Ok(StopVerdict::Refusal(StopRefusal::StaleIncarnation {
pid: record.pid,
recorded_started_at: record.started_at_unix_secs,
live_started_at: live_started_at_unix_secs,
live_exe: exe.map_or_else(String::new, |path| {
format!(" (running `{}`)", path.display())
}),
path: pid_file::pid_file_path(home),
})),
IncarnationProbe::Verified { .. } => match patience {
Ok(patience) => signal_and_wait(home, record, patience),
Err(unresolved) => Ok(StopVerdict::Refusal(StopRefusal::UnresolvedPatience {
pid: record.pid,
unresolved,
})),
},
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TerminationSignal {
Sent,
AlreadyGone,
}
#[cfg(unix)]
pub fn send_termination(record: &PidRecord) -> Result<TerminationSignal, StopRefusal> {
let Ok(pid_i32) = i32::try_from(record.pid) else {
return Err(StopRefusal::SignalFailed {
pid: record.pid,
message: "pid does not fit a signal target".to_owned(),
});
};
let target = nix::unistd::Pid::from_raw(pid_i32);
match nix::sys::signal::kill(target, nix::sys::signal::Signal::SIGTERM) {
Ok(()) => Ok(TerminationSignal::Sent),
Err(nix::errno::Errno::ESRCH) => Ok(TerminationSignal::AlreadyGone),
Err(errno) => Err(StopRefusal::SignalFailed {
pid: record.pid,
message: errno.to_string(),
}),
}
}
#[cfg(not(unix))]
pub fn send_termination(_record: &PidRecord) -> Result<TerminationSignal, StopRefusal> {
Err(StopRefusal::UnsupportedPlatform)
}
#[cfg(unix)]
fn signal_and_wait(
home: &Path,
record: PidRecord,
patience: Duration,
) -> Result<StopVerdict, ServerError> {
match send_termination(&record) {
Ok(TerminationSignal::Sent) => {}
Ok(TerminationSignal::AlreadyGone) => {
let (fate, pid_file_reconciled) = exit_bookkeeping(home, &record);
return Ok(StopVerdict::Outcome(Box::new(StopOutcome::AlreadyGone {
record,
fate,
pid_file_reconciled,
})));
}
Err(refusal) => return Ok(StopVerdict::Refusal(refusal)),
}
let started = Instant::now();
let cadence = wait_cadence(patience);
loop {
match incarnation::probe(&record) {
IncarnationProbe::Verified { .. } => {
let waited = started.elapsed();
if waited >= patience {
return Ok(StopVerdict::Outcome(Box::new(StopOutcome::StillDraining {
record,
waited,
})));
}
std::thread::sleep(cadence.min(patience.saturating_sub(waited)));
}
IncarnationProbe::ProcessGone | IncarnationProbe::DifferentIncarnation { .. } => {
let (fate, pid_file_reconciled) = exit_bookkeeping(home, &record);
return Ok(StopVerdict::Outcome(Box::new(StopOutcome::Stopped {
record,
fate,
waited: started.elapsed(),
pid_file_reconciled,
})));
}
}
}
}
#[cfg(not(unix))]
fn signal_and_wait(
_home: &Path,
_record: PidRecord,
_patience: Duration,
) -> Result<StopVerdict, ServerError> {
Ok(StopVerdict::Refusal(StopRefusal::UnsupportedPlatform))
}
fn exit_bookkeeping(
home: &Path,
record: &PidRecord,
) -> (Result<NoteFate, String>, Result<bool, String>) {
let fate = outcome::read_fate(home, record.pid).map_err(|error| error.to_string());
let pid_file_reconciled =
pid_file::remove_if_matches(home, record).map_err(|error| error.to_string());
(fate, pid_file_reconciled)
}
fn wait_cadence(patience: Duration) -> Duration {
(patience / 200).clamp(Duration::from_millis(25), Duration::from_millis(250))
}
#[cfg(test)]
#[path = "stop_tests.rs"]
mod tests;