Hotplug scripts in NixOS#
Thu, 15 Dec 2016 08:46:21 +0000
Prompted partly by the new Met police online traffic incident reporting tool (but it's been something i've been thinking about for a while) I bought an action camera the other day: it's a "Savfy", which is a cheap clone of the SJ4000 (which is itself a cheap clone of a Gopro).
Since I need to plug it into a USB port every day or two to recharge (battery life is a claimed 1.5 hours, haven't tested this yet) I thought it would be good to automate downloading the data off it as well. This is almost my first foray into systemd and udev, and certainly the first time I've tried it in NixOS, so I have reproduced my findings below.
First, we need to recognise when the camera is connected. It's USB
mass storage, which means it shows up as a SCSI disk device
(e.g. sdb
). In /etc/nixos/configuration.nix
we add a stanza something like this:
services.udev = { path = [ "/home/dan/udev/bin" ]; extraRules = '' ACTION=="add", KERNEL=="sd*[0-9]", ENV{ID_SERIAL}=="NOVATEKN_vt-DSC*", RUN+="${pkgs.systemd}/bin/systemctl --no-block start copyCamFiles@%k.service" '' ; };
There are a few things worth noting here.
- I got the string
NOVATEKN_vt-DSC*
by runningudevadm info /dev/sdc1
- The device name (
KERNEL
) is constrained to end in a number. This is because udev generates n+1 events on insertion: one for the disk and one for each partition it contains, and we only need to know about the partitions, we're not going to mount the whole disk
- The
RUN
value doesn't actually run a script directly, it tickles systemctl to make it start a separate service. This is because copying the files may take a while, and (per the udev manual page) "Running an event process for a long period of time may block all further events for this or a dependent device [...] Starting daemons or other long-running processes is not appropriate for udev; the forked processes, detached or not, will be unconditionally killed after the event handling has finished."
- The
path
attribute is probably left over from an earlier non-working version. Chances are I no longer need it
Great. We've got the trigger, where's the service? Again in configuration.nix
systemd.services."copyCamFiles@" = { bindsTo = [ "dev-%i.device"] ; environment = { RSYNC = "${pkgs.rsync}/bin/rsync"; MOUNT = "${pkgs.utillinux}/bin/mount"; UMOUNT = "${pkgs.utillinux}/bin/umount"; }; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.bash}/bin/bash /home/dan/udev/bin/cp-actioncam.sh %I"; }; };
The @-sign in the service name means this isn't actually a service,
it's a template (i.e. we can pass parameters to it to instantiate
services). When it's invoked by udev, the parameter passed will be
the device name of the newly-added partition. We export the
pathnames of some utilities that the script will need, because I
haven't built a nixos derivation for the script itself. simple
as a
service type means (I hope) that it's not started unless asked for
and that nothing is going to try to restart it when it terminates.
Finally, here's the cp-actioncam.sh
script
#!/usr/bin/env bash DEVNAME=$1 MP=/run/tmpmounts/$$/ set -e unmount() { $UMOUNT $MP; rmdir $MP; } trap unmount 0 OUT=/home/dan/Videos/actioncam mkdir -p $MP $OUT $MOUNT /dev/$DEVNAME $MP $RSYNC -a $MP $OUT
And there you have it. There are a bunch of refinements that could profitably be made: most obviously, notifying the user somehow when the copying is finished and the device may be unplugged, and not putting root-owned files into a non-root-users home directory. But this will do for now.
Some browser tabs I can now close:
- http://www.reactivated.net/writing_udev_rules.html#external-run - how to write a udev rule. Note, this is out of date or incompatible with NixOS, which uses
udevadm info
instead ofudevinfo
- https://forums.opensuse.org/showthread.php/485261-Script-run-from-udev-rule-gets-killed-shortly-after-start - how to use a systemd service to do something long-running on insertion without it getting killed
- https://github.com/NixOS/nixos/blob/master/modules/system/boot/systemd-unit-options.nix - all the options for defining a systemd service