Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/runtime/node/path_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,19 +1018,7 @@ impl Linux {
};

let is_dir_child = ev.mask & IN::ISDIR != 0;
let event_type: WatchEventKind = if ev.mask
& (IN::CREATE
| IN::DELETE
| IN::DELETE_SELF
| IN::MOVE_SELF
| IN::MOVED_FROM
| IN::MOVED_TO)
!= 0
{
WatchEventKind::Rename
} else {
WatchEventKind::Change
};
let is_structural = ev.mask & !(IN::ATTRIB | IN::MODIFY | IN::ISDIR) != 0;

// Dispatch to every owner of this wd. The recursive branch below calls
// `addOne`/`walkAndAdd`, which insert into `wd_map` via `getOrPut` and
Expand Down Expand Up @@ -1075,6 +1063,19 @@ impl Linux {
)
};

// libuv: a directory's attribute change is "rename" (IN_ISDIR is
// outside IN_ATTRIB|IN_MODIFY). node's recursive watcher drops it.
Comment thread
robobun marked this conversation as resolved.
let event_type = if is_structural {
WatchEventKind::Rename
} else if !is_dir_child {
WatchEventKind::Change
} else if watcher_recursive {
oi += 1;
continue;
} else {
WatchEventKind::Rename
};
Comment thread
robobun marked this conversation as resolved.

// Build the path relative to this owner's root.
let rel: &[u8] = if watcher_is_file {
path::basename(watcher_path)
Expand Down
61 changes: 61 additions & 0 deletions test/js/node/watch/fs.watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,67 @@ describe("fs.watch", () => {
]);
});

// 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, so node
// reports a directory's attribute change as "rename" and a file's as "change".
// https://github.com/oven-sh/bun/issues/43066
async function collectWatchEventsUntil(
target: string,
options: fs.WatchOptions,
count: number,
act: () => void,
): Promise<[string, string | null][]> {
const events: [string, string | null][] = [];
const { promise, resolve, reject } = Promise.withResolvers<[string, string | null][]>();
const watcher = fs.watch(target, options, (eventType, filename) => {
events.push([eventType, filename]);
if (events.length === count) resolve(events.slice());
});
watcher.once("error", reject);
try {
act();
return await promise;
} finally {
watcher.close();
}
}

const toggleMode = (p: string) => fs.chmodSync(p, fs.statSync(p).mode ^ 0o001);

test.skipIf(!isLinux)("changing the attributes of the watched directory reports rename", async () => {
using dir = tempDir("fs-watch-chmod-self", { "sub": {} });
const target = path.join(String(dir), "sub");
expect(await collectWatchEventsUntil(target, {}, 1, () => toggleMode(target))).toEqual([["rename", "sub"]]);
});

test.skipIf(!isLinux)("changing the attributes of a subdirectory reports rename, of a file change", async () => {
using dir = tempDir("fs-watch-chmod-child", { "sub": {}, "f.txt": "x" });
const root = String(dir);
const events = await collectWatchEventsUntil(root, {}, 2, () => {
toggleMode(path.join(root, "sub"));
toggleMode(path.join(root, "f.txt"));
});
expect(events).toEqual([
["rename", "sub"],
["change", "f.txt"],
]);
});

// node's recursive watcher is not libuv: an attribute change of a directory,
// the root or one below it, is not reported at all. A file's still is.
test.skipIf(!isLinux)("a recursive watch does not report an attribute change of a directory", async () => {
using dir = tempDir("fs-watch-chmod-recursive", { "sub": {}, "f.txt": "x" });
const root = String(dir);
// inotify delivers events in order, so a directory event would land
// before the chmod of f.txt.
const events = await collectWatchEventsUntil(root, { recursive: true }, 1, () => {
toggleMode(root);
toggleMode(path.join(root, "sub"));
toggleMode(path.join(root, "f.txt"));
});
expect(events).toEqual([["change", "f.txt"]]);
});

// Past fs.inotify.max_queued_events the kernel drops events and queues one
// IN_Q_OVERFLOW; Bun reports it as ('change', null) on every watcher sharing
// the inotify fd, the same shape node uses for overflow on Windows.
Expand Down
Loading