aion_server/control/
incarnation.rs1use std::path::PathBuf;
14
15use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
16
17use super::pid_file::PidRecord;
18use crate::error::ServerError;
19
20#[derive(Clone, Debug)]
23pub struct SelfIdentity {
24 pub pid: u32,
26 pub started_at_unix_secs: u64,
29 pub binary_sha256: String,
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum IncarnationProbe {
37 ProcessGone,
39 Verified {
42 exe: Option<PathBuf>,
45 },
46 DifferentIncarnation {
50 live_started_at_unix_secs: u64,
52 exe: Option<PathBuf>,
54 },
55}
56
57pub fn self_identity() -> Result<SelfIdentity, ServerError> {
67 let pid = std::process::id();
68 let started_at_unix_secs =
69 process_start_instant(pid).ok_or_else(|| ServerError::Incarnation {
70 message: format!(
71 "the process table has no entry for this process (pid {pid}); \
72 cannot record a verifiable incarnation"
73 ),
74 })?;
75 let exe = std::env::current_exe().map_err(|io_error| ServerError::Incarnation {
76 message: format!("cannot resolve this process's executable path: {io_error}"),
77 })?;
78 let bytes = std::fs::read(&exe).map_err(|io_error| ServerError::Incarnation {
79 message: format!(
80 "cannot read this process's executable `{}` for content hashing: {io_error}",
81 exe.display()
82 ),
83 })?;
84 Ok(SelfIdentity {
85 pid,
86 started_at_unix_secs,
87 binary_sha256: sha256_hex(&bytes),
88 })
89}
90
91#[must_use]
93pub fn probe(record: &PidRecord) -> IncarnationProbe {
94 let mut system = System::new();
95 let pid = sysinfo::Pid::from_u32(record.pid);
96 system.refresh_processes_specifics(
97 ProcessesToUpdate::Some(&[pid]),
98 true,
99 ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::Always),
100 );
101 let Some(process) = system.process(pid) else {
102 return IncarnationProbe::ProcessGone;
103 };
104 let live_started_at_unix_secs = process.start_time();
105 let exe = process.exe().map(std::path::Path::to_path_buf);
106 if live_started_at_unix_secs == record.started_at_unix_secs {
107 IncarnationProbe::Verified { exe }
108 } else {
109 IncarnationProbe::DifferentIncarnation {
110 live_started_at_unix_secs,
111 exe,
112 }
113 }
114}
115
116fn process_start_instant(pid: u32) -> Option<u64> {
119 let mut system = System::new();
120 let pid = sysinfo::Pid::from_u32(pid);
121 system.refresh_processes_specifics(
122 ProcessesToUpdate::Some(&[pid]),
123 true,
124 ProcessRefreshKind::nothing(),
125 );
126 system.process(pid).map(sysinfo::Process::start_time)
127}
128
129fn sha256_hex(bytes: &[u8]) -> String {
131 use sha2::Digest as _;
132 const HEX: &[u8; 16] = b"0123456789abcdef";
133 let digest = sha2::Sha256::digest(bytes);
134 let mut hex = String::with_capacity(digest.len() * 2);
135 for byte in digest {
136 hex.push(char::from(HEX[usize::from(byte >> 4)]));
137 hex.push(char::from(HEX[usize::from(byte & 0x0f)]));
138 }
139 hex
140}
141
142#[cfg(test)]
143mod tests {
144 use super::{IncarnationProbe, probe, self_identity};
145 use crate::control::pid_file::PidRecord;
146
147 type TestResult = Result<(), Box<dyn std::error::Error>>;
148
149 fn record_with(
150 pid: u32,
151 started_at_unix_secs: u64,
152 ) -> Result<PidRecord, std::net::AddrParseError> {
153 Ok(PidRecord {
154 pid,
155 started_at_unix_secs,
156 binary_sha256: "0".repeat(64),
157 version: "0.0.0-test".to_owned(),
158 commit: "test".to_owned(),
159 http_address: "127.0.0.1:0".parse()?,
160 grpc_address: "127.0.0.1:0".parse()?,
161 drain_timeout_seconds: 30,
162 })
163 }
164
165 #[test]
169 fn a_record_of_this_process_verifies() -> TestResult {
170 let me = self_identity()?;
171 let record = record_with(me.pid, me.started_at_unix_secs)?;
172 assert!(
173 matches!(probe(&record), IncarnationProbe::Verified { .. }),
174 "this process's own record must verify"
175 );
176 Ok(())
177 }
178
179 #[test]
183 fn a_wrong_start_instant_on_a_live_pid_is_a_different_incarnation() -> TestResult {
184 let me = self_identity()?;
185 let record = record_with(me.pid, me.started_at_unix_secs.wrapping_add(7))?;
186 match probe(&record) {
187 IncarnationProbe::DifferentIncarnation {
188 live_started_at_unix_secs,
189 ..
190 } => {
191 assert_eq!(
192 live_started_at_unix_secs, me.started_at_unix_secs,
193 "the refusal must carry the LIVE process's instant"
194 );
195 }
196 other => {
197 return Err(format!(
198 "a mismatched start instant must read as a different incarnation, got {other:?}"
199 )
200 .into());
201 }
202 }
203 Ok(())
204 }
205
206 #[test]
211 fn a_vacant_pid_is_process_gone() -> TestResult {
212 let child = std::process::Command::new("true").spawn();
215 let mut child = match child {
216 Ok(child) => child,
217 Err(io_error) => return Err(format!("cannot spawn probe child: {io_error}").into()),
218 };
219 let pid = child.id();
220 child.wait()?;
221 let record = record_with(pid, 1)?;
222 match probe(&record) {
226 IncarnationProbe::ProcessGone | IncarnationProbe::DifferentIncarnation { .. } => Ok(()),
227 IncarnationProbe::Verified { .. } => {
228 Err("a reaped child's record must never verify".into())
229 }
230 }
231 }
232}