use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::ServerError;
const PID_FILE_NAME: &str = "aion-server.pid";
const PID_LOCK_FILE_NAME: &str = "aion-server.pid.lock";
pub(super) 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, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IncarnationState {
Booting,
#[default]
Serving,
Draining,
}
impl IncarnationState {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Booting => "BOOTING",
Self::Serving => "SERVING",
Self::Draining => "DRAINING",
}
}
}
#[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,
#[serde(default)]
pub state: IncarnationState,
#[serde(default)]
pub http_address: Option<SocketAddr>,
#[serde(default)]
pub grpc_address: Option<SocketAddr>,
#[serde(default)]
pub intended_http_address: Option<SocketAddr>,
#[serde(default)]
pub intended_grpc_address: Option<SocketAddr>,
#[serde(default)]
pub stage: Option<String>,
#[serde(default)]
pub stage_detail: Option<String>,
#[serde(default)]
pub stage_seq: u64,
#[serde(default)]
pub stage_updated_at_unix_secs: u64,
#[serde(default)]
pub drain_timeout_seconds: u64,
}
impl PidRecord {
#[must_use]
pub const fn identity(&self) -> (u32, u64) {
(self.pid, self.started_at_unix_secs)
}
#[must_use]
pub fn is_same_incarnation(&self, other: &Self) -> bool {
self.identity() == other.identity()
}
#[must_use]
pub fn running_for_secs(&self) -> u64 {
now_unix_secs().saturating_sub(self.started_at_unix_secs)
}
#[must_use]
pub fn stage_age_secs(&self) -> Option<u64> {
if self.stage_updated_at_unix_secs == 0 {
return None;
}
Some(now_unix_secs().saturating_sub(self.stage_updated_at_unix_secs))
}
#[must_use]
pub fn stage_line(&self) -> Option<String> {
let stage = self.stage.as_ref()?;
match &self.stage_detail {
Some(detail) => Some(format!("{stage} — {detail}")),
None => Some(stage.clone()),
}
}
}
#[must_use]
pub fn now_unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StaleReconciliation {
NonePresent,
Collision(PidRecord),
DeadIncarnation(PidRecord),
ReusedPid(PidRecord),
LiveIncarnationElsewhere(PidRecord),
SucceededDrainer(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> {
read_path(&pid_file_path(home))
}
pub(super) fn read_path(path: &Path) -> Result<Option<PidRecord>, ServerError> {
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(super) fn write_record_atomically(path: &Path, record: &PidRecord) -> Result<(), ServerError> {
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()
))
})
}
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_path(&path)? {
Some(current) if current.is_same_incarnation(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)
}
pub(super) fn pid_file_error(message: impl Into<String>) -> ServerError {
ServerError::PidFile {
message: message.into(),
}
}
#[cfg(test)]
#[path = "pid_file_tests.rs"]
mod tests;