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/// The bound variable shared by every class of step, and the classes themselves.
22use super::child_class::{CHILD_WAIT_VARIABLE, ChildClass};
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/// The sink a notice goes to.
30use std::io::Write;
31/// Shared state between the waiting thread and the watcher.
32use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
33/// The watcher thread.
34use std::thread::JoinHandle;
35/// Time spans and the monotonic clock.
36use std::time::{Duration, Instant};
37
38/// The shortest sleep of the watcher, so a due moment that already passed cannot spin it.
39const SHORTEST_SLEEP: Duration = Duration::from_millis(1);
40
41/// What:
42///  When a watched wait prints and where it stops the child.
43/// Why:
44///   The production entry reads these from the environment and writes to standard error;
45///  tests choose short ones and read the notices from a buffer.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// type WatchTerms = { limitMs: number; cadence: Cadence; notices: Writable };
50/// ```
51pub struct WatchTerms {
52    /// How long one wait may last before the child is stopped.
53    pub limit: Duration,
54    /// When the notices of a wait are due.
55    pub cadence: Cadence,
56    /// Where the notices go.
57    pub notices: Box<dyn Write + Send>,
58}
59
60impl WatchTerms {
61    /// The terms a step of `class` runs under: the class's bound (or the variable's) and the class's grace before
62    ///  the first notice, the notice interval of the environment, notices on standard error.
63    ///  The grace never exceeds the interval, so a short interval still announces a wait quickly, nor half the bound,
64    ///  so a bound set below the class's grace still gets a notice before the step is stopped.
65    pub fn for_class(class: ChildClass) -> Self {
66        let interval: Cadence = Cadence::from_environment();
67        let limit: Duration = class.limit();
68        return Self {
69            limit,
70            cadence: Cadence { grace: grace_for(class, interval.interval, limit), interval: interval.interval },
71            notices: Box::new(std::io::stderr()),
72        };
73    }
74
75    /// The terms of a step that reads objects through a long-lived reader: those of an object-writing step, the longer
76    ///  class, because one request can name a large blob.
77    pub fn from_environment() -> Self {
78        return Self::for_class(ChildClass::Object);
79    }
80}
81
82/// What:
83///  How long a step of `class` runs before its first notice, for a notice `interval` and a `limit`.
84/// Why:
85///   The class's grace, but never past the interval (a short interval still announces a wait quickly) and never past
86///  half the bound (a bound set below the grace still gets a notice before the step is stopped).
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// Math.min(class.noticeGrace, interval, limit / 2)
91/// ```
92pub fn grace_for(class: ChildClass, interval: Duration, limit: Duration) -> Duration {
93    return class.notice_grace().min(interval).min(limit / 2);
94}
95
96/// What:
97///  What the watcher thread shares with the waiting thread, behind one lock.
98/// Why:
99///   The waiting thread marks a wait's start and end; the watcher reads the marks and the clock.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// type WatchState = { since?: number; ticker: Ticker; printed: number; stopped: boolean };
104/// ```
105struct WatchState {
106    /// When the current wait began, or nothing between waits.
107    since: Option<Instant>,
108    /// When the current wait's next notice is due.
109    ticker: Ticker,
110    /// How many notices the current wait printed.
111    printed: u32,
112    /// Whether the watch was dropped.
113    stopped: bool,
114    /// Whether a wait crossed the bound and the child was stopped.
115    expired: bool,
116}
117
118/// The lock and the signal the two threads share.
119struct Shared {
120    /// The marks.
121    state: Mutex<WatchState>,
122    /// Signalled when a wait begins or ends or the watch stops.
123    wake: Condvar,
124}
125
126/// Take the lock, ignoring poison: the state is plain marks that stay valid after a panic elsewhere.
127fn locked(shared: &Shared) -> MutexGuard<'_, WatchState> {
128    return shared.state.lock().unwrap_or_else(PoisonError::into_inner);
129}
130
131/// What:
132///  Ends a process the way a watched wait does when it crosses its bound.
133/// Why:
134///   The blocked read or wait on that process then returns, and the waiting thread reports the bound. The processes the
135///  child started end with it.
136///
137/// In TS you'd write (pseudocode):
138/// ```ts
139/// process.kill(pid, 'SIGKILL');
140/// ```
141#[cfg(unix)]
142fn stop_process(pid: u32) {
143    let Ok(raw) = i32::try_from(pid) else {
144        return;
145    };
146    // The processes below the child are read before it is stopped, because a process's link to its parent is gone once the
147    // parent has exited; a step's helpers (a transport, a hook) would otherwise outlive it and hold its output open, and the
148    // wait for that output would outlast the bound by as long as they live.
149    let below: Vec<super::process_tree::TreeMember> = super::process_tree::descendants_of(raw);
150    // SAFETY: `kill` takes no pointers. The watcher fires only while a wait is marked, and the waiting thread reaps
151    // the child only after it ends the mark, except for the window between a one-shot run's `wait_with_output`
152    // returning and its `end` call (microseconds), where a reaped identifier could in principle be reused by another
153    // process before the signal lands; the bound is far longer than that window, so a stop in it needs a wait that
154    // crossed the bound in the same instant.
155    unsafe {
156        libc::kill(raw, libc::SIGKILL);
157    }
158    super::process_tree::kill_members(below.as_slice());
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///  How the object reader's wait names itself in notices and in the timeout sentence.
320/// Why:
321///   The reader is one long-lived `cat-file --batch` whose single request can name a large blob, so it runs under the
322///  bulk class on purpose (a `cat-file` that answers one query through `run_git` is a quick query step), and its
323///  subject says so like every other step.
324///
325/// In TS you'd write (pseudocode):
326/// ```ts
327/// `git cat-file --batch, ${labelOf(BULK)}`
328/// ```
329pub fn reader_subject() -> String {
330    return format!("git cat-file --batch, {}", ChildClass::Object.label());
331}
332
333/// What:
334///  The sentence for a watched wait that crossed its bound.
335/// Why:
336///   It names what was waited for, the bound, where to change it and what to do next.
337///
338/// In TS you'd write (pseudocode):
339/// ```ts
340/// `${waitingFor} did not finish within ${limit} seconds and was stopped ...`
341/// ```
342pub fn watched_timeout_text(waiting_for: &str, limit: Duration) -> String {
343    return format!(
344        "{waiting_for} did not finish within {} seconds and was stopped ({CHILD_WAIT_VARIABLE} changes this). \
345         If the step belonged to a commit, the next Git command of this wrapper finishes or undoes that commit, so \
346         run the command again. If the step was only slow, raise the bound with {CHILD_WAIT_VARIABLE}=<seconds>; \
347         if it stalls again, check with `ps` what that process waits for.",
348        limit.as_secs()
349    );
350}
351
352/// What:
353///  Run a command to its end and collect its output, announcing the wait and stopping the child at the bound.
354///  The command's input must be set by the caller;
355///  its output streams are replaced by pipes.
356/// Why:
357///   `Command::output` blocks until the child ends and prints nothing meanwhile.
358///  A Git step that stalls under load or behind a filter must say so, and must not hold a commit for good.
359///
360/// In TS you'd write (pseudocode):
361/// ```ts
362/// const output = await spawnWatched(command, { waitingFor, terms }); // throws on the bound
363/// ```
364pub fn output_watched(command: &mut Command, waiting_for: &str, terms: WatchTerms) -> std::io::Result<Output> {
365    command.stdout(Stdio::piped()).stderr(Stdio::piped());
366    let child: Child = command.spawn_retrying()?;
367    let watch: ChildWatch = ChildWatch::start(waiting_for, child.id(), terms);
368    watch.begin();
369    let output: std::io::Result<Output> = child.wait_with_output();
370    watch.end();
371    if watch.expired() {
372        return Err(std::io::Error::new(
373            std::io::ErrorKind::TimedOut,
374            watched_timeout_text(waiting_for, watch.limit()),
375        ));
376    }
377    return output;
378}
379
380/// Watch controls stay out of the release executable.
381#[cfg(test)]
382#[path = "child_watch_tests.rs"]
383mod tests;