Skip to content

fix(run): raw-mode-safe CRLF for launch-path status banners#13

Open
evgeny-boger wants to merge 2 commits into
mainfrom
fix-update-banner-crlf
Open

fix(run): raw-mode-safe CRLF for launch-path status banners#13
evgeny-boger wants to merge 2 commits into
mainfrom
fix-update-banner-crlf

Conversation

@evgeny-boger

@evgeny-boger evgeny-boger commented Jun 13, 2026

Copy link
Copy Markdown
Member

Problem

Status lines printed during launch staircase under raw mode — the next line is shoved to the right:

==> A newer image is available in the registry (cached 273bc18ea563, registry 75fd20fd31c2)
                                                                                           ==> Run `agent-vm pull` to fetch it. Continuing with the cached image.

Root cause

Several launch-path messages are emitted from background tasks that can run after the interactive session has switched the terminal to raw mode (crossterm::enable_raw_mode in the exec path):

  • the update-available banner (notify_if_update_available), and
  • the --auto-publish "watching" line and per-port event lines.

In raw mode the terminal's ONLCR post-processing is off, so the bare \n from eprintln! is a line feed with no carriage return: the cursor drops a row but keeps its column.

Fix

Introduce one shared helper, status_line_ending(is_tty), that returns:

  • \r\n on a TTY — renders flush-left whether or not raw mode is active (harmless in cooked mode);
  • plain \n for redirected stderr — no stray carriage returns in logs/pipes.

Route every racing launch-path status line through it:

  • update_banner (update-available banner), and
  • the three --auto-publish messages (eprint!("…{nl}"); the per-event task samples is_terminal() once before its loop — a fd's TTY-ness is stable for the process).

Also converts the now-single-arm match in notify_if_update_available to if let (silences clippy::single_match).

Tests

  • status_line_ending_is_crlf_on_tty_and_lf_otherwise
  • update_banner_uses_crlf_on_tty_so_second_line_doesnt_staircase
  • update_banner_uses_plain_lf_off_tty
  • cargo build, cargo test -p agent-vm (131/131 with --test-threads=1; one unrelated pre-existing flake races on a process-wide env var under parallel runs), and cargo clippy -p agent-vm all pass — zero new clippy warnings (net −1).

End-to-end (real binary)

Ran the actual compiled agent-vm binary inside a raw-mode PTY (slave in cfmakeraw, i.e. the post-enable_raw_mode state), with a planted stale pulled-digests marker so check_for_update fires a live ghcr.io probe and hits UpdateAvailable:

  • Instrumented is_terminal() at banner time → true (stderr is the PTY).
  • Update-banner bytes on the wire (raw mode, no ONLCR translation): …)\r\n==> Run … cached image.\r\n — real CRLF, flush-left.

This exercises status_line_ending(stderr().is_terminal()) in the real binary. The --auto-publish lines call the identical helper but could not be driven end-to-end here: they print only after a successful Sandbox::create, and this nested dev box can't boot the guest microVM (entering VM → Vmm is stopping — nested KVM is unavailable). They're covered by the shared helper's real-binary proof + the unit test + code review.

🤖 Generated with Claude Code

evgeny-boger and others added 2 commits June 13, 2026 14:09
The "newer image available" banner is printed from a background task on
the launch path, which can resolve *after* the interactive session has
put the terminal into raw mode (crossterm::enable_raw_mode in the exec
path). In raw mode the terminal's ONLCR output post-processing is off,
so the bare "\n" from eprintln! is a line feed with no carriage return:
the cursor drops a row but keeps its column, staircasing the second line
off the end of the first.

Extract the banner into a pure `update_banner(cached, remote, is_tty)`
helper that terminates each line with CRLF on a TTY (rendering flush-left
regardless of raw mode) and plain LF for redirected stderr (no stray
carriage returns in logs/pipes). Add unit tests for both paths. Also
convert the now-single-arm match to `if let` (silences clippy::single_match).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… terminator

The `--auto-publish` "watching" line and per-port event lines (`run.rs`)
are emitted from the launch path — the events from a background task during
the live, raw-mode session — using bare-`\n` `eprintln!`. Same failure mode
as the update banner: under raw mode (ONLCR off) a bare LF doesn't return the
carriage, so the line staircases.

Extract the CRLF-vs-LF decision into a shared `status_line_ending(is_tty)`
helper (the "shared raw-mode-safe writer" the earlier review flagged),
have `update_banner` use it, and switch the three auto-publish messages to
`eprint!("…{nl}")` with `nl = status_line_ending(stderr().is_terminal())`.
The per-event task samples is_terminal() once before its loop — a fd's
TTY-ness is stable for the process.

Add a unit test for `status_line_ending`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@evgeny-boger evgeny-boger changed the title fix(run): emit CRLF for the update-available banner on a TTY fix(run): raw-mode-safe CRLF for launch-path status banners Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant