4.8 KiB
05 — inotify
Filesystem event notifications as a file descriptor. inotify_add_watch(dir, mask) installs a watch; reading the fd returns variable-length inotify_event records each time a matching file creates, modifies, or disappears. Replaces the S3 + Lambda pipeline from the v1 architecture — one fd on the event loop IS the content-sync engine.
Kernel source
| Path | What |
|---|---|
reference/linux/fs/notify/inotify/inotify_user.c |
SYSCALL_DEFINE1(inotify_init1, ...), SYSCALL_DEFINE3(inotify_add_watch, ...), SYSCALL_DEFINE2(inotify_rm_watch, ...). |
reference/linux/fs/notify/inotify/inotify_fsnotify.c |
The fsnotify backend that feeds events into the fd. |
reference/linux/include/uapi/linux/inotify.h |
struct inotify_event, IN_* masks. |
Man pages
man 7 inotify (overview + event semantics), man 2 inotify_init1, man 2 inotify_add_watch, man 2 inotify_rm_watch.
Rust FFI via libc
use libc::{inotify_init1, inotify_add_watch, inotify_rm_watch, inotify_event};
use libc::{IN_CLOEXEC, IN_NONBLOCK};
use libc::{IN_MODIFY, IN_CREATE, IN_DELETE, IN_CLOSE_WRITE};
use libc::{IN_MOVED_FROM, IN_MOVED_TO, IN_ISDIR, IN_Q_OVERFLOW};
Direct-syscall example
unsafe {
let fd = libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK);
if fd < 0 { return Err(io::Error::last_os_error()); }
let dir = std::ffi::CString::new("docs/examples/blog/types").unwrap();
let wd = libc::inotify_add_watch(
fd,
dir.as_ptr(),
(libc::IN_MODIFY | libc::IN_CLOSE_WRITE
| libc::IN_CREATE | libc::IN_DELETE
| libc::IN_MOVED_FROM | libc::IN_MOVED_TO) as u32,
);
if wd < 0 { return Err(io::Error::last_os_error()); }
// register fd on epoll. On EPOLLIN:
let mut buf = [0u8; 4096];
let n = libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len());
// parse the buffer: a packed sequence of inotify_event records,
// each followed by a variable-length name field (event.len bytes)
let mut offset = 0usize;
while offset < n as usize {
let ev = &*(buf.as_ptr().add(offset) as *const inotify_event);
let name_len = ev.len as usize;
let name_start = offset + std::mem::size_of::<inotify_event>();
let name = std::str::from_utf8(&buf[name_start..name_start + name_len])
.unwrap().trim_end_matches('\0');
// dispatch based on ev.mask + ev.wd → directory → full path
offset = name_start + name_len;
}
}
Key flags
| Flag | Meaning |
|---|---|
IN_CLOEXEC / IN_NONBLOCK |
Close on exec, non-blocking reads. Always set. |
IN_MODIFY |
File content written. Fires per-write(2) — noisy; prefer IN_CLOSE_WRITE. |
IN_CLOSE_WRITE |
File opened for writing was closed. Usual choice — one event per editor save. |
IN_CREATE / IN_DELETE |
Entry created / deleted inside a watched directory. |
IN_MOVED_FROM / IN_MOVED_TO |
The two halves of a rename. Paired by cookie. Editors often write tmp → rename → delete; you get both halves. |
IN_ISDIR |
Set on the event when the target is a directory. |
IN_Q_OVERFLOW |
Kernel event queue overflowed; wd = -1, rescan from scratch. Must handle. |
Gotchas
- Per-directory watches, not per-file. Watching individual files wastes descriptors and misses
IN_CREATE/IN_DELETEfor new entries. Watch the directory; filter by eventnamein userspace. - Recursive watching is manual. Walk the tree at init and add a watch per directory. React to
IN_CREATE | IN_ISDIRby adding a watch for the new subdirectory — and toIN_MOVED_TO | IN_ISDIRtoo. fs.inotify.max_user_watchesdefaults to 8192 on most distros. Recursive watches over a big node_modules or target dir exhaust it fast. Filter aggressively before adding.- Editors burst events. Tmp-file + rename + delete is 3–4 events per logical save. Debounce 100–200 ms with
timerfd. - Reading less than a full event is an
EINVAL. Use a buffer ≥sizeof(inotify_event) + NAME_MAX + 1(≈ 4 KiB is a safe size). wdis stable per-watch but reused afterrm_watch. Keep awd → pathmap; remove from it onIN_IGNORED.
Used by
07-inotify-content-watcher.md — the Stage-3 hot-reload feature. Future sub crate — the register-macro subscription model in 00-linux.md § Database Subscription.
v1 port source
reference/crates/wo-watch/src/lib.rs (280 LOC) — already does recursive watch setup, event parsing, and path resolution via a wd → PathBuf map.