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
//! What:
//! The wait of a command that is about to change the checkout for any program's hold on Git's `index.lock`: the commit
//! service's, a foreign `git`'s, or a lock nothing proves held. Event-driven, with a notice that names what is waited for
//! and the holder, bounded by the wait limit, and over as soon as the lock is gone.
//! Why:
//! Native Git never waits for `index.lock`: a merge that finds it held dies (`Unable to create ... index.lock` or, from the
//! stash that every merge commit takes first, `could not write index` and `stash failed`). Other sessions commit into a
//! shared checkout all the time, so a held lock is ordinary and passes by itself. This wait is the sibling of the wait for
//! the commit service's own hold (`service_index_wait.rs`), which returns at once for a foreign holder because a plain
//! forwarded writer keeps the behavior of native Git; a merge is held to the owner's rule that merges never fail
//! (`SPEC.md`, "Merges and commits never fail"). The wait blocks on an inotify watch set before the first look, so a
//! release between the look and the wait is not missed; where no watch can be set it looks again after a short pause.
//! It never removes a lock: an unproven holder is waited for to the limit, as the commit service waits.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const outcome = await waitForIndexRelease({ realIndex, command: 'merge', limit, cadence, stderr });
//! ```
/// Evidence about the holder of a lock.
use super::index_lock_evidence::{
Verdict, classify_index_lock, gather_with, proven_holder_line, proven_holder_progress_line,
};
/// The bounded scan of open descriptors.
use super::index_lock_holders::scan_platform_holders_bounded;
/// The start time of a process.
use super::process_start::resolve_process_start;
/// The place of the lock and the watch on its directory.
use super::service_index_lock::{Watch, lock_path_of};
/// The limit and the pace of the notices.
use super::wait_limit::live_wait_limit;
use super::wait_progress::{Cadence, Ticker};
/// Writing the notices.
use std::io::Write;
/// `Path` is a borrowed filesystem path.
use std::path::Path;
/// Time spans and the monotonic clock.
use std::time::{Duration, Instant};
/// How long the wait sleeps between looks where no watch can be set.
const POLL_PAUSE: Duration = Duration::from_millis(50);
/// What came of the wait.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexWait {
/// Nobody held the lock when the command asked.
NotHeld,
/// A holder had the lock and let it go.
Released(
/// How long the wait lasted.
Duration,
),
/// The lock was still held at the limit; nothing was changed and the lock was left in place.
GaveUp(
/// How long the wait lasted.
Duration,
),
}
/// The words for what the evidence says about the holder, as the tail of a notice.
fn holder_tail(real_index: &Path, waited: Duration, limit: Duration, first: bool) -> String {
let unproven = || {
return format!(
"waiting for {}, whose owner cannot be proven alive, after {} seconds; giving up after {} seconds",
lock_path_of(real_index).display(),
waited.as_secs(),
limit.as_secs()
);
};
let Ok(Some(evidence)) = gather_with(real_index, resolve_process_start, scan_platform_holders_bounded) else {
return unproven();
};
let Verdict::ProvenAlive { pid, by_descriptor, command } = classify_index_lock(&evidence) else {
return unproven();
};
let line: String = if first {
proven_holder_line(pid, by_descriptor, command.as_deref(), evidence.lock_path.as_path(), limit.as_secs())
} else {
proven_holder_progress_line(pid, by_descriptor, command.as_deref(), evidence.lock_path.as_path(), waited.as_secs(), limit.as_secs())
};
return line.strip_prefix("cli-git: ").unwrap_or(line.as_str()).trim_end().to_owned();
}
/// The notice: what the command waits for, why it is safe to wait, then the holder.
fn notice(command: &str, real_index: &Path, waited: Duration, limit: Duration, first: bool) -> String {
return format!(
"cli-git: git {command} has changed nothing yet and waits for the index lock of this repository; {}\n",
holder_tail(real_index, waited, limit, first)
);
}
/// What:
/// Wait while any program holds `index.lock` of `real_index`, telling `out` what is waited for.
/// Why:
/// Returns after one metadata read when nothing holds the lock. The first line comes at once for a holder that is already
/// known when the wait begins and then every notice interval, so a queued command can be told from a hung one.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await waitForIndexRelease(realIndex, 'merge', limit, cadence, stderr)
/// ```
pub fn wait_for_index_release(real_index: &Path, command: &str, limit: Duration, cadence: Cadence, out: &mut dyn Write) -> IndexWait {
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(Watch::on);
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::announced_at_start(cadence);
let mut held: bool = false;
let mut announced: bool = false;
loop {
if std::fs::symlink_metadata(&lock).is_err() {
return if held { IndexWait::Released(began.elapsed()) } else { IndexWait::NotHeld };
}
held = true;
let elapsed: Duration = began.elapsed();
if elapsed >= limit {
let _noted = out
.write_all(
format!(
"cli-git: the index lock of this repository was still held after {} seconds, so git {command} stops waiting and reports what Git says; nothing was changed and the lock was left in place.\n",
elapsed.as_secs()
)
.as_bytes(),
)
.and_then(|()| { return out.flush(); });
return IndexWait::GaveUp(elapsed);
}
// The holder is known when the wait begins, so the first line comes at once and the repeats follow the cadence.
if !announced || ticker.due(elapsed) {
// A closed terminal is not a reason to stop the command the person asked for.
let _noted = out.write_all(notice(command, real_index, elapsed, limit, !announced).as_bytes()).and_then(|()| { return out.flush(); });
announced = true;
}
let pause: Duration = ticker.next_in(elapsed).min(limit.saturating_sub(elapsed)).max(Duration::from_millis(1));
match watch.as_ref() {
Some(active) => {
if !active.wait(pause, lock_name.as_os_str()) {
// The watch ended with its directory: 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(Watch::on);
}
}
None => std::thread::sleep(pause.min(POLL_PAUSE)),
}
}
}
/// `wait_for_index_release` with the limit and the notice cadence this process runs under, on standard error.
pub fn wait_for_index_release_here(real_index: &Path, command: &str) -> IndexWait {
return wait_for_index_release(real_index, command, live_wait_limit(), Cadence::from_environment(), &mut std::io::stderr());
}
/// The wait is controlled in the test module.
#[cfg(test)]
#[path = "index_holder_wait_tests.rs"]
mod tests;