Skip to content

fs.watch: report a directory's attribute change as rename on Linux - #43083

Open
robobun wants to merge 4 commits into
mainfrom
robobun/0847ccca/fs-watch-dir-attrib-rename
Open

robobun wants to merge 4 commits into
mainfrom
robobun/0847ccca/fs-watch-dir-attrib-rename

Conversation

@robobun

@robobun robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • The event kind is now one decision per owner: a structural bit is rename, a file's attribute change is change, a directory's attribute change is rename for a plain watch and no event for a recursive watch.
  • A recursive watch drops the directory attribute event for the root and for a subdirectory. Node's recursive watcher (lib/internal/fs/recursive_watch.js) rescans a directory whose watch fires and reports only entries that came or went, so it reports nothing in both cases. Verified against node v26.3.0.
  • Verified: test/js/node/watch/fs.watch.test.ts (three new Linux-only tests, all fail on bun 1.4.3). Also all of test/js/node/watch/ and the test-fs-watch* and test-fs-promises-watch* files in test/js/node/test/parallel/.
  • Self-reviewed: 4 concerns raised, 3 addressed. Not addressed: the suggestion to stack this on fs.watch: report a watched symlink under its own name on Linux #43064. The change is independent of it. Both touch the same loop, so the second to land needs a small rebase.

Background

Notes

node v26.3.0 on Linux, same script as the issue, plus a recursive watch with a subdirectory:

nonrec chmod dir   node: rename:"watched"   bun 1.4.3: change:"watched"
nonrec chmod sub   node: rename:"sub"       bun 1.4.3: change:"sub"
nonrec chmod f.txt node: change:"f.txt"     bun 1.4.3: change:"f.txt"
rec    chmod root  node: (none)             bun 1.4.3: change:undefined
rec    chmod sub   node: (none)             bun 1.4.3: change:"sub"
rec    chmod f.txt node: change:"f.txt"     bun 1.4.3: change:"f.txt"

With this PR, bun matches node on every row.

IN_MODIFY never carries IN_ISDIR, so the only event this changes is IN_ATTRIB on a directory.

The new tests resolve on an event count and copy the list at that moment. writeFileSync queues IN_CREATE and IN_MODIFY in one batch, so the recursive test uses chmod of a file as its terminating event.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/watch/fs.watch.test.ts

libuv maps every inotify mask bit outside IN_ATTRIB|IN_MODIFY to rename,
and the kernel sets IN_ISDIR on every event about a directory. node thus
reports a chmod of the watched directory, or of a subdirectory, as
rename. Bun reported change.

node's recursive watcher is not libuv. It reports no event for an
attribute change of a directory, root or subdirectory. Bun reported
change, with an undefined filename for the root. A recursive watch now
reports nothing for it.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview 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: Essentials

Run ID: a6d879cf-65a9-4c85-9bec-a56d089fa4ed

📥 Commits

Reviewing files that changed from the base of the PR and between b52d513 and f6e9115.

📒 Files selected for processing (2)
  • src/runtime/node/path_watcher.rs
  • test/js/node/watch/fs.watch.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Linux inotify event classification now distinguishes structural, directory attribute, and file attribute events. Recursive watchers suppress directory-child events. Linux tests cover root directory, child directory, child file, and recursive behavior.

Changes

Linux fs.watch event handling

Layer / File(s) Summary
Inotify event classification and dispatch
src/runtime/node/path_watcher.rs
Structural masks now include all bits outside IN_ATTRIB, IN_MODIFY, and IN_ISDIR. Recursive watchers skip directory-child events, while nonrecursive directory watchers emit rename and file watchers emit change.
Attribute-event test coverage
test/js/node/watch/fs.watch.test.ts
Added an event collector and Linux tests for watched-directory, child-directory, child-file, and recursive attribute changes.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f6e91

The Linux watcher classification and its tests have no confirmed issue requiring changes before merge.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: Linux fs.watch now reports directory attribute changes as rename events.
Description check ✅ Passed The description explains the problem, fix, scope, verification steps, test coverage, and known limitations. It does not use the exact template headings, but it provides the required information in equ…
Linked Issues check ✅ Passed The changes satisfy the coding objectives in issue #43066 that this PR targets. src/runtime/node/path_watcher.rs classifies directory attribute events as rename, keeps file attribute events as `ch…
Out of Scope Changes check ✅ Passed The source change updates Linux inotify classification and recursive dispatch for the behaviors described in #43066. The added tests directly verify those behaviors. No unrelated product behavior or u…

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

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The automatic review was rate limited and produced no findings. The diff is two files: one decision in the inotify dispatch in src/runtime/node/path_watcher.rs and three Linux-only tests in test/js/node/watch/fs.watch.test.ts. CI is running. Nothing to address from this comment.

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the new continue for a recursive owner: it skips only the touched insertion and the add-watch branch, and the latter is gated on IN_CREATE|IN_MOVED_TO (structural), so no subdirectory watch is missed. The non-recursive root IN_ATTRIB case still reaches the nameless-event branch and reports basename(watched path), matching libuv.

Extended reasoning...

The inline IN_UNMOUNT finding stands on its own. Separately, I traced the early continue at path_watcher.rs:1087-1089 through the rest of the per-owner loop: the only work it bypasses is touched.get_or_put (only needed when something was emitted) and the recursive add-watch block, which requires a structural bit that the skipped branch can never have. I also confirmed the non-recursive root attribute event (no name, IN_ISDIR set) now maps to Rename and still falls into the basename(watcher_path) branch at line 1098, which is the libuv behavior the PR cites. The macOS/Windows arms are untouched, which is consistent with libuv's FSEvents/ReadDirectoryChangesW mapping of metadata changes to change rather than a divergence introduced here.

2 verified lower-impact observations (convention, logging or cleanup points) were not posted.

Comment thread src/runtime/node/path_watcher.rs
Comment thread src/runtime/node/path_watcher.rs Outdated
Comment thread src/runtime/node/path_watcher.rs Outdated
@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the trace. The IN_UNMOUNT point is real: a recursive watch on an unmounted filesystem would have gone silent. da526b1 changes the structural test to the libuv rule (every bit outside IN_ATTRIB|IN_MODIFY|IN_ISDIR is rename), which covers IN_UNMOUNT. The three new tests and the rest of test/js/node/watch/ still pass.

Comment thread src/runtime/node/path_watcher.rs Outdated
@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

50b7532 shortens the two comments the lint flagged. The first one is removed. The second is down to the libuv rule, its link, and the one line on node's recursive watcher. No code change.

@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:31 AM PT - Sep 17th, 2026

✅ @robobun, your commit f6e911585fb9bdbf472e93ed0d625e524d7acf5e passed in Build #117064! 🎉


🧪   To try this PR locally:

bunx bun-pr 43083

That installs a local version of the PR into your bun-43083 executable, so you can run:

bun-43083 --bun

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

f6e9115 trims the remaining comment to two lines. The coderabbit review produced no findings. No code change.

Comment thread src/runtime/node/path_watcher.rs
@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The comment lint flags the two-line comment at the event kind decision again. I keep it as is. It names the libuv rule the code mirrors and the reason a recursive watch skips the event. It is not a justification of a workaround. No further push for this.

@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.

I re-reviewed the current head after the follow-up commits and found no bugs; the IN_UNMOUNT concern from the earlier pass is now covered by the !(IN_ATTRIB|IN_MODIFY|IN_ISDIR) structural test. A human look is still worthwhile because the recursive-watch behavior (silently dropping a directory's IN_ATTRIB) is a Node-compat claim I could not verify against a Node binary here, and the new tests are Linux-only so proof rests on CI.

What was reviewed:

  • The per-owner event_type decision in path_watcher.rs:1066-1077: the recursive continue skips emit, touched, and the add-watch block, and none of those are needed for a pure IN_ATTRIB|IN_ISDIR event (add-watch requires IN_CREATE|IN_MOVED_TO, touched only drives flush).
  • The subscribed masks (WATCH_DIR_MASK/file mask) contain no IN_ACCESS/IN_OPEN/IN_CLOSE bits, so is_structural cannot misclassify events Bun never asked for; IN_Q_OVERFLOW and IN_IGNORED are still handled before this point.
  • The three new tests: each fails on the old classification (test 1 yields change), should_emit dedupe cannot fold the sub/f.txt pairs (different paths), and the watcher is closed in finally with errors wired to reject.
Extended reasoning...

Overview

The PR changes how the Linux inotify backend of fs.watch (src/runtime/node/path_watcher.rs, Linux::thread_main) classifies an event as rename vs change. The old single pre-loop decision (rename iff a create/delete/move bit is set) is replaced by a pre-loop is_structural = mask & !(IN_ATTRIB|IN_MODIFY|IN_ISDIR) != 0 and a per-owner decision: structural -> Rename; non-directory -> Change; directory attribute event on a recursive owner -> skipped; on a non-recursive owner -> Rename. Three Linux-only tests in test/js/node/watch/fs.watch.test.ts cover chmod of the watched directory, of a subdirectory and file under a plain watch, and of root/subdir/file under recursive: true.

Security risks

None identified. The change only alters which WatchEventKind is passed to emit for events on already-registered watch descriptors; it adds no new parsing of untrusted data, no new allocation, and no new unsafe blocks. The continue path is taken before any raw-pointer use beyond the scalar copies already made under manager.mutex, so the existing SAFETY invariants around wd_map rehashing are unaffected.

Level of scrutiny

Moderate. The diff is small (about 15 lines of logic) but sits in a subtle area: shared-inotify-fd dispatch with per-owner semantics and a Node-compat behavior claim for the recursive watcher. I traced the skipped side effects (emit, touched.get_or_put, the recursive add-watch branch) and confirmed none apply to a pure IN_ATTRIB|IN_ISDIR event: add-watch requires IN_CREATE|IN_MOVED_TO (structural, so never reaches the skip), and touched only drives flush, which is meaningless when nothing was emitted. I also checked the subscribed masks (WATCH_DIR_MASK and the file mask at lines 720-729) to confirm the widened structural rule cannot misclassify access/open/close events, since Bun never subscribes to them; IN_Q_OVERFLOW and IN_IGNORED are still intercepted earlier. The earlier concern about IN_UNMOUNT|IN_ISDIR being swallowed on recursive watches is addressed by excluding IN_ISDIR from the negated set. What I could not do is execute a Node binary in this environment to confirm the recursive-watcher rows of the author's table, so that compat claim rests on the author's verification against node v26.3.0.

Other factors

The tests are wired correctly: the helper resolves on an exact event count, rejects on watcher error, and closes in finally; the assertions are exact toEqual on the full event list; test 1 fails on the old code (change instead of rename), so the suite is not vacuous. should_emit dedupes only same-path same-type events within a millisecond, which cannot collapse the sub/f.txt pairs. The tests are Linux-only and the PR itself notes no local test proof, so CI is the first real run of them. Combined with the unverifiable Node-compat claim for the recursive case, deferring to a human rather than approving is the honest call.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Both reviews of f6e9115 report no findings. On the open point, the recursive rows of the table in the PR body come from node v26.3.0 on Linux x64, run with the same script as bun. With a recursive watch, node reports no event for a chmod of the root or of a subdirectory, and change:"f.txt" for a chmod of a file. The Linux-only tests pass on the debug build here. CI is the next signal.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fs.watch on Linux: an event about the watched directory itself differs from Node (event type, filename for a trailing slash, recursive root)

1 participant