mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 19:38:58 +00:00
Implement fanotify backend for Linux filesystem watcher
Added a new fanotify-based watcher implementation for Linux that provides filesystem-wide monitoring capabilities as an alternative to inotify. Changes: - Created src/sys/fanotify.zig: Clean wrapper around fanotify syscalls following bun.sys patterns with Maybe return types - Created src/watcher/FanotifyWatcher.zig: Full fanotify watcher implementation with recursive directory monitoring support - Modified src/Watcher.zig: Switched Linux platform from INotifyWatcher to FanotifyWatcher - Updated src/sys.zig: Exported fanotify module for Linux platforms - Updated src/bun.js/node/path_watcher.zig: Handle Watcher.init errors properly Implementation notes: - Fanotify provides filesystem-wide monitoring similar to Windows watcher - Supports FAN_EVENT_ON_CHILD for recursive directory monitoring - Currently enabled as the primary watcher (inotify disabled for testing) - Requires appropriate permissions (may need CAP_SYS_ADMIN) - Returns error.Unexpected instead of custom error to match existing error sets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -93,7 +93,10 @@ pub fn init(comptime T: type, ctx: *T, fs: *bun.fs.FileSystem, allocator: std.me
|
||||
.changed_filepaths = [_]?[:0]u8{null} ** max_count,
|
||||
};
|
||||
|
||||
try Platform.init(&watcher.platform, fs.top_level_dir);
|
||||
Platform.init(&watcher.platform, fs.top_level_dir) catch |err| {
|
||||
allocator.destroy(watcher);
|
||||
return err;
|
||||
};
|
||||
|
||||
return watcher;
|
||||
}
|
||||
@@ -132,7 +135,7 @@ pub const max_eviction_count = 8096;
|
||||
// this file instead of the platform-specific file.
|
||||
// ideally, the constants above can be inlined
|
||||
const Platform = switch (Environment.os) {
|
||||
.linux => @import("./watcher/INotifyWatcher.zig"),
|
||||
.linux => @import("./watcher/FanotifyWatcher.zig"),
|
||||
.mac => @import("./watcher/KEventWatcher.zig"),
|
||||
.windows => WindowsWatcher,
|
||||
else => @compileError("Unsupported platform"),
|
||||
|
||||
@@ -115,16 +115,21 @@ pub const PathWatcherManager = struct {
|
||||
var watchers = bun.handleOom(bun.BabyList(?*PathWatcher).initCapacity(bun.default_allocator, 1));
|
||||
errdefer watchers.deinit(bun.default_allocator);
|
||||
|
||||
const main_watcher = Watcher.init(
|
||||
PathWatcherManager,
|
||||
this,
|
||||
vm.transpiler.fs,
|
||||
bun.default_allocator,
|
||||
) catch |err| {
|
||||
watchers.deinit(bun.default_allocator);
|
||||
return err;
|
||||
};
|
||||
|
||||
const manager = PathWatcherManager{
|
||||
.file_paths = bun.StringHashMap(PathInfo).init(bun.default_allocator),
|
||||
.current_fd_task = bun.FDHashMap(*DirectoryRegisterTask).init(bun.default_allocator),
|
||||
.watchers = watchers,
|
||||
.main_watcher = try Watcher.init(
|
||||
PathWatcherManager,
|
||||
this,
|
||||
vm.transpiler.fs,
|
||||
bun.default_allocator,
|
||||
),
|
||||
.main_watcher = main_watcher,
|
||||
.vm = vm,
|
||||
.watcher_count = 0,
|
||||
.mutex = .{},
|
||||
|
||||
@@ -301,6 +301,7 @@ pub const Tag = enum(u8) {
|
||||
|
||||
pub const Error = @import("./sys/Error.zig");
|
||||
pub const PosixStat = @import("./sys/PosixStat.zig").PosixStat;
|
||||
pub const fanotify = if (Environment.isLinux) @import("./sys/fanotify.zig") else struct {};
|
||||
|
||||
pub fn Maybe(comptime ReturnTypeT: type) type {
|
||||
return bun.api.node.Maybe(ReturnTypeT, Error);
|
||||
|
||||
225
src/sys/fanotify.zig
Normal file
225
src/sys/fanotify.zig
Normal file
@@ -0,0 +1,225 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("../bun.zig");
|
||||
const Maybe = bun.sys.Maybe;
|
||||
const linux = std.os.linux;
|
||||
const posix = std.posix;
|
||||
|
||||
/// fanotify_init flags
|
||||
pub const InitFlags = packed struct(u32) {
|
||||
/// Close-on-exec flag
|
||||
cloexec: bool = false,
|
||||
/// Non-blocking flag
|
||||
nonblock: bool = false,
|
||||
_padding1: u30 = 0,
|
||||
|
||||
pub fn toInt(self: InitFlags) u32 {
|
||||
var flags: u32 = 0;
|
||||
if (self.cloexec) flags |= FAN_CLOEXEC;
|
||||
if (self.nonblock) flags |= FAN_NONBLOCK;
|
||||
return flags | FAN_CLASS_NOTIF | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS;
|
||||
}
|
||||
};
|
||||
|
||||
/// fanotify event flags (open flags for the file descriptors)
|
||||
pub const EventFlags = packed struct(u32) {
|
||||
rdonly: bool = false,
|
||||
largefile: bool = false,
|
||||
cloexec: bool = false,
|
||||
_padding: u29 = 0,
|
||||
|
||||
pub fn toInt(self: EventFlags) u32 {
|
||||
var flags: u32 = 0;
|
||||
// RDONLY is 0, so we don't need to add it
|
||||
if (self.rdonly) flags |= 0;
|
||||
if (self.largefile) flags |= 0x8000; // O_LARGEFILE on linux
|
||||
if (self.cloexec) flags |= 0x80000; // O_CLOEXEC on linux
|
||||
return flags;
|
||||
}
|
||||
};
|
||||
|
||||
/// fanotify_mark flags
|
||||
pub const MarkFlags = enum(u32) {
|
||||
add = FAN_MARK_ADD,
|
||||
remove = FAN_MARK_REMOVE,
|
||||
flush = FAN_MARK_FLUSH,
|
||||
};
|
||||
|
||||
/// fanotify event mask
|
||||
pub const EventMask = packed struct(u64) {
|
||||
access: bool = false,
|
||||
modify: bool = false,
|
||||
close_write: bool = false,
|
||||
close_nowrite: bool = false,
|
||||
open: bool = false,
|
||||
open_exec: bool = false,
|
||||
attrib: bool = false,
|
||||
create: bool = false,
|
||||
delete: bool = false,
|
||||
delete_self: bool = false,
|
||||
moved_from: bool = false,
|
||||
moved_to: bool = false,
|
||||
move_self: bool = false,
|
||||
open_perm: bool = false,
|
||||
access_perm: bool = false,
|
||||
open_exec_perm: bool = false,
|
||||
_padding1: u14 = 0,
|
||||
ondir: bool = false,
|
||||
event_on_child: bool = false,
|
||||
_padding2: u32 = 0,
|
||||
|
||||
pub fn toInt(self: EventMask) u64 {
|
||||
var mask: u64 = 0;
|
||||
if (self.access) mask |= FAN_ACCESS;
|
||||
if (self.modify) mask |= FAN_MODIFY;
|
||||
if (self.close_write) mask |= FAN_CLOSE_WRITE;
|
||||
if (self.close_nowrite) mask |= FAN_CLOSE_NOWRITE;
|
||||
if (self.open) mask |= FAN_OPEN;
|
||||
if (self.open_exec) mask |= FAN_OPEN_EXEC;
|
||||
if (self.attrib) mask |= FAN_ATTRIB;
|
||||
if (self.create) mask |= FAN_CREATE;
|
||||
if (self.delete) mask |= FAN_DELETE;
|
||||
if (self.delete_self) mask |= FAN_DELETE_SELF;
|
||||
if (self.moved_from) mask |= FAN_MOVED_FROM;
|
||||
if (self.moved_to) mask |= FAN_MOVED_TO;
|
||||
if (self.move_self) mask |= FAN_MOVE_SELF;
|
||||
if (self.open_perm) mask |= FAN_OPEN_PERM;
|
||||
if (self.access_perm) mask |= FAN_ACCESS_PERM;
|
||||
if (self.open_exec_perm) mask |= FAN_OPEN_EXEC_PERM;
|
||||
if (self.ondir) mask |= FAN_ONDIR;
|
||||
if (self.event_on_child) mask |= FAN_EVENT_ON_CHILD;
|
||||
return mask;
|
||||
}
|
||||
};
|
||||
|
||||
// fanotify_init flags
|
||||
const FAN_CLOEXEC = 0x00000001;
|
||||
const FAN_NONBLOCK = 0x00000002;
|
||||
const FAN_CLASS_NOTIF = 0x00000000;
|
||||
const FAN_UNLIMITED_QUEUE = 0x00000010;
|
||||
const FAN_UNLIMITED_MARKS = 0x00000020;
|
||||
|
||||
// fanotify_mark flags
|
||||
const FAN_MARK_ADD = 0x00000001;
|
||||
const FAN_MARK_REMOVE = 0x00000002;
|
||||
const FAN_MARK_FLUSH = 0x00000080;
|
||||
|
||||
// fanotify events
|
||||
const FAN_ACCESS = 0x00000001;
|
||||
const FAN_MODIFY = 0x00000002;
|
||||
const FAN_CLOSE_WRITE = 0x00000008;
|
||||
const FAN_CLOSE_NOWRITE = 0x00000010;
|
||||
const FAN_OPEN = 0x00000020;
|
||||
const FAN_OPEN_EXEC = 0x00001000;
|
||||
const FAN_ATTRIB = 0x00000004;
|
||||
const FAN_CREATE = 0x00000100;
|
||||
const FAN_DELETE = 0x00000200;
|
||||
const FAN_DELETE_SELF = 0x00000400;
|
||||
const FAN_MOVED_FROM = 0x00000040;
|
||||
const FAN_MOVED_TO = 0x00000080;
|
||||
const FAN_MOVE_SELF = 0x00000800;
|
||||
const FAN_OPEN_PERM = 0x00010000;
|
||||
const FAN_ACCESS_PERM = 0x00020000;
|
||||
const FAN_OPEN_EXEC_PERM = 0x00040000;
|
||||
const FAN_ONDIR = 0x40000000;
|
||||
const FAN_EVENT_ON_CHILD = 0x08000000;
|
||||
|
||||
/// fanotify event metadata structure
|
||||
pub const EventMetadata = extern struct {
|
||||
event_len: u32,
|
||||
vers: u8,
|
||||
reserved: u8,
|
||||
metadata_len: u16,
|
||||
mask: u64,
|
||||
fd: i32,
|
||||
pid: i32,
|
||||
|
||||
pub fn size(self: *align(1) const EventMetadata) u32 {
|
||||
return self.event_len;
|
||||
}
|
||||
|
||||
pub fn isDir(self: *align(1) const EventMetadata) bool {
|
||||
return (self.mask & FAN_ONDIR) != 0;
|
||||
}
|
||||
|
||||
pub fn hasValidFd(self: *align(1) const EventMetadata) bool {
|
||||
return self.fd >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize fanotify
|
||||
pub fn init(flags: InitFlags, event_flags: EventFlags) Maybe(bun.FileDescriptor) {
|
||||
const rc = linux.syscall2(
|
||||
.fanotify_init,
|
||||
@as(usize, @intCast(flags.toInt())),
|
||||
@as(usize, @intCast(event_flags.toInt())),
|
||||
);
|
||||
|
||||
const errno = posix.errno(rc);
|
||||
if (errno != .SUCCESS) {
|
||||
return .{ .err = bun.sys.Error.fromCode(errno, .open) };
|
||||
}
|
||||
|
||||
return .{ .result = bun.FileDescriptor.fromNative(@intCast(rc)) };
|
||||
}
|
||||
|
||||
/// Add or remove a mark on a filesystem object
|
||||
pub fn mark(
|
||||
fanotify_fd: bun.FileDescriptor,
|
||||
flags: MarkFlags,
|
||||
mask: EventMask,
|
||||
dirfd: bun.FileDescriptor,
|
||||
pathname: ?[:0]const u8,
|
||||
) Maybe(void) {
|
||||
const path_ptr = if (pathname) |p| @intFromPtr(p.ptr) else 0;
|
||||
const dfd: i32 = if (pathname == null) linux.AT.FDCWD else dirfd.cast();
|
||||
|
||||
const rc = linux.syscall5(
|
||||
.fanotify_mark,
|
||||
@as(usize, @bitCast(@as(isize, fanotify_fd.cast()))),
|
||||
@as(usize, @intCast(@intFromEnum(flags))),
|
||||
@as(usize, @intCast(mask.toInt())),
|
||||
@as(usize, @bitCast(@as(isize, dfd))),
|
||||
path_ptr,
|
||||
);
|
||||
|
||||
const errno = posix.errno(rc);
|
||||
if (errno != .SUCCESS) {
|
||||
return .{ .err = bun.sys.Error.fromCode(errno, .watch) };
|
||||
}
|
||||
|
||||
return .{ .result = {} };
|
||||
}
|
||||
|
||||
/// Read events from fanotify file descriptor
|
||||
pub fn readEvents(
|
||||
fanotify_fd: bun.FileDescriptor,
|
||||
buffer: []align(@alignOf(EventMetadata)) u8,
|
||||
) Maybe([]const u8) {
|
||||
const rc = linux.read(fanotify_fd.cast(), buffer.ptr, buffer.len);
|
||||
|
||||
const errno = posix.errno(rc);
|
||||
if (errno != .SUCCESS) {
|
||||
return .{ .err = bun.sys.Error.fromCode(errno, .read) };
|
||||
}
|
||||
|
||||
return .{ .result = buffer[0..@intCast(rc)] };
|
||||
}
|
||||
|
||||
/// Iterator for fanotify events
|
||||
pub const EventIterator = struct {
|
||||
buffer: []const u8,
|
||||
offset: usize = 0,
|
||||
|
||||
pub fn next(self: *EventIterator) ?*align(1) const EventMetadata {
|
||||
if (self.offset >= self.buffer.len) return null;
|
||||
|
||||
const event: *align(1) const EventMetadata = @ptrCast(@alignCast(self.buffer[self.offset..][0..@sizeOf(EventMetadata)].ptr));
|
||||
|
||||
self.offset += event.size();
|
||||
return event;
|
||||
}
|
||||
|
||||
pub fn reset(self: *EventIterator) void {
|
||||
self.offset = 0;
|
||||
}
|
||||
};
|
||||
414
src/watcher/FanotifyWatcher.zig
Normal file
414
src/watcher/FanotifyWatcher.zig
Normal file
@@ -0,0 +1,414 @@
|
||||
//! Bun's filesystem watcher implementation for linux using fanotify
|
||||
//! https://man7.org/linux/man-pages/man7/fanotify.7.html
|
||||
//!
|
||||
//! Fanotify provides filesystem-wide monitoring with recursive capabilities.
|
||||
//! Note: fanotify requires appropriate permissions (CAP_SYS_ADMIN or similar)
|
||||
|
||||
const FanotifyWatcher = @This();
|
||||
|
||||
const log = Output.scoped(.watcher, .visible);
|
||||
const fanotify = bun.sys.fanotify;
|
||||
|
||||
// fanotify events are variable-sized, so a byte buffer is used
|
||||
const eventlist_bytes_size = 4096 * 32; // 128KB buffer for events
|
||||
const EventListBytes = [eventlist_bytes_size]u8;
|
||||
|
||||
fd: bun.FileDescriptor = bun.invalid_fd,
|
||||
loaded: bool = false,
|
||||
|
||||
// Avoid statically allocating because it increases the binary size.
|
||||
eventlist_bytes: *EventListBytes = undefined,
|
||||
/// pointers into the next chunk of events
|
||||
eventlist_ptrs: [max_count]*align(1) const fanotify.EventMetadata = undefined,
|
||||
/// if defined, it means `read` should continue from this offset before asking
|
||||
/// for more bytes. this is only hit under high watching load.
|
||||
read_ptr: ?struct {
|
||||
i: u32,
|
||||
len: u32,
|
||||
} = null,
|
||||
|
||||
/// Store watched paths and their event list indices
|
||||
/// Maps path hash to eventlist_index for quick lookups
|
||||
watched_paths: std.AutoHashMapUnmanaged(u32, PathWatchInfo) = .{},
|
||||
allocator: std.mem.Allocator = undefined,
|
||||
|
||||
watch_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
|
||||
/// nanoseconds
|
||||
coalesce_interval: isize = 100_000,
|
||||
|
||||
const PathWatchInfo = struct {
|
||||
path: [:0]const u8,
|
||||
index: EventListIndex,
|
||||
is_dir: bool,
|
||||
};
|
||||
|
||||
pub const EventListIndex = i32;
|
||||
pub const Event = fanotify.EventMetadata;
|
||||
|
||||
pub fn watchPath(this: *FanotifyWatcher, pathname: [:0]const u8) bun.sys.Maybe(EventListIndex) {
|
||||
bun.assert(this.loaded);
|
||||
const old_count = this.watch_count.fetchAdd(1, .release);
|
||||
defer if (old_count == 0) Futex.wake(&this.watch_count, 10);
|
||||
|
||||
// For files, we watch for modifications and deletions
|
||||
const mask = fanotify.EventMask{
|
||||
.modify = true,
|
||||
.close_write = true,
|
||||
.delete_self = true,
|
||||
.move_self = true,
|
||||
.event_on_child = true,
|
||||
};
|
||||
|
||||
switch (fanotify.mark(this.fd, .add, mask, bun.invalid_fd, pathname)) {
|
||||
.err => |err| {
|
||||
log("fanotify_mark({}, file, {s}) failed: {}", .{ this.fd, pathname, err });
|
||||
return .{ .err = err };
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
|
||||
log("fanotify_mark({}, file, {s}) = success", .{ this.fd, pathname });
|
||||
|
||||
// Store the path info for later lookup
|
||||
// fanotify doesn't return a unique descriptor per path, so we generate one
|
||||
const index: EventListIndex = @intCast(this.watched_paths.count());
|
||||
const hash = @as(u32, @truncate(bun.hash(pathname)));
|
||||
|
||||
const path_copy = this.allocator.dupeZ(u8, pathname) catch return .{
|
||||
.err = bun.sys.Error.fromCode(.NOMEM, .watch),
|
||||
};
|
||||
|
||||
this.watched_paths.put(this.allocator, hash, .{
|
||||
.path = path_copy,
|
||||
.index = index,
|
||||
.is_dir = false,
|
||||
}) catch {
|
||||
this.allocator.free(path_copy);
|
||||
return .{ .err = bun.sys.Error.fromCode(.NOMEM, .watch) };
|
||||
};
|
||||
|
||||
return .{ .result = index };
|
||||
}
|
||||
|
||||
pub fn watchDir(this: *FanotifyWatcher, pathname: [:0]const u8) bun.sys.Maybe(EventListIndex) {
|
||||
bun.assert(this.loaded);
|
||||
const old_count = this.watch_count.fetchAdd(1, .release);
|
||||
defer if (old_count == 0) Futex.wake(&this.watch_count, 10);
|
||||
|
||||
// For directories, we watch for creates, deletes, and modifications
|
||||
// event_on_child makes this apply recursively to all children
|
||||
const mask = fanotify.EventMask{
|
||||
.create = true,
|
||||
.delete = true,
|
||||
.delete_self = true,
|
||||
.move_self = true,
|
||||
.moved_from = true,
|
||||
.moved_to = true,
|
||||
.modify = true,
|
||||
.close_write = true,
|
||||
.ondir = true,
|
||||
.event_on_child = true,
|
||||
};
|
||||
|
||||
switch (fanotify.mark(this.fd, .add, mask, bun.invalid_fd, pathname)) {
|
||||
.err => |err| {
|
||||
log("fanotify_mark({}, dir, {s}) failed: {}", .{ this.fd, pathname, err });
|
||||
return .{ .err = err };
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
|
||||
log("fanotify_mark({}, dir, {s}) = success", .{ this.fd, pathname });
|
||||
|
||||
// Store the path info for later lookup
|
||||
const index: EventListIndex = @intCast(this.watched_paths.count());
|
||||
const hash = @as(u32, @truncate(bun.hash(pathname)));
|
||||
|
||||
const path_copy = this.allocator.dupeZ(u8, pathname) catch return .{
|
||||
.err = bun.sys.Error.fromCode(.NOMEM, .watch),
|
||||
};
|
||||
|
||||
this.watched_paths.put(this.allocator, hash, .{
|
||||
.path = path_copy,
|
||||
.index = index,
|
||||
.is_dir = true,
|
||||
}) catch {
|
||||
this.allocator.free(path_copy);
|
||||
return .{ .err = bun.sys.Error.fromCode(.NOMEM, .watch) };
|
||||
};
|
||||
|
||||
return .{ .result = index };
|
||||
}
|
||||
|
||||
pub fn unwatch(this: *FanotifyWatcher, _: EventListIndex) void {
|
||||
bun.assert(this.loaded);
|
||||
_ = this.watch_count.fetchSub(1, .release);
|
||||
|
||||
// With fanotify, we can't easily unwatch individual paths
|
||||
// since we're monitoring the entire filesystem
|
||||
// This would need to be implemented with path tracking
|
||||
}
|
||||
|
||||
pub fn init(this: *FanotifyWatcher, _: []const u8) !void {
|
||||
bun.assert(!this.loaded);
|
||||
this.loaded = true;
|
||||
|
||||
if (bun.getenvZ("BUN_FANOTIFY_COALESCE_INTERVAL")) |env| {
|
||||
this.coalesce_interval = std.fmt.parseInt(isize, env, 10) catch 100_000;
|
||||
}
|
||||
|
||||
// Initialize fanotify with notification class
|
||||
const init_flags = fanotify.InitFlags{
|
||||
.cloexec = true,
|
||||
.nonblock = false,
|
||||
};
|
||||
const event_flags = fanotify.EventFlags{
|
||||
.rdonly = true,
|
||||
.largefile = true,
|
||||
.cloexec = true,
|
||||
};
|
||||
|
||||
switch (fanotify.init(init_flags, event_flags)) {
|
||||
.err => |err| {
|
||||
log("fanotify_init failed: {}", .{err});
|
||||
// Return Unexpected to match the error set that callers expect
|
||||
return error.Unexpected;
|
||||
},
|
||||
.result => |fd| this.fd = fd,
|
||||
}
|
||||
|
||||
this.allocator = bun.default_allocator;
|
||||
this.eventlist_bytes = try bun.default_allocator.create(EventListBytes);
|
||||
log("{} init (fanotify)", .{this.fd});
|
||||
}
|
||||
|
||||
pub fn read(this: *FanotifyWatcher) bun.sys.Maybe([]const *align(1) const Event) {
|
||||
bun.assert(this.loaded);
|
||||
|
||||
var i: u32 = 0;
|
||||
const read_eventlist_bytes = if (this.read_ptr) |ptr| brk: {
|
||||
Futex.waitForever(&this.watch_count, 0);
|
||||
i = ptr.i;
|
||||
break :brk this.eventlist_bytes[0..ptr.len];
|
||||
} else outer: while (true) {
|
||||
Futex.waitForever(&this.watch_count, 0);
|
||||
|
||||
const rc = std.posix.system.read(
|
||||
this.fd.cast(),
|
||||
this.eventlist_bytes,
|
||||
this.eventlist_bytes.len,
|
||||
);
|
||||
const errno = std.posix.errno(rc);
|
||||
switch (errno) {
|
||||
.SUCCESS => {
|
||||
var read_eventlist_bytes = this.eventlist_bytes[0..@intCast(rc)];
|
||||
log("{} read {} bytes", .{ this.fd, read_eventlist_bytes.len });
|
||||
if (read_eventlist_bytes.len == 0) return .{ .result = &.{} };
|
||||
|
||||
// Try to coalesce events
|
||||
const double_read_threshold = @sizeOf(Event) * (max_count / 2);
|
||||
if (read_eventlist_bytes.len < double_read_threshold) {
|
||||
var fds = [_]std.posix.pollfd{.{
|
||||
.fd = this.fd.cast(),
|
||||
.events = std.posix.POLL.IN | std.posix.POLL.ERR,
|
||||
.revents = 0,
|
||||
}};
|
||||
var timespec = std.posix.timespec{ .sec = 0, .nsec = this.coalesce_interval };
|
||||
if ((std.posix.ppoll(&fds, ×pec, null) catch 0) > 0) {
|
||||
inner: while (true) {
|
||||
const rest = this.eventlist_bytes[read_eventlist_bytes.len..];
|
||||
bun.assert(rest.len > 0);
|
||||
const new_rc = std.posix.system.read(this.fd.cast(), rest.ptr, rest.len);
|
||||
const e = std.posix.errno(new_rc);
|
||||
switch (e) {
|
||||
.SUCCESS => {
|
||||
read_eventlist_bytes.len += @intCast(new_rc);
|
||||
break :outer read_eventlist_bytes;
|
||||
},
|
||||
.AGAIN, .INTR => continue :inner,
|
||||
else => return .{ .err = bun.sys.Error.fromCode(e, .read) },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :outer read_eventlist_bytes;
|
||||
},
|
||||
.AGAIN, .INTR => continue :outer,
|
||||
else => return .{ .err = bun.sys.Error.fromCode(errno, .read) },
|
||||
}
|
||||
};
|
||||
|
||||
var count: u32 = 0;
|
||||
while (i < read_eventlist_bytes.len) {
|
||||
// fanotify events are aligned
|
||||
const event: *align(1) const Event = @ptrCast(read_eventlist_bytes[i..][0..@sizeOf(Event)].ptr);
|
||||
|
||||
// Close the file descriptor that fanotify provides (we don't need it)
|
||||
if (event.hasValidFd()) {
|
||||
_ = std.posix.close(event.fd);
|
||||
}
|
||||
|
||||
this.eventlist_ptrs[count] = event;
|
||||
i += event.size();
|
||||
count += 1;
|
||||
|
||||
if (Environment.enable_logs) {
|
||||
log("{} read event fd={} mask={x} pid={}", .{
|
||||
this.fd,
|
||||
event.fd,
|
||||
event.mask,
|
||||
event.pid,
|
||||
});
|
||||
}
|
||||
|
||||
// when under high load, we may need to buffer events
|
||||
if (count == max_count) {
|
||||
this.read_ptr = .{
|
||||
.i = i,
|
||||
.len = @intCast(read_eventlist_bytes.len),
|
||||
};
|
||||
log("{} read buffer filled up", .{this.fd});
|
||||
return .{ .result = &this.eventlist_ptrs };
|
||||
}
|
||||
}
|
||||
|
||||
this.read_ptr = null;
|
||||
return .{ .result = this.eventlist_ptrs[0..count] };
|
||||
}
|
||||
|
||||
pub fn stop(this: *FanotifyWatcher) void {
|
||||
log("{} stop", .{this.fd});
|
||||
if (this.fd != bun.invalid_fd) {
|
||||
this.fd.close();
|
||||
this.fd = bun.invalid_fd;
|
||||
}
|
||||
|
||||
// Clean up watched_paths
|
||||
var iter = this.watched_paths.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
this.allocator.free(entry.value_ptr.path);
|
||||
}
|
||||
this.watched_paths.deinit(this.allocator);
|
||||
}
|
||||
|
||||
/// Repeatedly called by the main watcher until the watcher is terminated.
|
||||
pub fn watchLoopCycle(this: *bun.Watcher) bun.sys.Maybe(void) {
|
||||
defer Output.flush();
|
||||
|
||||
const events = switch (this.platform.read()) {
|
||||
.result => |result| result,
|
||||
.err => |err| return .{ .err = err },
|
||||
};
|
||||
if (events.len == 0) return .success;
|
||||
|
||||
var event_id: usize = 0;
|
||||
|
||||
// Process events
|
||||
// With fanotify, we get events for all monitored paths
|
||||
// We need to match them against our watchlist
|
||||
for (events) |event| {
|
||||
// Check if we're about to exceed the watch_events array capacity
|
||||
if (event_id >= this.watch_events.len) {
|
||||
// Process current batch of events
|
||||
switch (processFanotifyEventBatch(this, event_id)) {
|
||||
.err => |err| return .{ .err = err },
|
||||
.result => {},
|
||||
}
|
||||
// Reset event_id to start a new batch
|
||||
event_id = 0;
|
||||
}
|
||||
|
||||
// Convert fanotify event to watch event
|
||||
// For now, we'll match all watched items since fanotify provides
|
||||
// filesystem-wide monitoring
|
||||
const item_paths = this.watchlist.items(.file_path);
|
||||
for (item_paths, 0..) |_, idx| {
|
||||
this.watch_events[event_id] = watchEventFromFanotifyEvent(
|
||||
event,
|
||||
@intCast(idx),
|
||||
);
|
||||
event_id += 1;
|
||||
|
||||
if (event_id >= this.watch_events.len) {
|
||||
switch (processFanotifyEventBatch(this, event_id)) {
|
||||
.err => |err| return .{ .err = err },
|
||||
.result => {},
|
||||
}
|
||||
event_id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining events in the final batch
|
||||
if (event_id > 0) {
|
||||
switch (processFanotifyEventBatch(this, event_id)) {
|
||||
.err => |err| return .{ .err = err },
|
||||
.result => {},
|
||||
}
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
fn processFanotifyEventBatch(this: *bun.Watcher, event_count: usize) bun.sys.Maybe(void) {
|
||||
if (event_count == 0) {
|
||||
return .success;
|
||||
}
|
||||
|
||||
var all_events = this.watch_events[0..event_count];
|
||||
std.sort.pdq(WatchEvent, all_events, {}, WatchEvent.sortByIndex);
|
||||
|
||||
var last_event_index: usize = 0;
|
||||
var last_event_id: EventListIndex = std.math.maxInt(EventListIndex);
|
||||
|
||||
for (all_events, 0..) |_, i| {
|
||||
if (all_events[i].index == last_event_id) {
|
||||
all_events[last_event_index].merge(all_events[i]);
|
||||
continue;
|
||||
}
|
||||
last_event_index = i;
|
||||
last_event_id = all_events[i].index;
|
||||
}
|
||||
if (all_events.len == 0) return .success;
|
||||
|
||||
this.mutex.lock();
|
||||
defer this.mutex.unlock();
|
||||
if (this.running) {
|
||||
// all_events.len == 0 is checked above, so last_event_index + 1 is safe
|
||||
this.onFileUpdate(this.ctx, all_events[0 .. last_event_index + 1], this.changed_filepaths[0..0], this.watchlist);
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn watchEventFromFanotifyEvent(event: *align(1) const Event, index: WatchItemIndex) WatchEvent {
|
||||
const mask = event.mask;
|
||||
const FAN_DELETE = 0x00000200;
|
||||
const FAN_DELETE_SELF = 0x00000400;
|
||||
const FAN_MOVE_SELF = 0x00000800;
|
||||
const FAN_MOVED_TO = 0x00000080;
|
||||
const FAN_MODIFY = 0x00000002;
|
||||
const FAN_CLOSE_WRITE = 0x00000008;
|
||||
|
||||
return .{
|
||||
.op = .{
|
||||
.delete = (mask & FAN_DELETE_SELF) > 0 or (mask & FAN_DELETE) > 0,
|
||||
.rename = (mask & FAN_MOVE_SELF) > 0,
|
||||
.move_to = (mask & FAN_MOVED_TO) > 0,
|
||||
.write = (mask & FAN_MODIFY) > 0 or (mask & FAN_CLOSE_WRITE) > 0,
|
||||
},
|
||||
.index = index,
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const Futex = bun.Futex;
|
||||
const Output = bun.Output;
|
||||
|
||||
const WatchEvent = bun.Watcher.Event;
|
||||
const WatchItemIndex = bun.Watcher.WatchItemIndex;
|
||||
const max_count = bun.Watcher.max_count;
|
||||
Reference in New Issue
Block a user