Repository navigation
lib: Route status output through the progress API, deny (e)println! - #1
cgwalters-bot wants to merge 5 commits into
Conversation
println!() panics if stdout is closed (e.g. a broken pipe when piping into head), whereas writeln!() lets us propagate the error normally. These call sites are the actual output of a subcommand (digests, JSON, tables, GC stats), so an explicit stdout writer is the right thing anyway. Prep for denying clippy::print_stdout in crates/lib, see bootc-dev#2449 (comment) Generated-by: AI
| let reverting = new_spec.boot_order == BootOrder::Default; | ||
| if reverting { | ||
| println!("notice: Reverting queued rollback state"); | ||
| cli_status!("notice: Reverting queued rollback state"); |
There was a problem hiding this comment.
What I'm thinking here is we should try to go all-in on the progress API, so basically we have a generic "message" event or so, then we're drinking our own champagne
There was a problem hiding this comment.
Makes sense. Proposed design:
-
Event: add
Event::Message { level: Info|Warning, text }, serialized as{"type":"Message","level":"info","text":"..."}. Consumers that match ontypestrictly will see a new variant. So I'd bumpAPI_VERSIONto 0.2.0, regenerateprogress-v0.schema.json, and say inexperimental-progress-fd.mdthat unknowntypes must be ignored. -
Rendering:
ProgressWriter::message(level, text)prints the text exactly as today (info to stdout, warning to stderr, viaindicatif'sprintlnwhile pull bars are active). If an fd is attached, it also emits the JSONL event. So non-JSONL output stays the same, and JSONL mode additionally gets the events. -
Getting a writer to call sites: this is where I'd like your call. Only upgrade, switch and
install resettake--progress-fdtoday, and they cover about 30 of the ~90cli_status!/cli_warn!sites. The rest are in install/to-disk, bootloader, rollback, baseline, fsck and edit, and many of them are sync, whileProgressWriter::sendis async (tokio mutex and pipe). Options:- A) Explicit threading (my recommendation; it's the "all-in" version). A prep commit makes
ProgressWriterusable from sync code (std mutex, blocking writes; events are tiny and rate-limited). Then&ProgressWriteris passed down, stored in install'sState. Commands without--progress-fdpassProgressWriter::default(), which just prints. It's a larger diff, but it's greppable and has no hidden state. - B) A process-global sink, set once in
cli.rsafter parsing--progress-fd.cli_status!would emit through it. It's a much smaller diff, but it's ambient state, and the macros stay.
Separately: should install and rollback grow
--progress-fdnow, or later? - A) Explicit threading (my recommendation; it's the "all-in" version). A prep commit makes
I'll hold until you pick A or B. With A, I'd restructure the commits as follows: make ProgressWriter sync-capable, add Message, convert the call sites, then deny.
Generated-by: https://github.com/cgwalters/#llms
There was a problem hiding this comment.
Option A though let's try to break this up into separate commits that at least compile check, not one big likely to conflict-fest "thread progress writer through everywhere"
There was a problem hiding this comment.
Done, with option A as 15 small commits. Each one passes cargo check -p bootc-lib --all-targets with no warnings, plus the bootc-lib unit tests, on its own:
lib,lib/fsck: the two existing prep commits (command results to an explicit stdout writer)progress_jsonl: makeProgressWriterusable from sync code (std mutex + blocking writes)progress_jsonl: addEvent::Message { level, text }andProgressWriter::message()/info()/warning(). These print the same text as before and also send the event when an fd is attached. The protocol goes to 0.2.0, the schema is regenerated, and the docs say that consumers must ignore unknown event types.- one commit each for
deploy,rollback,cli(upgrade/switch/edit),composefs,install(writer kept inState),install/*helpers,bootloader,boundimage,image,task lib: denyclippy::print_stdout/print_stderr, with four justified#[expect]s (global_init,journal_send, the progress writer's own error path, the spinner's no-tty fallback)
Output without --progress-fd is unchanged. Commands that don't take the flag use ProgressWriter::default(), which just prints.
For the ban I kept the crate-level restriction lints instead of disallowed-macros, because clippy.toml applies to the whole workspace.
Deferred (not in this PR): --progress-fd for install and rollback. Adding it now only means passing the fd to prepare_install() or to the writer rollback already creates. Task and the anaconda/ostree completion entry points also still use a plain printing writer.
Tested on a 16-core devspace. At the head I ran make validate, clippy with -D unfulfilled_lint_expectations, and cargo xtask update-generated {direct,from-code} --check, and checked that a stray println!() fails clippy. I didn't run tmt/VM tests.
Generated-by: https://github.com/cgwalters/#llms
There was a problem hiding this comment.
Pushed a follow-up round (head 973e510b). The changes are squashed into the commits they belong to:
- sync
ProgressWriter: the commit message now says that the CLI runtime is single-threaded, so a--progress-fdreader that stops reading stalls the whole process. It also says that tokio used to setO_NONBLOCKon the shared file description and we now clear it. The pipe check is gone, so the--progress-fdhelp text now just says "typically the write end of a pipe". The progress tests note that the pipe must not fill up, since the sender and the reader share a thread. - Message event:
test-image-pushpull-upgrade.nunow filtersMessageevents out before checking events by position, and asserts that at least oneMessageevent is there. I checked it withnu --ide-checkand against sample JSONL but haven't run it in a VM. - cli:
switch --mutate-in-placenow uses its--progress-fdwriter. - install: the
-----separator lines go only to the terminal (stderr) and are no longer sent as warning events. - deny:
anstream::{print,println,eprint,eprintln}are now indisallowed-macros, and onlybootc-libdenies that lint (it's also the only crate using them).medium_visibility_warninghas a justified#[expect]. - PR body: I fixed the reasoning for using crate-level lints (a
crates/lib/clippy.tomlwould replace the root config, not extend it). I also noted thatTask/child process output still goes to the inherited stdout, and listed hardening against a stalled reader (EAGAIN/poll) as a possible follow-up. On the prefix question, the recommendation is not to strip prefixes ad hoc: if we want bare text later,message()should addwarning:itself based on the level.notice:is info-level, so that prefix carries its own meaning.
I re-ran the checks on a 16-core devspace. Every commit passes cargo check -p bootc-lib --all-targets with no warnings, and the unit tests pass (271 passed, 1 ignored). At the head, these all pass: make validate, clippy with CI's flags plus -D unfulfilled_lint_expectations, update-generated {direct,from-code} --check and cargo fmt --check. A stray println!() or anstream::eprintln!() fails clippy.
Generated-by: https://github.com/cgwalters/#llms
Status output must reach progress-fd consumers, including from synchronous code. Add Message events and bump the protocol to 0.2.0; blocking writes can stall the single-threaded runtime if a reader stops draining. Generated-by: AI
Upgrade and switch status should reach their progress-fd consumers. Route deployment, rollback and composefs status through ProgressWriter while keeping terminal output unchanged. Generated-by: AI
Keep install status off direct stdout/stderr prints and ready for progress-fd support. Carry the writer in install State and pass it to installation, bootloader and bound-image helpers. Generated-by: AI
Direct library prints bypass progress consumers and are easy to miss in review. Convert the remaining image and task status, then deny the print lints with targeted exceptions for initialization and output infrastructure. Generated-by: AI
83201bd to
d95c3e9
Compare
d95c3e9 to
973e510
Compare
The Boot Loader Specification reserves `+` in entry file names for boot counting, so an os-release version like `1.2.3+42` gave an entry that systemd-boot treats as having 42 boot attempts left, and counts down. Generated-by: AI
systemd-boot puts entries whose boot counter has run out after all others, whatever their sort-key, so after a new deployment fails to boot the previous one becomes the default. Mirror that when ordering Type #1 entries, so that `bootc status` reports the failed deployment as the rollback rather than claiming a rollback is queued when it isn't, and `bootc rollback` swaps the entries the bootloader actually uses. Prep for enabling boot counting for new deployments. Generated-by: AI
The composefs backend always wrote systemd-boot's Type #1 entries, kernels and UKIs to the ESP, even when the disk had an XBOOTLDR partition. Per the Boot Loader Specification, XBOOTLDR is $BOOT when it exists, and it's how a system with a small ESP (e.g. one shared with another OS) gets room for several deployments. An unused XBOOTLDR also gets in the way at runtime: systemd-gpt-auto-generator mounts it on /boot, which is why image-builder now drops it for UKI images. Once bootc installs with the container's own repart.d definitions, a container that asks for XBOOTLDR should get one that's used. So when there's an XBOOTLDR partition next to the ESP, put the entries, kernels and UKIs there, keeping only systemd-boot and loader.conf on the ESP. systemd-boot reads XBOOTLDR through the firmware, which in practice means FAT, so another filesystem is ignored with a warning. GRUB is unchanged. On a booted system XBOOTLDR is only used if it has the booted deployment's entry, so existing systems that have an unused XBOOTLDR (for example from an older image-builder layout) keep using the ESP. With XBOOTLDR, bootc no longer mounts the ESP at runtime at all. Generated-by: AI
The Boot Loader Specification reserves `+` in entry file names for boot counting, so an os-release version like `1.2.3+42` gave an entry that systemd-boot treats as having 42 boot attempts left, and counts down. Generated-by: AI
systemd-boot puts entries whose boot counter has run out after all others, whatever their sort-key, so after a new deployment fails to boot the previous one becomes the default. Mirror that when ordering Type #1 entries, so that `bootc status` reports the failed deployment as the rollback rather than claiming a rollback is queued when it isn't, and `bootc rollback` swaps the entries the bootloader actually uses. Prep for enabling boot counting for new deployments. Generated-by: AI
973e510 to
c47ebf9
Compare
The composefs backend always wrote systemd-boot's Type #1 entries, kernels and UKIs to the ESP, even when the disk had an XBOOTLDR partition. Per the Boot Loader Specification, XBOOTLDR is $BOOT when it exists, and it's how a system with a small ESP (e.g. one shared with another OS) gets room for several deployments. An unused XBOOTLDR also gets in the way at runtime: systemd-gpt-auto-generator mounts it on /boot, which is why image-builder now drops it for UKI images. Once bootc installs with the container's own repart.d definitions, a container that asks for XBOOTLDR should get one that's used. So when there's an XBOOTLDR partition next to the ESP, put the entries, kernels and UKIs there, keeping only systemd-boot and loader.conf on the ESP. systemd-boot reads XBOOTLDR through the firmware, which in practice means FAT, so another filesystem is ignored with a warning. GRUB is unchanged. On a booted system XBOOTLDR is only used if it has the booted deployment's entry, so existing systems that have an unused XBOOTLDR (for example from an older image-builder layout) keep using the ESP. With XBOOTLDR, bootc no longer mounts the ESP at runtime at all. Generated-by: AI
The Boot Loader Specification reserves `+` in entry file names for boot counting, so an os-release version like `1.2.3+42` gave an entry that systemd-boot treats as having 42 boot attempts left, and counts down. Generated-by: AI
systemd-boot puts entries whose boot counter has run out after all others, whatever their sort-key, so after a new deployment fails to boot the previous one becomes the default. Mirror that when ordering Type #1 entries, so that `bootc status` reports the failed deployment as the rollback rather than claiming a rollback is queued when it isn't, and `bootc rollback` swaps the entries the bootloader actually uses. Prep for enabling boot counting for new deployments. Generated-by: AI
|
Fix round 1 of 2 for Generated-by: https://github.com/cgwalters/#llms |
c47ebf9 to
8094e78
Compare
|
The poll stopped following this pull request: a draft with no checkpoint; mark it ready if the work is complete. It is labelled Generated-by: https://github.com/cgwalters/#llms |
The Boot Loader Specification reserves `+` in entry file names for boot counting, so an os-release version like `1.2.3+42` gave an entry that systemd-boot treats as having 42 boot attempts left, and counts down. Generated-by: AI
systemd-boot puts entries whose boot counter has run out after all others, whatever their sort-key, so after a new deployment fails to boot the previous one becomes the default. Mirror that when ordering Type #1 entries, so that `bootc status` reports the failed deployment as the rollback rather than claiming a rollback is queued when it isn't, and `bootc rollback` swaps the entries the bootloader actually uses. Prep for enabling boot counting for new deployments. Generated-by: AI
Library code that writes straight to stdout/stderr is easy to add and hard to spot in review. It also bypasses
--progress-fd, so anything consuming the progress API never sees bootc's status output. This came up in bootc-dev#2449 (comment), where a strayprintln!()slipped into the composefs pull path ("We may need a blanket ban on (e)println! in crates/lib").This series sends human-facing status through the progress API, then denies
clippy::print_stdoutandclippy::print_stderrinbootc-lib.The progress protocol gets a generic
Messageevent ({"type":"Message","level":"info"|"warning","text":"..."}).ProgressWriter::message()(and itsinfo()/warning()shorthands) prints the text exactly as before, info to stdout and warnings to stderr. If a progress fd is attached, it also sends the event. So output without--progress-fddoesn't change, andProgressWriter::default()works as a plain printer for commands that don't take the flag. Adding an event type can break consumers that match strictly ontype, so the protocol goes to 0.2.0, and the docs now say that unknown event types must be ignored.To keep each step reviewable and rebase-friendly, the conversion is split into small commits, and each one builds on its own:
lib: write command results (digests, JSON, tables, GC stats) to an explicit stdout writerlib/fsck: write passing checks to the writerfsck()is givenprogress_jsonl: makeProgressWriterusable from sync code (std mutex, blocking writes). Tokio used to setO_NONBLOCKon the fd; this now clears it. The CLI runs a single-threaded runtime, so a--progress-fdreader that stops reading now stalls the whole process. Any writable fd is accepted now, not just a pipe, and the help text says so.progress_jsonl: add theMessageevent andmessage(), bump to 0.2.0, update the schema and docs, and have the booted pushpull-upgrade tmt test check forMessageevents; the first user is the staging summarydeploy: pull "No changes", prune and fstab statusrollback: ostree and composefscli: upgrade, switch (including--mutate-in-place) and edit (upgrade/switch pass their--progress-fdwriter)composefs: update, switch, soft-reboot, usr-overlayinstall: keep aProgressWriterinState, convert install.rs (red warnings keep their highlighting and are also sent as events)install: the helpers underinstall/(block setup, config, completion), and the non-empty target warning ofinstall mountbootloaderboundimageimage: push and set-unifiedtask: task descriptions and command lineslib: denyclippy::print_stdout/print_stderrat the crate rootOnly a few direct prints are left, each with a targeted
#[expect(..., reason = ...)]:global_init()runs before tracing is set up,journal_send()and the progress writer report that they themselves are failing, the spinner has a plain-text fallback when there's no tty, andmedium_visibility_warning()colors its warning for the terminal (and also sends it as an event). Tests are exempt viaallow-print-in-testsinclippy.toml.The ban uses the
print_stdout/print_stderrrestriction lints, denied by abootc-libcrate attribute. This scopes it to that crate: the rootclippy.tomlis shared by the whole workspace, and acrates/lib/clippy.tomlwould replace the root config rather than extend it.anstream's print macros aren't covered by those lints, so they're listed indisallowed-macrosinclippy.toml, and onlybootc-libdenies that lint (it's also the only crate using them).Not in this PR: install (to-disk/to-filesystem/to-existing-root) and rollback still don't take
--progress-fd. With this series, adding it only means passing the fd toprepare_install(), or to the writer rollback already creates.Taskand the anaconda/ostree completion paths also use a plain printing writer for now, and the output of child processes (e.g.Taskcommands, bootupctl) still goes straight to the inherited stdout/stderr.Other possible follow-ups:
EAGAIN(with a poll or timeout for required ones).warning: ...andnotice: ...so the terminal output stays identical, which means the JSONtextcan repeat thelevel. I'd rather not strip them ad hoc. If we want bare text later,message()should add thewarning:prefix itself based on the level and send the bare text in the event.notice:is info-level, so that prefix carries its own meaning.Testing
Rebased onto main b06e0b0 (c47ebf9). The rebase was clean, apart from one real conflict: bootc-dev#2483 added an
eprintln!warning toinstall mount(crates/lib/src/mount.rs), which the new deny rejected. That warning now goes through aProgressWriter(squashed into theinstallhelpers commit). Theexperimental-progress-fd.mdedit follows the bootc-dev#2508 rename tobootc-experimental-progress-fd.7.md. At c47ebf9, on a 16-core devspace (cgwalters-devspace-36601734885, CentOS Stream 10 base):just validateandjust unit-tests(465 passed) passed. validate's clippy run (-A clippy::all, CI's config) warns that thedisallowed_macrosexpectation onmedium_visibility_warning()is unfulfilled; that isn't new with the rebase.just test-tmt plan-20-image-pushpull-upgradepassed, including the new check forMessageevents on the progress fd.Before the rebase, on a 16-core RHEL 10 devspace:
On a 16-core RHEL 10 devspace:
cargo check -p bootc-lib --all-targets(no warnings) andcargo test -p bootc-lib --lib.nu --ide-check), and the filter logic was run against sample JSONL (passes with aMessageevent, fails without one). I haven't run it in a VM.make validate(fmt,cargo test --no-run,--no-default-featureschecks, clippy with CI's flags,cargo docwith-D warnings), clippy with-D unfulfilled_lint_expectations, andcargo xtask update-generated {direct,from-code} --check. I also added a strayprintln!()and a strayanstream::eprintln!()to check that clippy rejects them.Apart from plan-20 above, I haven't run the integration tests (tmt/VM). The terminal output is meant to be byte-for-byte unchanged.
Caveats
crates/lib, mostly one-lineprintln!→prog.info()swaps. Because the commits are split by subsystem, a conflict should only affect one of them.Generated-by: https://github.com/cgwalters/#llms
Review draft in cgwalters-forge, not upstream yet. This section is removed when the PR is opened upstream.
bootc-dev/bootc, basemainPVTI_lADOE9oHIs4BlJLczg9kEIgTo review:
/promoteon a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.Signed-off-by: Colin Walters <walters@verbum.org>to the commits lacking it (the bot's and yours; anyone else's only if you ask), with you as committer./draftline (in the same comment or before) to open it upstream as a draft (/readyundoes that).