Cross-platform Unix-style handling of broken pipe errors.
When any call to its underlying writer returns a BrokenPipe
error, a [Writer] terminates the current process with a SIGPIPE signal, or exits with code 1
on non-Unix systems.
Why is this useful?
When a process runs in a Unix shell pipeline, it's good form for the process to exit quickly and silently as soon as its downstream process stops accepting input. Unix simplifies the handling of this case with the SIGPIPE signal: when a process writes to a pipe where all file descriptors referring to the read end have been closed, the system sends it this signal, which by default terminates it.
The existence of SIGPIPE introduces two challenges. First, it's Unix-specific, so portable CLIs
might not be able to rely on it. Second, a networked server can generate SIGPIPE when writing
to a socket whose client has closed its read end, and terminating the server would break other
clients' connections. Given these challenges, the Rust developers chose to override Unix's
default behavior by globally ignoring SIGPIPE prior to calling main, causing all writes to
broken pipes to return a plain BrokenPipe error.
Unfortunately, a well-meaning CLI that wants to handle broken pipes with a silent exit might
find it difficult using error values alone. Experience shows that real-world Rust libraries
don't always expose enough detail to easily distinguish this from other errors. For example,
the source implementation in a library's custom error type might
not expose an underlying [io::Error] even when traversing the entire chain of sources.
[Writer] instead plumbs this logic directly into every write operation, catching broken pipe
errors and terminating the process before anything else in the call stack has a chance to
obscure them. Unlike an up-front modification of the process-wide SIGPIPE behavior, this
approach is more cross-platform and better scoped to the specific writes where termination is
desired (generally on standard output and error streams).
Note that termination on Unix invokes the real default behavior of SIGPIPE; Writer does not
employ incorrect hacks like exiting with code 141 (mimicking the shell return code of a process
terminated by SIGPIPE).
Further Reading
For further background on SIGPIPE, Rust's handling of it, and cross-platform portability concerns surrounding broken pipes, see:
- https://github.com/rust-lang/rust/issues/62569
- https://stackoverflow.com/a/65760807
- https://github.com/BurntSushi/ripgrep/issues/200#issuecomment-616884727
The concept of pipecheck was directly inspired by Go's default behavior for broken pipes:
terminating the program if the write was to a standard output or error stream, and otherwise
returning a plain error. For background on Go's behavior and runtime implementation, see: