git_policy_cli/child_watch.rs
1//! What:
2//! A watcher for a wait that blocks inside a read or a wait on a child process:
3//! it prints a notice after the grace and at every interval while the wait lasts,
4//! and stops the child when the wait crosses its bound.
5//! Why:
6//! The wrapper reads Git objects from a long-lived `git cat-file --batch` and runs Git steps of a commit
7//! with their output captured.
8//! Both block in the operating system with nothing printed and no end,
9//! so a stalled child looks the same as a slow one and holds the commit for good (issue 1811).
10//! A watcher thread sees the wait from outside:
11//! the waiting thread marks where a wait begins and ends,
12//! the watcher prints, and at the bound it ends the child so the blocked call returns.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const watch = startWatch({ waitingFor, pid, limitMs, cadence, notices });
17//! watch.begin(); try { await child.read(); } finally { watch.end(); }
18//! if (watch.expired) throw new Error(`${waitingFor} did not finish within ${limit} seconds`);
19//! ```
20
21/// Positive-seconds parsing shared with the lock wait limits.
22use super::bounded_push::positive_seconds;
23/// Starts that ride out a short shortage of processes.
24use super::start_retry::RetryStart;
25/// The notice cadence, its ticker and the child-wait line.
26use super::wait_progress::{Cadence, ChildWaitFacts, Ticker, child_wait_line};
27/// The child and command types of the one-shot run.
28use std::process::{Child, Command, Output, Stdio};
29/// Borrowed OS text of the environment value.
30use std::ffi::OsStr;
31/// The sink a notice goes to.
32use std::io::Write;
33/// Shared state between the waiting thread and the watcher.
34use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
35/// The watcher thread.
36use std::thread::JoinHandle;
37/// Time spans and the monotonic clock.
38use std::time::{Duration, Instant};
39
40/// Environment variable that sets, in whole seconds, how long one Git process the wrapper starts for itself
41/// (the object reader and the steps of a commit) may run before it is stopped.
42pub const CHILD_WAIT_VARIABLE: &str = "CLI_GIT_CHILD_WAIT_SECONDS";
43
44/// How long such a process may run when the variable is absent or unusable:
45/// longer than any ordinary step, short enough that a stalled process is noticed within the hour.
46pub const DEFAULT_CHILD_WAIT: Duration = Duration::from_secs(30 * 60);
47
48/// The shortest sleep of the watcher, so a due moment that already passed cannot spin it.
49const SHORTEST_SLEEP: Duration = Duration::from_millis(1);
50
51/// What:
52/// The child wait limit for a value of the variable.
53/// Why:
54/// A missing, zero or malformed value falls back to the default, never to "no limit".
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// positiveSeconds(value) ?? DEFAULT_CHILD_WAIT_MS
59/// ```
60pub fn child_wait_limit_from(value: Option<&OsStr>) -> Duration {
61 let Some(text) = value else {
62 return DEFAULT_CHILD_WAIT;
63 };
64 return positive_seconds(CHILD_WAIT_VARIABLE, text).unwrap_or(DEFAULT_CHILD_WAIT);
65}
66
67/// The child wait limit this process runs under: the variable in its environment, or the default.
68pub fn child_wait_limit() -> Duration {
69 return child_wait_limit_from(std::env::var_os(CHILD_WAIT_VARIABLE).as_deref());
70}
71
72/// What:
73/// When a watched wait prints and where it stops the child.
74/// Why:
75/// The production entry reads these from the environment and writes to standard error;
76/// tests choose short ones and read the notices from a buffer.
77///
78/// In TS you'd write (pseudocode):
79/// ```ts
80/// type WatchTerms = { limitMs: number; cadence: Cadence; notices: Writable };
81/// ```
82pub struct WatchTerms {
83 /// How long one wait may last before the child is stopped.
84 pub limit: Duration,
85 /// When the notices of a wait are due.
86 pub cadence: Cadence,
87 /// Where the notices go.
88 pub notices: Box<dyn Write + Send>,
89}
90
91impl WatchTerms {
92 /// The terms this process runs under: the variables of its environment, notices on standard error.
93 pub fn from_environment() -> Self {
94 return Self {
95 limit: child_wait_limit(),
96 cadence: Cadence::from_environment(),
97 notices: Box::new(std::io::stderr()),
98 };
99 }
100}
101
102/// What:
103/// What the watcher thread shares with the waiting thread, behind one lock.
104/// Why:
105/// The waiting thread marks a wait's start and end; the watcher reads the marks and the clock.
106///
107/// In TS you'd write (pseudocode):
108/// ```ts
109/// type WatchState = { since?: number; ticker: Ticker; printed: number; stopped: boolean };
110/// ```
111struct WatchState {
112 /// When the current wait began, or nothing between waits.
113 since: Option<Instant>,
114 /// When the current wait's next notice is due.
115 ticker: Ticker,
116 /// How many notices the current wait printed.
117 printed: u32,
118 /// Whether the watch was dropped.
119 stopped: bool,
120 /// Whether a wait crossed the bound and the child was stopped.
121 expired: bool,
122}
123
124/// The lock and the signal the two threads share.
125struct Shared {
126 /// The marks.
127 state: Mutex<WatchState>,
128 /// Signalled when a wait begins or ends or the watch stops.
129 wake: Condvar,
130}
131
132/// Take the lock, ignoring poison: the state is plain marks that stay valid after a panic elsewhere.
133fn locked(shared: &Shared) -> MutexGuard<'_, WatchState> {
134 return shared.state.lock().unwrap_or_else(PoisonError::into_inner);
135}
136
137/// What:
138/// Ends a process the way a watched wait does when it crosses its bound.
139/// Why:
140/// The blocked read or wait on that process then returns, and the waiting thread reports the bound.
141///
142/// In TS you'd write (pseudocode):
143/// ```ts
144/// process.kill(pid, 'SIGKILL');
145/// ```
146#[cfg(unix)]
147fn stop_process(pid: u32) {
148 let Ok(raw) = i32::try_from(pid) else {
149 return;
150 };
151 // SAFETY: `kill` takes no pointers. The watcher fires only while a wait is marked, and the waiting thread reaps
152 // the child only after it ends the mark, except for the window between a one-shot run's `wait_with_output`
153 // returning and its `end` call (microseconds), where a reaped identifier could in principle be reused by another
154 // process before the signal lands; the bound is far longer than that window, so a stop in it needs a wait that
155 // crossed the bound in the same instant.
156 unsafe {
157 libc::kill(raw, libc::SIGKILL);
158 }
159}
160
161/// A platform without signals cannot end the child from the watcher; the wait is only announced.
162// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
163#[cfg(not(unix))]
164fn stop_process(_pid: u32) {}
165
166/// What:
167/// The watcher of one child process.
168/// Why:
169/// One watch serves every wait on the same child:
170/// a long-lived reader marks one wait per request, a one-shot command marks one wait.
171/// Dropping the watch ends its thread.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// class ChildWatch implements Disposable { begin(); end(); get expired(): boolean }
176/// ```
177pub struct ChildWatch {
178 /// The state shared with the watcher thread.
179 shared: Arc<Shared>,
180 /// The watcher thread, joined when the watch is dropped.
181 thread: Option<JoinHandle<()>>,
182 /// The bound a wait may last.
183 limit: Duration,
184 /// When the notices of each wait are due.
185 cadence: Cadence,
186}
187
188impl ChildWatch {
189 /// Start watching `pid`:
190 /// waits marked with `begin` and `end` are announced as waiting for `waiting_for` and bounded by the terms' limit.
191 /// A watch whose thread cannot start watches nothing, and the waits it marks run unannounced and unbounded.
192 pub fn start(waiting_for: &str, pid: u32, terms: WatchTerms) -> Self {
193 let shared: Arc<Shared> = Arc::new(Shared {
194 state: Mutex::new(WatchState {
195 since: None,
196 ticker: Ticker::new(terms.cadence),
197 printed: 0,
198 stopped: false,
199 expired: false,
200 }),
201 wake: Condvar::new(),
202 });
203 let watcher: Arc<Shared> = Arc::clone(&shared);
204 let subject: String = String::from(waiting_for);
205 let limit: Duration = terms.limit;
206 let cadence: Cadence = terms.cadence;
207 let thread: Option<JoinHandle<()>> = std::thread::Builder::new()
208 .name(String::from("cli-git-child-watch"))
209 .spawn(move || {
210 watch_loop(&watcher, subject.as_str(), pid, terms);
211 })
212 .ok();
213 return Self { shared, thread, limit, cadence };
214 }
215
216 /// Mark the start of a wait on the child.
217 pub fn begin(&self) {
218 let mut state: MutexGuard<'_, WatchState> = locked(&self.shared);
219 state.since = Some(Instant::now());
220 state.ticker = Ticker::new(self.cadence);
221 state.printed = 0;
222 drop(state);
223 self.shared.wake.notify_all();
224 }
225
226 /// Mark the end of the wait.
227 pub fn end(&self) {
228 let mut state: MutexGuard<'_, WatchState> = locked(&self.shared);
229 state.since = None;
230 drop(state);
231 self.shared.wake.notify_all();
232 }
233
234 /// Whether a wait crossed the bound and the child was stopped.
235 pub fn expired(&self) -> bool {
236 return locked(&self.shared).expired;
237 }
238
239 /// The bound a wait may last.
240 pub fn limit(&self) -> Duration {
241 return self.limit;
242 }
243}
244
245/// Stops the watcher thread and waits for it.
246impl Drop for ChildWatch {
247 fn drop(&mut self) {
248 locked(&self.shared).stopped = true;
249 self.shared.wake.notify_all();
250 if let Some(thread) = self.thread.take() {
251 // A watcher that panicked has nothing left to report; the wait it watched is over.
252 let _joined = thread.join();
253 }
254 }
255}
256
257/// What:
258/// The watcher thread's loop: sleep until the next notice or the bound is due, print or stop, repeat.
259/// Why:
260/// Between waits the thread sleeps without a timeout, so an idle watch costs nothing.
261///
262/// In TS you'd write (pseudocode):
263/// ```ts
264/// while (!stopped) { if (since === undefined) await signal(); else if (elapsed >= limit) stop(pid); else if (ticker.due(elapsed)) notice(); else await sleep(next); }
265/// ```
266fn watch_loop(shared: &Shared, waiting_for: &str, pid: u32, mut terms: WatchTerms) {
267 let mut state: MutexGuard<'_, WatchState> = locked(shared);
268 loop {
269 if state.stopped {
270 return;
271 }
272 let Some(since) = state.since else {
273 state = shared.wake.wait(state).unwrap_or_else(PoisonError::into_inner);
274 continue;
275 };
276 let mut waited: Duration = since.elapsed();
277 // A notice that is due goes out before the bound is checked, so a child stopped at its bound was announced.
278 if state.ticker.due(waited) {
279 let line: String = child_wait_line(&ChildWaitFacts {
280 waiting_for,
281 pid,
282 waited,
283 limit: terms.limit,
284 limit_note: " (CLI_GIT_CHILD_WAIT_SECONDS changes this)",
285 repeat: state.printed > 0,
286 });
287 state.printed += 1;
288 // The lock is not held while writing: a slow terminal must not hold up the waiting thread's marks.
289 drop(state);
290 // Terminal output error: a notice is advisory; a failed write must not stop the watch.
291 let _written = writeln!(terms.notices, "{line}");
292 state = locked(shared);
293 // The wait may have ended, or another begun, while the line was written.
294 if state.stopped || state.since != Some(since) {
295 continue;
296 }
297 // The bound is checked right after a notice, however long the notice took, so printing can never keep a
298 // child past its bound.
299 waited = since.elapsed();
300 }
301 if waited >= terms.limit {
302 state.expired = true;
303 state.since = None;
304 stop_process(pid);
305 continue;
306 }
307 let until_notice: Duration = state.ticker.next_in(waited);
308 let until_bound: Duration = terms.limit.saturating_sub(waited);
309 let sleep: Duration = until_notice.min(until_bound).max(SHORTEST_SLEEP);
310 state = shared
311 .wake
312 .wait_timeout(state, sleep)
313 .unwrap_or_else(PoisonError::into_inner)
314 .0;
315 }
316}
317
318/// What:
319/// The sentence for a watched wait that crossed its bound.
320/// Why:
321/// It names what was waited for, the bound, where to change it and what to do next.
322///
323/// In TS you'd write (pseudocode):
324/// ```ts
325/// `${waitingFor} did not finish within ${limit} seconds and was stopped ...`
326/// ```
327pub fn watched_timeout_text(waiting_for: &str, limit: Duration) -> String {
328 return format!(
329 "{waiting_for} did not finish within {} seconds and was stopped ({CHILD_WAIT_VARIABLE} changes this). \
330 Run the command again; if it stalls again, check with `ps` what that process waits for.",
331 limit.as_secs()
332 );
333}
334
335/// What:
336/// Run a command to its end and collect its output, announcing the wait and stopping the child at the bound.
337/// The command's input must be set by the caller;
338/// its output streams are replaced by pipes.
339/// Why:
340/// `Command::output` blocks until the child ends and prints nothing meanwhile.
341/// A Git step that stalls under load or behind a filter must say so, and must not hold a commit for good.
342///
343/// In TS you'd write (pseudocode):
344/// ```ts
345/// const output = await spawnWatched(command, { waitingFor, terms }); // throws on the bound
346/// ```
347pub fn output_watched(command: &mut Command, waiting_for: &str, terms: WatchTerms) -> std::io::Result<Output> {
348 command.stdout(Stdio::piped()).stderr(Stdio::piped());
349 let child: Child = command.spawn_retrying()?;
350 let watch: ChildWatch = ChildWatch::start(waiting_for, child.id(), terms);
351 watch.begin();
352 let output: std::io::Result<Output> = child.wait_with_output();
353 watch.end();
354 if watch.expired() {
355 return Err(std::io::Error::new(
356 std::io::ErrorKind::TimedOut,
357 watched_timeout_text(waiting_for, watch.limit()),
358 ));
359 }
360 return output;
361}
362
363/// Watch controls stay out of the release executable.
364#[cfg(test)]
365#[path = "child_watch_tests.rs"]
366mod tests;