fix(cli): exit quietly on a closed output pipe #81
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/FJ-63-broken-pipe-exit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
fj version | head -1printed its first line and then panicked with exit 101. Rust installsSIG_IGNforSIGPIPEat 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:WriterCompatdiscarded theio::ErrorKindand the print macros called.expect("failed to write newline"), while the bareprintln!sites hit std's own "failed printing to stdout".Handle it at the write layer instead of at the call sites.
WriterCompatnow keeps the failing write'sio::Error(fmt::Errorcannot carry it) and exits onBrokenPipeitself, so every localized print inherits the behaviour;ftl_println!/ftl_eprintln!/ftl_readline!replace their.expect(...)with a handler that is quiet onBrokenPipeand still panics with the underlying error on anything else, so a full disk or a closed redirect target stays visible.mainalso restores the defaultSIGPIPEdisposition, which covers the bareprintln!sites without touching them and makes the pipe convention 141, the same status every other Unix CLI reports.fj mcp serveputsSIG_IGNback 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... | headidiom throughshand assert the reported status, and a/dev/fullcase 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