I've run into a similar issue in Rust with SIGPIPE. The compiler adds the call to signal() to ignore SIGPIPEs when a Rust program starts. Apparently this was to stop a listening server from exiting when a client closed their connection. But in my book I'm teaching Rust by rewriting a classic BSD utility (cat, head, wc, ...) in Rust and none of those utilities have to care about broken pipes because they don't ignore the signal and the OS silently kills the process. Instead, if a Rust program doesn't handle a write failing with a EPIPE error then the process panics and exits with a backtrace. The worst part is that Rust standard library doesn't include a module for handling signals so it's not possible to undo the signal() call ignoring SIGPIPEs without using another crate.
In the end it's fine and you just have to handle broken pipes but it adds a lot of boilerplate to small CLI programs that in C just work as expected. Another twist (and possible further subtle bug) is that most shells set the exit status of a program that exits from a broken pipe to 141 (128 + the signal number, in this case 13). So when you catch the pipe you can exit the process with a status of 141, but that's the shell's behaviour and there's no safe way to fake an exit status.
> The compiler adds the call to signal() to ignore SIGPIPEs when a Rust program starts. Apparently this was to stop a listening server from exiting when a client closed their connection
That’s appalling and irresponsible if true. Not every program is a server! If you want to ignore SIGPIPE you can do that yourself.
Well, that's the source of the bug in Python as well so it's not exactly a unique choice or default. It's tricky to manage this in a cross-platform way.
Python is a high-level language, though, and python code is run through an interpreter, so it's not a surprise that it might handle signals (and other things) differently than your average program. (Not restoring signals handlers to their default on fork()/exec() is definitely a bug, though.)
Rust is intended to be a systems programming language, and a replacement for C/C++. It should not be mucking about with signal handler defaults before main() runs.
The one thing that Rust does right, though, is it resets SIGPIPE to SIG_DFL when spawning processes[0]. Of course, I assume it only does that if you use std::process::Command, not if you use libc::fork()/libc::exec(), or get there some other way.
> Well, that's the source of the bug in Python as well so it's not exactly a unique choice or default. It's tricky to manage this in a cross-platform way.
Well, Python wasn't intended to write the sort of programs that C and Rust gets used for.
In Python, such a choice is not necessarily a design failure. In systems languages it is.
Thank you thank you thank you! Docker processes randomly exiting with "137" suddenly make a lot more sense!
137 - 128 == 9
... that's SIGKILL, probably via some sort of pipe/subshell. We'd figured it was OOM-killer or something similar, but now it seems like there's a bit more of the dots connected between.
Who can/will send random `kill -9`s? Probably the kernel, or some sort of supervisory process.
The usual way to generate an exit status for a signal that normally terminates the process is to explicitly unset the handler and then raise the signal yourself. In C, the latter half of that can be done with the ‘raise’ function; on (at least one) Linux with glibc, this uses tgkill underneath. I assume Rust doesn't expose this though from what you said?
That's a cool trick. There are crates for signal handling, and the libc crate exports raise(), they just aren't part of the standard library which attempts to be cross-platform. But if you're already going to unset the signal handler you might as well do that from the start (in this particular case).
In the end it's fine and you just have to handle broken pipes but it adds a lot of boilerplate to small CLI programs that in C just work as expected. Another twist (and possible further subtle bug) is that most shells set the exit status of a program that exits from a broken pipe to 141 (128 + the signal number, in this case 13). So when you catch the pipe you can exit the process with a status of 141, but that's the shell's behaviour and there's no safe way to fake an exit status.