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

+20
View File
@@ -88,4 +88,24 @@ impl Progress for TermProgress {
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 {
/// Block until the entry is free. `no_cache` callers skip the
/// lock entirely: an explicit fresh download never waits.
pub(crate) fn acquire(entry: &Path) -> anyhow::Result<Self> {
/// Block until the entry is free, telling the sink what the run
/// is waiting on (a silent block reads as a hang). `no_cache`
/// 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;
let lock_path = entry.with_extension("lock");
@@ -120,6 +122,10 @@ impl EntryLock {
"waiting for another ecrou run to finish preparing {}",
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) };
if held != 0 {
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
/// 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 {
/// An operation labelled `label` starts, counting `unit`s; `total`
/// is the unit count when known upfront.
@@ -30,6 +32,11 @@ pub trait Progress: Send + Sync {
/// The current operation ended.
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.
+2 -1
View File
@@ -88,7 +88,8 @@ impl RootfsCache {
// One writer per entry: a concurrent run preparing the
// same image waits here instead of downloading a second
// 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
// competing run may have filled or refreshed the entry
// while we waited.