v1.29-notify-3: rewrite daemon in Go, fix record size
Two changes from v1.29-notify-2: 1. kmod: fix a latent ring-size compile error. The record struct was 56 bytes with an _pad[6] trailer, which meant NOTIFY_RING_BYTES / sizeof(record) = 1170 events per ring — not a power of two, so the BUILD_BUG_ON in notify_setup would have fired. Bumped _pad to 14 bytes so the record is 64 bytes, capacity is 1024, and every record is cache-line aligned. Wire ABI bumped accordingly; no deployed consumers yet. 2. scoutfs-notifyd: rewritten in Go using the standard library only. Three goroutines (reader / accept / broadcaster) with channel communication replace the single-thread epoll + ioctl loop from the C version. Slow-client handling is done by setting an in-the-past write deadline; the broadcaster drops clients whose kernel send buffer is full. Packaging changes: utils/Makefile now invokes "go build" with CGO_ENABLED=0 and GOPROXY=off; utils/scoutfs-utils.spec.in adds BuildRequires: golang >= 1.21.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# scoutfs-notify
|
||||
|
||||
Observer-only file access notifications for [ScoutFS](https://github.com/versity/scoutfs),
|
||||
distributed as a rebasable `git format-patch` series plus a small userspace
|
||||
relay daemon. Layers onto each upstream release with minimal maintenance.
|
||||
distributed as a rebasable `git format-patch` series plus a small Go
|
||||
userspace relay daemon. Layers onto each upstream release with minimal
|
||||
maintenance.
|
||||
|
||||
## What the series adds
|
||||
|
||||
@@ -18,11 +19,11 @@ Three patches against the scoutfs source tree:
|
||||
`scoutfs_file_aio_read` / `scoutfs_file_read_iter`. Every hook is a
|
||||
single predicted-false branch when no reader is attached. Nothing in
|
||||
the data-waiter state machine is touched.
|
||||
3. **scoutfs-notifyd** — userspace daemon that binds
|
||||
3. **scoutfs-notifyd (Go)** — userspace daemon that binds
|
||||
`/run/scoutfs/<fsid>/notify.sock` (AF_UNIX SOCK_SEQPACKET, mode 0600,
|
||||
root-only), drains the ring, and broadcasts each record to connected
|
||||
clients. Shipped with a systemd template unit
|
||||
`scoutfs-notifyd@<mountpoint>.service`.
|
||||
clients. Pure-stdlib Go; no external module dependencies. Shipped
|
||||
with a systemd template unit `scoutfs-notifyd@<mountpoint>.service`.
|
||||
|
||||
## Base
|
||||
|
||||
@@ -32,6 +33,17 @@ Currently rebased against:
|
||||
|
||||
See [base.txt](./base.txt).
|
||||
|
||||
## Build dependencies added
|
||||
|
||||
On top of the stock scoutfs build requirements, patch 3 adds:
|
||||
|
||||
* `golang >= 1.21` on the build host (RHEL/EL9: `golang`, EL8: `go-toolset`).
|
||||
Debian 12 / Ubuntu 22.04+: `golang-go`.
|
||||
|
||||
The Go build is offline (`GOPROXY=off`) — no network access required at
|
||||
build time. `CGO_ENABLED=0` so the produced binary is a pure-Go static
|
||||
ELF.
|
||||
|
||||
## Applying
|
||||
|
||||
```sh
|
||||
@@ -39,7 +51,7 @@ See [base.txt](./base.txt).
|
||||
```
|
||||
|
||||
Runs `git am --3way` on each patch. For a tarball instead of a git tree,
|
||||
use `patch -p1` in a loop (see script).
|
||||
loop `patch -p1 < patches/*.patch`.
|
||||
|
||||
## Rebasing onto a new upstream release
|
||||
|
||||
@@ -52,17 +64,17 @@ git format-patch v1.30..notify -o patches/
|
||||
# update base.txt and commit
|
||||
```
|
||||
|
||||
## Tag pinning
|
||||
## Tag history
|
||||
|
||||
Each release of this patch set is tagged as `v<upstream>-notify-<rev>`:
|
||||
|
||||
v1.29-notify-1 (retired — included mount options)
|
||||
v1.29-notify-2 (current — daemon-driven, no knobs)
|
||||
v1.29-notify-3 (current — Go daemon, struct alignment fix)
|
||||
v1.29-notify-2 (retired — C daemon; had latent ring-size compile bug)
|
||||
v1.29-notify-1 (retired — shipped mount options)
|
||||
|
||||
## Quick smoke test after installation
|
||||
|
||||
```sh
|
||||
systemctl enable --now scoutfs-notifyd@mnt-scoutfs.service
|
||||
socat - UNIX-CONNECT:/run/scoutfs/$(stat -c %d /mnt/scoutfs)/notify.sock \
|
||||
| xxd | head # watch events stream as you touch files
|
||||
FSID=$(stat -f -c %i /mnt/scoutfs) # or use your scoutfs cli
|
||||
socat - UNIX-CONNECT:/run/scoutfs/${FSID}/notify.sock | xxd | head
|
||||
# touch some files in /mnt/scoutfs in another terminal
|
||||
```
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
From 7564fa7095161816c28c774af97636a8b9ad6f17 Mon Sep 17 00:00:00 2001
|
||||
From c73736df8b8206e66a72a432781034c56765a64a Mon Sep 17 00:00:00 2001
|
||||
From: William Gill <claude@williamgill.net>
|
||||
Date: Wed, 22 Apr 2026 13:46:39 -0500
|
||||
Date: Wed, 22 Apr 2026 14:21:34 -0500
|
||||
Subject: [PATCH 1/3] notify: core file-access notification infrastructure
|
||||
|
||||
Adds a per-mount observer-only notification ring and an ioctl to
|
||||
drain it. The feature has no user-facing configuration: the ring
|
||||
is always allocated at mount and emits are enabled automatically
|
||||
is always allocated at mount, and emits are enabled automatically
|
||||
while a userspace reader is attached.
|
||||
|
||||
What this patch adds:
|
||||
|
||||
- New files kmod/src/notify.{h,c}. A fixed-size 64 KiB ring of
|
||||
64-byte records, a single-reader drain ioctl, and setup/destroy
|
||||
hooks for scoutfs_sb_info.
|
||||
hooks for scoutfs_sb_info. The 64-byte record layout pads the
|
||||
struct so NOTIFY_RING_BYTES / sizeof(record) is a power of two
|
||||
(1024) and each record lands on a cache line.
|
||||
- New ioctl SCOUTFS_IOC_READ_NOTIFY (nr 25). Fills the caller's
|
||||
array of scoutfs_ioctl_notify_event; supports a timeout_ms wait
|
||||
policy. CAP_SYS_ADMIN required. atomic_cmpxchg on
|
||||
@@ -32,7 +34,8 @@ Design properties:
|
||||
- Emit is non-blocking: one leaf spinlock, no allocations, no
|
||||
sleeping locks, drop-on-full with seq still advancing so readers
|
||||
see gaps.
|
||||
- Ring size (64 KiB = 1024 events) is a compile-time constant.
|
||||
- Ring size (64 KiB = 1024 64-byte events) is a compile-time
|
||||
constant.
|
||||
- No mount option, no sysfs toggle. The watcher daemon's
|
||||
attach/detach is the only gate.
|
||||
- Event type values 1..2 are defined for file OPEN and READ.
|
||||
@@ -45,12 +48,12 @@ follow-up patch.
|
||||
kmod/src/Makefile | 1 +
|
||||
kmod/src/counters.h | 3 +
|
||||
kmod/src/ioctl.c | 3 +
|
||||
kmod/src/ioctl.h | 78 +++++++++
|
||||
kmod/src/ioctl.h | 84 ++++++++++
|
||||
kmod/src/notify.c | 373 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
kmod/src/notify.h | 39 +++++
|
||||
kmod/src/super.c | 3 +
|
||||
kmod/src/super.h | 5 +
|
||||
8 files changed, 505 insertions(+)
|
||||
8 files changed, 511 insertions(+)
|
||||
create mode 100644 kmod/src/notify.c
|
||||
create mode 100644 kmod/src/notify.h
|
||||
|
||||
@@ -102,10 +105,10 @@ index 0a5fc4c..b3a4d9a 100644
|
||||
|
||||
return -ENOTTY;
|
||||
diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h
|
||||
index c0d2285..fc58f8c 100644
|
||||
index c0d2285..d468d4c 100644
|
||||
--- a/kmod/src/ioctl.h
|
||||
+++ b/kmod/src/ioctl.h
|
||||
@@ -876,4 +876,82 @@ struct scoutfs_ioctl_punch_offline {
|
||||
@@ -876,4 +876,88 @@ struct scoutfs_ioctl_punch_offline {
|
||||
#define SCOUTFS_IOC_PUNCH_OFFLINE \
|
||||
_IOW(SCOUTFS_IOCTL_MAGIC, 24, struct scoutfs_ioctl_punch_offline)
|
||||
|
||||
@@ -156,7 +159,13 @@ index c0d2285..fc58f8c 100644
|
||||
+ __u32 uid;
|
||||
+ __u8 type;
|
||||
+ __u8 flags;
|
||||
+ __u8 _pad[6];
|
||||
+ /*
|
||||
+ * Pads the record to 64 bytes for cache-line alignment and to
|
||||
+ * make ring capacity a power of two for a 64 KiB ring. Also
|
||||
+ * reserves space for future event-type fields without breaking
|
||||
+ * the wire size.
|
||||
+ */
|
||||
+ __u8 _pad[14];
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
From 1b6569c33f7337c6af16933441241c84874a6f5d Mon Sep 17 00:00:00 2001
|
||||
From 34a879b1f6e77915df8930c173eef9f17887da2f Mon Sep 17 00:00:00 2001
|
||||
From: William Gill <claude@williamgill.net>
|
||||
Date: Wed, 22 Apr 2026 13:47:13 -0500
|
||||
Date: Wed, 22 Apr 2026 14:21:50 -0500
|
||||
Subject: [PATCH 2/3] notify: file open/read hook sites
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
@@ -12,9 +12,9 @@ operations that carry user-visible activity we want to observe.
|
||||
OPEN (data.c):
|
||||
A new scoutfs_file_open() wrapper is installed as ->open in
|
||||
scoutfs_file_fops. The wrapper calls generic_file_open() to
|
||||
preserve the existing VFS default semantics for regular files,
|
||||
then — only on success, and only when notifications are enabled
|
||||
— emits a SCOUTFS_NOTIFY_TYPE_OPEN record. The file mode is
|
||||
preserve existing VFS default semantics for regular files,
|
||||
then — only on success and only when notifications are enabled
|
||||
— emits a SCOUTFS_NOTIFY_TYPE_OPEN record. File mode is
|
||||
inspected for FMODE_WRITE to set SCOUTFS_NOTIFY_F_WRITE_OPEN.
|
||||
|
||||
READ (file.c):
|
||||
@@ -26,14 +26,11 @@ READ (file.c):
|
||||
iocb->ki_pos.
|
||||
|
||||
Every hook is behind unlikely(READ_ONCE(sbi->notify_enabled)), so
|
||||
when no userspace reader is attached the hook is a single
|
||||
when no userspace reader is attached the hook reduces to a single
|
||||
predicted-false branch. No scoutfs state is mutated, no error is
|
||||
propagated, and no existing control flow is altered.
|
||||
|
||||
Nothing in the data-waiter state machine (scoutfs_data_wait_check,
|
||||
scoutfs_data_wait, scoutfs_data_wait_changed, the waiter rbtree,
|
||||
or SCOUTFS_IOC_DATA_WAITING / DATA_WAIT_ERR) is touched. That
|
||||
work is deferred to a future series.
|
||||
Nothing in the data-waiter state machine is touched.
|
||||
---
|
||||
kmod/src/data.c | 28 ++++++++++++++++++++++++++++
|
||||
kmod/src/file.c | 11 +++++++++++
|
||||
|
||||
@@ -0,0 +1,797 @@
|
||||
From 4286c88487c986db0d468be540aca3ad67d32c25 Mon Sep 17 00:00:00 2001
|
||||
From: William Gill <claude@williamgill.net>
|
||||
Date: Wed, 22 Apr 2026 14:31:15 -0500
|
||||
Subject: [PATCH 3/3] notify: scoutfs-notifyd userspace relay daemon (Go)
|
||||
|
||||
Adds the userspace side of the file access notification feature.
|
||||
The kmod provides a ring and a drain ioctl; this daemon binds the
|
||||
well-known per-mount socket, drains the ring, and broadcasts each
|
||||
record to connected clients.
|
||||
|
||||
Written in Go using the standard library only. The go.mod has no
|
||||
external module requirements, so builds work offline and the
|
||||
packaging path sets GOPROXY=off to make that guarantee explicit.
|
||||
|
||||
Socket:
|
||||
|
||||
/run/scoutfs/<fsid>/notify.sock
|
||||
AF_UNIX / SOCK_SEQPACKET ("unixpacket" network in Go)
|
||||
mode 0600, owner root:root
|
||||
|
||||
The fsid is discovered at startup via SCOUTFS_IOC_STATFS_MORE on
|
||||
the given mountpoint, so the daemon never consults its
|
||||
environment or a config file. The shipped systemd unit creates
|
||||
/run/scoutfs via RuntimeDirectory=scoutfs.
|
||||
|
||||
Protocol:
|
||||
|
||||
Each broadcast is one SOCK_SEQPACKET message carrying exactly one
|
||||
64-byte struct scoutfs_ioctl_notify_event record. Clients issue
|
||||
recv(2) in a loop; each returned message is one event.
|
||||
|
||||
Concurrency:
|
||||
|
||||
Three goroutines: a reader blocking on the drain ioctl, an
|
||||
acceptLoop blocking on the listener, and a broadcaster that owns
|
||||
the client set and does all sends. Communication via channels;
|
||||
no shared mutable state across goroutines.
|
||||
|
||||
Slow-client protection: writes are issued with an in-the-past
|
||||
deadline so a full kernel buffer returns a timeout error
|
||||
immediately, dropping the client.
|
||||
|
||||
Reader exclusivity is owned by the kmod ioctl (returns -EBUSY to
|
||||
a second attached reader); starting a second instance against
|
||||
the same mount logs and backs off.
|
||||
|
||||
Graceful shutdown on SIGTERM / SIGINT / SIGQUIT via a cancelled
|
||||
context plus listener close; all goroutines rendezvous via a
|
||||
sync.WaitGroup before the socket inode is unlinked.
|
||||
|
||||
Packaging:
|
||||
|
||||
- utils/Makefile: new target notifyd/scoutfs-notifyd built by
|
||||
"go build" with CGO_ENABLED=0, GOPROXY=off, -trimpath, and
|
||||
-ldflags="-s -w" for a small, reproducible static binary.
|
||||
- utils/scoutfs-utils.spec.in: adds BuildRequires on golang
|
||||
(>= 1.21); installs /usr/sbin/scoutfs-notifyd and
|
||||
/usr/lib/systemd/system/scoutfs-notifyd@.service.
|
||||
- utils/man/scoutfs-notifyd.8 documents the tool, protocol and
|
||||
drop policy.
|
||||
|
||||
Activation:
|
||||
|
||||
systemctl enable --now scoutfs-notifyd@mnt-scoutfs.service
|
||||
|
||||
Watchers then connect to /run/scoutfs/<fsid>/notify.sock and
|
||||
recv() events.
|
||||
---
|
||||
utils/Makefile | 28 +-
|
||||
utils/man/scoutfs-notifyd.8 | 120 +++++++
|
||||
utils/notifyd/go.mod | 3 +
|
||||
utils/notifyd/main.go | 434 +++++++++++++++++++++++++
|
||||
utils/notifyd/scoutfs-notifyd@.service | 40 +++
|
||||
utils/scoutfs-utils.spec.in | 6 +
|
||||
6 files changed, 628 insertions(+), 3 deletions(-)
|
||||
create mode 100644 utils/man/scoutfs-notifyd.8
|
||||
create mode 100644 utils/notifyd/go.mod
|
||||
create mode 100644 utils/notifyd/main.go
|
||||
create mode 100644 utils/notifyd/scoutfs-notifyd@.service
|
||||
|
||||
diff --git a/utils/Makefile b/utils/Makefile
|
||||
index e0f7614..d882e30 100644
|
||||
--- a/utils/Makefile
|
||||
+++ b/utils/Makefile
|
||||
@@ -18,7 +18,13 @@ BIN := src/scoutfs
|
||||
OBJ := $(patsubst %.c,%.o,$(wildcard src/*.c))
|
||||
DEPS := $(wildcard */*.d)
|
||||
|
||||
-all: $(BIN)
|
||||
+# scoutfs-notifyd is a Go program. Go manages its own dependency and
|
||||
+# build graph, so we just treat the binary as a single target with a
|
||||
+# coarse source-dependency on every .go file under notifyd/.
|
||||
+NOTIFYD := notifyd/scoutfs-notifyd
|
||||
+NOTIFYD_SRC := $(wildcard notifyd/*.go) notifyd/go.mod
|
||||
+
|
||||
+all: $(BIN) $(NOTIFYD)
|
||||
|
||||
ifneq ($(DEPS),)
|
||||
-include $(DEPS)
|
||||
@@ -29,13 +35,28 @@ QU = @echo
|
||||
VE = @
|
||||
else
|
||||
QU = @:
|
||||
-VE =
|
||||
+VE =
|
||||
endif
|
||||
|
||||
$(BIN): $(OBJ)
|
||||
$(QU) [BIN $@]
|
||||
$(VE)gcc -o $@ $^ -luuid -lm -lcrypto -lblkid
|
||||
|
||||
+# Build the Go daemon. No external module dependencies, so the build
|
||||
+# works offline (as in rpmbuild). We force GOPROXY=off to make that
|
||||
+# guarantee explicit; GOCACHE is redirected into the source dir so a
|
||||
+# read-only HOME doesn't break anything.
|
||||
+$(NOTIFYD): $(NOTIFYD_SRC)
|
||||
+ $(QU) [GO $@]
|
||||
+ $(VE)cd notifyd && \
|
||||
+ GOCACHE=$${GOCACHE:-$$(pwd)/.gocache} \
|
||||
+ GOFLAGS=-mod=mod \
|
||||
+ GOPROXY=off \
|
||||
+ CGO_ENABLED=0 \
|
||||
+ go build -trimpath -buildvcs=false \
|
||||
+ -ldflags "-s -w" \
|
||||
+ -o scoutfs-notifyd .
|
||||
+
|
||||
%.o %.d: %.c Makefile sparse.sh
|
||||
$(QU) [CC $<]
|
||||
$(VE)gcc $(CFLAGS) -MD -MP -MF $*.d -c $< -o $*.o
|
||||
@@ -65,4 +86,5 @@ dist: $(RPM_DIR) scoutfs-utils.spec
|
||||
tar rf $(TARFILE) --transform="s@.*\(src/.*\)@scoutfs-utils-$(RPM_VERSION)/\1@" $(FMTIOC_KMOD)
|
||||
|
||||
clean:
|
||||
- @rm -f $(BIN) $(OBJ) $(DEPS) .sparse.*
|
||||
+ @rm -f $(BIN) $(OBJ) $(NOTIFYD) $(DEPS) .sparse.*
|
||||
+ @rm -rf notifyd/.gocache
|
||||
diff --git a/utils/man/scoutfs-notifyd.8 b/utils/man/scoutfs-notifyd.8
|
||||
new file mode 100644
|
||||
index 0000000..a0dcd46
|
||||
--- /dev/null
|
||||
+++ b/utils/man/scoutfs-notifyd.8
|
||||
@@ -0,0 +1,120 @@
|
||||
+.TH scoutfs-notifyd 8
|
||||
+.SH NAME
|
||||
+scoutfs-notifyd \- scoutfs file access notification relay daemon
|
||||
+
|
||||
+.SH SYNOPSIS
|
||||
+.B scoutfs-notifyd
|
||||
+.I mountpoint
|
||||
+
|
||||
+.SH DESCRIPTION
|
||||
+The
|
||||
+.B scoutfs-notifyd
|
||||
+daemon drains the file access notification ring of a scoutfs mount
|
||||
+and broadcasts each event to every process connected to
|
||||
+.I /run/scoutfs/<fsid>/notify.sock.
|
||||
+
|
||||
+Events are observer-only records of file
|
||||
+.B open
|
||||
+and
|
||||
+.B read
|
||||
+activity, produced by the scoutfs kernel module for the mount
|
||||
+identified by
|
||||
+.IR mountpoint .
|
||||
+The daemon is the single privileged reader of the ring and is expected
|
||||
+to be started by the systemd template unit
|
||||
+.I scoutfs-notifyd@.service
|
||||
+with the instance name set to the escaped mountpoint path.
|
||||
+
|
||||
+.SH OPTIONS
|
||||
+
|
||||
+.TP
|
||||
+.I mountpoint
|
||||
+An absolute path to a directory on the scoutfs volume to monitor.
|
||||
+Typically this is the mountpoint itself. The daemon opens the path,
|
||||
+reads the volume's fsid via the
|
||||
+.B SCOUTFS_IOC_STATFS_MORE
|
||||
+ioctl, and builds the listener socket path from that fsid.
|
||||
+
|
||||
+.SH SOCKET
|
||||
+
|
||||
+The listener is an
|
||||
+.B AF_UNIX
|
||||
+.B SOCK_SEQPACKET
|
||||
+socket at
|
||||
+.I /run/scoutfs/<fsid>/notify.sock
|
||||
+with mode
|
||||
+.B 0600
|
||||
+and owner
|
||||
+.BR root:root .
|
||||
+
|
||||
+Each broadcast is one atomic message carrying a single packed 64-byte
|
||||
+record matching
|
||||
+.BR "struct scoutfs_ioctl_notify_event" .
|
||||
+Clients read exactly one record per
|
||||
+.BR recv (2)
|
||||
+call. The fields are:
|
||||
+
|
||||
+.nf
|
||||
+.RS 4
|
||||
+__u64 seq; /* monotonic; gap = dropped events */
|
||||
+__u64 ino; /* scoutfs inode number */
|
||||
+__u64 offset; /* byte offset of the read, 0 for OPEN */
|
||||
+__u64 length; /* byte length of the read, 0 for OPEN */
|
||||
+__u64 time_ns; /* CLOCK_REALTIME at the event */
|
||||
+__u32 pid; /* task group id */
|
||||
+__u32 uid; /* effective uid (init user namespace) */
|
||||
+__u8 type; /* 1 = OPEN, 2 = READ */
|
||||
+__u8 flags; /* bit 0 = opened writable */
|
||||
+__u8 _pad[6];
|
||||
+.RE
|
||||
+.fi
|
||||
+
|
||||
+Numeric values 3 and higher in
|
||||
+.B type
|
||||
+are reserved for future scoutfs events (data-waiter observation).
|
||||
+
|
||||
+.SH DROP POLICY
|
||||
+
|
||||
+The in-kernel ring is lossy. When it fills, records are discarded but
|
||||
+the monotonic
|
||||
+.B seq
|
||||
+field still advances, so consumers that track seq will detect gaps
|
||||
+rather than silent loss.
|
||||
+
|
||||
+The daemon forwards to clients with
|
||||
+.B MSG_DONTWAIT
|
||||
+and
|
||||
+.BR MSG_NOSIGNAL .
|
||||
+A client that cannot keep up is disconnected immediately; it is
|
||||
+expected to reconnect.
|
||||
+
|
||||
+.SH PERMISSIONS
|
||||
+
|
||||
+The daemon requires
|
||||
+.B CAP_SYS_ADMIN
|
||||
+to call
|
||||
+.BR SCOUTFS_IOC_READ_NOTIFY .
|
||||
+The shipped systemd unit grants that capability and strips everything
|
||||
+else. Only one reader may be attached to a scoutfs mount's ring at a
|
||||
+time; starting a second daemon against the same mount returns
|
||||
+.BR EBUSY .
|
||||
+
|
||||
+.SH EXIT STATUS
|
||||
+
|
||||
+The daemon exits 0 on clean shutdown (SIGTERM, SIGINT, SIGQUIT) and
|
||||
+non-zero on error.
|
||||
+
|
||||
+.SH FILES
|
||||
+.TP
|
||||
+.I /run/scoutfs/<fsid>/notify.sock
|
||||
+Listener socket, per scoutfs fsid.
|
||||
+
|
||||
+.TP
|
||||
+.I /usr/lib/systemd/system/scoutfs-notifyd@.service
|
||||
+Template unit. Enable and start a specific mount with, e.g.,
|
||||
+.BR "systemctl enable --now scoutfs-notifyd@mnt-scoutfs.service" .
|
||||
+
|
||||
+.SH SEE ALSO
|
||||
+.BR scoutfs (5),
|
||||
+.BR scoutfs (8),
|
||||
+.BR systemd.unit (5)
|
||||
diff --git a/utils/notifyd/go.mod b/utils/notifyd/go.mod
|
||||
new file mode 100644
|
||||
index 0000000..2d5fd0e
|
||||
--- /dev/null
|
||||
+++ b/utils/notifyd/go.mod
|
||||
@@ -0,0 +1,3 @@
|
||||
+module scoutfs.org/notifyd
|
||||
+
|
||||
+go 1.21
|
||||
diff --git a/utils/notifyd/main.go b/utils/notifyd/main.go
|
||||
new file mode 100644
|
||||
index 0000000..9478bae
|
||||
--- /dev/null
|
||||
+++ b/utils/notifyd/main.go
|
||||
@@ -0,0 +1,434 @@
|
||||
+// Copyright (C) 2026 Versity Software, Inc. All rights reserved.
|
||||
+//
|
||||
+// This program is free software; you can redistribute it and/or
|
||||
+// modify it under the terms of the GNU General Public
|
||||
+// License v2 as published by the Free Software Foundation.
|
||||
+
|
||||
+// scoutfs-notifyd drains the scoutfs file access notification ring for
|
||||
+// one mount and broadcasts each record to clients connected to
|
||||
+// /run/scoutfs/<fsid>/notify.sock.
|
||||
+//
|
||||
+// Usage: scoutfs-notifyd <mountpoint>
|
||||
+//
|
||||
+// Design:
|
||||
+//
|
||||
+// - Three goroutines: reader (blocks on the drain ioctl), accept
|
||||
+// (blocks on listener), broadcaster (owns the client set and does
|
||||
+// all sends). Communication via unbuffered / small-buffered
|
||||
+// channels; no shared mutable state across goroutines.
|
||||
+//
|
||||
+// - Socket type is AF_UNIX / SOCK_SEQPACKET ("unixpacket" network in
|
||||
+// Go's net package). Each Write is one atomic 64-byte message.
|
||||
+//
|
||||
+// - Slow-client protection: writes are done with an in-the-past
|
||||
+// deadline so a full kernel send buffer returns a timeout error
|
||||
+// immediately; the offending client is closed and dropped.
|
||||
+//
|
||||
+// - Requires CAP_SYS_ADMIN for SCOUTFS_IOC_READ_NOTIFY. The socket
|
||||
+// file is chmod'd to 0600.
|
||||
+//
|
||||
+// - Clean shutdown on SIGTERM / SIGINT / SIGQUIT via a cancelled
|
||||
+// context plus listener close.
|
||||
+package main
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "errors"
|
||||
+ "fmt"
|
||||
+ "log"
|
||||
+ "net"
|
||||
+ "os"
|
||||
+ "os/signal"
|
||||
+ "path/filepath"
|
||||
+ "sync"
|
||||
+ "syscall"
|
||||
+ "time"
|
||||
+ "unsafe"
|
||||
+)
|
||||
+
|
||||
+// ---------------------------------------------------------------------------
|
||||
+// ABI — matches kmod/src/ioctl.h
|
||||
+// ---------------------------------------------------------------------------
|
||||
+
|
||||
+// Event mirrors struct scoutfs_ioctl_notify_event. The in-memory
|
||||
+// layout must match the kernel record exactly so the daemon can memcpy
|
||||
+// events from the drain buffer directly onto the wire.
|
||||
+type Event struct {
|
||||
+ Seq uint64
|
||||
+ Ino uint64
|
||||
+ Offset uint64
|
||||
+ Length uint64
|
||||
+ TimeNs uint64
|
||||
+ Pid uint32
|
||||
+ Uid uint32
|
||||
+ Type uint8
|
||||
+ Flags uint8
|
||||
+ _pad [14]byte
|
||||
+}
|
||||
+
|
||||
+const eventSize = 64
|
||||
+
|
||||
+// ReadNotifyArgs mirrors struct scoutfs_ioctl_read_notify.
|
||||
+type ReadNotifyArgs struct {
|
||||
+ EventsPtr uint64
|
||||
+ EventsNr uint32
|
||||
+ TimeoutMs uint32
|
||||
+ Flags uint64
|
||||
+}
|
||||
+
|
||||
+// StatfsMore mirrors struct scoutfs_ioctl_statfs_more.
|
||||
+type StatfsMore struct {
|
||||
+ Fsid uint64
|
||||
+ Rid uint64
|
||||
+ CommittedSeq uint64
|
||||
+ TotalMetaBlocks uint64
|
||||
+ TotalDataBlocks uint64
|
||||
+ ReservedMetaBlocks uint64
|
||||
+}
|
||||
+
|
||||
+// Linux _IOC encoding. The values below are the generic ABI used on
|
||||
+// x86, ARM, ARM64, RISC-V, and all architectures scoutfs currently
|
||||
+// targets. PowerPC/Alpha/MIPS/SPARC would need different constants.
|
||||
+const (
|
||||
+ iocNrShift = 0
|
||||
+ iocTypeShift = 8
|
||||
+ iocSizeShift = 16
|
||||
+ iocDirShift = 30
|
||||
+
|
||||
+ iocDirRead = 2
|
||||
+ iocDirWrite = 1
|
||||
+
|
||||
+ scoutfsIoctlMagic = 0xE8
|
||||
+
|
||||
+ // sizeof(struct scoutfs_ioctl_statfs_more)
|
||||
+ statfsMoreSize = 48
|
||||
+ // sizeof(struct scoutfs_ioctl_read_notify)
|
||||
+ readNotifySize = 24
|
||||
+)
|
||||
+
|
||||
+func iocEncode(dir, typ, nr, size uintptr) uintptr {
|
||||
+ return (dir << iocDirShift) |
|
||||
+ (size << iocSizeShift) |
|
||||
+ (typ << iocTypeShift) |
|
||||
+ (nr << iocNrShift)
|
||||
+}
|
||||
+
|
||||
+var (
|
||||
+ scoutfsIocStatfsMore = iocEncode(iocDirRead,
|
||||
+ scoutfsIoctlMagic, 10, statfsMoreSize)
|
||||
+ scoutfsIocReadNotify = iocEncode(iocDirRead|iocDirWrite,
|
||||
+ scoutfsIoctlMagic, 25, readNotifySize)
|
||||
+)
|
||||
+
|
||||
+func init() {
|
||||
+ // Hard fail at startup if the Go struct layout doesn't match the
|
||||
+ // ABI. Cheap insurance against a compiler or platform that pads
|
||||
+ // differently from what we expect.
|
||||
+ if sz := unsafe.Sizeof(Event{}); sz != eventSize {
|
||||
+ panic(fmt.Sprintf("Event struct is %d bytes, expected %d",
|
||||
+ sz, eventSize))
|
||||
+ }
|
||||
+ if sz := unsafe.Sizeof(ReadNotifyArgs{}); sz != readNotifySize {
|
||||
+ panic(fmt.Sprintf("ReadNotifyArgs is %d bytes, expected %d",
|
||||
+ sz, readNotifySize))
|
||||
+ }
|
||||
+ if sz := unsafe.Sizeof(StatfsMore{}); sz != statfsMoreSize {
|
||||
+ panic(fmt.Sprintf("StatfsMore is %d bytes, expected %d",
|
||||
+ sz, statfsMoreSize))
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// ---------------------------------------------------------------------------
|
||||
+// Syscall wrappers
|
||||
+// ---------------------------------------------------------------------------
|
||||
+
|
||||
+// getFsid opens the mountpoint and resolves the volume's fsid.
|
||||
+// Returns (fsid, mountFd, nil) on success. The caller owns mountFd.
|
||||
+func getFsid(mountpoint string) (uint64, int, error) {
|
||||
+ fd, err := syscall.Open(mountpoint,
|
||||
+ syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0)
|
||||
+ if err != nil {
|
||||
+ return 0, 0, fmt.Errorf("open %s: %w", mountpoint, err)
|
||||
+ }
|
||||
+ var sm StatfsMore
|
||||
+ _, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
|
||||
+ uintptr(fd),
|
||||
+ scoutfsIocStatfsMore,
|
||||
+ uintptr(unsafe.Pointer(&sm)))
|
||||
+ if errno != 0 {
|
||||
+ syscall.Close(fd)
|
||||
+ return 0, 0, fmt.Errorf(
|
||||
+ "SCOUTFS_IOC_STATFS_MORE on %s: %w "+
|
||||
+ "(is this a scoutfs mount?)", mountpoint, errno)
|
||||
+ }
|
||||
+ return sm.Fsid, fd, nil
|
||||
+}
|
||||
+
|
||||
+// drainNotify blocks for up to timeoutMs and fills buf with events.
|
||||
+// Returns the number of events actually filled. errno ETIMEDOUT is
|
||||
+// expected every timeout window with no activity.
|
||||
+func drainNotify(fd int, buf []Event, timeoutMs uint32) (int, error) {
|
||||
+ if len(buf) == 0 {
|
||||
+ return 0, errors.New("empty batch")
|
||||
+ }
|
||||
+ args := ReadNotifyArgs{
|
||||
+ EventsPtr: uint64(uintptr(unsafe.Pointer(&buf[0]))),
|
||||
+ EventsNr: uint32(len(buf)),
|
||||
+ TimeoutMs: timeoutMs,
|
||||
+ }
|
||||
+ ret, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
|
||||
+ uintptr(fd),
|
||||
+ scoutfsIocReadNotify,
|
||||
+ uintptr(unsafe.Pointer(&args)))
|
||||
+ if errno != 0 {
|
||||
+ return 0, errno
|
||||
+ }
|
||||
+ return int(ret), nil
|
||||
+}
|
||||
+
|
||||
+// eventBytes returns a byte slice aliasing ev's memory. Safe to pass
|
||||
+// to Write; the returned slice must not outlive ev.
|
||||
+func eventBytes(ev *Event) []byte {
|
||||
+ return (*[eventSize]byte)(unsafe.Pointer(ev))[:]
|
||||
+}
|
||||
+
|
||||
+// ---------------------------------------------------------------------------
|
||||
+// Socket setup
|
||||
+// ---------------------------------------------------------------------------
|
||||
+
|
||||
+const (
|
||||
+ ioctlBatchEvents = 64
|
||||
+ ioctlTimeoutMs = 100
|
||||
+ maxClients = 128
|
||||
+ runDirMode = 0o755
|
||||
+ sockMode = 0o600
|
||||
+)
|
||||
+
|
||||
+// ensureRunDir creates /run/scoutfs and /run/scoutfs/<fsid>/.
|
||||
+func ensureRunDir(dir string) error {
|
||||
+ if err := os.MkdirAll(dir, runDirMode); err != nil {
|
||||
+ return fmt.Errorf("mkdir %s: %w", dir, err)
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+// createListener binds the AF_UNIX SOCK_SEQPACKET listener at path and
|
||||
+// restricts it to root via chmod 0600.
|
||||
+func createListener(path string) (*net.UnixListener, error) {
|
||||
+ // Remove stale socket from a prior crashed instance.
|
||||
+ _ = os.Remove(path)
|
||||
+
|
||||
+ addr := &net.UnixAddr{Net: "unixpacket", Name: path}
|
||||
+ l, err := net.ListenUnix("unixpacket", addr)
|
||||
+ if err != nil {
|
||||
+ return nil, fmt.Errorf("ListenUnix %s: %w", path, err)
|
||||
+ }
|
||||
+
|
||||
+ if err := os.Chmod(path, sockMode); err != nil {
|
||||
+ l.Close()
|
||||
+ os.Remove(path)
|
||||
+ return nil, fmt.Errorf("chmod %s: %w", path, err)
|
||||
+ }
|
||||
+
|
||||
+ return l, nil
|
||||
+}
|
||||
+
|
||||
+// ---------------------------------------------------------------------------
|
||||
+// Goroutines
|
||||
+// ---------------------------------------------------------------------------
|
||||
+
|
||||
+// reader blocks on the drain ioctl and feeds events into evCh. It
|
||||
+// exits when ctx is cancelled, when the ioctl returns a fatal error,
|
||||
+// or when the mount goes away. evCh is closed on exit so the
|
||||
+// broadcaster can unblock.
|
||||
+func reader(ctx context.Context, fd int, evCh chan<- Event) {
|
||||
+ defer close(evCh)
|
||||
+ batch := make([]Event, ioctlBatchEvents)
|
||||
+
|
||||
+ for {
|
||||
+ if err := ctx.Err(); err != nil {
|
||||
+ return
|
||||
+ }
|
||||
+ n, err := drainNotify(fd, batch, ioctlTimeoutMs)
|
||||
+ if err != nil {
|
||||
+ switch {
|
||||
+ case errors.Is(err, syscall.ETIMEDOUT),
|
||||
+ errors.Is(err, syscall.EINTR):
|
||||
+ continue
|
||||
+ case errors.Is(err, syscall.EBUSY):
|
||||
+ log.Printf("another reader is attached; " +
|
||||
+ "backing off 1s")
|
||||
+ select {
|
||||
+ case <-ctx.Done():
|
||||
+ return
|
||||
+ case <-time.After(time.Second):
|
||||
+ }
|
||||
+ continue
|
||||
+ default:
|
||||
+ log.Printf("SCOUTFS_IOC_READ_NOTIFY: %v", err)
|
||||
+ return
|
||||
+ }
|
||||
+ }
|
||||
+ for i := 0; i < n; i++ {
|
||||
+ select {
|
||||
+ case <-ctx.Done():
|
||||
+ return
|
||||
+ case evCh <- batch[i]:
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// acceptLoop hands each accepted connection off on acCh and exits when
|
||||
+// the listener is closed. acCh is closed on exit.
|
||||
+func acceptLoop(l *net.UnixListener, acCh chan<- *net.UnixConn) {
|
||||
+ defer close(acCh)
|
||||
+ for {
|
||||
+ c, err := l.AcceptUnix()
|
||||
+ if err != nil {
|
||||
+ if !errors.Is(err, net.ErrClosed) {
|
||||
+ log.Printf("accept: %v", err)
|
||||
+ }
|
||||
+ return
|
||||
+ }
|
||||
+ acCh <- c
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// broadcaster owns the client set. It consumes events from evCh and
|
||||
+// new connections from acCh; it exits when ctx is cancelled or both
|
||||
+// input channels are closed. On exit it closes every remaining
|
||||
+// client connection.
|
||||
+func broadcaster(ctx context.Context, evCh <-chan Event, acCh <-chan *net.UnixConn) {
|
||||
+ clients := make(map[*net.UnixConn]struct{})
|
||||
+ defer func() {
|
||||
+ for c := range clients {
|
||||
+ c.Close()
|
||||
+ }
|
||||
+ }()
|
||||
+
|
||||
+ for {
|
||||
+ select {
|
||||
+ case <-ctx.Done():
|
||||
+ return
|
||||
+
|
||||
+ case c, ok := <-acCh:
|
||||
+ if !ok {
|
||||
+ acCh = nil
|
||||
+ if evCh == nil {
|
||||
+ return
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ if len(clients) >= maxClients {
|
||||
+ log.Printf("client limit (%d) reached, "+
|
||||
+ "rejecting", maxClients)
|
||||
+ c.Close()
|
||||
+ continue
|
||||
+ }
|
||||
+ clients[c] = struct{}{}
|
||||
+
|
||||
+ case ev, ok := <-evCh:
|
||||
+ if !ok {
|
||||
+ evCh = nil
|
||||
+ if acCh == nil {
|
||||
+ return
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ buf := eventBytes(&ev)
|
||||
+ for c := range clients {
|
||||
+ // Make Write non-blocking: a deadline in the
|
||||
+ // past causes a full kernel buffer to return
|
||||
+ // a timeout error immediately.
|
||||
+ _ = c.SetWriteDeadline(time.Now())
|
||||
+ if _, err := c.Write(buf); err != nil {
|
||||
+ delete(clients, c)
|
||||
+ c.Close()
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// ---------------------------------------------------------------------------
|
||||
+// Main
|
||||
+// ---------------------------------------------------------------------------
|
||||
+
|
||||
+func usage(out *os.File) {
|
||||
+ fmt.Fprintf(out,
|
||||
+ "usage: %s <mountpoint>\n\n"+
|
||||
+ " Drains the scoutfs file access notification ring "+
|
||||
+ "for the given\n"+
|
||||
+ " mount and broadcasts events to clients "+
|
||||
+ "connected to\n"+
|
||||
+ " /run/scoutfs/<fsid>/notify.sock.\n\n"+
|
||||
+ " Requires CAP_SYS_ADMIN. Socket is root-only "+
|
||||
+ "(mode 0600).\n",
|
||||
+ filepath.Base(os.Args[0]))
|
||||
+}
|
||||
+
|
||||
+func main() {
|
||||
+ log.SetFlags(log.LstdFlags)
|
||||
+ log.SetPrefix("scoutfs-notifyd: ")
|
||||
+
|
||||
+ if len(os.Args) != 2 ||
|
||||
+ os.Args[1] == "" ||
|
||||
+ os.Args[1] == "-h" ||
|
||||
+ os.Args[1] == "--help" {
|
||||
+ usage(os.Stderr)
|
||||
+ os.Exit(2)
|
||||
+ }
|
||||
+ mountpoint := os.Args[1]
|
||||
+
|
||||
+ fsid, mntFd, err := getFsid(mountpoint)
|
||||
+ if err != nil {
|
||||
+ log.Fatalf("%v", err)
|
||||
+ }
|
||||
+ defer syscall.Close(mntFd)
|
||||
+
|
||||
+ runDir := fmt.Sprintf("/run/scoutfs/%d", fsid)
|
||||
+ sockPath := filepath.Join(runDir, "notify.sock")
|
||||
+
|
||||
+ if err := ensureRunDir(runDir); err != nil {
|
||||
+ log.Fatalf("%v", err)
|
||||
+ }
|
||||
+
|
||||
+ listener, err := createListener(sockPath)
|
||||
+ if err != nil {
|
||||
+ log.Fatalf("%v", err)
|
||||
+ }
|
||||
+
|
||||
+ log.Printf("listening on %s", sockPath)
|
||||
+
|
||||
+ ctx, cancel := context.WithCancel(context.Background())
|
||||
+ defer cancel()
|
||||
+
|
||||
+ sigCh := make(chan os.Signal, 1)
|
||||
+ signal.Notify(sigCh,
|
||||
+ syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
|
||||
+
|
||||
+ evCh := make(chan Event, 512)
|
||||
+ acCh := make(chan *net.UnixConn, 16)
|
||||
+
|
||||
+ var wg sync.WaitGroup
|
||||
+ wg.Add(3)
|
||||
+ go func() { defer wg.Done(); reader(ctx, mntFd, evCh) }()
|
||||
+ go func() { defer wg.Done(); acceptLoop(listener, acCh) }()
|
||||
+ go func() { defer wg.Done(); broadcaster(ctx, evCh, acCh) }()
|
||||
+
|
||||
+ sig := <-sigCh
|
||||
+ log.Printf("caught %v, shutting down", sig)
|
||||
+
|
||||
+ // Cancel the context (stops reader + broadcaster at next check),
|
||||
+ // close the listener (unblocks acceptLoop), then wait.
|
||||
+ cancel()
|
||||
+ listener.Close()
|
||||
+ wg.Wait()
|
||||
+
|
||||
+ // Remove the socket inode we created. systemd's RuntimeDirectory
|
||||
+ // cleans the enclosing dir on service stop.
|
||||
+ _ = os.Remove(sockPath)
|
||||
+
|
||||
+ log.Printf("exited cleanly")
|
||||
+}
|
||||
diff --git a/utils/notifyd/scoutfs-notifyd@.service b/utils/notifyd/scoutfs-notifyd@.service
|
||||
new file mode 100644
|
||||
index 0000000..2e87f17
|
||||
--- /dev/null
|
||||
+++ b/utils/notifyd/scoutfs-notifyd@.service
|
||||
@@ -0,0 +1,40 @@
|
||||
+[Unit]
|
||||
+Description=ScoutFS file access notification daemon for %I
|
||||
+Documentation=man:scoutfs-notifyd(8)
|
||||
+# The mount must exist before we can open it and read its fsid.
|
||||
+RequiresMountsFor=/%I
|
||||
+
|
||||
+[Service]
|
||||
+Type=simple
|
||||
+User=root
|
||||
+Group=root
|
||||
+UMask=0077
|
||||
+# scoutfs-notifyd creates /run/scoutfs/<fsid>/ and binds the socket
|
||||
+# inside it; the top-level /run/scoutfs is created here so the daemon
|
||||
+# never has to.
|
||||
+RuntimeDirectory=scoutfs
|
||||
+RuntimeDirectoryMode=0755
|
||||
+ExecStart=/usr/sbin/scoutfs-notifyd /%I
|
||||
+Restart=on-failure
|
||||
+RestartSec=5s
|
||||
+StartLimitBurst=5
|
||||
+
|
||||
+# The daemon needs CAP_SYS_ADMIN to drive the read-notify ioctl.
|
||||
+# Everything else can be stripped.
|
||||
+CapabilityBoundingSet=CAP_SYS_ADMIN
|
||||
+AmbientCapabilities=CAP_SYS_ADMIN
|
||||
+NoNewPrivileges=yes
|
||||
+PrivateTmp=yes
|
||||
+ProtectSystem=strict
|
||||
+ProtectHome=yes
|
||||
+ProtectKernelTunables=yes
|
||||
+ProtectKernelModules=yes
|
||||
+ProtectControlGroups=yes
|
||||
+RestrictAddressFamilies=AF_UNIX
|
||||
+LockPersonality=yes
|
||||
+MemoryDenyWriteExecute=yes
|
||||
+RestrictRealtime=yes
|
||||
+RestrictSUIDSGID=yes
|
||||
+
|
||||
+[Install]
|
||||
+WantedBy=multi-user.target
|
||||
diff --git a/utils/scoutfs-utils.spec.in b/utils/scoutfs-utils.spec.in
|
||||
index fb24b81..2c8a5d8 100644
|
||||
--- a/utils/scoutfs-utils.spec.in
|
||||
+++ b/utils/scoutfs-utils.spec.in
|
||||
@@ -17,6 +17,8 @@ BuildRequires: gzip
|
||||
BuildRequires: libuuid-devel
|
||||
BuildRequires: openssl-devel
|
||||
BuildRequires: libblkid-devel
|
||||
+# scoutfs-notifyd is a Go program (pure stdlib, no external modules).
|
||||
+BuildRequires: golang >= 1.21
|
||||
|
||||
#Requires: kmod-scoutfs = %{version}
|
||||
|
||||
@@ -52,19 +54,23 @@ cp man/*.5.gz $RPM_BUILD_ROOT%{_mandir}/man5/.
|
||||
cp man/*.7.gz $RPM_BUILD_ROOT%{_mandir}/man7/.
|
||||
cp man/*.8.gz $RPM_BUILD_ROOT%{_mandir}/man8/.
|
||||
install -m 755 -D src/scoutfs $RPM_BUILD_ROOT%{_sbindir}/scoutfs
|
||||
+install -m 755 -D notifyd/scoutfs-notifyd $RPM_BUILD_ROOT%{_sbindir}/scoutfs-notifyd
|
||||
install -m 644 -D src/ioctl.h $RPM_BUILD_ROOT%{_includedir}/scoutfs/ioctl.h
|
||||
install -m 644 -D src/format.h $RPM_BUILD_ROOT%{_includedir}/scoutfs/format.h
|
||||
install -m 755 -D fenced/scoutfs-fenced $RPM_BUILD_ROOT%{_libexecdir}/scoutfs-fenced/scoutfs-fenced
|
||||
install -m 644 -D fenced/scoutfs-fenced.service $RPM_BUILD_ROOT%{_unitdir}/scoutfs-fenced.service
|
||||
install -m 644 -D fenced/scoutfs-fenced.conf.example $RPM_BUILD_ROOT%{_sysconfdir}/scoutfs/scoutfs-fenced.conf.example
|
||||
+install -m 644 -D notifyd/scoutfs-notifyd@.service $RPM_BUILD_ROOT%{_unitdir}/scoutfs-notifyd@.service
|
||||
|
||||
%files
|
||||
%defattr(644,root,root,755)
|
||||
%{_mandir}/man*/scoutfs*.gz
|
||||
/%{_unitdir}/scoutfs-fenced.service
|
||||
+/%{_unitdir}/scoutfs-notifyd@.service
|
||||
%{_sysconfdir}/scoutfs
|
||||
%defattr(755,root,root,755)
|
||||
%{_sbindir}/scoutfs
|
||||
+%{_sbindir}/scoutfs-notifyd
|
||||
%{_libexecdir}/scoutfs-fenced
|
||||
|
||||
%files -n scoutfs-devel
|
||||
--
|
||||
2.49.0.windows.1
|
||||
|
||||
@@ -1,828 +0,0 @@
|
||||
From 4a2b2f062c4d733444f1fa40257725625d9bebe3 Mon Sep 17 00:00:00 2001
|
||||
From: William Gill <claude@williamgill.net>
|
||||
Date: Wed, 22 Apr 2026 13:47:45 -0500
|
||||
Subject: [PATCH 3/3] notify: scoutfs-notifyd userspace relay daemon
|
||||
|
||||
Adds the userspace side of the file access notification feature.
|
||||
The kmod provides a ring and a drain ioctl; this daemon binds the
|
||||
well-known socket, drains the ring, and broadcasts each record to
|
||||
connected clients.
|
||||
|
||||
Socket:
|
||||
|
||||
/run/scoutfs/<fsid>/notify.sock
|
||||
AF_UNIX / SOCK_SEQPACKET
|
||||
mode 0600, owner root:root
|
||||
|
||||
The fsid is discovered at startup via SCOUTFS_IOC_STATFS_MORE on
|
||||
the given mountpoint, so the daemon never consults its
|
||||
environment or a config file for the path. The shipped systemd
|
||||
unit creates /run/scoutfs/ via RuntimeDirectory=scoutfs.
|
||||
|
||||
Protocol:
|
||||
|
||||
Each broadcast is one SOCK_SEQPACKET message carrying exactly one
|
||||
64-byte struct scoutfs_ioctl_notify_event record. Clients issue
|
||||
recv(2) in a loop; each returned message is one event.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Single-threaded with epoll. The drain ioctl uses a 100 ms
|
||||
timeout so the loop paces itself; new clients, client
|
||||
disconnects, and shutdown signals are handled between ioctl
|
||||
calls non-blocking.
|
||||
- Slow-client protection: sends use MSG_DONTWAIT | MSG_NOSIGNAL.
|
||||
Any send error drops the client immediately.
|
||||
- MAX_CLIENTS is 128; extras are rejected with a log warning.
|
||||
- Reader exclusivity is owned by the kmod ioctl (returns -EBUSY
|
||||
to a second attached reader); starting a second instance of
|
||||
the daemon against the same mount logs the error and exits.
|
||||
- Graceful shutdown on SIGTERM / SIGINT / SIGQUIT via signalfd.
|
||||
|
||||
Packaging:
|
||||
|
||||
- utils/Makefile: new target notifyd/scoutfs-notifyd built from
|
||||
utils/notifyd/scoutfs-notifyd.c with -I../kmod/src for the
|
||||
shared ioctl.h. No extra libs beyond libc.
|
||||
- utils/scoutfs-utils.spec.in: installs the binary at
|
||||
/usr/sbin/scoutfs-notifyd and the template unit at
|
||||
/usr/lib/systemd/system/scoutfs-notifyd@.service.
|
||||
- utils/man/scoutfs-notifyd.8 documents the tool, socket
|
||||
protocol, and drop policy.
|
||||
|
||||
Activation:
|
||||
|
||||
systemctl enable --now scoutfs-notifyd@mnt-scoutfs.service
|
||||
|
||||
where the instance name is the escaped mountpoint path. Watchers
|
||||
then connect to /run/scoutfs/<fsid>/notify.sock and recv() events.
|
||||
---
|
||||
utils/Makefile | 21 +-
|
||||
utils/man/scoutfs-notifyd.8 | 120 ++++++
|
||||
utils/notifyd/scoutfs-notifyd.c | 501 +++++++++++++++++++++++++
|
||||
utils/notifyd/scoutfs-notifyd@.service | 40 ++
|
||||
utils/scoutfs-utils.spec.in | 4 +
|
||||
5 files changed, 683 insertions(+), 3 deletions(-)
|
||||
create mode 100644 utils/man/scoutfs-notifyd.8
|
||||
create mode 100644 utils/notifyd/scoutfs-notifyd.c
|
||||
create mode 100644 utils/notifyd/scoutfs-notifyd@.service
|
||||
|
||||
diff --git a/utils/Makefile b/utils/Makefile
|
||||
index e0f7614..3535928 100644
|
||||
--- a/utils/Makefile
|
||||
+++ b/utils/Makefile
|
||||
@@ -18,7 +18,10 @@ BIN := src/scoutfs
|
||||
OBJ := $(patsubst %.c,%.o,$(wildcard src/*.c))
|
||||
DEPS := $(wildcard */*.d)
|
||||
|
||||
-all: $(BIN)
|
||||
+NOTIFYD := notifyd/scoutfs-notifyd
|
||||
+NOTIFYD_OBJ := $(patsubst %.c,%.o,$(wildcard notifyd/*.c))
|
||||
+
|
||||
+all: $(BIN) $(NOTIFYD)
|
||||
|
||||
ifneq ($(DEPS),)
|
||||
-include $(DEPS)
|
||||
@@ -29,13 +32,25 @@ QU = @echo
|
||||
VE = @
|
||||
else
|
||||
QU = @:
|
||||
-VE =
|
||||
+VE =
|
||||
endif
|
||||
|
||||
$(BIN): $(OBJ)
|
||||
$(QU) [BIN $@]
|
||||
$(VE)gcc -o $@ $^ -luuid -lm -lcrypto -lblkid
|
||||
|
||||
+# scoutfs-notifyd is a single-source daemon; it needs -I../kmod/src for
|
||||
+# ioctl.h but no external libraries beyond libc.
|
||||
+$(NOTIFYD): $(NOTIFYD_OBJ)
|
||||
+ $(QU) [BIN $@]
|
||||
+ $(VE)gcc -o $@ $^
|
||||
+
|
||||
+notifyd/%.o notifyd/%.d: notifyd/%.c Makefile sparse.sh
|
||||
+ $(QU) [CC $<]
|
||||
+ $(VE)gcc $(CFLAGS) -I../kmod/src -MD -MP -MF notifyd/$*.d -c $< -o notifyd/$*.o
|
||||
+ $(QU) [SP $<]
|
||||
+ $(VE)./sparse.sh -Wbitwise -D__CHECKER__ $(CFLAGS) -I../kmod/src $<
|
||||
+
|
||||
%.o %.d: %.c Makefile sparse.sh
|
||||
$(QU) [CC $<]
|
||||
$(VE)gcc $(CFLAGS) -MD -MP -MF $*.d -c $< -o $*.o
|
||||
@@ -65,4 +80,4 @@ dist: $(RPM_DIR) scoutfs-utils.spec
|
||||
tar rf $(TARFILE) --transform="s@.*\(src/.*\)@scoutfs-utils-$(RPM_VERSION)/\1@" $(FMTIOC_KMOD)
|
||||
|
||||
clean:
|
||||
- @rm -f $(BIN) $(OBJ) $(DEPS) .sparse.*
|
||||
+ @rm -f $(BIN) $(OBJ) $(NOTIFYD) $(NOTIFYD_OBJ) $(DEPS) .sparse.*
|
||||
diff --git a/utils/man/scoutfs-notifyd.8 b/utils/man/scoutfs-notifyd.8
|
||||
new file mode 100644
|
||||
index 0000000..a0dcd46
|
||||
--- /dev/null
|
||||
+++ b/utils/man/scoutfs-notifyd.8
|
||||
@@ -0,0 +1,120 @@
|
||||
+.TH scoutfs-notifyd 8
|
||||
+.SH NAME
|
||||
+scoutfs-notifyd \- scoutfs file access notification relay daemon
|
||||
+
|
||||
+.SH SYNOPSIS
|
||||
+.B scoutfs-notifyd
|
||||
+.I mountpoint
|
||||
+
|
||||
+.SH DESCRIPTION
|
||||
+The
|
||||
+.B scoutfs-notifyd
|
||||
+daemon drains the file access notification ring of a scoutfs mount
|
||||
+and broadcasts each event to every process connected to
|
||||
+.I /run/scoutfs/<fsid>/notify.sock.
|
||||
+
|
||||
+Events are observer-only records of file
|
||||
+.B open
|
||||
+and
|
||||
+.B read
|
||||
+activity, produced by the scoutfs kernel module for the mount
|
||||
+identified by
|
||||
+.IR mountpoint .
|
||||
+The daemon is the single privileged reader of the ring and is expected
|
||||
+to be started by the systemd template unit
|
||||
+.I scoutfs-notifyd@.service
|
||||
+with the instance name set to the escaped mountpoint path.
|
||||
+
|
||||
+.SH OPTIONS
|
||||
+
|
||||
+.TP
|
||||
+.I mountpoint
|
||||
+An absolute path to a directory on the scoutfs volume to monitor.
|
||||
+Typically this is the mountpoint itself. The daemon opens the path,
|
||||
+reads the volume's fsid via the
|
||||
+.B SCOUTFS_IOC_STATFS_MORE
|
||||
+ioctl, and builds the listener socket path from that fsid.
|
||||
+
|
||||
+.SH SOCKET
|
||||
+
|
||||
+The listener is an
|
||||
+.B AF_UNIX
|
||||
+.B SOCK_SEQPACKET
|
||||
+socket at
|
||||
+.I /run/scoutfs/<fsid>/notify.sock
|
||||
+with mode
|
||||
+.B 0600
|
||||
+and owner
|
||||
+.BR root:root .
|
||||
+
|
||||
+Each broadcast is one atomic message carrying a single packed 64-byte
|
||||
+record matching
|
||||
+.BR "struct scoutfs_ioctl_notify_event" .
|
||||
+Clients read exactly one record per
|
||||
+.BR recv (2)
|
||||
+call. The fields are:
|
||||
+
|
||||
+.nf
|
||||
+.RS 4
|
||||
+__u64 seq; /* monotonic; gap = dropped events */
|
||||
+__u64 ino; /* scoutfs inode number */
|
||||
+__u64 offset; /* byte offset of the read, 0 for OPEN */
|
||||
+__u64 length; /* byte length of the read, 0 for OPEN */
|
||||
+__u64 time_ns; /* CLOCK_REALTIME at the event */
|
||||
+__u32 pid; /* task group id */
|
||||
+__u32 uid; /* effective uid (init user namespace) */
|
||||
+__u8 type; /* 1 = OPEN, 2 = READ */
|
||||
+__u8 flags; /* bit 0 = opened writable */
|
||||
+__u8 _pad[6];
|
||||
+.RE
|
||||
+.fi
|
||||
+
|
||||
+Numeric values 3 and higher in
|
||||
+.B type
|
||||
+are reserved for future scoutfs events (data-waiter observation).
|
||||
+
|
||||
+.SH DROP POLICY
|
||||
+
|
||||
+The in-kernel ring is lossy. When it fills, records are discarded but
|
||||
+the monotonic
|
||||
+.B seq
|
||||
+field still advances, so consumers that track seq will detect gaps
|
||||
+rather than silent loss.
|
||||
+
|
||||
+The daemon forwards to clients with
|
||||
+.B MSG_DONTWAIT
|
||||
+and
|
||||
+.BR MSG_NOSIGNAL .
|
||||
+A client that cannot keep up is disconnected immediately; it is
|
||||
+expected to reconnect.
|
||||
+
|
||||
+.SH PERMISSIONS
|
||||
+
|
||||
+The daemon requires
|
||||
+.B CAP_SYS_ADMIN
|
||||
+to call
|
||||
+.BR SCOUTFS_IOC_READ_NOTIFY .
|
||||
+The shipped systemd unit grants that capability and strips everything
|
||||
+else. Only one reader may be attached to a scoutfs mount's ring at a
|
||||
+time; starting a second daemon against the same mount returns
|
||||
+.BR EBUSY .
|
||||
+
|
||||
+.SH EXIT STATUS
|
||||
+
|
||||
+The daemon exits 0 on clean shutdown (SIGTERM, SIGINT, SIGQUIT) and
|
||||
+non-zero on error.
|
||||
+
|
||||
+.SH FILES
|
||||
+.TP
|
||||
+.I /run/scoutfs/<fsid>/notify.sock
|
||||
+Listener socket, per scoutfs fsid.
|
||||
+
|
||||
+.TP
|
||||
+.I /usr/lib/systemd/system/scoutfs-notifyd@.service
|
||||
+Template unit. Enable and start a specific mount with, e.g.,
|
||||
+.BR "systemctl enable --now scoutfs-notifyd@mnt-scoutfs.service" .
|
||||
+
|
||||
+.SH SEE ALSO
|
||||
+.BR scoutfs (5),
|
||||
+.BR scoutfs (8),
|
||||
+.BR systemd.unit (5)
|
||||
diff --git a/utils/notifyd/scoutfs-notifyd.c b/utils/notifyd/scoutfs-notifyd.c
|
||||
new file mode 100644
|
||||
index 0000000..d81c74e
|
||||
--- /dev/null
|
||||
+++ b/utils/notifyd/scoutfs-notifyd.c
|
||||
@@ -0,0 +1,501 @@
|
||||
+/*
|
||||
+ * Copyright (C) 2026 Versity Software, Inc. All rights reserved.
|
||||
+ *
|
||||
+ * This program is free software; you can redistribute it and/or
|
||||
+ * modify it under the terms of the GNU General Public
|
||||
+ * License v2 as published by the Free Software Foundation.
|
||||
+ */
|
||||
+
|
||||
+/*
|
||||
+ * scoutfs-notifyd
|
||||
+ *
|
||||
+ * Drains the scoutfs file access notification ring (via
|
||||
+ * SCOUTFS_IOC_READ_NOTIFY) and fans each record out to every client
|
||||
+ * connected to /run/scoutfs/<fsid>/notify.sock.
|
||||
+ *
|
||||
+ * Usage: scoutfs-notifyd <mountpoint>
|
||||
+ *
|
||||
+ * Design notes:
|
||||
+ *
|
||||
+ * - One process, one thread. A short blocking ioctl (100 ms) drives
|
||||
+ * the loop; new clients and shutdown signals are handled between
|
||||
+ * ioctl calls via epoll with timeout 0. No cross-thread state.
|
||||
+ *
|
||||
+ * - Socket type is SOCK_SEQPACKET so each broadcast is one atomic
|
||||
+ * 64-byte message on the wire. Clients read exactly one
|
||||
+ * struct scoutfs_ioctl_notify_event per recv().
|
||||
+ *
|
||||
+ * - Permissions: socket owner root, mode 0600. The listener directory
|
||||
+ * /run/scoutfs/<fsid>/ is created with mode 0755.
|
||||
+ *
|
||||
+ * - Slow-client protection: sends use MSG_DONTWAIT | MSG_NOSIGNAL.
|
||||
+ * Any send error (EAGAIN, EPIPE, ECONNRESET, ...) drops the client.
|
||||
+ * The kmod-side ring absorbs the brief lag.
|
||||
+ *
|
||||
+ * - Exit cleanly on SIGTERM/SIGINT/SIGQUIT; systemd restarts on
|
||||
+ * failure per the shipped unit file.
|
||||
+ */
|
||||
+
|
||||
+#include <errno.h>
|
||||
+#include <fcntl.h>
|
||||
+#include <signal.h>
|
||||
+#include <stdarg.h>
|
||||
+#include <stdbool.h>
|
||||
+#include <stdint.h>
|
||||
+#include <stdio.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <string.h>
|
||||
+#include <sys/epoll.h>
|
||||
+#include <sys/ioctl.h>
|
||||
+#include <sys/signalfd.h>
|
||||
+#include <sys/socket.h>
|
||||
+#include <sys/stat.h>
|
||||
+#include <sys/types.h>
|
||||
+#include <sys/un.h>
|
||||
+#include <syslog.h>
|
||||
+#include <unistd.h>
|
||||
+#include <linux/types.h>
|
||||
+
|
||||
+#include "ioctl.h"
|
||||
+
|
||||
+#define PROGNAME "scoutfs-notifyd"
|
||||
+
|
||||
+#define RUN_DIR_FMT "/run/scoutfs/%llu"
|
||||
+#define SOCK_PATH_FMT "/run/scoutfs/%llu/notify.sock"
|
||||
+
|
||||
+#define IOCTL_BATCH_EVENTS 64
|
||||
+#define IOCTL_TIMEOUT_MS 100
|
||||
+#define MAX_CLIENTS 128
|
||||
+#define SOCK_BACKLOG 16
|
||||
+
|
||||
+static volatile sig_atomic_t g_shutdown;
|
||||
+
|
||||
+struct ctx {
|
||||
+ int mnt_fd;
|
||||
+ int listen_fd;
|
||||
+ int epoll_fd;
|
||||
+ int signal_fd;
|
||||
+ char sock_path[128];
|
||||
+ char run_dir[128];
|
||||
+ int clients[MAX_CLIENTS];
|
||||
+ int nclients;
|
||||
+};
|
||||
+
|
||||
+static void logmsg(int prio, const char *fmt, ...)
|
||||
+ __attribute__((format(printf, 2, 3)));
|
||||
+
|
||||
+static void logmsg(int prio, const char *fmt, ...)
|
||||
+{
|
||||
+ va_list ap;
|
||||
+
|
||||
+ va_start(ap, fmt);
|
||||
+ vsyslog(prio, fmt, ap);
|
||||
+ va_end(ap);
|
||||
+
|
||||
+ /* also to stderr when not yet daemonized (systemd captures it) */
|
||||
+ va_start(ap, fmt);
|
||||
+ vfprintf(stderr, fmt, ap);
|
||||
+ fputc('\n', stderr);
|
||||
+ va_end(ap);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Open the mountpoint and resolve its fsid via SCOUTFS_IOC_STATFS_MORE.
|
||||
+ * The fsid is unique per scoutfs mount and forms the per-mount socket
|
||||
+ * path component.
|
||||
+ */
|
||||
+static int get_fsid(const char *mountpoint, uint64_t *fsid_out, int *mnt_fd_out)
|
||||
+{
|
||||
+ struct scoutfs_ioctl_statfs_more sm = { 0 };
|
||||
+ int fd;
|
||||
+
|
||||
+ fd = open(mountpoint, O_RDONLY | O_DIRECTORY);
|
||||
+ if (fd < 0) {
|
||||
+ logmsg(LOG_ERR, "open(%s): %s", mountpoint, strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ if (ioctl(fd, SCOUTFS_IOC_STATFS_MORE, &sm) < 0) {
|
||||
+ logmsg(LOG_ERR, "SCOUTFS_IOC_STATFS_MORE on %s: %s (is this a scoutfs mount?)",
|
||||
+ mountpoint, strerror(errno));
|
||||
+ close(fd);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ *fsid_out = sm.fsid;
|
||||
+ *mnt_fd_out = fd;
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * mkdir -p for a single-level run dir. We only have to create the
|
||||
+ * leaf; systemd (or the shipped tmpfiles.d) creates /run/scoutfs.
|
||||
+ */
|
||||
+static int ensure_run_dir(const char *dir)
|
||||
+{
|
||||
+ struct stat st;
|
||||
+
|
||||
+ if (mkdir("/run/scoutfs", 0755) < 0 && errno != EEXIST) {
|
||||
+ logmsg(LOG_ERR, "mkdir /run/scoutfs: %s", strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (mkdir(dir, 0755) < 0 && errno != EEXIST) {
|
||||
+ logmsg(LOG_ERR, "mkdir %s: %s", dir, strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (stat(dir, &st) < 0 || !S_ISDIR(st.st_mode)) {
|
||||
+ logmsg(LOG_ERR, "%s: not a directory", dir);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int create_listener(const char *path)
|
||||
+{
|
||||
+ struct sockaddr_un addr = { .sun_family = AF_UNIX };
|
||||
+ int fd;
|
||||
+
|
||||
+ if (strlen(path) >= sizeof(addr.sun_path)) {
|
||||
+ logmsg(LOG_ERR, "socket path too long: %s", path);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
|
||||
+
|
||||
+ fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
+ if (fd < 0) {
|
||||
+ logmsg(LOG_ERR, "socket(): %s", strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ /* Remove any stale socket left by a previous instance. */
|
||||
+ (void)unlink(path);
|
||||
+
|
||||
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
+ logmsg(LOG_ERR, "bind(%s): %s", path, strerror(errno));
|
||||
+ close(fd);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ /* Root-only access. chmod after bind so there's no brief window. */
|
||||
+ if (chmod(path, 0600) < 0) {
|
||||
+ logmsg(LOG_ERR, "chmod(%s, 0600): %s", path, strerror(errno));
|
||||
+ close(fd);
|
||||
+ unlink(path);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ if (listen(fd, SOCK_BACKLOG) < 0) {
|
||||
+ logmsg(LOG_ERR, "listen(): %s", strerror(errno));
|
||||
+ close(fd);
|
||||
+ unlink(path);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ return fd;
|
||||
+}
|
||||
+
|
||||
+static int setup_signalfd(void)
|
||||
+{
|
||||
+ sigset_t mask;
|
||||
+ int fd;
|
||||
+
|
||||
+ sigemptyset(&mask);
|
||||
+ sigaddset(&mask, SIGTERM);
|
||||
+ sigaddset(&mask, SIGINT);
|
||||
+ sigaddset(&mask, SIGQUIT);
|
||||
+
|
||||
+ if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) {
|
||||
+ logmsg(LOG_ERR, "sigprocmask: %s", strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ fd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
|
||||
+ if (fd < 0)
|
||||
+ logmsg(LOG_ERR, "signalfd: %s", strerror(errno));
|
||||
+ return fd;
|
||||
+}
|
||||
+
|
||||
+static int epoll_add(int epfd, int fd, uint32_t events, uint64_t tag)
|
||||
+{
|
||||
+ struct epoll_event ev = { .events = events, .data.u64 = tag };
|
||||
+
|
||||
+ return epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Tag encoding so epoll events route cleanly without a per-fd lookup.
|
||||
+ * tag[63..56] = kind
|
||||
+ * tag[ 7.. 0] = client index (when kind == KIND_CLIENT)
|
||||
+ */
|
||||
+#define KIND_LISTEN 1
|
||||
+#define KIND_SIGNAL 2
|
||||
+#define KIND_CLIENT 3
|
||||
+
|
||||
+#define TAG_KIND(t) ((int)((t) >> 56))
|
||||
+#define TAG_INDEX(t) ((int)((t) & 0xff))
|
||||
+#define TAG_MAKE(k, i) (((uint64_t)(k) << 56) | (uint32_t)(i))
|
||||
+
|
||||
+static void client_close(struct ctx *c, int idx)
|
||||
+{
|
||||
+ if (idx < 0 || idx >= c->nclients)
|
||||
+ return;
|
||||
+ (void)epoll_ctl(c->epoll_fd, EPOLL_CTL_DEL, c->clients[idx], NULL);
|
||||
+ close(c->clients[idx]);
|
||||
+ c->clients[idx] = c->clients[c->nclients - 1];
|
||||
+ c->nclients--;
|
||||
+ /* re-tag the swapped-in client to its new index */
|
||||
+ if (idx < c->nclients) {
|
||||
+ struct epoll_event ev = {
|
||||
+ .events = EPOLLIN | EPOLLHUP | EPOLLERR,
|
||||
+ .data.u64 = TAG_MAKE(KIND_CLIENT, idx),
|
||||
+ };
|
||||
+ (void)epoll_ctl(c->epoll_fd, EPOLL_CTL_MOD,
|
||||
+ c->clients[idx], &ev);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static void client_accept(struct ctx *c)
|
||||
+{
|
||||
+ int fd;
|
||||
+
|
||||
+ for (;;) {
|
||||
+ fd = accept4(c->listen_fd, NULL, NULL,
|
||||
+ SOCK_CLOEXEC | SOCK_NONBLOCK);
|
||||
+ if (fd < 0) {
|
||||
+ if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
+ return;
|
||||
+ logmsg(LOG_WARNING, "accept: %s", strerror(errno));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (c->nclients >= MAX_CLIENTS) {
|
||||
+ logmsg(LOG_WARNING, "client limit (%d) reached, rejecting",
|
||||
+ MAX_CLIENTS);
|
||||
+ close(fd);
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ if (epoll_add(c->epoll_fd, fd,
|
||||
+ EPOLLIN | EPOLLHUP | EPOLLERR,
|
||||
+ TAG_MAKE(KIND_CLIENT, c->nclients)) < 0) {
|
||||
+ logmsg(LOG_WARNING, "epoll_add client: %s",
|
||||
+ strerror(errno));
|
||||
+ close(fd);
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ c->clients[c->nclients++] = fd;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Send one event to all connected clients. Errors drop the client.
|
||||
+ * Iterate backwards so client_close's swap-remove doesn't skip entries.
|
||||
+ */
|
||||
+static void broadcast_event(struct ctx *c,
|
||||
+ const struct scoutfs_ioctl_notify_event *ev)
|
||||
+{
|
||||
+ int i;
|
||||
+
|
||||
+ for (i = c->nclients - 1; i >= 0; i--) {
|
||||
+ ssize_t sent = send(c->clients[i], ev, sizeof(*ev),
|
||||
+ MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
+ if (sent == (ssize_t)sizeof(*ev))
|
||||
+ continue;
|
||||
+ /*
|
||||
+ * SOCK_SEQPACKET is atomic per-message; partial send
|
||||
+ * shouldn't happen, but treat anything less as a drop.
|
||||
+ */
|
||||
+ client_close(c, i);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Drain kernel events with a short timeout. A positive return means
|
||||
+ * we should keep polling without delay; zero means go to epoll_wait
|
||||
+ * with a longer sleep.
|
||||
+ */
|
||||
+static int drain_kernel(struct ctx *c)
|
||||
+{
|
||||
+ struct scoutfs_ioctl_notify_event events[IOCTL_BATCH_EVENTS];
|
||||
+ struct scoutfs_ioctl_read_notify req = {
|
||||
+ .events_ptr = (uint64_t)(uintptr_t)events,
|
||||
+ .events_nr = IOCTL_BATCH_EVENTS,
|
||||
+ .timeout_ms = IOCTL_TIMEOUT_MS,
|
||||
+ .flags = 0,
|
||||
+ };
|
||||
+ int ret;
|
||||
+ int i;
|
||||
+
|
||||
+ ret = ioctl(c->mnt_fd, SCOUTFS_IOC_READ_NOTIFY, &req);
|
||||
+ if (ret < 0) {
|
||||
+ if (errno == ETIMEDOUT || errno == EINTR)
|
||||
+ return 0;
|
||||
+ if (errno == EBUSY) {
|
||||
+ logmsg(LOG_ERR,
|
||||
+ "another reader is attached; backing off");
|
||||
+ sleep(1);
|
||||
+ return 0;
|
||||
+ }
|
||||
+ logmsg(LOG_ERR, "SCOUTFS_IOC_READ_NOTIFY: %s",
|
||||
+ strerror(errno));
|
||||
+ g_shutdown = 1;
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ for (i = 0; i < ret; i++)
|
||||
+ broadcast_event(c, &events[i]);
|
||||
+
|
||||
+ return ret;
|
||||
+}
|
||||
+
|
||||
+static int handle_epoll(struct ctx *c, int timeout_ms)
|
||||
+{
|
||||
+ struct epoll_event evs[16];
|
||||
+ int n;
|
||||
+ int i;
|
||||
+
|
||||
+ n = epoll_wait(c->epoll_fd, evs,
|
||||
+ (int)(sizeof(evs) / sizeof(evs[0])), timeout_ms);
|
||||
+ if (n < 0) {
|
||||
+ if (errno == EINTR)
|
||||
+ return 0;
|
||||
+ logmsg(LOG_ERR, "epoll_wait: %s", strerror(errno));
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ for (i = 0; i < n; i++) {
|
||||
+ uint64_t tag = evs[i].data.u64;
|
||||
+ int kind = TAG_KIND(tag);
|
||||
+
|
||||
+ switch (kind) {
|
||||
+ case KIND_LISTEN:
|
||||
+ client_accept(c);
|
||||
+ break;
|
||||
+
|
||||
+ case KIND_SIGNAL:
|
||||
+ g_shutdown = 1;
|
||||
+ return 0;
|
||||
+
|
||||
+ case KIND_CLIENT:
|
||||
+ /* any readability / hangup → drop client */
|
||||
+ client_close(c, TAG_INDEX(tag));
|
||||
+ break;
|
||||
+
|
||||
+ default:
|
||||
+ logmsg(LOG_WARNING, "unexpected epoll tag kind %d",
|
||||
+ kind);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static void cleanup(struct ctx *c)
|
||||
+{
|
||||
+ int i;
|
||||
+
|
||||
+ for (i = 0; i < c->nclients; i++)
|
||||
+ close(c->clients[i]);
|
||||
+ c->nclients = 0;
|
||||
+
|
||||
+ if (c->listen_fd >= 0)
|
||||
+ close(c->listen_fd);
|
||||
+ if (c->sock_path[0])
|
||||
+ unlink(c->sock_path);
|
||||
+ if (c->signal_fd >= 0)
|
||||
+ close(c->signal_fd);
|
||||
+ if (c->epoll_fd >= 0)
|
||||
+ close(c->epoll_fd);
|
||||
+ if (c->mnt_fd >= 0)
|
||||
+ close(c->mnt_fd);
|
||||
+}
|
||||
+
|
||||
+static void usage(FILE *f)
|
||||
+{
|
||||
+ fprintf(f,
|
||||
+ "usage: %s <mountpoint>\n"
|
||||
+ "\n"
|
||||
+ " Drains the scoutfs file access notification ring for the\n"
|
||||
+ " given mount and broadcasts events to clients connected to\n"
|
||||
+ " /run/scoutfs/<fsid>/notify.sock.\n"
|
||||
+ "\n"
|
||||
+ " Requires CAP_SYS_ADMIN. Socket is root-only (mode 0600).\n",
|
||||
+ PROGNAME);
|
||||
+}
|
||||
+
|
||||
+int main(int argc, char **argv)
|
||||
+{
|
||||
+ struct ctx c = {
|
||||
+ .mnt_fd = -1,
|
||||
+ .listen_fd = -1,
|
||||
+ .epoll_fd = -1,
|
||||
+ .signal_fd = -1,
|
||||
+ };
|
||||
+ uint64_t fsid;
|
||||
+ int ret = EXIT_FAILURE;
|
||||
+
|
||||
+ openlog(PROGNAME, LOG_PID | LOG_PERROR, LOG_DAEMON);
|
||||
+
|
||||
+ if (argc != 2 || argv[1][0] == '-') {
|
||||
+ usage(stderr);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ if (get_fsid(argv[1], &fsid, &c.mnt_fd) < 0)
|
||||
+ goto out;
|
||||
+
|
||||
+ snprintf(c.run_dir, sizeof(c.run_dir),
|
||||
+ RUN_DIR_FMT, (unsigned long long)fsid);
|
||||
+ snprintf(c.sock_path, sizeof(c.sock_path),
|
||||
+ SOCK_PATH_FMT, (unsigned long long)fsid);
|
||||
+
|
||||
+ if (ensure_run_dir(c.run_dir) < 0)
|
||||
+ goto out;
|
||||
+
|
||||
+ c.listen_fd = create_listener(c.sock_path);
|
||||
+ if (c.listen_fd < 0)
|
||||
+ goto out;
|
||||
+
|
||||
+ c.signal_fd = setup_signalfd();
|
||||
+ if (c.signal_fd < 0)
|
||||
+ goto out;
|
||||
+
|
||||
+ c.epoll_fd = epoll_create1(EPOLL_CLOEXEC);
|
||||
+ if (c.epoll_fd < 0) {
|
||||
+ logmsg(LOG_ERR, "epoll_create1: %s", strerror(errno));
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ if (epoll_add(c.epoll_fd, c.listen_fd, EPOLLIN,
|
||||
+ TAG_MAKE(KIND_LISTEN, 0)) < 0 ||
|
||||
+ epoll_add(c.epoll_fd, c.signal_fd, EPOLLIN,
|
||||
+ TAG_MAKE(KIND_SIGNAL, 0)) < 0) {
|
||||
+ logmsg(LOG_ERR, "epoll_ctl: %s", strerror(errno));
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ logmsg(LOG_INFO, "listening on %s", c.sock_path);
|
||||
+
|
||||
+ while (!g_shutdown) {
|
||||
+ /*
|
||||
+ * The ioctl blocks up to IOCTL_TIMEOUT_MS when the ring
|
||||
+ * is empty, so this loop naturally paces itself and the
|
||||
+ * daemon is idle between events. Client / signal fds
|
||||
+ * are drained non-blocking between ioctl calls.
|
||||
+ */
|
||||
+ if (drain_kernel(&c) < 0)
|
||||
+ break;
|
||||
+ if (handle_epoll(&c, 0) < 0)
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ logmsg(LOG_INFO, "shutting down");
|
||||
+ ret = EXIT_SUCCESS;
|
||||
+
|
||||
+out:
|
||||
+ cleanup(&c);
|
||||
+ closelog();
|
||||
+ return ret;
|
||||
+}
|
||||
diff --git a/utils/notifyd/scoutfs-notifyd@.service b/utils/notifyd/scoutfs-notifyd@.service
|
||||
new file mode 100644
|
||||
index 0000000..2e87f17
|
||||
--- /dev/null
|
||||
+++ b/utils/notifyd/scoutfs-notifyd@.service
|
||||
@@ -0,0 +1,40 @@
|
||||
+[Unit]
|
||||
+Description=ScoutFS file access notification daemon for %I
|
||||
+Documentation=man:scoutfs-notifyd(8)
|
||||
+# The mount must exist before we can open it and read its fsid.
|
||||
+RequiresMountsFor=/%I
|
||||
+
|
||||
+[Service]
|
||||
+Type=simple
|
||||
+User=root
|
||||
+Group=root
|
||||
+UMask=0077
|
||||
+# scoutfs-notifyd creates /run/scoutfs/<fsid>/ and binds the socket
|
||||
+# inside it; the top-level /run/scoutfs is created here so the daemon
|
||||
+# never has to.
|
||||
+RuntimeDirectory=scoutfs
|
||||
+RuntimeDirectoryMode=0755
|
||||
+ExecStart=/usr/sbin/scoutfs-notifyd /%I
|
||||
+Restart=on-failure
|
||||
+RestartSec=5s
|
||||
+StartLimitBurst=5
|
||||
+
|
||||
+# The daemon needs CAP_SYS_ADMIN to drive the read-notify ioctl.
|
||||
+# Everything else can be stripped.
|
||||
+CapabilityBoundingSet=CAP_SYS_ADMIN
|
||||
+AmbientCapabilities=CAP_SYS_ADMIN
|
||||
+NoNewPrivileges=yes
|
||||
+PrivateTmp=yes
|
||||
+ProtectSystem=strict
|
||||
+ProtectHome=yes
|
||||
+ProtectKernelTunables=yes
|
||||
+ProtectKernelModules=yes
|
||||
+ProtectControlGroups=yes
|
||||
+RestrictAddressFamilies=AF_UNIX
|
||||
+LockPersonality=yes
|
||||
+MemoryDenyWriteExecute=yes
|
||||
+RestrictRealtime=yes
|
||||
+RestrictSUIDSGID=yes
|
||||
+
|
||||
+[Install]
|
||||
+WantedBy=multi-user.target
|
||||
diff --git a/utils/scoutfs-utils.spec.in b/utils/scoutfs-utils.spec.in
|
||||
index fb24b81..0aa91f5 100644
|
||||
--- a/utils/scoutfs-utils.spec.in
|
||||
+++ b/utils/scoutfs-utils.spec.in
|
||||
@@ -52,19 +52,23 @@ cp man/*.5.gz $RPM_BUILD_ROOT%{_mandir}/man5/.
|
||||
cp man/*.7.gz $RPM_BUILD_ROOT%{_mandir}/man7/.
|
||||
cp man/*.8.gz $RPM_BUILD_ROOT%{_mandir}/man8/.
|
||||
install -m 755 -D src/scoutfs $RPM_BUILD_ROOT%{_sbindir}/scoutfs
|
||||
+install -m 755 -D notifyd/scoutfs-notifyd $RPM_BUILD_ROOT%{_sbindir}/scoutfs-notifyd
|
||||
install -m 644 -D src/ioctl.h $RPM_BUILD_ROOT%{_includedir}/scoutfs/ioctl.h
|
||||
install -m 644 -D src/format.h $RPM_BUILD_ROOT%{_includedir}/scoutfs/format.h
|
||||
install -m 755 -D fenced/scoutfs-fenced $RPM_BUILD_ROOT%{_libexecdir}/scoutfs-fenced/scoutfs-fenced
|
||||
install -m 644 -D fenced/scoutfs-fenced.service $RPM_BUILD_ROOT%{_unitdir}/scoutfs-fenced.service
|
||||
install -m 644 -D fenced/scoutfs-fenced.conf.example $RPM_BUILD_ROOT%{_sysconfdir}/scoutfs/scoutfs-fenced.conf.example
|
||||
+install -m 644 -D notifyd/scoutfs-notifyd@.service $RPM_BUILD_ROOT%{_unitdir}/scoutfs-notifyd@.service
|
||||
|
||||
%files
|
||||
%defattr(644,root,root,755)
|
||||
%{_mandir}/man*/scoutfs*.gz
|
||||
/%{_unitdir}/scoutfs-fenced.service
|
||||
+/%{_unitdir}/scoutfs-notifyd@.service
|
||||
%{_sysconfdir}/scoutfs
|
||||
%defattr(755,root,root,755)
|
||||
%{_sbindir}/scoutfs
|
||||
+%{_sbindir}/scoutfs-notifyd
|
||||
%{_libexecdir}/scoutfs-fenced
|
||||
|
||||
%files -n scoutfs-devel
|
||||
--
|
||||
2.49.0.windows.1
|
||||
|
||||
Reference in New Issue
Block a user