diff --git a/pkgs/anoia/fs.fnl b/pkgs/anoia/fs.fnl index 1e3aa03..8d60571 100644 --- 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 index f910146..5949cfe 100644 --- 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 new file mode 100644 index 0000000..304d258 --- /dev/null +++ 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 new file mode 100644 index 0000000..3da77b6 --- /dev/null +++ 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 new file mode 100644 index 0000000..1c8fb7b --- /dev/null +++ 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 new file mode 100644 index 0000000..b1297ed --- /dev/null +++ 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? }