Skip to main content

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;