Path Units Are Easy! - 9 August 2017 No matter how you are using your computer, anyone would love to have an easy way to get their computer to do a little magic when they plug something in. In this Reddit thread replying to my post about using borg in an automated backup system, user valkun expressed an interest in automating backup with USB hard drives. The optimal behavior would be to have backup run, periodically, after plugging in a USB-powered portable hard drive for backups, and then fall back to a monitoring ready-state when disconnected. This is something I'd really like to have for my laptop (which doesn't really have a backup solution at all, because it's not easy enough). But maybe what you really want is to automatically download your videos and pictures from your camera and feed them into a processing pipeline, or sync music to your phone in a fancy way that's not easy with other solutions. So, rather than post a lightly sanitized version of a setup I've worked out for myself, in this post we'll play with some systemd units and fairly simple bash , and build our way up to more generalizable ideas. Getting set to play Googling around, the first systemd units I thought to try would be mount units, because duh we want to watch for a block device mounting and unmounting as the first trigger. Although possible, messing with two OS layers (both systemd units and udev rules) instead of one is an obvious increase in work. Also, I'm not so excited about the added not-quite-portable quality of writing udev rules, seems quite hand-made, meaning hand-installed and fussy. It might be that some distros don't have automatic device mounting built in, but since I run Ubuntu it's not something I worry about. Obviously they have their uses, but not needed when we already have automount and a powerful set of abstractions that are built into a familiar interface: the filesystem. A little mise-en-place before we start: α Terminals We are going to need at least two terminals: one to do all our sudo ing in (moving unit files, enabling and disabling them), and one to watch the .service units as they're working. I would also need one to use an editor with root privileges to modify our .path and .service units. I use nvim for basically everything (bit of a mental illness I suppose), so I'm not sure if something like atom or sublime or gedit or whatever might easily take the place of $ sudo nano to edit files in a root -owned directory. Additionally, if you have a favourite editor for modifying the bash files we'll be using, obviously you'll want to have that too. It doesn't really matter if you have lots of windows or just one β Unit files and scripts I suppose I'll be doing more of these semi-instructional posts in the future, so here's a repo I've made to stash related files to save you a little bit of copy & paste. If you don't have git or don't want to mess with it, the files are easily downloadable in many different ways. Or you can just Ctrl-C Ctrl-V . Finally, very important, you need to modify the files as they are to put your username (which you see at the terminal prompt in most default setups) in place of the $USER variable. systemd does not expand variables in the units like bash or your shell of choice does with .sh files. If the examples aren't working right away, check to make sure you don't have any $ENVVAR s hanging around in your unit files. γ Storage device, at least a thumbdrive For the last step, we'll want to have a device just for the fun of it, especially if you are going to use this for an automatic portable backup system.



First demo, watch a directory Let's first look at the content of the units we're using here, and remember to edit the copies you've got for this demo with your username: test-path.path [Unit] Description=PathTest [Path] PathModified=/home/$USER/Desktop [Install] WantedBy=multi-user.target test-path.service [Unit] Description=PathTest [Service] ExecStart=/bin/echo "Something changed on your Desktop!" Very basic unit files, and if you're unfamiliar with the [Install] attribute, it basically designates the portion of the service unit that you enable and start . Copy test-path.path and test-path.service into place: Terminal 1 $ sudo cp test-path* /etc/systemd/system/ Now in your terminal for watching things, start watching before you flick the switch: Terminal 2 $ journalctl -f -u test-path Terminal 1 $ sudo systemctl enable test-path.path $ sudo systemctl start test-path.path $ systemctl status test-path.path So far, the service unit should be correctly installed, and you shouldn't have anything going on the journalctl -f as it's actually watching test-path.service for its events. Now put an empty file on the ~/Desktop and delete it, use touch , do whatever and see it in action on the monitoring terminal: Terminal 1 $ echo "" > ~/Desktop/test.file $ touch ~/Desktop/test.file $ rm ~/Desktop/test.file So by now you've hopefully seen that everything works as expected, and you can stop the journalctl -f with Ctrl-C in the terminal (it inputs a byte called SIGINT which signals the terminal to interrupt the process). So let's clean up and move to the next demo: Terminal 1 $ sudo systemctl stop test-path.path $ sudo systemctl disable test-path.path $ sudo rm /etc/systemd/system/test-path*



Second demo, periodic service execution For this second demo, we're going to have some more complex logic watching a path with error codes, and a RestartSec= attribute on the .service unit. As an aside, my expierments with .timer units was really frustrating, as they are designed for continuous execution, rather than be interrupted, meaning that once triggered they wouldn't stop running, which is not the behavior we're looking for. That led me to eventually finding the RestartSec= attribute option for .service s, which is exactly what we want. There's also some logic in the .service file which can watch for the exit codes from the process it starts, restarting itself on a normal 0 exit (in this case, there are lots of options), or not when exit is 1. Let's have a look at the files: filecheck.sh #!/usr/bin/env bash if [ -f $1 ]; then echo "File $1 exists!" exit 0 else echo "File $1 does not exist." exit 1 fi test-timer.path [Unit] Description=Testing Paths & Timer [Path] PathExists=/home/$USER/Desktop/test.file [Install] WantedBy=multi-user.target test-timer.service [Unit] Description=Testing Paths & Timer [Service] # If your install location of filecheck.sh differs, change here ExecStart=/home/$USER/.local/bin/filecheck.sh /home/$USER/Desktop/test.file RestartSec=3s Restart=on-success As you can see, filecheck.sh is passed a file, in this case ~/Desktop/test.file as defined in the .service file, checks to see if it exists, and prints a message and corresponding exit code. This service will execute every three seconds, unless test.file doesn't exist where test-timer.path is watching and sends the error exit code, stopping the service. Copy test-timer.path , test-timer.service and filecheck.sh into place, enable and start the watching .path unit, and monitor the service that unit fires with journalctl -f : Terminal 1 $ cp filecheck.sh ~/.local/bin $ chmod +x ~/.local/bin/filecheck.sh $ sudo cp test-timer* /etc/systemd/system/ $ sudo systemctl enable test-timer.path $ sudo systemctl start test-timer.path Terminal 2 $ journalctl -f -u test-timer Now again make our test.file on the Desktop and watch what happens. Delete the file, change its name, move it somewhere else, and watch the service error. Restore the file to where it should be, watch the service resume. Amazing, right?! Stop, disable, and delete the timer-test unit files, and let's continue.