1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! What:
//! The marker the service writes into the `index.lock` it holds,
//! and the questions asked of a marker found there.
//! Why:
//! A restart tells its own stale lock from a foreign one,
//! and a live holder from a dead one,
//! by the marker alone:
//! the batch,
//! the holding process and that process's birth identity.
//! The module is split from `service_index_lock.rs`,
//! which re-exports the public items here under their old names.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const marker = markerText(seq); const reclaimable = isReclaimable(lockPath);
//! ```
use super::claim_time::current_boot_id;
use super::process_identity::{ProcessIdentityError, current_birth_identity, process_birth_identity};
use std::io::Read;
use std::path::Path;
/// The first bytes of a lock the service holds,
/// followed by the batch number,
/// the holding process and its birth identity.
pub const LOCK_MARKER: &str = "cli-git-service ";
/// The longest marker this reader looks at.
const MARKER_LIMIT: u64 = 512;
/// What a service marker names.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Marked {
/// The batch the lock was taken for.
pub seq: u64,
/// The holding process and its birth identity;
/// none in a marker a release before this one wrote.
pub holder: Option<(i64, String)>,
}
/// What:
/// The marker this process writes for batch `seq`:
/// the batch,
/// the process and its birth identity,
/// one line.
/// Why:
/// A restart reclaims a marked lock only when that process is gone,
/// so a slow but live holder keeps its lock.
/// A marker without a birth identity reads as a dead instance's,
/// so a process that cannot name its own identity writes none:
/// it must not take the lock.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `cli-git-service ${seq} ${process.pid} ${birth}\n`
/// ```
///
/// # Errors
///
/// The identity error when this process's birth identity cannot be read.
pub fn marker_text(seq: u64, identity: &dyn Fn() -> Result<String, ProcessIdentityError>) -> Result<String, ProcessIdentityError> {
let birth: String = identity()?;
return Ok(format!("{LOCK_MARKER}{seq} {} {birth}\n", std::process::id()));
}
/// What:
/// The batch and holder a lock's text names,
/// or nothing when it is not a service marker.
/// Why:
/// A marker without a holder is one a release before this one wrote;
/// one with a malformed holder is not the service's.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseMarker(text) // { seq, holder?: [pid, birth] } | undefined
/// ```
pub fn parse_marker(text: &str) -> Option<Marked> {
let rest: &str = text.strip_prefix(LOCK_MARKER)?;
let line: &str = rest.strip_suffix('\n').unwrap_or(rest);
let mut parts = line.splitn(3, ' ');
let seq: u64 = parts.next()?.parse::<u64>().ok()?;
let holder: Option<(i64, String)> = match (parts.next(), parts.next()) {
(None, None) => None,
(Some(pid), Some(birth)) if !birth.is_empty() => Some((pid.parse::<i64>().ok()?, birth.to_string())),
_ => return None,
};
return Some(Marked { seq, holder });
}
/// What separates a birth identity from the boot ID in a marker.
pub const BOOT_SEPARATOR: char = '#';
/// What:
/// The PID that a `/proc/self/stat` text names,
/// when it is one.
/// Why:
/// The record is the first thing a process reads of itself in `/proc`;
/// its first field is the PID as the mounted `/proc` numbers it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// Number(stat.split(' ')[0])
/// ```
pub fn pid_of_self_stat(stat: &str) -> Option<i64> {
return stat.split(' ').next()?.parse::<i64>().ok();
}
/// What:
/// This process's birth identity as a marker names it:
/// the identity and the boot it was read in.
/// Why:
/// A PID and a start tick since boot recur after a reboot,
/// so a marker from another boot is told by the boot ID.
/// A `/proc` that belongs to another PID namespace numbers processes differently from the process itself,
/// and an identity read there names another process,
/// so a process whose `/proc` does not agree with its own PID takes no lock.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `${await birthIdentity(process.pid)}#${bootId}`
/// ```
///
/// # Errors
///
/// The identity error when `/proc` is another namespace's or the identity cannot be read.
pub fn own_identity() -> Result<String, ProcessIdentityError> {
let own: u32 = std::process::id();
#[cfg(target_os = "linux")]
let stat: String = std::fs::read_to_string("/proc/self/stat").map_err(|error| { return ProcessIdentityError::Io { pid: i64::from(own), error }; })?;
#[cfg(not(target_os = "linux"))]
let stat: String = format!("{own} ");
return identity_in_view(stat.as_str(), own);
}
/// `own_identity` for the process `own`,
/// whose `/proc/self/stat` reads `stat`.
///
/// # Errors
///
/// As `own_identity`;
/// `ProcMismatch` when `stat` names another PID than `own`.
pub fn identity_in_view(stat: &str, own: u32) -> Result<String, ProcessIdentityError> {
check_procfs_view(stat, own)?;
let birth: String = current_birth_identity()?;
let boot: &str = current_boot_id();
return Ok(if boot.is_empty() { birth } else { format!("{birth}{BOOT_SEPARATOR}{boot}") });
}
/// Whether the `/proc/self/stat` text `stat` numbers the process `own` the way the process itself is numbered.
///
/// # Errors
///
/// `ProcMismatch` naming both numbers when `/proc` belongs to another PID namespace,
/// and `MalformedStat` when the text names no PID.
pub fn check_procfs_view(stat: &str, own: u32) -> Result<(), ProcessIdentityError> {
let procfs: i64 = pid_of_self_stat(stat).ok_or(ProcessIdentityError::MalformedStat { pid: i64::from(own) })?;
if procfs != i64::from(own) {
return Err(ProcessIdentityError::ProcMismatch { procfs, own });
}
return Ok(());
}
/// The birth identity and the boot ID a marker names;
/// a marker an earlier release wrote names no boot.
fn split_birth(birth: &str) -> (&str, Option<&str>) {
return match birth.rsplit_once(BOOT_SEPARATOR) {
Some((identity, boot)) => (identity, Some(boot)),
None => (birth, None),
};
}
/// Whether the boot a marker was written in is not the current one.
/// A marker without a boot,
/// and a host that has none,
/// never differ.
fn from_another_boot(recorded: Option<&str>) -> bool {
let current: &str = current_boot_id();
return recorded.is_some_and(|boot| { return !current.is_empty() && boot != current; });
}
/// Whether the process a marker names is gone:
/// the boot ended,
/// no process runs under that number,
/// or another one was born since.
/// A question that cannot be answered keeps the lock,
/// which is the safe side.
pub fn holder_is_gone(holder: Option<&(i64, String)>) -> bool {
let Some((pid, birth)) = holder else { return true };
let (identity, boot) = split_birth(birth.as_str());
if from_another_boot(boot) {
return true;
}
return match process_birth_identity(*pid) {
Ok(None) => true,
Ok(Some(now)) => now != identity,
Err(_) => false,
};
}
/// Whether the lock at `lock` carries the service's mark naming a holder that is gone:
/// a dead instance's leftover,
/// which the next landing removes.
/// A mark that names no holder (one an earlier release wrote) is not claimed here,
/// because its age is the only evidence a reader has of it.
pub fn mark_names_a_gone_holder(lock: &Path) -> bool {
return marker_of(lock).is_some_and(|marked| { return marked.holder.is_some() && holder_is_gone(marked.holder.as_ref()); });
}
/// Whether the process a marker names is proven to run now:
/// the same boot,
/// and the process under that number is the one born then.
/// A question that cannot be answered proves nothing.
pub fn holder_is_running(holder: Option<&(i64, String)>) -> bool {
let Some((pid, birth)) = holder else { return false };
let (identity, boot) = split_birth(birth.as_str());
return !from_another_boot(boot) && matches!(process_birth_identity(*pid), Ok(Some(now)) if now == identity);
}
/// The marker of the file at `path`,
/// when it holds one.
pub fn marker_of(path: &Path) -> Option<Marked> {
let mut text: String = String::new();
std::fs::File::open(path).ok()?.take(MARKER_LIMIT).read_to_string(&mut text).ok()?;
return parse_marker(text.as_str());
}
/// Whether the lock at `lock` is the service's and its holder is gone,
/// so it may be removed.
pub fn is_reclaimable(lock: &Path) -> bool {
return marker_of(lock).is_some_and(|marked| { return holder_is_gone(marked.holder.as_ref()); });
}