commit ea2b25168ea9021d1869a1e2f67f9569c09cf71b
parent 5564cf055458868e01fd8d950ed571194cfb912b
Author: Daniel Barlow <dan@telent.net>
Date: Mon, 15 Apr 2024 21:15:07 +0100
add uevent-watch, which toggles services based on uevent msgs
Diffstat:
6 files changed, 297 insertions(+), 0 deletions(-)
diff --git a/pkgs/anoia/fs.fnl b/pkgs/anoia/fs.fnl
@@ -33,4 +33,5 @@
: mktree
: rmtree
: directory?
+ :symlink (fn [from to] (lfs.link from to true))
}
diff --git a/pkgs/default.nix b/pkgs/default.nix
@@ -106,6 +106,7 @@ in {
swconfig = callPackage ./swconfig {};
systemconfig = callPackage ./systemconfig {};
tufted = callPackage ./tufted {};
+ uevent-watch = callPackage ./uevent-watch {};
writeAshScript = callPackage ./write-ash-script {};
writeFennel = callPackage ./write-fennel {};
writeFennelScript = callPackage ./write-fennel-script {};
diff --git a/pkgs/uevent-watch/default.nix b/pkgs/uevent-watch/default.nix
@@ -0,0 +1,23 @@
+{
+ lua
+, nellie
+, writeFennelScript
+, runCommand
+, anoia
+, fennel
+, stdenv
+, fennelrepl
+}:
+stdenv.mkDerivation {
+ name = "uevent-watch";
+ src = ./.;
+ nativeBuildInputs = [ fennelrepl ];
+ installPhase = ''
+ mkdir -p $out/bin
+ cp -p ${writeFennelScript "uevent-watch" [fennel anoia nellie lua.pkgs.luafilesystem] ./watch.fnl} $out/bin/uevent-watch
+ '';
+ checkPhase = ''
+ fennelrepl ./test.fnl
+ '';
+ doCheck = true;
+}
diff --git a/pkgs/uevent-watch/events.txt b/pkgs/uevent-watch/events.txt
@@ -0,0 +1,154 @@
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1
+SUBSYSTEM=usb
+MAJOR=189
+MINOR=1
+DEVNAME=bus/usb/001/002
+DEVTYPE=usb_device
+PRODUCT=46f4/1/0
+TYPE=0/0/0
+BUSNUM=001
+DEVNUM=002
+SEQNUM=1513
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0
+SUBSYSTEM=usb
+DEVTYPE=usb_interface
+PRODUCT=46f4/1/0
+TYPE=0/0/0
+INTERFACE=8/6/80
+MODALIAS=usb:v46F4p0001d0000dc00dsc00dp00ic08isc06ip50in00
+SEQNUM=1514
+
+add@/devices/virtual/workqueue/scsi_tmf_0
+ACTION=add
+DEVPATH=/devices/virtual/workqueue/scsi_tmf_0
+SUBSYSTEM=workqueue
+SEQNUM=1515
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0
+SUBSYSTEM=scsi
+DEVTYPE=scsi_host
+SEQNUM=1516
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/scsi_host/host0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/scsi_host/host0
+SUBSYSTEM=scsi_host
+SEQNUM=1517
+
+bind@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0
+ACTION=bind
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0
+SUBSYSTEM=usb
+DEVTYPE=usb_interface
+DRIVER=usb-storage
+PRODUCT=46f4/1/0
+TYPE=0/0/0
+INTERFACE=8/6/80
+MODALIAS=usb:v46F4p0001d0000dc00dsc00dp00ic08isc06ip50in00
+SEQNUM=1518
+
+bind@/devices/pci0000:00/0000:00:13.0/usb1/1-1
+ACTION=bind
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1
+SUBSYSTEM=usb
+MAJOR=189
+MINOR=1
+DEVNAME=bus/usb/001/002
+DEVTYPE=usb_device
+DRIVER=usb
+PRODUCT=46f4/1/0
+TYPE=0/0/0
+BUSNUM=001
+DEVNUM=002
+SEQNUM=1519
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0
+SUBSYSTEM=scsi
+DEVTYPE=scsi_target
+SEQNUM=1520
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+SUBSYSTEM=scsi
+DEVTYPE=scsi_device
+MODALIAS=scsi:t-0x00
+SEQNUM=1521
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/scsi_device/0:0:0:0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/scsi_device/0:0:0:0
+SUBSYSTEM=scsi_device
+SEQNUM=1522
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/scsi_disk/0:0:0:0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/scsi_disk/0:0:0:0
+SUBSYSTEM=scsi_disk
+SEQNUM=1523
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/bsg/0:0:0:0
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/bsg/0:0:0:0
+SUBSYSTEM=bsg
+MAJOR=252
+MINOR=0
+DEVNAME=bsg/0:0:0:0
+SEQNUM=1524
+
+change@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+ACTION=change
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+SUBSYSTEM=scsi
+SDEV_UA=POWER_ON_RESET_OCCURRED
+DEVTYPE=scsi_device
+DRIVER=sd
+MODALIAS=scsi:t-0x00
+SEQNUM=1525
+
+add@/devices/virtual/bdi/8:0
+ACTION=add
+DEVPATH=/devices/virtual/bdi/8:0
+SUBSYSTEM=bdi
+SEQNUM=1526
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda
+SUBSYSTEM=block
+MAJOR=8
+MINOR=0
+DEVNAME=sda
+DEVTYPE=disk
+DISKSEQ=2
+SEQNUM=1527
+
+add@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
+ACTION=add
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1
+SUBSYSTEM=block
+MAJOR=8
+MINOR=1
+DEVNAME=sda1
+DEVTYPE=partition
+DISKSEQ=2
+PARTN=1
+SEQNUM=1528
+
+bind@/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+ACTION=bind
+DEVPATH=/devices/pci0000:00/0000:00:13.0/usb1/1-1/1-1:1.0/host0/target0:0:0/0:0:0:0
+SUBSYSTEM=scsi
+DEVTYPE=scsi_device
+DRIVER=sd
+MODALIAS=scsi:t-0x00
+SEQNUM=1529
diff --git a/pkgs/uevent-watch/test.fnl b/pkgs/uevent-watch/test.fnl
@@ -0,0 +1,48 @@
+(local { : view} (require :fennel))
+
+(set _G.arg (doto [] (tset 0 "test")))
+(local subject (require :watch))
+
+(macro expect= [actual expected]
+ `(let [ve# (view ,expected)
+ va# (view ,actual)]
+ (when (not (= ve# va#))
+ (assert false
+ (.. "\nexpected " ve# "\ngot " va#)
+ ))))
+
+(let [params
+ {:matches {:devname "foo" :partname "my-usbstick"}}]
+ (expect= (subject.event-matches? params {}) false)
+ (expect= (subject.event-matches? params {:devname "bill"}) false)
+ (expect= (subject.event-matches? params {:devname "foo" :partname "my-usbstick"}) true)
+ (expect= (subject.event-matches? params {:devname "foo" :otherthing "bar" :partname "my-usbstick"}) true)
+ )
+
+
+;; Events come from the netlink socket as an initial summary line
+;; followed by a NUL character followed by newline-separated key=value
+;; pairs. For ease of editing we don't have NULs in events.txt,
+;; so we need to massage it into shape here
+
+(local events
+ (with-open [f (io.open "./events.txt" :r)]
+ (let [text (string.gsub (f:read "*a") "\n\n" "\0") ]
+ (icollect [n (string.gmatch text "([^\0]+)")]
+ (string.gsub n "\n" "\0" 1)))))
+
+
+(fn next-event []
+ (var i 0)
+ (fn []
+ (let [i_ (+ 1 i)
+ e (. events i_)]
+ (set i i_)
+ e)))
+
+;; this tests event parsing but not whether anything
+;; happens as a result of processing them
+(subject.run
+ ["-s" "foo" "-n" (os.getenv "TMPDIR") "partname=backup-disk" ]
+ { :read (next-event) }
+ )
diff --git a/pkgs/uevent-watch/watch.fnl b/pkgs/uevent-watch/watch.fnl
@@ -0,0 +1,70 @@
+(local { : assoc : system : dirname } (require :anoia))
+(local { : mktree : rmtree : symlink } (require :anoia.fs))
+
+(fn parse-match [s] (string.match s "(.-)=(.+)"))
+
+(fn parse-args [args]
+ (match args
+ ["-s" service & rest] (assoc (parse-args rest) :service service)
+ ["-n" path & rest] (assoc (parse-args rest) :linkname path)
+ matches { :matches (collect [_ m (ipairs matches)] (parse-match m)) }
+ _ nil))
+
+(fn %% [fmt ...] (string.format fmt ...))
+
+(fn event-matches? [params e]
+ (and
+ e
+ (accumulate [match? true
+ name value (pairs params.matches)]
+ (and match? (= value (. e name))))))
+
+
+(var up :unknown)
+
+(fn start-service [devname linkname service]
+ (match (symlink (.. "/dev/" devname ) linkname)
+ ok (pcall system (%% "s6-rc -b -u change %q" service))
+ (nil err) false))
+
+(fn stop-service [linkname service]
+ (match (pcall system (%% "s6-rc -b -d change %q" linkname service))
+ ok (os.remove linkname)
+ (nil err) false))
+
+(fn toggle-service [devname linkname service wanted?]
+ (when (not (= up wanted?))
+ (set up
+ (if wanted?
+ (start-service devname linkname service)
+ (not (stop-service linkname service))))))
+
+(fn parse-uevent [s]
+ (when s
+ (let [(nl nxt) (string.find s "\0" 1 true)]
+ (collect [k v (string.gmatch
+ (string.sub s (+ 1 nxt))
+ "(%g-)=(%g+)")]
+ (k:lower) v))))
+
+(fn run [args fh]
+ (set up :unknown)
+ (let [parameters
+ (assert (parse-args args) (.. "can't parse args: " (table.concat args " ")))]
+ (mktree (dirname parameters.linkname))
+ (var finished? false)
+
+ (while (not finished?)
+ (let [e (parse-uevent (fh:read 5000))]
+ (when (event-matches? parameters e)
+ (let [wanted? (. {:add true :change true} e.action)]
+ (toggle-service e.devname parameters.linkname parameters.service wanted?)))
+ (set finished? (= e nil))
+ ))))
+
+(when (not (= (. arg 0) "test"))
+ (let [nellie (require :nellie)
+ netlink (nellie.open 4)]
+ (run arg netlink)))
+
+{ : run : event-matches? }