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;