Skip to main content

git_policy_cli/push/
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 ends the child when it has shown no sign of work for the quiet window.
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 when the child has shown no sign of work for the window it ends the child so the blocked call
13//!  returns.
14//!  Elapsed time never ends a child (owner, 2026-11-03: "Stop only on no progress"; pick 4 of
15//!  `doc/planning/cli-git-shipped-limits-audit.md`, the coordinator's under the owner's delegation and open to the owner's
16//!  veto). The signs of work are those the push reads (`push_progress.rs`): bytes moved, processor time, a task able to run or
17//!  waiting for the disk. A child that is slow and works is waited for however long it takes.
18//!  The window is not measured: no log held a quiet gap before this change, so the class numbers stay as labelled
19//!  unmeasured windows (`child_class.rs`), and the longest quiet gap of every watched step is kept for the timing records
20//!  (`quiet_ledger.rs`) so a measured interval can replace them.
21//!  A person's hook is never ended: every Git the wrapper starts carries `core.hooksPath` below `/dev/null` and the hook
22//!  switches (`hooks_off.rs`), so no hook of the person is ever below a watched child, and the stop signals the child and what
23//!  it started, never a process beside it.
24//!
25//! In TS you'd write (pseudocode):
26//! ```ts
27//! const watch = startWatch({ waitingFor, pid, limitMs, cadence, notices });
28//! watch.begin(); try { await child.read(); } finally { watch.end(); }
29//! if (watch.expired) throw new Error(`${waitingFor} showed no sign of work for ${limit} seconds`);
30//! ```
31
32/// The bound variable shared by every class of step, and the classes themselves.
33use crate::child_class::{CHILD_WAIT_VARIABLE, ChildClass};
34/// The longest one step runs.
35use crate::wait_ceilings::OBJECT_STEP_CEILING;
36/// The captured output of a step that is never ended, in files that outlive the command.
37use crate::output_capture::Capture;
38/// Starts that ride out a short shortage of processes.
39use crate::start_retry::RetryStart;
40/// The notice cadence, its ticker and the child-wait line.
41use crate::wait_progress::{Cadence, ChildWaitFacts, Ticker, child_wait_line};
42/// The patience of a run that ends on a stall, and the stop it reports.
43use crate::bounded_push::{Patience, StopCause};
44/// The clock that reads the signs of work of a process tree, and how often it reads.
45use crate::push_progress::{Clock, SAMPLE_INTERVAL};
46/// The longest quiet gap of each step, kept for the timing records.
47use crate::quiet_ledger;
48/// The child and command types of the one-shot run.
49use std::process::{Child, Command, Output, Stdio};
50/// The sink a notice goes to.
51use std::io::Write;
52/// The flag the watcher raises when a wait crossed the bound, read by the waiting thread.
53use std::sync::Arc;
54use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
55/// The marks the waiting thread sends the watcher; the watcher owns every other piece of state.
56use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
57/// The lock files a stopped step leaves.
58use crate::stopped_git_locks::LockLedger;
59/// How long a stopped step may take to end after the polite request.
60use crate::bounded_push::STOP_GRACE;
61/// The processes below the step, and the identity checks that make signalling them safe.
62use crate::process_tree::{TreeMember, descendants_of, kill_members, member_of, still_running, terminate_members};
63/// The watcher thread.
64use std::thread::JoinHandle;
65/// Time spans and the monotonic clock.
66use std::time::{Duration, Instant};
67
68/// The shortest sleep of the watcher, so a due moment that already passed cannot spin it.
69const SHORTEST_SLEEP: Duration = Duration::from_millis(1);
70
71/// What:
72///  When a watched wait prints and where it stops the child.
73/// Why:
74///   The production entry reads these from the environment and writes to standard error;
75///  tests choose short ones and read the notices from a buffer.
76///
77/// In TS you'd write (pseudocode):
78/// ```ts
79/// type WatchTerms = { limitMs: number; cadence: Cadence; notices: Writable };
80/// ```
81pub struct WatchTerms {
82    /// The quiet window: how long one wait may show no sign of work before the child is ended.
83    pub limit: Duration,
84    /// When the notices of a wait are due.
85    pub cadence: Cadence,
86    /// Where the notices go.
87    pub notices: Box<dyn Write + Send>,
88}
89
90impl WatchTerms {
91    /// The terms a step of `class` runs under: the class's bound (or the variable's) and the class's grace before
92    ///  the first notice, the notice interval of the environment, notices on standard error.
93    ///  The grace never exceeds the interval, so a short interval still announces a wait quickly, nor half the bound,
94    ///  so a bound set below the class's grace still gets a notice before the step is stopped.
95    pub fn for_class(class: ChildClass) -> Self {
96        let interval: Cadence = Cadence::from_environment();
97        let limit: Duration = class.limit();
98        return Self {
99            limit,
100            cadence: Cadence { grace: grace_for(class, interval.interval, limit), interval: interval.interval },
101            notices: Box::new(std::io::stderr()),
102        };
103    }
104
105    /// The terms of a step that reads objects through a long-lived reader: those of an object-writing step, the longer
106    ///  class, because one request can name a large blob.
107    pub fn from_environment() -> Self {
108        return Self::for_class(ChildClass::Object);
109    }
110}
111
112/// What:
113///  How long a step of `class` runs before its first notice, for a notice `interval` and a `limit`.
114/// Why:
115///   The class's grace, but never past the interval (a short interval still announces a wait quickly) and never past
116///  half the bound (a bound set below the grace still gets a notice before the step is stopped).
117///
118/// In TS you'd write (pseudocode):
119/// ```ts
120/// Math.min(class.noticeGrace, interval, limit / 2)
121/// ```
122pub fn grace_for(class: ChildClass, interval: Duration, limit: Duration) -> Duration {
123    return class.notice_grace().min(interval).min(limit / 2);
124}
125
126/// What:
127///  What the waiting thread tells the watcher: a wait began at a moment, or the wait ended.
128/// Why:
129///   The watcher owns the marks it acts on (when the wait began, when its next notice is due, how many notices went out),
130///  so no state is shared and no lock is held: the waiting thread only sends, and the watcher reads at its own pace.
131///
132/// In TS you'd write (pseudocode):
133/// ```ts
134/// type Mark = { kind: 'begin'; at: number } | { kind: 'end' };
135/// ```
136enum Mark {
137    /// A wait on the child began at this moment.
138    Begin(Instant),
139    /// The wait ended.
140    End,
141}
142
143/// How often a stopped step is looked at while it ends.
144const STOP_POLL: Duration = Duration::from_millis(10);
145
146/// The marks of the wait the watcher is watching, kept by the watcher thread alone.
147struct Marks {
148    /// When the current wait began, or nothing between waits.
149    since: Option<Instant>,
150    /// When the current wait's next notice is due.
151    ticker: Ticker,
152    /// How many notices the current wait printed.
153    printed: u32,
154    /// The clock that reads the signs of work of the child during the current wait; nothing between waits.
155    clock: Option<Clock>,
156    /// The child.
157    pid: u32,
158    /// The quiet window.
159    window: Duration,
160    /// The longest quiet gap of any wait on this child, in microseconds, read by the waiting thread.
161    longest: Arc<AtomicU64>,
162}
163
164impl Marks {
165    /// Take one mark from the waiting thread into account; a wait that begins starts a clock on the child with the window as its
166    ///  stall and no wall-clock fallback, because elapsed time alone never ends a step.
167    fn apply(&mut self, mark: Mark, cadence: Cadence) {
168        match mark {
169            Mark::Begin(at) => {
170                self.since = Some(at);
171                self.ticker = Ticker::new(cadence);
172                self.printed = 0;
173                self.clock = Some(Clock::start(self.pid, Patience::Progress { stall: self.window }).without_wall_fallback());
174            }
175            Mark::End => {
176                self.keep_gap();
177                self.since = None;
178                self.clock = None;
179            }
180        }
181    }
182
183    /// Keep the longest quiet gap of the wait that is ending, the one still running included.
184    fn keep_gap(&self) {
185        // A clock that reads nothing (no `/proc`) has no quiet gap to keep: its figure would be the elapsed time.
186        if let Some(clock) = self.clock.as_ref().filter(|measuring| { return measuring.measures(); }) {
187            let micros: u64 = u64::try_from(clock.longest_quiet().as_micros()).unwrap_or(u64::MAX);
188            self.longest.fetch_max(micros, Ordering::AcqRel);
189        }
190    }
191}
192
193/// What:
194///  Ends a process the way a watched wait does when it crosses its bound: a catchable request first, a bounded grace,
195///  then the kill, and then the removal of the lock files it could not remove itself.
196/// Why:
197///   Git removes its lock files only when it is asked to end with a signal it catches, so a step that is killed at once
198///  leaves its `.lock` files and every later Git command on the same files fails. A step that answers the request ends
199///  in the grace; one that does not (it was stopped by the kernel in an uninterruptible call) is killed, and its locks are
200///  swept after it is gone. The blocked read or wait on that process then returns, and the waiting thread reports the
201///  bound. The processes the child started end with it.
202///
203/// In TS you'd write (pseudocode):
204/// ```ts
205/// process.kill(pid, 'SIGTERM'); await Promise.race([exited, delay(graceMs)]) || process.kill(pid, 'SIGKILL'); locks.sweep(tree.as_slice());
206/// ```
207#[cfg(unix)]
208fn stop_process(pid: u32, locks: &LockLedger, incoming: &Receiver<Mark>) {
209    let Ok(raw) = i32::try_from(pid) else {
210        return;
211    };
212    // The processes below the child are read before it is stopped, because a process's link to its parent is gone once the
213    // parent has exited; a step's helpers (a transport, a hook) would otherwise outlive it and hold its output open, and the
214    // wait for that output would outlast the bound by as long as they live.
215    let below: Vec<TreeMember> = descendants_of(raw);
216    let root: Option<TreeMember> = member_of(raw);
217    let mut tree: Vec<TreeMember> = root.iter().cloned().collect();
218    tree.extend(below.iter().cloned());
219    // A process number can be reused once the waiting thread has reaped the child, and the child is reaped as soon as it
220    // ends, which may be well before the grace is over (a helper below it can outlive it). The signals therefore go to the
221    // root only while it is still the process that was found, by its recorded start time; where no start time can be read
222    // (a platform without `/proc`) the root is signalled without that check, and with nothing to look at the grace ends at once,
223    // as the kill alone did before.
224    let identified: bool = cfg!(target_os = "linux");
225    let signal_root = |signal: libc::c_int| {
226        if root.as_ref().map_or(!identified, still_running) {
227            // SAFETY: `kill` takes no pointers; the process was checked a moment ago to be the one that was found.
228            unsafe {
229                libc::kill(raw, signal);
230            }
231        }
232    };
233    signal_root(libc::SIGTERM);
234    terminate_members(below.as_slice());
235    let asked: Instant = Instant::now();
236    while asked.elapsed() < STOP_GRACE && tree.iter().any(still_running) {
237        std::thread::sleep(STOP_POLL);
238    }
239    signal_root(libc::SIGKILL);
240    kill_members(below.as_slice());
241    // The waiting thread reaps the child, so the watcher only looks. A step in an uninterruptible call ends when the kernel
242    // lets it go, which can be long after the bound; the locks it creates on the way out are swept then, or when the watch
243    // is dropped, whichever comes first.
244    while tree.iter().any(still_running) {
245        match incoming.recv_timeout(STOP_POLL) {
246            Err(RecvTimeoutError::Disconnected) => break,
247            Ok(_) | Err(RecvTimeoutError::Timeout) => {}
248        }
249    }
250    locks.sweep(tree.as_slice());
251}
252
253/// A platform without signals cannot end the child from the watcher; the wait is only announced.
254// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
255#[cfg(not(unix))]
256fn stop_process(_pid: u32, _locks: &LockLedger, _incoming: &Receiver<Mark>) {}
257
258/// What:
259///  The three flags the waiting thread and the watcher thread share.
260/// Why:
261///   They travel together to the watcher, which writes `expired` and `longest` and reads `stoppable`.
262///
263/// In TS you'd write (pseudocode):
264/// ```ts
265/// type WatchFlags = { expired: AtomicBoolean; stoppable: AtomicBoolean; longest: AtomicNumber };
266/// ```
267struct WatchFlags {
268    /// Raised by the watcher when it ended the child for being quiet.
269    expired: Arc<AtomicBool>,
270    /// Whether a quiet child may be ended.
271    stoppable: Arc<AtomicBool>,
272    /// The longest quiet gap so far, in microseconds.
273    longest: Arc<AtomicU64>,
274    /// How long one wait may last in all, in microseconds, whatever the child shows; the largest value means no ceiling.
275    ceiling: Arc<AtomicU64>,
276    /// Raised by the watcher when the child was ended because the ceiling passed, not because it was quiet.
277    ceiling_hit: Arc<AtomicBool>,
278}
279
280/// What:
281///  The watcher of one child process.
282/// Why:
283///   One watch serves every wait on the same child:
284///  a long-lived reader marks one wait per request, a one-shot command marks one wait.
285///  Dropping the watch ends its thread and hands the longest quiet gap it saw to the ledger, if the watch was named.
286///
287/// In TS you'd write (pseudocode):
288/// ```ts
289/// class ChildWatch implements Disposable { begin(); end(); get expired(): boolean }
290/// ```
291pub struct ChildWatch {
292    /// Where the marks go; dropped first when the watch is dropped, which tells the watcher thread to end.
293    marks: Option<Sender<Mark>>,
294    /// Whether a wait showed no sign of work for the window and the child was ended; raised by the watcher before it signals
295    ///  the child.
296    expired: Arc<AtomicBool>,
297    /// The watcher thread, joined when the watch is dropped.
298    thread: Option<JoinHandle<()>>,
299    /// The quiet window.
300    limit: Duration,
301    /// Whether a child that shows no sign of work for the window may be ended; cleared for a step that is not safe to run
302    ///  twice, which is announced but only waited for.
303    stoppable: Arc<AtomicBool>,
304    /// The longest quiet gap of the waits so far, in microseconds, written by the watcher.
305    longest: Arc<AtomicU64>,
306    /// The step name the longest quiet gap is kept under in the ledger when the watch is dropped; nothing keeps no figure.
307    ledger_step: Option<String>,
308    /// How long one wait may last in all (shared with the watcher), in microseconds.
309    ceiling: Arc<AtomicU64>,
310    /// Whether the child was ended because the ceiling passed.
311    ceiling_hit: Arc<AtomicBool>,
312}
313
314impl ChildWatch {
315    /// Start watching `pid`, a child that holds no Git lock: see `start_with_locks`.
316    pub fn start(waiting_for: &str, pid: u32, terms: WatchTerms) -> Self {
317        return Self::start_with_locks(waiting_for, pid, terms, LockLedger::none());
318    }
319
320    /// Start watching `pid`:
321    ///  waits marked with `begin` and `end` are announced as waiting for `waiting_for`, the child is ended when it shows no
322    ///  sign of work for the terms' window, and the lock files the child leaves when it is ended are swept by `locks`.
323    ///  A watch whose thread cannot start watches nothing, and the waits it marks run unannounced and unended.
324    pub fn start_with_locks(waiting_for: &str, pid: u32, terms: WatchTerms, locks: LockLedger) -> Self {
325        let (marks, incoming): (Sender<Mark>, Receiver<Mark>) = channel();
326        let expired: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
327        let stoppable: Arc<AtomicBool> = Arc::new(AtomicBool::new(true));
328        let longest: Arc<AtomicU64> = Arc::new(AtomicU64::new(0));
329        let ceiling: Arc<AtomicU64> = Arc::new(AtomicU64::new(u64::MAX));
330        let ceiling_hit: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
331        let flags: WatchFlags = WatchFlags {
332            expired: Arc::clone(&expired),
333            stoppable: Arc::clone(&stoppable),
334            longest: Arc::clone(&longest),
335            ceiling: Arc::clone(&ceiling),
336            ceiling_hit: Arc::clone(&ceiling_hit),
337        };
338        let subject: String = String::from(waiting_for);
339        let limit: Duration = terms.limit;
340        let thread: Option<JoinHandle<()>> = std::thread::Builder::new()
341            .name(String::from("cli-git-child-watch"))
342            .spawn(move || {
343                watch_loop(&incoming, &flags, subject.as_str(), pid, terms, &locks);
344            })
345            .ok();
346        return Self { marks: Some(marks), expired, thread, limit, stoppable, longest, ledger_step: None, ceiling, ceiling_hit };
347    }
348
349    /// Keep the longest quiet gap of this watch in the ledger under `step` when the watch is dropped.
350    pub fn kept_as(mut self, step: &str) -> Self {
351        self.ledger_step = Some(String::from(step));
352        return self;
353    }
354
355    /// Announce the waits of this child but never end it for being quiet: a step that is not safe to run twice (a reference
356    ///  update, a commit Git builds) is ended in the middle of its write only by the person.
357    pub fn never_stop(&self) {
358        self.stoppable.store(false, Ordering::Release);
359    }
360
361    /// Give every wait of this child a ceiling: the child is ended when one wait has lasted this long, whatever it shows and
362    ///  whether or not a quiet child may be ended (owner, 2026-11-03: "nothing should ever wait forever or indefinitely").
363    pub fn within(self, ceiling: Duration) -> Self {
364        self.ceiling.store(u64::try_from(ceiling.as_micros()).unwrap_or(u64::MAX - 1), Ordering::Release);
365        return self;
366    }
367
368    /// Whether the child was ended because its ceiling passed.
369    pub fn ceiling_hit(&self) -> bool {
370        return self.ceiling_hit.load(Ordering::Acquire);
371    }
372
373    /// Send one mark; a watcher that is gone has nothing to be told.
374    fn send(&self, mark: Mark) {
375        if let Some(marks) = &self.marks {
376            // A watcher whose thread did not start, or that panicked, dropped its receiver; the wait runs unwatched.
377            let _sent = marks.send(mark);
378        }
379    }
380
381    /// Mark the start of a wait on the child.
382    pub fn begin(&self) {
383        self.send(Mark::Begin(Instant::now()));
384    }
385
386    /// Mark the end of the wait.
387    pub fn end(&self) {
388        self.send(Mark::End);
389    }
390
391    /// Whether a wait showed no sign of work for the window and the child was ended.
392    pub fn expired(&self) -> bool {
393        return self.expired.load(Ordering::Acquire);
394    }
395
396    /// The quiet window.
397    pub fn limit(&self) -> Duration {
398        return self.limit;
399    }
400}
401
402/// Stops the watcher thread, waits for it, and keeps the longest quiet gap it saw.
403impl Drop for ChildWatch {
404    fn drop(&mut self) {
405        drop(self.marks.take());
406        if let Some(thread) = self.thread.take() {
407            // A watcher that panicked has nothing left to report; the wait it watched is over.
408            let _joined = thread.join();
409        }
410        if let Some(step) = self.ledger_step.take() {
411            let gap: Duration = Duration::from_micros(self.longest.load(Ordering::Acquire));
412            quiet_ledger::note(step.as_str(), gap, self.expired());
413        }
414    }
415}
416
417/// What:
418///  The watcher thread's loop: sleep until the next notice or the next look at the child's work is due, print or end, repeat.
419/// Why:
420///   Between waits the thread blocks on the next mark without a timeout, so an idle watch costs nothing, and a dropped
421///  watch ends it because its sender is gone. During a wait it wakes at the notice cadence and at the sampling interval of
422///  the push meter, whichever is sooner, and at the window itself when that is shorter.
423///
424/// In TS you'd write (pseudocode):
425/// ```ts
426/// for await (const mark of marksUntilTimeout()) { if (since === undefined) continue; if (stoppable && clock.stalled()) stop(pid); else if (ticker.due(elapsed)) notice(); }
427/// ```
428fn watch_loop(incoming: &Receiver<Mark>, flags: &WatchFlags, waiting_for: &str, pid: u32, mut terms: WatchTerms, locks: &LockLedger) {
429    let expired: &AtomicBool = flags.expired.as_ref();
430    let stoppable: &AtomicBool = flags.stoppable.as_ref();
431    let longest: Arc<AtomicU64> = Arc::clone(&flags.longest);
432    let mut marks: Marks = Marks { since: None, ticker: Ticker::new(terms.cadence), printed: 0, clock: None, pid, window: terms.limit, longest };
433    loop {
434        let Some(since) = marks.since else {
435            match incoming.recv() {
436                Ok(mark) => marks.apply(mark, terms.cadence),
437                Err(_) => return,
438            }
439            continue;
440        };
441        let waited: Duration = since.elapsed();
442        // A notice that is due goes out before the stall is judged, so a child ended for being quiet was announced.
443        if marks.ticker.due(waited) {
444            let line: String = child_wait_line(&ChildWaitFacts {
445                waiting_for,
446                pid,
447                waited,
448                limit: terms.limit,
449                limit_note: " (CLI_GIT_CHILD_WAIT_SECONDS changes this)",
450                repeat: marks.printed > 0,
451                ends_when_quiet: stoppable.load(Ordering::Acquire),
452            });
453            marks.printed += 1;
454            // Terminal output error: a notice is advisory; a failed write must not stop the watch.
455            let _written = writeln!(terms.notices, "{line}");
456            // The wait may have ended, or another begun, while the line was written.
457            while let Ok(mark) = incoming.try_recv() {
458                marks.apply(mark, terms.cadence);
459            }
460            if marks.since != Some(since) {
461                continue;
462            }
463        }
464        // The clock is read at every look, a step that may not be run twice included, so its quiet gap is measured even though
465        // the step is never ended. Printing can never keep a child that shows no sign of work past its window, because the
466        // judgment follows the notice however long the notice took.
467        // The ceiling ends the child whatever it shows and whether or not a quiet child may be ended.
468        let ceiling: Duration = Duration::from_micros(flags.ceiling.load(Ordering::Acquire));
469        if waited >= ceiling {
470            flags.ceiling_hit.store(true, Ordering::Release);
471            expired.store(true, Ordering::Release);
472            marks.keep_gap();
473            marks.since = None;
474            marks.clock = None;
475            stop_process(pid, locks, incoming);
476            continue;
477        }
478        let verdict: Option<StopCause> = marks.clock.as_mut().and_then(Clock::check);
479        let quiet: bool = verdict.is_some_and(|cause| { return matches!(cause, StopCause::Stalled(_) | StopCause::Deadline); });
480        if quiet && stoppable.load(Ordering::Acquire) {
481            // Raised before the child is signalled, so the waiting thread that its end releases reads it as true.
482            expired.store(true, Ordering::Release);
483            marks.keep_gap();
484            marks.since = None;
485            marks.clock = None;
486            stop_process(pid, locks, incoming);
487            continue;
488        }
489        let until_notice: Duration = marks.ticker.next_in(since.elapsed());
490        let until_ceiling: Duration = Duration::from_micros(flags.ceiling.load(Ordering::Acquire)).saturating_sub(since.elapsed());
491        let sleep: Duration = until_notice.min(SAMPLE_INTERVAL).min(terms.limit).min(until_ceiling).max(SHORTEST_SLEEP);
492        match incoming.recv_timeout(sleep) {
493            Ok(mark) => marks.apply(mark, terms.cadence),
494            Err(RecvTimeoutError::Timeout) => {}
495            Err(RecvTimeoutError::Disconnected) => return,
496        }
497    }
498}
499
500/// What:
501///  How the object reader's wait names itself in notices and in the timeout sentence.
502/// Why:
503///   The reader is one long-lived `cat-file --batch` whose single request can name a large blob, so it runs under the
504///  bulk class on purpose (a `cat-file` that answers one query through `run_git` is a quick query step), and its
505///  subject says so like every other step.
506///
507/// In TS you'd write (pseudocode):
508/// ```ts
509/// `git cat-file --batch, ${labelOf(BULK)}`
510/// ```
511pub fn reader_subject() -> String {
512    return format!("git cat-file --batch, {}", ChildClass::Object.label());
513}
514
515/// What:
516///  The sentence for a watched wait that showed no sign of work for the quiet window.
517/// Why:
518///   It names what was waited for, the window, where to change it and what to do next.
519///
520/// In TS you'd write (pseudocode):
521/// ```ts
522/// `${waitingFor} showed no sign of work for ${limit} seconds and was stopped ...`
523/// ```
524pub fn watched_timeout_text(waiting_for: &str, limit: Duration) -> String {
525    return format!(
526        "{waiting_for} showed no sign of work for {} seconds and was stopped ({CHILD_WAIT_VARIABLE} changes this). \
527         If the step belonged to a commit, the next Git command of this wrapper finishes or undoes that commit, so \
528         run the command again. If the step is quiet while it works, raise the window with {CHILD_WAIT_VARIABLE}=<seconds>; \
529         if it stalls again, check with `ps` what that process waits for.",
530        limit.as_secs()
531    );
532}
533
534/// What:
535///  Run a command to its end and collect its output, announcing the wait and ending the child when it shows no sign of work
536///  for the window.
537///  The command's input must be set by the caller;
538///  its output streams are replaced by pipes.
539/// Why:
540///   `Command::output` blocks until the child ends and prints nothing meanwhile.
541///  A Git step that stalls under load or behind a filter must say so, and must not hold a commit for good; a step that is slow
542///  and works is waited for however long it takes.
543///
544/// In TS you'd write (pseudocode):
545/// ```ts
546/// const output = await spawnWatched(command, { waitingFor, terms }); // throws on a stall
547/// ```
548pub fn output_watched(command: &mut Command, waiting_for: &str, terms: WatchTerms) -> std::io::Result<Output> {
549    return output_watched_stopping(command, waiting_for, waiting_for, terms, true);
550}
551
552/// What:
553///  `output_watched`, where `stoppable` says whether a child that shows no sign of work for the window is ended, and
554///  `ledger_step` names the step under which its longest quiet gap is kept.
555/// Why:
556///   A step that is not safe to run twice is announced at the usual interval and waited for as long as it lives; the caller
557///  starts a stoppable step again when this returns the `TimedOut` error.
558///
559/// In TS you'd write (pseudocode):
560/// ```ts
561/// const output = await spawnWatched(command, { waitingFor, ledgerStep, terms, stoppable }); // throws on a stall
562/// ```
563pub fn output_watched_stopping(
564    command: &mut Command,
565    waiting_for: &str,
566    ledger_step: &str,
567    terms: WatchTerms,
568    stoppable: bool,
569) -> std::io::Result<Output> {
570    return output_watched_within(command, waiting_for, ledger_step, terms, stoppable, StepBound::EndAt(OBJECT_STEP_CEILING));
571}
572
573/// What:
574///  How long a watched step is waited for, and what happens at the end.
575/// Why:
576///   A step that can be run twice is ended at a ceiling (owner, 2026-11-03: "nothing should ever wait forever or indefinitely"). A
577///  write in place that cannot be run twice is never ended (owner, 2026-11-04: "Never cut a branch write (Recommended)"): the
578///  command that started it returns at its own bound plus a short extra wait with an error that says the result is not known,
579///  and the step is left running (owner, commit protocol revision 12, "Short extra wait (Recommended)"); only the commit service's
580///  landing thread, whose clients are told through the protocol, waits for it without a bound.
581///
582/// In TS you'd write (pseudocode):
583/// ```ts
584/// type StepBound = { kind: 'end-at'; ms } | { kind: 'leave-running'; afterMs; extraMs } | { kind: 'unbounded' };
585/// ```
586#[derive(Clone, Copy, Debug, Eq, PartialEq)]
587pub enum StepBound {
588    /// The step is ended, with its whole process tree, when it has run this long, whatever it shows.
589    EndAt(Duration),
590    /// The command stops waiting `after` plus `extra` after the step began and returns an error saying the result is not known;
591    ///  the step is left running and reaped when it ends.
592    LeaveRunning {
593        /// The command's own bound.
594        after: Duration,
595        /// The short extra wait for the write the step started.
596        extra: Duration,
597    },
598    /// The step is waited for as long as it lives.
599    Unbounded,
600}
601
602/// The error of a step ended at its ceiling, which tells it apart from a step ended for being quiet: a step that may be run again
603///  is run again after the second and not after the first.
604#[derive(Debug)]
605pub struct CeilingReached(
606    /// The sentence that names the step and the ceiling.
607    pub String,
608);
609
610impl std::fmt::Display for CeilingReached {
611    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612        return formatter.write_str(self.0.as_str());
613    }
614}
615
616impl std::error::Error for CeilingReached {}
617
618/// The output of a step that ended: its status and what it wrote to the captured files.
619fn output_of(status: std::process::ExitStatus, captured: Option<&Capture>) -> Output {
620    let Some(files) = captured else {
621        return Output { status, stdout: Vec::new(), stderr: Vec::new() };
622    };
623    let shown: crate::output_capture::Captured = files.snapshot(crate::output_capture::STEP_OUTPUT_LIMIT);
624    let mut stderr: Vec<u8> = shown.stderr;
625    for note in &shown.notes {
626        stderr.extend_from_slice(format!("\ncli-git: {note}\n").as_bytes());
627    }
628    return Output { status, stdout: shown.stdout, stderr };
629}
630
631/// What:
632///  The sentence for a step left running: what it was, how long it was waited for, and what to check.
633/// Why:
634///   The owner's rule for a write the command started and cannot confirm (commit protocol revision 12, "Short extra wait"): exit
635///  non-zero saying the result is not known and what to look at. The step is a write in place, so the repository is what to check.
636///
637/// In TS you'd write (pseudocode):
638/// ```ts
639/// `${waitingFor} was still running ${after + extra} seconds after it began, and was left running ...`
640/// ```
641pub fn not_known_text(waiting_for: &str, after: Duration, extra: Duration) -> String {
642    return format!(
643        "{waiting_for} was still running {} seconds after it began (its bound of {} seconds and {} more for the write it started), so it \
644         was left running and not ended: it is not known whether it finished. Check what it changes with `git status` and `git log -1`, \
645         look at the process with `ps` and at the disk it writes to, and run the command again if it did not finish.",
646        after.saturating_add(extra).as_secs(),
647        after.as_secs(),
648        extra.as_secs()
649    );
650}
651
652/// The error of a step left running: the command stopped waiting for it, so it is not known whether it finished.
653#[derive(Debug)]
654pub struct NotKnown(
655    /// The sentence that names the step, how long it was waited for, and what to check.
656    pub String,
657);
658
659impl std::fmt::Display for NotKnown {
660    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
661        return formatter.write_str(self.0.as_str());
662    }
663}
664
665impl std::error::Error for NotKnown {}
666
667/// Whether `error` is the error of a step left running because the command stopped waiting for it.
668pub fn is_not_known(error: &std::io::Error) -> bool {
669    return error.get_ref().is_some_and(|inner| { return inner.is::<NotKnown>(); });
670}
671
672/// Whether `error` is the error of a step ended at its ceiling.
673pub fn is_ceiling(error: &std::io::Error) -> bool {
674    return error.get_ref().is_some_and(|inner| { return inner.is::<CeilingReached>(); });
675}
676
677/// What:
678///  `output_watched_stopping` where the step is ended at `ceiling` whatever it shows, or never ended for its length when there is
679///  none.
680/// Why:
681///   The owner's rule is that nothing waits for ever; the ceiling is what ends a step that keeps working and one that may not be
682///  ended for being quiet. A step with no ceiling is a write in place that is not safe to run twice (owner, 2026-11-04: "Never cut
683///  a branch write (Recommended)"; `transaction_git.rs`, `step_bound`): ending it in the middle of its write could cost the
684///  person's data, so only the person ends it.
685///
686/// In TS you'd write (pseudocode):
687/// ```ts
688/// const output = await spawnWatched(command, { waitingFor, ledgerStep, terms, stoppable, ceilingMs }); // throws on a stall or the ceiling
689/// ```
690pub fn output_watched_within(
691    command: &mut Command,
692    waiting_for: &str,
693    ledger_step: &str,
694    terms: WatchTerms,
695    stoppable: bool,
696    bound: StepBound,
697) -> std::io::Result<Output> {
698    let ceiling: Option<Duration> = match bound {
699        StepBound::EndAt(limit) => Some(limit),
700        StepBound::LeaveRunning { .. } | StepBound::Unbounded => None,
701    };
702    // A step that is never ended writes into files that outlive this command, not into pipes this command holds: when the command
703    // stops waiting and exits, the step must find its output open, or it dies of SIGPIPE at its next write, which is a cut.
704    let captured: Option<Capture> = if matches!(bound, StepBound::EndAt(_)) { None } else { Some(Capture::new()?) };
705    match captured.as_ref() {
706        Some(files) => {
707            let (out, err) = files.stdio()?;
708            command.stdout(out).stderr(err);
709        }
710        None => {
711            command.stdout(Stdio::piped()).stderr(Stdio::piped());
712        }
713    }
714    // Taken before the child starts, so a lock born earlier is never taken for the child's.
715    let locks: LockLedger = LockLedger::before_start(command);
716    let child: Child = command.spawn_retrying()?;
717    let started: ChildWatch = ChildWatch::start_with_locks(waiting_for, child.id(), terms, locks).kept_as(ledger_step);
718    // A step with no ceiling is waited for as long as it lives: the watch starts without one.
719    let watch: ChildWatch = match ceiling {
720        Some(limit) => started.within(limit),
721        None => started,
722    };
723    if !stoppable {
724        watch.never_stop();
725    }
726    watch.begin();
727    let output: std::io::Result<Output> = match bound {
728        StepBound::LeaveRunning { after, extra } => {
729            // The child moves to a thread that waits for it and collects its output, so the command can stop waiting and the child
730            // goes on; the thread reaps it when it ends.
731            let (sender, receiver) = std::sync::mpsc::channel::<std::io::Result<std::process::ExitStatus>>();
732            let waiter = std::thread::Builder::new().name(String::from("cli-git-left-running-step")).spawn(move || {
733                let mut leaving: Child = child;
734                // The command may have stopped waiting; a closed channel is that case, and there is nobody to tell.
735                let _abandoned = sender.send(leaving.wait());
736            });
737            if let Err(error) = waiter {
738                watch.end();
739                return Err(std::io::Error::other(format!("{waiting_for} was started but no thread could be started to wait for it: {error}")));
740            }
741            match receiver.recv_timeout(after.saturating_add(extra)) {
742                Ok(finished) => finished.map(|status| { return output_of(status, captured.as_ref()); }),
743                Err(_timeout_or_gone) => {
744                    watch.end();
745                    // Nobody will read what the step writes from here on, and it is held in memory until the step ends.
746                    if let Some(files) = captured.as_ref() {
747                        files.discard();
748                    }
749                    return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, NotKnown(not_known_text(waiting_for, after, extra))));
750                }
751            }
752        }
753        StepBound::EndAt(_) => child.wait_with_output(),
754        StepBound::Unbounded => {
755            let mut waited_for: Child = child;
756            waited_for.wait().map(|status| { return output_of(status, captured.as_ref()); })
757        }
758    };
759    watch.end();
760    if let Some(limit) = ceiling
761        && watch.ceiling_hit()
762    {
763        return Err(std::io::Error::new(
764            std::io::ErrorKind::TimedOut,
765            CeilingReached(format!("{waiting_for} ran for {} seconds, the longest the wrapper lets one step run, and was ended", limit.as_secs())),
766        ));
767    }
768    if watch.expired() {
769        return Err(std::io::Error::new(
770            std::io::ErrorKind::TimedOut,
771            watched_timeout_text(waiting_for, watch.limit()),
772        ));
773    }
774    return output;
775}
776
777/// Watch controls stay out of the release executable.
778#[cfg(test)]
779#[path = "child_watch_tests.rs"]
780mod tests;