The main functionality ====================== `pristine-etc-keeper` is a program that hooks into `etckeeper` to maintain a git branch that is a "pristine" version of `/etc`--just the raw files provided by installed packages. It makes several assumptions that make it a bit less general than etckeeper: - the real directory being tracked by etckeeper is `/etc` - the VCS using used is `git` - the system package manager is `pacman` It works by asking `pacman` which packages are installed, and extracting `/etc` files from the cached copies of the package tarballs. A litteral pristine version of `/etc` lives at `/var/lib/pristine-etc/etc`. Invocation ========== The normal operation of pristine-etc-keeper is for it to be called by etckeeper's post-install hook. You usually don't need to worry about it. However, sometimes it may be useful to call it from somewhere else. The `pristine-etc-keeper` program essentially has two modes of operation: 1. With arguments: Run, and use "$*" for the commit message. 2. No arguments: If a run was previously requested, but never fulfilled; do that now. Really though, if it has arguments, it requests a run with that commit message; then, whether it requested a run or not, it launches another process to fulfill any outstanding requests. If you're thinking "What!? Why request runs? Why not just run directly? It's needless complexity!" Instinctively, I'd agree with that line of reasoning; but see "The spool" below for justification of the complexity. Implementation details ====================== The spool --------- It would be really simple to just provide a program that you run when you want to update the pristine-etc. Unfortunately, pristine-etc-keeper is very slow, and this would make many tasks painful. So we run it asyncronously. But that opens a whole other slew of problems. What happens if we try to run it again while it is already running? The second instance should wait until the first instance is finished. But only one instance should be queued at a time; if a 3rd instance tries to start, it should just be discarded/merged with the one already waiting ("coalescing"). A simple thing to do would have been to write a daemon that is always running, waiting to receive a signal that it should run. I don't like that because I don't really want the process to be long lived, nor for it to be started at boot. But, let's take that idea, tweak it a bit. Let's conceptualize the "signal" as the invoker adding an item to a filesystem spool; each time the daemon runs a job, it empties the spool. Now, we modify this idea by saying that the daemon may exit when the spool is empty (ie, it has no work to do, and would just idle-wait until it is asked to run again). When the invoker adds an item to the spool, we simply have it ensure that the daemon is running. To translate this into the source code names, the invoker is "fill" because it adds an item to the spool, and the so-called-daemon that runs the jobs is "drain" because it drains the spool. Systemd integration ------------------- In the above section, we conceptualized the "drain" program as a daemon, even though it isn't truly one. This conceptualization is useful in administration as well. So, if systemd is being used, we have it litterally show up as pristine-etc-keeper.service. This has a number of benefits: - cgroup isolation - uniform logging with other system services - most importantly: systemd v230 and up won't kill a run in progress when you log out But, if systemd wasn't used to boot the system, then it simply forks to the background. If you'd like to see similar integration with another service manager, patches welcome!