12 comments

[ 1.8 ms ] story [ 32.2 ms ] thread
This title was close to being a garden path sentence, but ultimately avoided it.
I’ve definitely seen all of these problems in Rust programs but they certainly aren’t limited to Rust programs. I do think it would be nice if Rust libraries were a bit more misuse-resistant when it came to preserving a coherent terminal.

I also long for a more misuse-resistant terminal but that seems like a bigger problem.

Are you saying that after the main process has exited, child processes can still run and write to stdout/stderr?
Nothing here is specific to Rust and applies to any terminal app in any language that spawns a child process.
Fearless concurrency with Rust unless you are worried about lifecycle management, threads/co-operation and general ergonomics. Even modern c++ might be better at this (gasp!) with std::jthread
(comment deleted)
The graceful shutdown stuff is good, but always piping the output of child processes is not necessarily the right thing to do. Some processes need stdin (what if it's a shell?) and some processes will be checking if stdout is a tty. What you should do (and Rust doesn't make this easy) is allocate a new pty for your child processes if your own stdout is a tty. Some programs default to this (eg: ssh), others make it configurable (eg: docker).

You're also missing the standard techniques for managing and reaping your children, which I don't see mentioned. You don't need to maintain a registry of child processes for example, at least on Linux there are a few things you can do for this without any global state (PR_SET_PDEATHSIG, PR_SET_CHILD_SUBREAPER, PID namespaces). On MacOS you can write a routine to reap children like a linux init() process would. The registry approach is also fragile: what if library code spawns children?

Also, if the terminal is in raw mode then you'll never get ctrl+C. This is really about signal handling. You also can't gracefully shutdown if you get a SIGKILL, which is why PR_SET_PDEATHSIG and PID namespaces are very nice - they guarantee descendants get killed.

I hate to be the guy, but I could barely see the code snippets. Is contrast an issue for anyone else? Reader mode improves thins slightly but at the cost of code being unhighlighted and wrapping like crazy.
I believe the real fun is when doing this on Windows, because it doesn't use Unix signals (and generally speaking you only get the equivalent of SIGKILL but not SIGTERM, but you can opt into ~SIGINT). I was hoping this would actually deal with that…
It seems wrong that the app would be responsible for cleanup... Shouldn't this be solved in the shell / terminal ? What if kill -9?
Handling SIGINT (ctrl-c) with child processes is tricky, but a more pervasive problem for Rust CLI programs is handling SIGPIPE. For historical reasons the compiler adds a signal handler before calling main() which ignores SIGPIPE. It means when you pipe your Rust CLI program's output to something like head, instead of being killed by the signal sent when head closes the pipe, you get a write error and usually print an error message instead of silently dying. You can match on the type of write error and skip printing a message when it's from a broken pipe, but a more subtle problem is that the shell sets the exit status of a program killed by a signal to 128 + the signal number, so 141 in the case of a broken pipe. You can emulate this behaviour by checking for a broken pipe and explicitly exiting with a 141 status code, but it's not possible to fully reproduce being killed by a signal. There's been an issue to make this configurable (the latest proposal is via a compiler flag) for years.