use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use crate::error::ServerError;
const PID_FILE_NAME: &str = "aion-server.pid";
const PID_LOCK_FILE_NAME: &str = "aion-server.pid.lock";
fn lock_pid_mutation(pid_path: &Path) -> Result<std::fs::File, ServerError> {
let lock_path = pid_path.with_file_name(PID_LOCK_FILE_NAME);
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&lock_path)
.map_err(|io_error| {
pid_file_error(format!(
"could not open the pid mutation lock `{}`: {io_error}",
lock_path.display()
))
})?;
file.lock().map_err(|io_error| {
pid_file_error(format!(
"could not take the pid mutation lock `{}`: {io_error}",
lock_path.display()
))
})?;
Ok(file)
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PidRecord {
pub pid: u32,
pub started_at_unix_secs: u64,
pub binary_sha256: String,
pub version: String,
pub commit: String,
pub http_address: SocketAddr,
pub grpc_address: SocketAddr,
#[serde(default)]
pub drain_timeout_seconds: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StaleReconciliation {
NonePresent,
DeadIncarnation(PidRecord),
ReusedPid(PidRecord),
LiveIncarnation(PidRecord),
LiveIncarnationElsewhere(PidRecord),
Unreadable {
reason: String,
},
}
#[must_use]
pub fn pid_file_path(home: &Path) -> PathBuf {
home.join("run").join(PID_FILE_NAME)
}
pub fn read(home: &Path) -> Result<Option<PidRecord>, ServerError> {
let path = pid_file_path(home);
let content = match std::fs::read_to_string(&path) {
Ok(content) => content,
Err(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(io_error) => {
return Err(pid_file_error(format!(
"could not read pid file `{}`: {io_error}",
path.display()
)));
}
};
let record = serde_json::from_str::<PidRecord>(&content).map_err(|parse_error| {
pid_file_error(format!(
"pid file `{}` exists but does not parse as a pid record: {parse_error}. \
If it was written by hand (the pre-verb restart ritual wrote a bare pid), \
remove it and restart the server so the server writes its own record",
path.display()
))
})?;
Ok(Some(record))
}
pub fn claim(home: &Path, record: &PidRecord) -> Result<PidFileGuard, ServerError> {
let path = pid_file_path(home);
let run_dir = path.parent().ok_or_else(|| {
pid_file_error(format!(
"pid file path `{}` has no parent directory",
path.display()
))
})?;
std::fs::create_dir_all(run_dir).map_err(|io_error| {
pid_file_error(format!(
"could not create run directory `{}`: {io_error}",
run_dir.display()
))
})?;
let mutation_lock = lock_pid_mutation(&path)?;
let mut reconciliation = reconcile_existing(&path);
if let StaleReconciliation::LiveIncarnation(existing) = &reconciliation
&& (existing.http_address, existing.grpc_address)
!= (record.http_address, record.grpc_address)
{
reconciliation = StaleReconciliation::LiveIncarnationElsewhere(existing.clone());
}
if let StaleReconciliation::LiveIncarnationElsewhere(existing) = &reconciliation {
warn!(
path = %path.display(),
recorded_pid = existing.pid,
recorded_http_address = %existing.http_address,
this_http_address = %record.http_address,
"a live server already holds this home's pid file on different \
addresses; this incarnation boots UNCLAIMED — `aion server \
stop`/`status` will address the recorded server, not this one. \
Give each server its own AION_HOME to make both addressable"
);
drop(mutation_lock);
return Ok(PidFileGuard {
path,
record: record.clone(),
reconciliation,
holds_claim: false,
});
}
report_reconciliation(&path, &reconciliation);
let content = serde_json::to_string(record).map_err(|serialize_error| {
pid_file_error(format!(
"could not serialize the pid record: {serialize_error}"
))
})?;
let temp_path = path.with_extension("pid.tmp");
std::fs::write(&temp_path, format!("{content}\n")).map_err(|io_error| {
pid_file_error(format!(
"could not write pid file staging `{}`: {io_error}",
temp_path.display()
))
})?;
std::fs::rename(&temp_path, &path).map_err(|io_error| {
pid_file_error(format!(
"could not move pid file into place at `{}`: {io_error}",
path.display()
))
})?;
drop(mutation_lock);
info!(
path = %path.display(),
pid = record.pid,
started_at_unix_secs = record.started_at_unix_secs,
"pid file written; this incarnation has claimed the home"
);
Ok(PidFileGuard {
path,
record: record.clone(),
reconciliation,
holds_claim: true,
})
}
pub fn remove_if_matches(home: &Path, record: &PidRecord) -> Result<bool, ServerError> {
let path = pid_file_path(home);
if !path.exists() {
return Ok(false);
}
let mutation_lock = lock_pid_mutation(&path)?;
let removed = match read(home)? {
Some(current) if current == *record => {
std::fs::remove_file(&path).map_err(|io_error| {
pid_file_error(format!(
"could not remove pid file `{}`: {io_error}",
path.display()
))
})?;
true
}
Some(_) | None => false,
};
drop(mutation_lock);
Ok(removed)
}
fn reconcile_existing(path: &Path) -> StaleReconciliation {
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => {
return StaleReconciliation::NonePresent;
}
Err(io_error) => {
return StaleReconciliation::Unreadable {
reason: format!("could not read the existing file: {io_error}"),
};
}
};
let record = match serde_json::from_str::<PidRecord>(&content) {
Ok(record) => record,
Err(parse_error) => {
return StaleReconciliation::Unreadable {
reason: format!("existing content does not parse as a pid record: {parse_error}"),
};
}
};
match super::incarnation::probe(&record) {
super::incarnation::IncarnationProbe::ProcessGone => {
StaleReconciliation::DeadIncarnation(record)
}
super::incarnation::IncarnationProbe::DifferentIncarnation { .. } => {
StaleReconciliation::ReusedPid(record)
}
super::incarnation::IncarnationProbe::Verified { .. } => {
StaleReconciliation::LiveIncarnation(record)
}
}
}
fn report_reconciliation(path: &Path, reconciliation: &StaleReconciliation) {
match reconciliation {
StaleReconciliation::NonePresent | StaleReconciliation::LiveIncarnationElsewhere(_) => {}
StaleReconciliation::DeadIncarnation(record) => {
warn!(
path = %path.display(),
stale_pid = record.pid,
stale_started_at_unix_secs = record.started_at_unix_secs,
stale_version = %record.version,
"stale pid file: the recorded server (pid gone) died without removing \
its record; replacing it with this incarnation's"
);
}
StaleReconciliation::ReusedPid(record) => {
warn!(
path = %path.display(),
stale_pid = record.pid,
stale_started_at_unix_secs = record.started_at_unix_secs,
"stale pid file: the recorded pid is alive but belongs to a different \
process (pid reused after the recorded server died); replacing the \
record and leaving that process untouched"
);
}
StaleReconciliation::LiveIncarnation(record) => {
warn!(
path = %path.display(),
recorded_pid = record.pid,
recorded_http_address = %record.http_address,
"pid file names a LIVE matching incarnation recorded on THESE SAME \
addresses, yet this boot bound them — a contradiction (the recorded \
server cannot be serving them). Replacing the record as debris of an \
incompletely observed exit"
);
}
StaleReconciliation::Unreadable { reason } => {
warn!(
path = %path.display(),
%reason,
"pid file present but unreadable as a record (hand-written or \
corrupt); replacing it with this incarnation's"
);
}
}
}
#[derive(Debug)]
pub struct PidFileGuard {
path: PathBuf,
record: PidRecord,
reconciliation: StaleReconciliation,
holds_claim: bool,
}
impl PidFileGuard {
#[must_use]
pub fn reconciliation(&self) -> &StaleReconciliation {
&self.reconciliation
}
#[must_use]
pub fn record(&self) -> &PidRecord {
&self.record
}
#[must_use]
pub fn holds_claim(&self) -> bool {
self.holds_claim
}
}
impl Drop for PidFileGuard {
fn drop(&mut self) {
if !self.holds_claim {
return;
}
let mutation_lock = match lock_pid_mutation(&self.path) {
Ok(lock) => lock,
Err(error) => {
warn!(
path = %self.path.display(),
%error,
"could not take the pid mutation lock on exit; leaving the pid \
file for stale reconciliation"
);
return;
}
};
let current = match std::fs::read_to_string(&self.path) {
Ok(content) => match serde_json::from_str::<PidRecord>(&content) {
Ok(record) => Some(record),
Err(parse_error) => {
warn!(
path = %self.path.display(),
%parse_error,
"the pid file does not parse at exit time; leaving it for \
stale reconciliation rather than deleting a record this \
binary cannot compare"
);
None
}
},
Err(io_error) if io_error.kind() == std::io::ErrorKind::NotFound => {
warn!(
path = %self.path.display(),
"the pid file is GONE at exit time although this incarnation \
held the claim — it was removed out from under the running \
server; nothing to reconcile, recording the disappearance"
);
None
}
Err(io_error) => {
warn!(
path = %self.path.display(),
%io_error,
"could not read the pid file at exit time; leaving it for \
stale reconciliation"
);
None
}
};
match current {
Some(record) if record == self.record => {
if let Err(io_error) = std::fs::remove_file(&self.path) {
warn!(
path = %self.path.display(),
%io_error,
"could not remove the pid file on exit; `aion server stop` \
and the next boot both reconcile it as stale"
);
} else {
info!(path = %self.path.display(), "pid file removed on exit");
}
}
Some(_) => {
info!(
path = %self.path.display(),
"pid file now holds a different incarnation's record; leaving it"
);
}
None => {}
}
drop(mutation_lock);
}
}
fn pid_file_error(message: impl Into<String>) -> ServerError {
ServerError::PidFile {
message: message.into(),
}
}
#[cfg(test)]
#[path = "pid_file_tests.rs"]
mod tests;