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;