Transparent inotify injection for NFS filesystems
FakeNotify solves the fundamental problem that NFS mounts don't emit inotify events, breaking applications like Jellyfin, Plex, Sonarr, Radarr, and qBittorrent that rely on filesystem change notifications.
┌─────────────────────────────────────────────────┐
│ Application (Jellyfin, Sonarr, etc.) │
│ │
│ inotify_init() ──→ [HOOK] ──→ returns pipe fd │
│ inotify_add_watch() ──→ [HOOK] ──→ returns wd │
│ read(fd) ←── receives inotify_event structs ────┤
└─────────────────────────────────────────────────┘
↑
LD_PRELOAD=libfakenotify.so
↑
Unix Socket IPC
↓
┌─────────────────────────────────────────────────┐
│ fakenotifyd (daemon) │
│ │
│ • Polls NFS mounts for changes │
│ • Tracks watch descriptors ↔ paths │
│ • Writes synthetic inotify_event to pipes │
│ • CLI for runtime configuration │
└─────────────────────────────────────────────────┘
Two components:
libfakenotify.so- LD_PRELOAD library that interceptsinotify_init,inotify_add_watch,inotify_rm_watch, returns pipe fds instead, and connects to the daemonfakenotifyd- Background daemon that polls NFS paths, detects changes, and writes syntheticinotify_eventstructs to connected applications
- Transparent - No application modifications needed, just
LD_PRELOAD - Docker-friendly - Works with containerized apps via volume-mounted socket
- Configurable polling - Adjust intervals per-path based on your needs
- Runtime CLI - Add/remove watched paths without restart
curl -sSL https://raw.githubusercontent.com/zachhandley/FakeNotify/main/install-release.sh | sudo bashThen configure and start:
sudo nano /etc/fakenotify/config.toml # Add your NFS paths
sudo systemctl enable --now fakenotifygit clone https://github.com/zachhandley/FakeNotify.git
cd FakeNotify
cargo build --release
sudo ./install.sh# Start with default config
fakenotifyd start
# Or specify config file
fakenotifyd start --config /etc/fakenotify/config.toml# Add an NFS path to monitor
fakenotifyd add /mnt/media --poll-interval 5s
# Remove a path
fakenotifyd remove /mnt/media
# List watched paths
fakenotifyd list
# Check status
fakenotifyd status# Single application
LD_PRELOAD=/usr/lib/libfakenotify.so jellyfin
# Docker container
docker run -e LD_PRELOAD=/fakenotify/libfakenotify.so \
-v /usr/lib/libfakenotify.so:/fakenotify/libfakenotify.so:ro \
-v /run/fakenotify.sock:/run/fakenotify.sock \
jellyfin/jellyfinThe daemon runs on the host, containers just need the library and socket mounted.
# docker-compose.yml
services:
jellyfin:
image: jellyfin/jellyfin
environment:
- LD_PRELOAD=/usr/local/lib/libfakenotify_preload.so
volumes:
# Mount the socket directory and library
- /run/fakenotify:/run/fakenotify:ro
- /usr/local/lib/libfakenotify_preload.so:/usr/local/lib/libfakenotify_preload.so:ro
# Your NFS media
- /mnt/media:/mediaTwo mounts: the socket directory and the library file.
For LSIO containers (Sonarr, Radarr, etc.), use the DockerMod - zero config needed:
services:
sonarr:
image: linuxserver/sonarr
environment:
- DOCKER_MODS=ghcr.io/zachhandley/fakenotify-mod:latest
volumes:
- /run/fakenotify:/run/fakenotify:ro
- /usr/local/lib/libfakenotify_preload.so:/usr/local/lib/libfakenotify_preload.so:ro
- /mnt/media:/mediaThe mod automatically configures LD_PRELOAD.
/etc/fakenotify/config.toml:
[daemon]
socket = "/run/fakenotify.sock"
log_level = "info"
[[watch]]
path = "/mnt/media"
poll_interval = "5s"
recursive = true
[[watch]]
path = "/mnt/downloads"
poll_interval = "2s"
recursive = trueLinux's inotify monitors filesystem changes at the kernel VFS layer. When files change on an NFS server (or from another NFS client), the local kernel never sees the operation - it happens remotely. Therefore, inotify watches on NFS mounts are silent.
This affects:
- Jellyfin/Plex/Emby - Library not updating when new media added
- Sonarr/Radarr - Downloads not detected (when using folder watching)
- qBittorrent - Watched folders not triggering
- Any app using
inotify,fswatch,watchdog, etc.
Uses the redhook crate to intercept:
inotify_init()/inotify_init1()- Returns a pipe fd insteadinotify_add_watch()- Registers path with daemon, returns synthetic wdinotify_rm_watch()- Unregisters path with daemon
The pipe fd is indistinguishable from a real inotify fd to the application - it works with poll(), epoll(), select(), and blocking read().
Uses the notify crate with PollWatcher backend:
- Periodic
stat()calls to detect mtime/ctime changes - Directory listing comparison for create/delete detection
- Debouncing via
notify-debouncer-fullto coalesce rapid changes
Writes standard inotify_event structs to pipes:
struct inotify_event {
int wd; // Watch descriptor
uint32_t mask; // Event mask (IN_CREATE, IN_MODIFY, etc.)
uint32_t cookie; // Cookie for rename pairing
uint32_t len; // Length of name field
char name[]; // Filename (variable length)
};- Only affects dynamically linked binaries - Static binaries bypass LD_PRELOAD
- Polling latency - Changes detected on poll interval, not instantly
- NFS attribute caching - May need
actimeo=0mount option for immediate visibility - No rename cookie pairing -
IN_MOVED_FROM/IN_MOVED_TOwon't have matching cookies across polls
- Linux (uses Linux-specific inotify API)
- Rust 1.75+ (for building)
- NFS mounts accessible to the daemon
MIT