writeonce/docs/plan/exploration/linux/05-inotify.md
shoney.arickathil c0b0dbb846 docs: audit all markdown against the code, fix findings, flatten status folders
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
  available"; "no package manager" contradicted [deps]; the deps example
  would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
  dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
  shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
  prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
  audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
  links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
  families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
  CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
  source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:20:22 +02:00

4.8 KiB
Raw Permalink Blame History

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_DELETE for new entries. Watch the directory; filter by event name in userspace.
  • Recursive watching is manual. Walk the tree at init and add a watch per directory. React to IN_CREATE | IN_ISDIR by adding a watch for the new subdirectory — and to IN_MOVED_TO | IN_ISDIR too.
  • fs.inotify.max_user_watches defaults 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).
  • wd is stable per-watch but reused after rm_watch. Keep a wd → path map; remove from it on IN_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.