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
232
233
234
235
//! What:
//! The wait of a wrapped command that writes the index for the commit service's own hold on the index: event-driven, with a
//! notice that names what is waited for, and over as soon as the hold is released.
//! Why:
//! The service holds Git's `index.lock` while it lands a commit and marks it as its own. A wrapped `git add` run by another
//! process in the same worktree during that time must not fail, and must not be told the hold belongs to a program
//! nobody can prove alive: the hold is the product's own and ends by itself. A lock without the service's mark is a
//! foreign program's and keeps the behavior of native Git. The wait blocks on an inotify watch set before the first look,
//! so a release between the look and the wait is not missed, and it ends at the limit every wait on a live program ends at.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const outcome = await waitForServiceHold({ realIndex, command: 'add', limit, cadence, stderr });
//! ```
use super::wait_limit::live_wait_limit;
use super::service_index_lock::{LOCK_MARKER, Watch, lock_path_of};
use super::service_index_marker::{holder_is_running, mark_names_a_gone_holder, marker_of};
use super::wait_progress::{Cadence, Ticker};
use std::io::{Read, Write};
use std::path::Path;
use std::time::{Duration, Instant};
/// How many times an empty lock is read again before it is taken for a foreign program's,
/// and the pause between the reads.
/// The service places its lock with the whole mark already in it
/// (the lock is the staged file,
/// renamed),
/// so an empty lock is never the service's.
/// The reads are for a lock of a release that wrote its mark after creating the lock,
/// and for a foreign program,
/// which leaves the file empty until it has an index to put in it.
const EMPTY_LOOKS: u32 = 5;
const EMPTY_PAUSE: Duration = Duration::from_millis(2);
/// The age after which a hold is taken for a leftover
/// when nothing else says whether its holder runs:
/// a mark that names no holder (an earlier release's),
/// or a holder this process cannot ask about
/// (another PID namespace,
/// a record it cannot read).
/// Every step of a landing is bounded,
/// so such a hold older than this
/// is taken for a dead service's.
/// A mark that names a holder decides by the holder instead:
/// one that runs is waited for however old the file is,
/// and one that is gone is not waited for.
pub const HOLD_LIMIT: Duration = Duration::from_secs(60);
/// Who holds the index.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Holder {
/// Nobody: there is no lock.
Nobody,
/// The commit service, by its mark.
Service,
/// A program that is not the service, or a lock that cannot be read.
Foreign,
}
/// What came of the wait.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Waited {
/// The service did not hold the index when the command asked.
NotHeld,
/// The service held the index and let it go.
Released,
/// The service still held the index at the limit.
GaveUp,
/// The hold is older than any landing lasts,
/// or names a holder that no longer runs,
/// so it is a dead service's leftover and was not waited for.
Stale,
}
/// Who holds the lock at `lock` now.
fn holder_of(lock: &Path) -> Holder {
for look in 0..EMPTY_LOOKS {
let mut file = match std::fs::File::open(lock) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Holder::Nobody,
Err(_) => return Holder::Foreign,
};
let mut head: [u8; LOCK_MARKER.len()] = [0; LOCK_MARKER.len()];
let mut filled: usize = 0;
while filled < head.len() {
match file.read(&mut head[filled..]) {
Ok(0) => break,
Ok(count) => filled += count,
Err(_) => return Holder::Foreign,
}
}
if filled == head.len() {
return if head == *LOCK_MARKER.as_bytes() { Holder::Service } else { Holder::Foreign };
}
if filled > 0 && !LOCK_MARKER.as_bytes().starts_with(&head[..filled]) {
return Holder::Foreign;
}
if look + 1 < EMPTY_LOOKS {
std::thread::sleep(EMPTY_PAUSE);
}
}
return Holder::Foreign;
}
/// Whether a program other than the service holds the index of `real_index` now.
pub fn foreign_holds(real_index: &Path) -> bool {
return holder_of(lock_path_of(real_index).as_path()) == Holder::Foreign;
}
/// The line for the person while the wait lasts.
fn waiting_line(command: &str, waited: Duration, limit: Duration) -> String {
return format!(
"cli-git: git {command} is waiting for the commit service to finish a commit in this repository, {} seconds so far; giving up after {} seconds.\n",
waited.as_secs(),
limit.as_secs()
);
}
/// How long ago the lock at `lock` was made.
fn age_of(lock: &Path) -> Duration {
return std::fs::metadata(lock)
.and_then(|metadata| { return metadata.modified(); })
.ok()
.and_then(|made| { return std::time::SystemTime::now().duration_since(made).ok(); })
.unwrap_or(Duration::ZERO);
}
/// The line for the person when the hold is a leftover.
fn stale_line(command: &str, age: Duration) -> String {
return format!(
"cli-git: the commit service's hold on this repository's index is {} seconds old, longer than a commit takes, so git {command} does not wait for it.\n",
age.as_secs()
);
}
/// The line for the person when the hold names a service process that is gone.
fn gone_line(command: &str) -> String {
return format!(
"cli-git: the commit service that held this repository's index no longer runs, so git {command} does not wait for it. The next commit made through the service removes the lock it left.\n"
);
}
/// The line for the person when the wait ran out.
fn gave_up_line(command: &str, waited: Duration) -> String {
return format!(
"cli-git: the commit service was still finishing a commit in this repository after {} seconds, so git {command} runs now and reports what Git says.\n",
waited.as_secs()
);
}
/// What:
/// Waits while the commit service holds the index of `real_index`, telling `out` what it waits for.
/// Why:
/// Returns at once when nobody holds the index or a foreign program does, so a command pays one file read when there is
/// nothing to wait for. The first line comes after the grace every wait notice has, so a landing that ends at once prints
/// nothing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await waitForServiceHold(realIndex, 'add', limit, cadence, stderr)
/// ```
pub fn wait_for_service_hold(real_index: &Path, command: &str, limit: Duration, cadence: Cadence, out: &mut dyn Write) -> Waited {
return wait_with_watches(real_index, command, limit, cadence, out, &Watch::on);
}
/// What:
/// `wait_for_service_hold` with the function that sets a watch on the lock's directory,
/// so a control can hand it a watch that is already over.
/// Why:
/// A watch ends when its directory is unmounted or removed,
/// and reports nothing afterwards;
/// the wait sets a new one then,
/// so a release after the end is seen as an event and not at the next timer.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await waitWithWatches(realIndex, 'add', limit, cadence, stderr, watchDirectory)
/// ```
fn wait_with_watches(real_index: &Path, command: &str, limit: Duration, cadence: Cadence, out: &mut dyn Write, set_watch: &dyn Fn(&Path) -> Option<Watch>) -> Waited {
let lock = lock_path_of(real_index);
// The watch comes before the first look: a release between the look and the wait is then seen as an event.
let mut watch: Option<Watch> = lock.parent().and_then(set_watch);
// Only the lock's own removal ends the wait; the service's staging files and Git's other files come and go meanwhile.
let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
let began: Instant = Instant::now();
let mut ticker: Ticker = Ticker::new(cadence);
let mut held: bool = false;
loop {
if holder_of(&lock) != Holder::Service {
return if held { Waited::Released } else { Waited::NotHeld };
}
// Without a watch there is nothing to block on, and polling is not what this wait is for.
let Some(active) = watch.as_ref() else { return Waited::NotHeld };
// A holder the mark names and the system no longer has will never release the lock, however young the lock is.
if mark_names_a_gone_holder(&lock) {
let _noted = out.write_all(gone_line(command).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::Stale;
}
let age: Duration = age_of(&lock);
// A service the marker proves to run is finishing a landing, however long ago its marker was written: the file's age
// is only a guide for a hold that proves nothing (a mark without a process, or a process this one cannot see).
let running: bool = marker_of(&lock).is_some_and(|marked| { return holder_is_running(marked.holder.as_ref()); });
if age >= HOLD_LIMIT && !running {
let _noted = out.write_all(stale_line(command, age).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::Stale;
}
held = true;
let elapsed: Duration = began.elapsed();
if elapsed >= limit {
let _noted = out.write_all(gave_up_line(command, elapsed).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::GaveUp;
}
if ticker.due(elapsed) {
// A closed terminal is not a reason to stop the command the person asked for.
let _noted = out.write_all(waiting_line(command, elapsed, limit).as_bytes()).and_then(|()| { return out.flush(); });
}
if !active.wait(ticker.next_in(elapsed).min(limit.saturating_sub(elapsed)).max(Duration::from_millis(1)), lock_name.as_os_str()) {
// The watch ended with its directory or by the directory being moved: the ended watch is dropped first, because a
// new one may need the descriptor and the quota it holds, then the lock is looked at again with a new watch set.
drop(watch.take());
watch = lock.parent().and_then(set_watch);
}
}
}
/// `wait_for_service_hold` with the limit and the notice cadence this process runs under.
pub fn wait_for_service_hold_here(real_index: &Path, command: &str) -> Waited {
return wait_for_service_hold(real_index, command, live_wait_limit(), Cadence::from_environment(), &mut std::io::stderr());
}
/// The wait is controlled in the test module.
#[cfg(test)]
#[path = "service_index_wait_tests.rs"]
mod tests;