#![cfg(unix)]
use std::io::Write as _;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicI32, Ordering};
static SIGNAL_SENDER_PID: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ExitReason {
Signal(&'static str),
Idle,
Error,
}
impl std::fmt::Display for ExitReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Signal(name) => write!(f, "signal {name}"),
Self::Idle => f.write_str("idle"),
Self::Error => f.write_str("error"),
}
}
}
pub(super) fn exit_record_path(pid_file: &Path) -> PathBuf {
let mut name = pid_file.as_os_str().to_owned();
name.push(".exit");
PathBuf::from(name)
}
pub(super) const SENDER_CAPTURE_LATE: &str = "signal sender capture registered after another handler for this signal; a shutdown it starts can be recorded with sender_pid=unknown";
pub(super) fn capture_signal_senders() -> std::io::Result<()> {
static INSTALLED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
let mut installed = INSTALLED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *installed {
return Ok(());
}
for (signal, name) in [(libc::SIGTERM, "SIGTERM"), (libc::SIGINT, "SIGINT")] {
match has_handler(signal) {
Ok(true) => {
tracing::warn!(target: "khive.boot", signal = name, "{SENDER_CAPTURE_LATE}")
}
Ok(false) => {}
Err(error) => tracing::warn!(
target: "khive.boot",
signal = name,
error = %error,
"could not read the signal disposition before registering the sender recorder"
),
}
unsafe { signal_hook_registry::register_sigaction(signal, record_signal_sender) }?;
}
*installed = true;
Ok(())
}
fn has_handler(signal: libc::c_int) -> std::io::Result<bool> {
let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
if unsafe { libc::sigaction(signal, std::ptr::null(), &mut current) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(current.sa_sigaction != libc::SIG_DFL && current.sa_sigaction != libc::SIG_IGN)
}
fn record_signal_sender(info: &libc::siginfo_t) {
let sender = unsafe { info.si_pid() };
let _ = SIGNAL_SENDER_PID.compare_exchange(0, sender, Ordering::Relaxed, Ordering::Relaxed);
}
pub(super) fn signal_sender_pid() -> Option<u32> {
u32::try_from(SIGNAL_SENDER_PID.load(Ordering::Relaxed))
.ok()
.filter(|pid| *pid > 0)
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum PreviousExit {
Absent,
Exited(String),
NoExit { pid: String },
Unreadable(String),
}
pub(super) fn read_previous(record: &Path) -> PreviousExit {
let contents = match std::fs::read_to_string(record) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return PreviousExit::Absent,
Err(error) => return PreviousExit::Unreadable(error.to_string()),
};
let line = contents.trim();
if line.starts_with("exited ") {
return PreviousExit::Exited(line.to_owned());
}
if line.starts_with("running ") {
let pid = line
.split_whitespace()
.find_map(|field| field.strip_prefix("pid="))
.unwrap_or("unknown");
return PreviousExit::NoExit {
pid: pid.to_owned(),
};
}
PreviousExit::Unreadable(format!("unrecognized record {line:?}"))
}
pub(super) struct ExitRecord {
pid_file: PathBuf,
record: PathBuf,
}
impl ExitRecord {
pub(super) fn claim(pid_file: &Path) -> Self {
let this = Self {
pid_file: pid_file.to_path_buf(),
record: exit_record_path(pid_file),
};
match read_previous(&this.record) {
PreviousExit::Absent => {}
PreviousExit::Exited(line) => tracing::warn!(
target: "khive.boot",
record = ?this.record,
"previous exit: {line}"
),
PreviousExit::NoExit { pid } => tracing::warn!(
target: "khive.boot",
record = ?this.record,
"previous incarnation pid={pid} recorded no exit (killed or crashed)"
),
PreviousExit::Unreadable(error) => tracing::warn!(
target: "khive.boot",
record = ?this.record,
error = %error,
"previous exit record unreadable"
),
}
log_descriptor_limit();
let line = format!("running pid={} started={}", std::process::id(), now());
if let Err(error) = this.write_if_owned(&line) {
tracing::warn!(target: "khive.boot", record = ?this.record, error = %error, "could not write the running record");
}
this
}
pub(super) fn exiting(&self, reason: ExitReason) {
let pid = std::process::id();
let sender_pid = match (reason, signal_sender_pid()) {
(ExitReason::Signal(_), Some(sender)) => sender.to_string(),
_ => "unknown".to_owned(),
};
tracing::warn!(
target: "khive.boot",
pid,
reason = %reason,
sender_pid = %sender_pid,
"khived exiting"
);
let line = format!(
"exited pid={pid} reason={reason} sender_pid={sender_pid} at={}",
now()
);
if let Err(error) = self.write_if_owned(&line) {
tracing::warn!(target: "khive.boot", record = ?self.record, error = %error, "could not write the exit record");
}
}
fn write_if_owned(&self, line: &str) -> std::io::Result<()> {
let owner = std::fs::read_to_string(&self.pid_file)?;
if owner.trim().parse::<u32>().ok() != Some(std::process::id()) {
return Err(std::io::Error::other(format!(
"the PID file {} no longer names this process",
self.pid_file.display()
)));
}
write_atomically(&self.record, line)
}
}
fn write_atomically(record: &Path, line: &str) -> std::io::Result<()> {
let file_name = record
.file_name()
.ok_or_else(|| std::io::Error::other("exit record path has no file name"))?;
let mut tmp_name = std::ffi::OsString::from(".");
tmp_name.push(file_name);
tmp_name.push(format!(".{}.tmp", std::process::id()));
let tmp = record.with_file_name(tmp_name);
match std::fs::remove_file(&tmp) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let written = (|| {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.custom_flags(libc::O_NOFOLLOW)
.open(&tmp)?;
file.write_all(line.as_bytes())?;
file.write_all(b"\n")?;
file.sync_all()?;
std::fs::rename(&tmp, record)
})();
if written.is_err() {
let _ = std::fs::remove_file(&tmp);
}
written
}
fn now() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
fn log_descriptor_limit() {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } != 0 {
let error = std::io::Error::last_os_error();
tracing::warn!(target: "khive.boot", error = %error, "descriptor limit unreadable");
return;
}
let show = |value: libc::rlim_t| {
if value == libc::RLIM_INFINITY {
"unlimited".to_owned()
} else {
value.to_string()
}
};
tracing::warn!(
target: "khive.boot",
nofile_soft = %show(limit.rlim_cur),
nofile_hard = %show(limit.rlim_max),
"descriptor limit"
);
}