git_policy_cli/service_index_identity.rs
1//! What:
2//! Who a marker's holder is:
3//! the identity this process writes into a marker,
4//! and the three answers a reader gives about a holder named in one:
5//! gone,
6//! running,
7//! or unknown.
8//! Why:
9//! A marker names a process by its PID and start tick,
10//! which mean something only in the boot and the PID namespace they were read in.
11//! The identity therefore ends in the boot ID and the PID namespace,
12//! a writer whose `/proc` numbers processes differently from the process itself writes none,
13//! and a reader that cannot compare like with like answers unknown,
14//! which keeps the lock and proves nothing,
15//! instead of reading a number of another namespace as a local process and calling a live service gone.
16//! The module is split from `service_index_marker.rs`,
17//! which re-exports the public items here under their old names.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! const identity = ownIdentity(); const state = holderState(holder, readerView); // 'gone' | 'running' | 'unknown'
22//! ```
23
24use super::claim_time::current_boot_id;
25use super::process_identity::{ProcessIdentityError, current_birth_identity, process_birth_identity};
26use super::service_index_marker::{Marked, marker_of};
27use std::path::Path;
28use std::sync::OnceLock;
29
30/// What separates the parts of the identity in a marker:
31/// the birth identity,
32/// the boot ID,
33/// and the PID namespace.
34pub const BOOT_SEPARATOR: char = '#';
35
36/// What a reader makes of the holder a marker names.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum HolderState {
39 /// No such process,
40 /// a process born since,
41 /// or a boot that has ended.
42 Gone,
43 /// The process runs now,
44 /// in the same boot and the same PID namespace.
45 Running,
46 /// The reader cannot compare:
47 /// another PID namespace,
48 /// a `/proc` that does not agree with the reader's own PID,
49 /// no boot ID to compare,
50 /// or a record that cannot be read.
51 Unknown,
52}
53
54/// What a reader can say about its own numbering of processes.
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct ReaderView<'a> {
57 /// The boot ID of the host now,
58 /// empty where it cannot be read.
59 pub boot: &'a str,
60 /// The PID namespace of the reader,
61 /// where it can be read.
62 pub scope: Option<&'a str>,
63 /// Whether the reader's `/proc` numbers the reader as the operating system does.
64 pub procfs_agrees: bool,
65}
66
67/// The PID namespace of this process as `/proc/self/ns/pid` names it (the number inside `pid:[...]`),
68/// once.
69fn current_pid_scope() -> Option<&'static str> {
70 static SCOPE: OnceLock<Option<String>> = OnceLock::new();
71 return SCOPE
72 .get_or_init(|| {
73 let target = std::fs::read_link("/proc/self/ns/pid").ok()?;
74 let text: String = target.to_string_lossy().into_owned();
75 let number: &str = text.strip_prefix("pid:[")?.strip_suffix(']')?;
76 return (!number.is_empty() && number.bytes().all(|byte| { return byte.is_ascii_digit(); })).then(|| { return number.to_string(); });
77 })
78 .as_deref();
79}
80
81/// Whether this process's `/proc` numbers it as the operating system does,
82/// once.
83fn current_procfs_agrees() -> bool {
84 static AGREES: OnceLock<bool> = OnceLock::new();
85 return *AGREES.get_or_init(|| {
86 let Ok(stat) = std::fs::read("/proc/self/stat") else { return false };
87 return check_procfs_view(stat.as_slice(), std::process::id()).is_ok();
88 });
89}
90
91impl ReaderView<'static> {
92 /// What this process can say now.
93 pub fn current() -> ReaderView<'static> {
94 return ReaderView { boot: current_boot_id(), scope: current_pid_scope(), procfs_agrees: current_procfs_agrees() };
95 }
96}
97
98/// What:
99/// The PID that a `/proc/self/stat` record names,
100/// when it is one.
101/// Why:
102/// The record is the first thing a process reads of itself in `/proc`;
103/// its first field is the PID as the mounted `/proc` numbers it.
104/// The name of the process in the record may hold any byte,
105/// so the record is read as bytes and only the leading digits are looked at.
106///
107/// In TS you'd write (pseudocode):
108/// ```ts
109/// Number(stat.split(' ')[0])
110/// ```
111pub fn pid_of_self_stat(stat: &[u8]) -> Option<i64> {
112 let digits: &[u8] = stat.split(|byte| { return *byte == b' '; }).next()?;
113 return std::str::from_utf8(digits).ok()?.parse::<i64>().ok();
114}
115
116/// What:
117/// This process's birth identity as a marker names it:
118/// the identity,
119/// the boot it was read in,
120/// and the PID namespace.
121/// Why:
122/// A PID and a start tick since boot recur after a reboot and mean another process in another PID namespace,
123/// so a marker is told by the boot ID and the namespace.
124/// A `/proc` that belongs to another PID namespace numbers processes differently from the process itself,
125/// and an identity read there names another process,
126/// so a process whose `/proc` does not agree with its own PID takes no lock.
127/// On Linux a process that cannot name its boot or its namespace takes no lock either:
128/// a marker without them cannot be told from a reboot's collision.
129///
130/// In TS you'd write (pseudocode):
131/// ```ts
132/// `${await birthIdentity(process.pid)}#${bootId}#${pidNamespace}`
133/// ```
134///
135/// # Errors
136///
137/// The identity error when `/proc` is another namespace's,
138/// the boot or the namespace is unknown,
139/// or the identity cannot be read.
140pub fn own_identity() -> Result<String, ProcessIdentityError> {
141 let own: u32 = std::process::id();
142 #[cfg(target_os = "linux")]
143 let stat: Vec<u8> = std::fs::read("/proc/self/stat").map_err(|error| { return ProcessIdentityError::Io { pid: i64::from(own), error }; })?;
144 #[cfg(not(target_os = "linux"))]
145 let stat: Vec<u8> = format!("{own} ").into_bytes();
146 return identity_in_view(stat.as_slice(), own, current_boot_id(), current_pid_scope());
147}
148
149/// `own_identity` for the process `own`,
150/// whose `/proc/self/stat` reads `stat`,
151/// on a host whose boot is `boot` and in the PID namespace `scope`.
152///
153/// # Errors
154///
155/// As `own_identity`;
156/// `ProcMismatch` when `stat` names another PID than `own`,
157/// `BootUnavailable` and `ScopeUnavailable` on Linux when the boot or the namespace is not known.
158pub fn identity_in_view(stat: &[u8], own: u32, boot: &str, scope: Option<&str>) -> Result<String, ProcessIdentityError> {
159 check_procfs_view(stat, own)?;
160 let birth: String = current_birth_identity()?;
161 #[cfg(target_os = "linux")]
162 {
163 if boot.is_empty() {
164 return Err(ProcessIdentityError::BootUnavailable);
165 }
166 if scope.is_none() {
167 return Err(ProcessIdentityError::ScopeUnavailable);
168 }
169 }
170 let mut identity: String = birth;
171 for part in [Some(boot).filter(|text| { return !text.is_empty(); }), scope] {
172 if let Some(text) = part {
173 identity.push(BOOT_SEPARATOR);
174 identity.push_str(text);
175 }
176 }
177 return Ok(identity);
178}
179
180/// Whether the `/proc/self/stat` record `stat` numbers the process `own` the way the process itself is numbered.
181///
182/// # Errors
183///
184/// `ProcMismatch` naming both numbers when `/proc` belongs to another PID namespace,
185/// and `MalformedStat` when the record names no PID.
186pub fn check_procfs_view(stat: &[u8], own: u32) -> Result<(), ProcessIdentityError> {
187 let procfs: i64 = pid_of_self_stat(stat).ok_or(ProcessIdentityError::MalformedStat { pid: i64::from(own) })?;
188 if procfs != i64::from(own) {
189 return Err(ProcessIdentityError::ProcMismatch { procfs, own });
190 }
191 return Ok(());
192}
193
194/// The birth identity,
195/// the boot ID and the PID namespace a marker names;
196/// a marker an earlier release wrote names no boot,
197/// and one of the release before the namespace was recorded names no namespace.
198fn split_birth(birth: &str) -> (&str, Option<&str>, Option<&str>) {
199 let mut parts = birth.splitn(3, BOOT_SEPARATOR);
200 let identity: &str = parts.next().unwrap_or(birth);
201 return (identity, parts.next(), parts.next());
202}
203
204/// What:
205/// What a reader with the view `view` makes of `holder`,
206/// asking `ask` about the process.
207/// Why:
208/// A boot that has ended ends every process in it,
209/// whatever the reader's numbering.
210/// Beyond that the reader compares like with like or says unknown:
211/// its `/proc` must agree with its own PID,
212/// it must know its boot when the marker names one,
213/// and its PID namespace must be the marker's when the marker names one.
214/// A holder the marker does not name (an earlier release's marker) is gone,
215/// as such markers always were.
216///
217/// In TS you'd write (pseudocode):
218/// ```ts
219/// holderStateIn(holder, view, ask) // 'gone' | 'running' | 'unknown'
220/// ```
221pub fn holder_state_in(
222 holder: Option<&(i64, String)>,
223 view: &ReaderView,
224 ask: &dyn Fn(i64) -> Result<Option<String>, ProcessIdentityError>,
225) -> HolderState {
226 let Some((pid, birth)) = holder else { return HolderState::Gone };
227 let (identity, boot, scope) = split_birth(birth.as_str());
228 if let Some(recorded) = boot
229 && !view.boot.is_empty()
230 && recorded != view.boot
231 {
232 return HolderState::Gone;
233 }
234 if !view.procfs_agrees || (boot.is_some() && view.boot.is_empty()) {
235 return HolderState::Unknown;
236 }
237 if let Some(recorded) = scope
238 && view.scope != Some(recorded)
239 {
240 return HolderState::Unknown;
241 }
242 return match ask(*pid) {
243 Ok(None) => HolderState::Gone,
244 Ok(Some(now)) if now == identity => HolderState::Running,
245 Ok(Some(_)) => HolderState::Gone,
246 Err(_) => HolderState::Unknown,
247 };
248}
249
250/// What this process makes of `holder` now.
251pub fn holder_state(holder: Option<&(i64, String)>) -> HolderState {
252 return holder_state_in(holder, &ReaderView::current(), &process_birth_identity);
253}
254
255/// Whether the process a marker names is gone;
256/// a question that cannot be answered keeps the lock,
257/// which is the safe side.
258pub fn holder_is_gone(holder: Option<&(i64, String)>) -> bool {
259 return holder_state(holder) == HolderState::Gone;
260}
261
262/// Whether the process a marker names is proven to run now.
263pub fn holder_is_running(holder: Option<&(i64, String)>) -> bool {
264 return holder_state(holder) == HolderState::Running;
265}
266
267/// Whether the lock at `lock` carries the service's mark naming a holder that is gone:
268/// a dead instance's leftover,
269/// which the next landing removes.
270/// A mark that names no holder (one an earlier release wrote) is not claimed here,
271/// because its age is the only evidence a reader has of it.
272pub fn mark_names_a_gone_holder(lock: &Path) -> bool {
273 return marker_of(lock).is_some_and(|marked: Marked| { return marked.holder.is_some() && holder_is_gone(marked.holder.as_ref()); });
274}