fix(cli): exit quietly on a closed output pipe #81

Merged
Claude-Run merged 1 commit from fix/FJ-63-broken-pipe-exit into main 2026-07-25 14:55:00 +02:00
Member

fj version | head -1 printed its first line and then panicked with exit 101. Rust installs SIG_IGN for SIGPIPE at startup, so a consumer that stops reading surfaces as a write error instead of ending the process, and every stdout write path turned that error into a panic: WriterCompat discarded the io::ErrorKind and the print macros called .expect("failed to write newline"), while the bare println! sites hit std's own "failed printing to stdout".

Handle it at the write layer instead of at the call sites. WriterCompat now keeps the failing write's io::Error (fmt::Error cannot carry it) and exits on BrokenPipe itself, so every localized print inherits the behaviour; ftl_println! / ftl_eprintln! / ftl_readline! replace their .expect(...) with a handler that is quiet on BrokenPipe and still panics with the underlying error on anything else, so a full disk or a closed redirect target stays visible. main also restores the default SIGPIPE disposition, which covers the bare println! sites without touching them and makes the pipe convention 141, the same status every other Unix CLI reports. fj mcp serve puts SIG_IGN back before serving: a server must not be killed by a client that hangs up mid-write.

The regression test drives the binary with a stdout whose reader is already gone, which fails the very first write deterministically instead of depending on the pipe buffer filling, and covers the ftl_println! path, clap's own help printing, and a many-line command. Two more cases run the real ... | head idiom through sh and assert the reported status, and a /dev/full case pins the loud half of the contract.

#FJ-63

`fj version | head -1` printed its first line and then panicked with exit 101. Rust installs `SIG_IGN` for `SIGPIPE` at startup, so a consumer that stops reading surfaces as a write error instead of ending the process, and every stdout write path turned that error into a panic: `WriterCompat` discarded the `io::ErrorKind` and the print macros called `.expect("failed to write newline")`, while the bare `println!` sites hit std's own "failed printing to stdout". Handle it at the write layer instead of at the call sites. `WriterCompat` now keeps the failing write's `io::Error` (`fmt::Error` cannot carry it) and exits on `BrokenPipe` itself, so every localized print inherits the behaviour; `ftl_println!` / `ftl_eprintln!` / `ftl_readline!` replace their `.expect(...)` with a handler that is quiet on `BrokenPipe` and still panics with the underlying error on anything else, so a full disk or a closed redirect target stays visible. `main` also restores the default `SIGPIPE` disposition, which covers the bare `println!` sites without touching them and makes the pipe convention 141, the same status every other Unix CLI reports. `fj mcp serve` puts `SIG_IGN` back before serving: a server must not be killed by a client that hangs up mid-write. The regression test drives the binary with a stdout whose reader is already gone, which fails the very first write deterministically instead of depending on the pipe buffer filling, and covers the `ftl_println!` path, clap's own help printing, and a many-line command. Two more cases run the real `... | head` idiom through `sh` and assert the reported status, and a `/dev/full` case pins the loud half of the contract. #FJ-63
fix(cli): exit quietly on a closed output pipe
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 30s
Create release / Create release from merged PR (pull_request) Has been skipped
6c4e687f09
`fj version | head -1` printed its first line and then panicked with exit 101. Rust installs `SIG_IGN` for `SIGPIPE` at startup, so a consumer that stops reading surfaces as a write error instead of ending the process, and every stdout write path turned that error into a panic: `WriterCompat` discarded the `io::ErrorKind` and the print macros called `.expect("failed to write newline")`, while the bare `println!` sites hit std's own "failed printing to stdout".

Handle it at the write layer instead of at the call sites. `WriterCompat` now keeps the failing write's `io::Error` (`fmt::Error` cannot carry it) and exits on `BrokenPipe` itself, so every localized print inherits the behaviour; `ftl_println!` / `ftl_eprintln!` / `ftl_readline!` replace their `.expect(...)` with a handler that is quiet on `BrokenPipe` and still panics with the underlying error on anything else, so a full disk or a closed redirect target stays visible. `main` also restores the default `SIGPIPE` disposition, which covers the bare `println!` sites without touching them and makes the pipe convention 141, the same status every other Unix CLI reports. `fj mcp serve` puts `SIG_IGN` back before serving: a server must not be killed by a client that hangs up mid-write.

The regression test drives the binary with a stdout whose reader is already gone, which fails the very first write deterministically instead of depending on the pipe buffer filling, and covers the `ftl_println!` path, clap's own help printing, and a many-line command. Two more cases run the real `... | head` idiom through `sh` and assert the reported status, and a `/dev/full` case pins the loud half of the contract.

#FJ-63
Claude-Run deleted branch fix/FJ-63-broken-pipe-exit 2026-07-25 14:55:00 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pandoras-box/forgejo-cli!81
No description provided.