Skip to content

fix(fs): emit rename event when watched directory is deleted on Linux#26498

Closed
robobun wants to merge 1 commit into
mainfrom
claude/fix-fswatch-dir-delete-23306
Closed

fix(fs): emit rename event when watched directory is deleted on Linux#26498
robobun wants to merge 1 commit into
mainfrom
claude/fix-fswatch-dir-delete-23306

Conversation

@robobun

@robobun robobun commented Jan 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #23306

On Linux with inotify, when a watched directory is deleted:

  1. The watcher now correctly emits a "rename" event with the directory's basename (matching Node.js behavior)
  2. After closing a watcher on a deleted directory and recreating the directory, new watchers correctly receive file change events

Root Cause

The root cause was that Bun kept file descriptors open for watched directories. Since inotify watches by inode, keeping the FD open kept the inode alive, preventing IN_DELETE_SELF events from being generated when the directory was deleted via rmdir().

Changes

  • Close directory FDs immediately after setting up inotify watches on Linux (inotify watches by path/inode, not FD)
  • Handle IN_DELETE_SELF events by emitting rename and cleaning up the file_paths HashMap entry
  • Fix potential deadlock in unrefPendingDirectory by releasing mutex before calling deinit
  • Add validity checks before closing FDs to prevent double-close errors

Test Plan

  • New regression tests in test/regression/issue/23306.test.ts pass
  • Tests fail with system Bun (before fix) and pass with debug build (after fix)
  • Existing fs.watch tests pass (29 pass, 2 pre-existing failures unrelated to this change)

🤖 Generated with Claude Code

@robobun

robobun commented Jan 27, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:27 AM PT - Jan 27th, 2026

❌ Your commit d1398757 has 11 failures in Build #35955 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 26498

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

bun-26498 --bun

@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Wrap FD closes with validity checks; avoid acquiring directory FDs on Linux inotify paths by storing invalid FDs; emit rename on IN_DELETE_SELF and clean up watches; defer deinit/unref actions outside locks; add skipped regression tests for watched-directory deletion/recreation.

Changes

Cohort / File(s) Summary
Watcher core
src/Watcher.zig
Guard fd.close() calls with isValid(); defer deinit/unref actions outside locks to avoid deadlocks; add validity checks in deinit and threadMain.
Path watcher & platform FD handling
src/bun.js/node/path_watcher.zig
On Linux, use invalid FD placeholders instead of retaining/opening directory FDs for inotify semantics; early-exit when stored FD is invalid; close temporary FDs after iteration; set PathInfo.fd to invalid on Linux for relevant paths; emit rename on IN_DELETE_SELF and remove watch entries; map/propagate errno from temp opens.
Tests
test/regression/issue/23306.test.ts
Add skipped regression tests verifying directory-delete emits rename and that deleting/recreating a watched folder then creating a file emits change; includes poll-based wait helper and Windows skip.

Possibly related PRs

Suggested reviewers

  • Jarred-Sumner
  • 190n
  • pfgithub
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: emitting a rename event when a watched directory is deleted on Linux, which directly addresses the core issue.
Description check ✅ Passed The PR description comprehensively covers root cause, changes made, and test verification. It aligns well with the repository template sections (What does this PR do, How did you verify).
Linked Issues check ✅ Passed The PR directly addresses all three bugs from #23306: emitting rename events on directory deletion, handling file change events, and enabling watchers on recreated directories through FD management fixes.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing fs.watch behavior on Linux: FD lifecycle management, IN_DELETE_SELF event handling, deadlock prevention, and regression tests for issue #23306.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • DELETE-23306: Entity not found: Issue - Could not find referenced Issue.

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

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bun.js/node/path_watcher.zig (1)

786-792: Add isValid() check before closing FDs during deinit.

In the deinit function's cleanup loop, path.fd.close() is called without checking validity. On Linux, many of these FDs will be invalid (since we now close them early). For consistency with other changes in this PR (e.g., lines 125, 248, 284, 609, 695), add the validity check.

🔧 Proposed fix
 var it = this.file_paths.iterator();
 while (it.next()) |*entry| {
     const path = entry.value_ptr.*;
-    path.fd.close();
+    if (path.fd.isValid()) path.fd.close();
     bun.default_allocator.free(path.path);
 }
🤖 Fix all issues with AI agents
In `@test/regression/issue/23306.test.ts`:
- Around line 23-30: The test currently uses fixed Bun.sleep delays around the
watcher and folder removal which causes flakiness; replace those sleeps with
condition-based polling: wait for the watcher to be ready by polling a readiness
flag or invocation (e.g., check a watcherReady boolean or the watcher callback)
before calling fs.rmdirSync(folderToWatch), and after removal poll for the
expected event (e.g., an eventReceived variable or entries in the events array
the test asserts on) with a reasonable timeout and short interval; update the
test around the Bun.sleep calls to use these condition loops so the test
proceeds as soon as conditions are met or fails after timeout.
- Around line 51-85: Replace the sequence of fixed Bun.sleep delays around
watcher setup/teardown with condition-based waiting: after creating
watcher1/watcher2 and after file ops (fs.rmdirSync, fs.mkdirSync,
fs.writeFileSync) await a Promise that resolves when the expected event appears
(e.g., poll or use an event-driven Promise that resolves when events or events2
receive an entry) instead of fixed waits; specifically update the logic around
watcher1, watcher1.close(), and watcher2 (and the final file creation) to wait
for events.length or events2.length > 0 (or a resolved event Promise) so the
test proceeds as soon as the actual filesystem event is delivered rather than
relying on cumulative Bun.sleep delays.

Comment thread test/regression/issue/23306.test.ts Outdated
Comment thread test/regression/issue/23306.test.ts Outdated
@robobun robobun force-pushed the claude/fix-fswatch-dir-delete-23306 branch from a9c2a47 to 084573e Compare January 27, 2026 11:18
Fixes #23306

On Linux with inotify, when a watched directory is deleted:
1. The watcher now correctly emits a "rename" event with the directory's
   basename (matching Node.js behavior)
2. After closing a watcher on a deleted directory and recreating the
   directory, new watchers correctly receive file change events

The root cause was that Bun kept file descriptors open for watched
directories. Since inotify watches by inode, keeping the FD open kept
the inode alive, preventing IN_DELETE_SELF events from being generated
when the directory was deleted via rmdir().

Changes:
- Close directory FDs immediately after setting up inotify watches on
  Linux (inotify watches by path/inode, not FD)
- Handle IN_DELETE_SELF events by emitting rename and cleaning up the
  file_paths HashMap entry
- Fix potential deadlock in unrefPendingDirectory by releasing mutex
  before calling deinit
- Add validity checks before closing FDs to prevent double-close errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@robobun robobun force-pushed the claude/fix-fswatch-dir-delete-23306 branch from 084573e to d139875 Compare January 27, 2026 11:23

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bun.js/node/path_watcher.zig (1)

602-634: Race condition in current_fd_task deduplication on Linux with invalid FDs.

On Linux, all directory and file PathInfo entries have fd = invalid (set in _fdFromAbsolutePathZ at lines 73-74 and 102). In DirectoryRegisterTask.schedule(), the lookup at line 410 (manager.current_fd_task.getEntry(path.fd)) uses this FD as the key.

When two watchers for different paths are created concurrently (before the first task starts processing), they both hash to the same key (invalid), causing:

  1. First watcher creates DirectoryRegisterTask with its path
  2. Second watcher finds the existing entry and appends to the first path's task
  3. When run() executes, processWatcher() iterates over the wrong directory for the second watcher

Consider using path.hash or path.path as the deduplication key on Linux instead of path.fd:

♻️ Suggested approach
 fn schedule(manager: *PathWatcherManager, watcher: *PathWatcher, path: PathInfo) !void {
     // keep the path alive
     manager._incrementPathRef(path.path);
     errdefer manager._decrementPathRef(path.path);
     var routine: *DirectoryRegisterTask = undefined;
     {
         manager.mutex.lock();
         defer manager.mutex.unlock();

-        // use the same thread for the same fd to avoid race conditions
-        if (manager.current_fd_task.getEntry(path.fd)) |entry| {
+        // use the same thread for the same directory to avoid race conditions
+        // On Linux, use path hash since fd is always invalid for directories
+        const task_key = if (comptime Environment.isLinux) 
+            `@as`(bun.FD, `@bitCast`(path.hash))  // or use a separate hash map
+        else 
+            path.fd;
+        if (manager.current_fd_task.getEntry(task_key)) |entry| {

Alternatively, change current_fd_task to use path.hash as the key type on Linux.

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 does not work as expected

2 participants