đŸ Archived View for dcreager.net âș redo âș background-processes.gmi captured on 2024-05-12 at 14:52:01. Gemini links have been rewritten to link to archived content
âŹ ïž Previous capture (2024-05-10)
-=-=-=-=-=-=-
A recent conversation with a coworker reminded me of a âmakeâ replacement named âredoâ. On a lark I decided to update a couple of my projects to use âredoâ â in particular, the repo that builds my personal website from a collection of gemtext files.
While working on new posts, I often like to view them locally before syncing them up to the public-facing web server. Because I use vanity paths and âindex.htmlâ files liberally, I can't just load the HTML files directly via âfile:â URLs. Instead I spin up a lightweight HTTP server (lighttpd) to serve files from the local output directory contain the built site content. And crucially, I have an additional âmake serveâ target that spins up the server in the background for me, so that I don't have to remember the magic âlighttpdâ incantation to use. This takes advantage of how lighttpd will daemonize itself as a background process by default: the make target's rule invokes âlighttpdâ, which spins up a background process, but make does not wait for that background process to exit before make itself exits.
When porting that make target over to redo, I discovered that redo handles background jobs differently, in a way that required some additional tweaking. Make is presumably waiting for the _shell process_ that it invokes to exit, but does not wait for any background child processes to exit. (I haven't verified this, but I assume it actually waits for the shell process's entire _process group_ to exit, but lighttpd's daemonization logic would remove the HTTP server process from its parent's process group.)
Redo wasn't doing thisâinvoking âredo serveâ would fire off the background HTTP server as expected, but then redo would block waiting for the server to complete. I was very confused how this could be happening!
Redo seems to do something more complicated than make. It does call âwaitpidâ for each target's subprocess (but not its process group), but only uses that to clean up the child process entry in its process table. That's not the mechanism that it uses to _wait_ for the process to finish. Instead, redo creates a pipe for each job subprocess that it creates, and uses a âselectâ to call to wait for the read ends of any subprocess pipe to be readable.
redo job control file descriptors
redo waiting for a target process
Those pipes are never written into, and so they only become âreadableâ when the write file descriptor is closed. This is similar to a common shutdown notification pattern in Go using channels.
Starting and stopping things with a signal channel
However, Unix pipes add a wrinkle, because file descriptors are inherited by child processes when you âforkâ. You have to close _all_ copies of the pipe's write file descriptor before the âselectâ call unblocks anything waiting on the read file descriptor.
And that file descriptor inheritance even carries over to the HTTP server's background process! The server inherits a copy of the write file descriptor, and since âlighttpdâ itself doesn't know anything about it, it never closes it explicitlyâit gets closed by default when the process exits, just like all file descriptors do. And since redo's âselectâ call won't unblock until every copy of the write fd is closed, redo ends up waiting for the server process to exit.
To get around this, I had to update the âserve.doâ job file to create a subshell where I close all file descriptors other than stdin, stdout, and stderr; and then invoke âlighttpdâ from that subshell. (I can't close the file descriptors in the parent shell, since I do still want redo to wait for the parent shell to finish!) That ensures that the background process does not inherit a copy of the write file descriptor, and therefore that redo will not block waiting for it to exit.