Skip to content

watcher: report metadata and move_from events, and recover from queue overflow - #36416

Open
Properrr wants to merge 1 commit into
oven-sh:mainfrom
Properrr:claude/watcher-report-unrequested-events
Open

Properrr wants to merge 1 commit into
oven-sh:mainfrom
Properrr:claude/watcher-report-unrequested-events

Conversation

@Properrr

Copy link
Copy Markdown
Contributor

What does this PR do?

src/watcher mapped several Op bits it never asked the OS for, so those bits were unreachable and some filesystem changes produced no event at all.

  • Op::METADATA was dead on every platform. kqueue read NOTE_ATTRIB in watch_event_from_kevent, but add_file_descriptor_to_kqueue_without_checks only ever requested NOTE_WRITE|NOTE_RENAME|NOTE_DELETE; inotify never had IN_ATTRIB in either mask. So touch and chmod produced no watcher event at all on Linux, while ReadDirectoryChangesW reported them as a write. Now requested for file watches on both backends.

    Deliberately kept off directory watches: on inotify, IN_ATTRIB on a directory reports metadata changes of every entry inside it by name, and consumers treat a named directory event as "re-resolve this entry" — so a bare touch would reload the module. Threading WatchItemKind into the kqueue registration is what makes that split expressible.

  • A rename inside a watched directory only reported its arrival half (IN_MOVED_TO). IN_MOVED_FROM is now requested and mapped to a new Op::MOVE_FROM, so the vacated name is invalidated too.

  • On Windows, FILE_ACTION_ADDED and FILE_ACTION_RENAMED_NEW_NAME produced a WatchEvent with no op bits at all, making file creation invisible to every consumer matching on WRITE|DELETE|RENAME. They now map to CREATE and MOVE_TO. RENAMED_OLD_NAME keeps RENAME alongside the new MOVE_FROM so existing consumers are unaffected.

Dropped events were also ignored: IN_Q_OVERFLOW arrives with wd == -1, matches no watchlist entry, and fell through the lookup in watch_loop_cycle, so changes the kernel discarded were never noticed. resync_after_overflow replays a synthetic WRITE per watched path. WRITE rather than DELETE is deliberate: it makes consumers re-check without driving the eviction path and its fixed 8096-entry evict_list.

The fixing lines are the two mask expressions in watch_path/watch_dir, the fflags computation in add_file_descriptor_to_kqueue_without_checks, the create_watch_event match, and the is_queue_overflow() branch in watch_loop_cycle. Everything else is the WatchItemKind plumbing those need.

How did you verify your code works?

Two new tests assert the events through BUN_WATCHER_TRACE, and both fail against a build without this change — utimes produces no event at all, and a rename emits only move_to:

  • utimes on a watched file records a metadata event
  • renaming inside a watched directory records move_from

Platform skips are capability-based, not workarounds: ReadDirectoryChangesW cannot distinguish metadata from a content write, and kqueue has no per-entry departure event to map to move_from.

Linux (x86_64): test/cli/watch, test/cli/hot — 24 pass, 0 fail.

macOS (arm64, 26.5.2): utimes on a watched file records a metadata event passes, which is the end-to-end confirmation that Op::METADATA is now reachable on kqueue.

The kqueue semantics were checked against macOS 26.5.2 with a standalone probe rather than assumed — NOTE_ATTRIB fires for utimes/chmod on a file vnode, and a directory vnode reports it only for its own metadata, never its entries. That probe also falsified an earlier revision of this change which requested NOTE_EXTEND: it never arrives without NOTE_WRITE, so it was removed.

Windows is compile-checked only (cargo check --target x86_64-pc-windows-msvc); I have no Windows host, so CI is the gate for those hunks.

Two pre-existing macOS failures are worth noting since they show up in any local macOS run of these suites, and reproduce identically on main built from the same commit — neither is from this change: test/cli/hot/hot.test.ts hangs after should work with sourcemap generation (indefinitely, because that file sets longTimeout = isDebug ? Infinity : 30_000), and fs.watch(dir) on macOS does not leak the resolved FSEvents path times out.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: acb5ce68-77fb-4c96-90bd-f1c5f52a0354

📥 Commits

Reviewing files that changed from the base of the PR and between e61c15e and 7b49013.

📒 Files selected for processing (4)
  • src/watcher/INotifyWatcher.rs
  • src/watcher/Watcher.rs
  • src/watcher/WindowsWatcher.rs
  • test/cli/watch/watcher-trace.test.ts

Walkthrough

Changes

The watcher now reports metadata and move-from operations across inotify, kqueue, and Windows backends. Inotify queue overflow triggers batched synthetic write updates for watched paths. Trace tests add helpers and coverage for metadata and rename events.

Watcher event handling

Layer / File(s) Summary
Platform event contracts and kqueue registration
src/watcher/Watcher.rs
Adds MOVE_FROM, trace-name mapping, and kind-specific kqueue attribute flags for files and directories.
Inotify event processing and overflow resynchronization
src/watcher/INotifyWatcher.rs
Adds metadata and move-from masks, translates them into watcher operations, and resynchronizes watched paths after queue overflow.
Windows action-to-operation mapping
src/watcher/WindowsWatcher.rs
Maps file actions through a single match, including rename-old move-from behavior.
Trace helpers and platform-specific coverage
test/cli/watch/watcher-trace.test.ts
Adds trace process/event helpers and tests metadata and rename trace events.

Possibly related PRs

  • oven-sh/bun#36248: Both changes add inotify attribute watching and translate IN::ATTRIB into metadata operations.

Suggested reviewers: robobun, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly summarizes the main watcher changes, including metadata, move_from, and overflow recovery.
Description check ✅ Passed The description follows the required template and includes both what changed and how it was verified with concrete details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Properrr

Copy link
Copy Markdown
Contributor Author

Disclosing an overlap I should have found before opening this: #36248 already contains one hunk from this PR.

It adds IN::ATTRIB to the inotify file mask and the IN::ATTRIB → Op::METADATA mapping, reached from a different direction — as support for watching parent directories of imports outside cwd, with the rationale "IN_ATTRIB catches the link-count drop when a held-open inode is renamed over." If #36248 lands first, that part of this diff becomes a trivial conflict to drop; happy to rebase whenever.

The rest of this PR is not covered by #36248, which only touches the inotify side:

  • kqueue never requested NOTE_ATTRIB. watch_event_from_kevent read it, but add_file_descriptor_to_kqueue_without_checks only ever asked for NOTE_WRITE|NOTE_RENAME|NOTE_DELETE. So Op::METADATA stayed dead on macOS even with watcher: report a file that was replaced by rename on Linux #36248's inotify change. Fixing that is what the WatchItemKind threading is for — it is requested for files only, because a directory vnode raises NOTE_ATTRIB solely for its own metadata, never for its entries (verified on macOS 26.5.2 with a standalone probe).
  • IN_MOVED_FROM / Op::MOVE_FROM — the departure half of a rename inside a watched directory.
  • Windows FILE_ACTION_ADDED / RENAMED_NEW_NAME producing a WatchEvent with no op bits at all.
  • IN_Q_OVERFLOW being silently discarded by the watchlist lookup.

If it would be easier to review, I can strip the IN_ATTRIB inotify hunk from this PR and rebase it onto #36248 so the two don't touch the same lines. Just say which you prefer.

I also closed #36417 (my watcher-shutdown PR) as a duplicate of #36253 for the same reason — that one was fully superseded.

… overflow

`src/watcher` mapped several `Op` bits it never asked the OS for, so those bits
were unreachable and some filesystem changes produced no event at all.

- `Op::METADATA` was dead on *every* platform. kqueue read `NOTE_ATTRIB` in
  `watch_event_from_kevent`, but `add_file_descriptor_to_kqueue_without_checks`
  only requested `NOTE_WRITE|NOTE_RENAME|NOTE_DELETE`; inotify never had
  `IN_ATTRIB` in either mask. `touch` and `chmod` therefore produced no watcher
  event on Linux at all, while ReadDirectoryChangesW reported them as a write.
  Now requested for **file** watches on both backends. Deliberately kept off
  directory watches: on inotify, `IN_ATTRIB` on a directory reports metadata
  changes of every entry inside it *by name*, and consumers treat a named
  directory event as "re-resolve this entry", so a bare `touch` would reload the
  module. Threading `WatchItemKind` into the kqueue registration is what makes
  that split expressible.

- A rename inside a watched directory only reported its arrival half
  (`IN_MOVED_TO`). `IN_MOVED_FROM` is now requested and mapped to a new
  `Op::MOVE_FROM`, so the vacated name is invalidated too.

- On Windows, `FILE_ACTION_ADDED` and `FILE_ACTION_RENAMED_NEW_NAME` produced a
  `WatchEvent` with no op bits at all, making file creation invisible to every
  consumer matching on `WRITE|DELETE|RENAME`. They now map to `CREATE` and
  `MOVE_TO`. `RENAMED_OLD_NAME` keeps `RENAME` alongside the new `MOVE_FROM` so
  existing consumers are unaffected.

Dropped events were also ignored: `IN_Q_OVERFLOW` arrives with `wd == -1`,
matches no watchlist entry, and fell through the lookup in `watch_loop_cycle`, so
changes the kernel discarded were never noticed. `resync_after_overflow` now
replays a synthetic `WRITE` per watched path. `WRITE` rather than `DELETE` is
deliberate: it makes consumers re-check without driving the eviction path and its
fixed 8096-entry `evict_list`.

Two tests assert the new events through `BUN_WATCHER_TRACE`; both fail against a
build without this change (no event at all for `utimes`, and only `move_to` for a
rename). Platform skips are capability-based: ReadDirectoryChangesW cannot
distinguish metadata from a content write, and kqueue has no per-entry departure
event to map to `move_from`.

Verified on Linux (test/cli/watch, test/cli/hot: 24 pass, 0 fail) and on macOS
arm64, where `utimes on a watched file records a metadata event` passes — the
end-to-end confirmation that `Op::METADATA` is reachable on kqueue. The kqueue
semantics were checked against macOS 26.5.2 with a standalone probe: NOTE_ATTRIB
fires for utimes/chmod on a file vnode, and a directory vnode reports it only for
its own metadata, never its entries. Windows is compile-checked only.
@Properrr
Properrr force-pushed the claude/watcher-report-unrequested-events branch from 7b49013 to 139cd5f Compare July 30, 2026 22:02
@Properrr

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (was conflicting, now mergeable). Force-pushed 7b490133b → 139cd5fe6; no behavior change from the rebase.

Two conflicts, both from main narrowing visibility on functions this PR also touches:

  • Watcher.rs — add_file_descriptor_to_kqueue_without_checks became pub(crate) upstream while this PR added the kind: WatchItemKind parameter. Kept both.
  • WindowsWatcher.rs — create_watch_event became private upstream while this PR rewrote its body to the match that adds MOVE_FROM/MOVE_TO/CREATE. Kept upstream's visibility and this PR's body.

Worth flagging that the Watcher.rs conflict sits inside #[cfg(any(target_os = "macos", target_os = "freebsd"))], so a Linux-only cargo check would not have caught a bad resolution. Verified with cargo check -p bun_watcher --target aarch64-apple-darwin and --target aarch64-pc-windows-msvc, both clean. test/cli/watch/watcher-trace.test.ts: 6 pass, 0 fail.

The overlap with #36248 I noted above still stands — it has not merged, so nothing to drop yet.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant