feat(progress): report uncounted status through the sink

A run blocked on the cache-entry flock was silent: the wait went to
the log sink, which renders nowhere by default, so the CLI read as
a hang. The Progress trait gains message() - a status line outside
the counted-operation protocol, defaulting to a no-op so existing
sinks keep compiling - and the flock wait reports what it is
waiting for. The CLI renders messages on the active bar and spins
one up when no operation is active.
This commit is contained in:
vhaudiquet committed 2026-10-30 01:04:20 +01:00
1 parent 89a1028d46
commit c2e8d82cd3
5 files changed
+42 -7

No files matched your search

+3 -2
View File
@@ -18,8 +18,9 @@ propagates exit codes. The split is enforced by the dependency edges:
the library links no clap and no rendering crate. It never writes to the library links no clap and no rendering crate. It never writes to
the terminal: diagnostics go through the `log` facade, filtered by the terminal: diagnostics go through the `log` facade, filtered by
`RunOptions::log_level` (an engine-owned enum, not UI booleans), long `RunOptions::log_level` (an engine-owned enum, not UI booleans), long
operations report through the `Progress` sink, all output is the operations report through the `Progress` sink (sequential
CLI's. In-process failures come back as typed errors; the one `begin`/`advance`/`finish` operations plus uncounted `message`
status lines), all output is the CLI's. In-process failures come back as typed errors; the one
exception is the forked machine child's last-gasp diagnostics, which exception is the forked machine child's last-gasp diagnostics, which
land in the console file the parent streams. The CLI owns clap, the land in the console file the parent streams. The CLI owns clap, the
indicatif rendering of progress events, the log sink, and exit codes. indicatif rendering of progress events, the log sink, and exit codes.
+20
View File
@@ -88,4 +88,24 @@ impl Progress for TermProgress {
bar.finish_and_clear(); bar.finish_and_clear();
} }
} }
fn message(&self, text: &str) {
let mut slot = self.bar.lock().unwrap();
if slot.is_none() {
// No operation is active: the message becomes a spinner
// until the next `begin` replaces it.
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap(),
);
pb.set_message(text.to_string());
*slot = Some(pb);
return;
}
if let Some(bar) = &*slot {
bar.set_message(text.to_string());
}
}
} }
+9 -3
View File
@@ -94,9 +94,11 @@ pub(crate) struct EntryLock {
} }
impl EntryLock { impl EntryLock {
/// Block until the entry is free. `no_cache` callers skip the /// Block until the entry is free, telling the sink what the run
/// lock entirely: an explicit fresh download never waits. /// is waiting on (a silent block reads as a hang). `no_cache`
pub(crate) fn acquire(entry: &Path) -> anyhow::Result<Self> { /// callers skip the lock entirely: an explicit fresh download
/// never waits.
pub(crate) fn acquire(entry: &Path, progress: &dyn Progress) -> anyhow::Result<Self> {
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
let lock_path = entry.with_extension("lock"); let lock_path = entry.with_extension("lock");
@@ -120,6 +122,10 @@ impl EntryLock {
"waiting for another ecrou run to finish preparing {}", "waiting for another ecrou run to finish preparing {}",
entry.display() entry.display()
); );
progress.message(&format!(
"Waiting for another ecrou run to finish preparing {}",
entry.display()
));
let held = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) }; let held = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) };
if held != 0 { if held != 0 {
return Err(anyhow::Error::from(std::io::Error::last_os_error()) return Err(anyhow::Error::from(std::io::Error::last_os_error())
+8 -1
View File
@@ -19,7 +19,9 @@ pub enum Unit {
/// ///
/// Operations are sequential: `begin` starts the next operation (any /// Operations are sequential: `begin` starts the next operation (any
/// previous one is implicitly over), `advance` counts completed units, /// previous one is implicitly over), `advance` counts completed units,
/// `finish` marks its end. Sinks must be callable from any thread. /// `finish` marks its end. `message` reports uncounted status outside
/// that protocol — a wait, a decision, a retry — when there is nothing
/// to count. Sinks must be callable from any thread.
pub trait Progress: Send + Sync { pub trait Progress: Send + Sync {
/// An operation labelled `label` starts, counting `unit`s; `total` /// An operation labelled `label` starts, counting `unit`s; `total`
/// is the unit count when known upfront. /// is the unit count when known upfront.
@@ -30,6 +32,11 @@ pub trait Progress: Send + Sync {
/// The current operation ended. /// The current operation ended.
fn finish(&self); fn finish(&self);
/// A status line outside the counted-operation protocol. The
/// default discards it; rendering sinks surface it as a spinner
/// message, starting one if no operation is active.
fn message(&self, _text: &str) {}
} }
/// A [`Progress`] sink that discards every event. /// A [`Progress`] sink that discards every event.
+2 -1
View File
@@ -88,7 +88,8 @@ impl RootfsCache {
// One writer per entry: a concurrent run preparing the // One writer per entry: a concurrent run preparing the
// same image waits here instead of downloading a second // same image waits here instead of downloading a second
// copy the atomic rename would throw away. // copy the atomic rename would throw away.
let _entry_lock = EntryLock::acquire(&cache_path).map_err(Error::Other)?; let _entry_lock =
EntryLock::acquire(&cache_path, progress).map_err(Error::Other)?;
// Re-derive the freshness answer under the lock: a // Re-derive the freshness answer under the lock: a
// competing run may have filled or refreshed the entry // competing run may have filled or refreshed the entry
// while we waited. // while we waited.