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