Skip to main content

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 number inside the `pid:[...]` a `/proc/self/ns/pid` link reads,
68/// when the text is exactly that with digits only.
69/// Why:
70///   The scope is written into every marker and compared by readers,
71/// so a text that is not a namespace number is no scope.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// /^pid:\[(\d+)\]$/.exec(text)?.[1]
76/// ```
77pub fn scope_of_link(text: &str) -> Option<String> {
78    let number: &str = text.strip_prefix("pid:[")?.strip_suffix(']')?;
79    return (!number.is_empty() && number.as_bytes().iter().all(u8::is_ascii_digit)).then_some(number.to_string());
80}
81
82/// The PID namespace of this process as `/proc/self/ns/pid` names it (the number inside `pid:[...]`),
83/// once.
84pub fn current_pid_scope() -> Option<&'static str> {
85    static SCOPE: OnceLock<Option<String>> = OnceLock::new();
86    return SCOPE
87        .get_or_init(|| {
88            let target = std::fs::read_link("/proc/self/ns/pid").ok()?;
89            return scope_of_link(target.to_string_lossy().as_ref());
90        })
91        .as_deref();
92}
93
94/// Whether the record `read` returned from `/proc/self/stat` numbers the process `own` as the operating system does:
95/// a record that could not be read,
96/// one that names no PID and one that names another PID do not.
97pub fn procfs_agrees_with(read: std::io::Result<Vec<u8>>, own: u32) -> bool {
98    let Ok(stat) = read else { return false };
99    return check_procfs_view(stat.as_slice(), own).is_ok();
100}
101
102/// Whether this process's `/proc` numbers it as the operating system does,
103/// once.
104fn current_procfs_agrees() -> bool {
105    static AGREES: OnceLock<bool> = OnceLock::new();
106    return *AGREES.get_or_init(read_procfs_agreement);
107}
108
109/// What the first call of `current_procfs_agrees` remembers:
110/// this process's own record,
111/// read now.
112fn read_procfs_agreement() -> bool {
113    return procfs_agrees_with(std::fs::read("/proc/self/stat"), std::process::id());
114}
115
116impl ReaderView<'static> {
117    /// What this process can say now.
118    pub fn current() -> ReaderView<'static> {
119        return ReaderView { boot: current_boot_id(), scope: current_pid_scope(), procfs_agrees: current_procfs_agrees() };
120    }
121}
122
123/// What:
124///  The PID that a `/proc/self/stat` record names,
125/// when it is one.
126/// Why:
127///   The record is the first thing a process reads of itself in `/proc`;
128/// its first field is the PID as the mounted `/proc` numbers it.
129/// The name of the process in the record may hold any byte,
130/// so the record is read as bytes and only the leading digits are looked at.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// Number(stat.split(' ')[0])
135/// ```
136pub fn pid_of_self_stat(stat: &[u8]) -> Option<i64> {
137    let digits: &[u8] = stat.split(|byte| { return *byte == b' '; }).next()?;
138    return std::str::from_utf8(digits).ok()?.parse::<i64>().ok();
139}
140
141/// What:
142///  This process's birth identity as a marker names it:
143/// the identity,
144/// the boot it was read in,
145/// and the PID namespace.
146/// Why:
147///   A PID and a start tick since boot recur after a reboot and mean another process in another PID namespace,
148/// so a marker is told by the boot ID and the namespace.
149/// A `/proc` that belongs to another PID namespace numbers processes differently from the process itself,
150/// and an identity read there names another process,
151/// so a process whose `/proc` does not agree with its own PID takes no lock.
152/// On Linux a process that cannot name its boot or its namespace takes no lock either:
153/// a marker without them cannot be told from a reboot's collision.
154///
155/// In TS you'd write (pseudocode):
156/// ```ts
157/// `${await birthIdentity(process.pid)}#${bootId}#${pidNamespace}`
158/// ```
159///
160/// # Errors
161///
162/// The identity error when `/proc` is another namespace's,
163/// the boot or the namespace is unknown,
164/// or the identity cannot be read.
165pub fn own_identity() -> Result<String, ProcessIdentityError> {
166    let own: u32 = std::process::id();
167    #[cfg(target_os = "linux")]
168    let stat: Vec<u8> = std::fs::read("/proc/self/stat").map_err(|error| { return ProcessIdentityError::Io { pid: i64::from(own), error }; })?;
169    #[cfg(not(target_os = "linux"))]
170    let stat: Vec<u8> = format!("{own} ").into_bytes();
171    return identity_in_view(stat.as_slice(), own, current_boot_id(), current_pid_scope());
172}
173
174/// `own_identity` for the process `own`,
175/// whose `/proc/self/stat` reads `stat`,
176/// on a host whose boot is `boot` and in the PID namespace `scope`.
177///
178/// # Errors
179///
180/// As `own_identity`;
181/// `ProcMismatch` when `stat` names another PID than `own`,
182/// `BootUnavailable` and `ScopeUnavailable` on Linux when the boot or the namespace is not known.
183pub fn identity_in_view(stat: &[u8], own: u32, boot: &str, scope: Option<&str>) -> Result<String, ProcessIdentityError> {
184    check_procfs_view(stat, own)?;
185    let birth: String = current_birth_identity()?;
186    #[cfg(target_os = "linux")]
187    {
188        if boot.is_empty() {
189            return Err(ProcessIdentityError::BootUnavailable);
190        }
191        if scope.is_none() {
192            return Err(ProcessIdentityError::ScopeUnavailable);
193        }
194    }
195    let mut identity: String = birth;
196    for part in [Some(boot).filter(|text| { return !text.is_empty(); }), scope] {
197        if let Some(text) = part {
198            identity.push(BOOT_SEPARATOR);
199            identity.push_str(text);
200        }
201    }
202    return Ok(identity);
203}
204
205/// Whether the `/proc/self/stat` record `stat` numbers the process `own` the way the process itself is numbered.
206///
207/// # Errors
208///
209/// `ProcMismatch` naming both numbers when `/proc` belongs to another PID namespace,
210/// and `MalformedStat` when the record names no PID.
211pub fn check_procfs_view(stat: &[u8], own: u32) -> Result<(), ProcessIdentityError> {
212    let procfs: i64 = pid_of_self_stat(stat).ok_or(ProcessIdentityError::MalformedStat { pid: i64::from(own) })?;
213    if procfs != i64::from(own) {
214        return Err(ProcessIdentityError::ProcMismatch { procfs, own });
215    }
216    return Ok(());
217}
218
219/// The birth identity,
220/// the boot ID and the PID namespace a marker names;
221/// a marker an earlier release wrote names no boot,
222/// and one of the release before the namespace was recorded names no namespace.
223fn split_birth(birth: &str) -> (&str, Option<&str>, Option<&str>) {
224    let mut parts = birth.splitn(3, BOOT_SEPARATOR);
225    let identity: &str = parts.next().unwrap_or(birth);
226    return (identity, parts.next(), parts.next());
227}
228
229/// What:
230///  What a reader with the view `view` makes of `holder`,
231/// asking `ask` about the process.
232/// Why:
233///   A boot that has ended ends every process in it,
234/// whatever the reader's numbering.
235/// Beyond that the reader compares like with like or says unknown:
236/// its `/proc` must agree with its own PID,
237/// it must know its boot when the marker names one,
238/// and its PID namespace must be the marker's when the marker names one.
239/// A holder the marker does not name (an earlier release's marker) is gone,
240/// as such markers always were.
241///
242/// In TS you'd write (pseudocode):
243/// ```ts
244/// holderStateIn(holder, view, ask) // 'gone' | 'running' | 'unknown'
245/// ```
246pub fn holder_state_in(
247    holder: Option<&(i64, String)>,
248    view: &ReaderView,
249    ask: &dyn Fn(i64) -> Result<Option<String>, ProcessIdentityError>,
250) -> HolderState {
251    let Some((pid, birth)) = holder else { return HolderState::Gone };
252    let (identity, boot, scope) = split_birth(birth.as_str());
253    if let Some(recorded) = boot
254        && !view.boot.is_empty()
255        && recorded != view.boot
256    {
257        return HolderState::Gone;
258    }
259    if !view.procfs_agrees || (boot.is_some() && view.boot.is_empty()) {
260        return HolderState::Unknown;
261    }
262    if let Some(recorded) = scope
263        && view.scope != Some(recorded)
264    {
265        return HolderState::Unknown;
266    }
267    return match ask(*pid) {
268        Ok(None) => HolderState::Gone,
269        Ok(Some(now)) if now == identity => HolderState::Running,
270        Ok(Some(_)) => HolderState::Gone,
271        Err(_) => HolderState::Unknown,
272    };
273}
274
275/// What this process makes of `holder` now.
276pub fn holder_state(holder: Option<&(i64, String)>) -> HolderState {
277    return holder_state_in(holder, &ReaderView::current(), &process_birth_identity);
278}
279
280/// Whether the process a marker names is gone;
281/// a question that cannot be answered keeps the lock,
282/// which is the safe side.
283pub fn holder_is_gone(holder: Option<&(i64, String)>) -> bool {
284    return holder_state(holder) == HolderState::Gone;
285}
286
287/// Whether the process a marker names is proven to run now.
288pub fn holder_is_running(holder: Option<&(i64, String)>) -> bool {
289    return holder_state(holder) == HolderState::Running;
290}
291
292/// Whether the lock at `lock` carries the service's mark naming a holder that is gone:
293/// a dead instance's leftover,
294/// which the next landing removes.
295/// A mark that names no holder (one an earlier release wrote) is not claimed here,
296/// because its age is the only evidence a reader has of it.
297pub fn mark_names_a_gone_holder(lock: &Path) -> bool {
298    return marker_of(lock).is_some_and(|marked: Marked| { return marked.holder.is_some() && holder_is_gone(marked.holder.as_ref()); });
299}