From b728df2ff5a168d9c36da1be81137c0f22b9b19d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sat, 29 Nov 2025 12:31:24 +0100 Subject: [PATCH] refactor: add file implementation from zine --- src/watcher/LinuxWatcher.zig | 439 +++++++++++++++++++++++++++++++++ src/watcher/MacosWatcher.zig | 114 +++++++++ src/watcher/WindowsWatcher.zig | 225 +++++++++++++++++ 3 files changed, 778 insertions(+) create mode 100644 src/watcher/LinuxWatcher.zig create mode 100644 src/watcher/MacosWatcher.zig create mode 100644 src/watcher/WindowsWatcher.zig diff --git a/src/watcher/LinuxWatcher.zig b/src/watcher/LinuxWatcher.zig new file mode 100644 index 0000000..ab06a06 --- /dev/null +++ b/src/watcher/LinuxWatcher.zig @@ -0,0 +1,439 @@ +const LinuxWatcher = @This(); + +const std = @import("std"); +const fatal = @import("../../../fatal.zig"); +const Debouncer = @import("../../serve.zig").Debouncer; +const Allocator = std.mem.Allocator; + +const log = std.log.scoped(.watcher); + +gpa: Allocator, +debouncer: *Debouncer, + +notify_fd: std.posix.fd_t, +/// active watch entries +watch_fds: std.AutoHashMapUnmanaged(std.posix.fd_t, WatchEntry) = .{}, +/// direct descendant tracker +children_fds: std.AutoHashMapUnmanaged( + std.posix.fd_t, + std.ArrayListUnmanaged(std.posix.fd_t), +) = .{}, +/// inotify cookie tracker for move events +cookie_fds: std.AutoHashMapUnmanaged(u32, std.posix.fd_t) = .{}, + +const WatchEntry = struct { + dir_path: []const u8, + name: []const u8, +}; + +pub fn init( + gpa: std.mem.Allocator, + debouncer: *Debouncer, + dir_paths: []const []const u8, +) LinuxWatcher { + errdefer |err| fatal.msg("error: unable to start the file watcher: {s}", .{ + @errorName(err), + }); + + const notify_fd = try std.posix.inotify_init1(0); + var watcher: LinuxWatcher = .{ + .gpa = gpa, + .notify_fd = notify_fd, + .debouncer = debouncer, + }; + for (dir_paths) |p| { + _ = try watcher.addTree(p); + } + return watcher; +} + +/// Register `child` with the `parent` +fn addChild( + watcher: *LinuxWatcher, + parent: std.posix.fd_t, + child: std.posix.fd_t, +) !void { + const gpa = watcher.gpa; + const children = try watcher.children_fds.getOrPut(gpa, parent); + if (!children.found_existing) { + children.value_ptr.* = .{}; + } + try children.value_ptr.append(gpa, child); +} + +/// Remove `child` from the `parent`, if present +fn removeChild( + self: *LinuxWatcher, + parent: std.posix.fd_t, + child: std.posix.fd_t, +) ?std.posix.fd_t { + if (self.children_fds.getEntry(parent)) |entry| { + for (0.., entry.value_ptr.items) |i, fd| { + if (child == fd) { + return entry.value_ptr.swapRemove(i); + } + } + } + return null; +} + +/// Remove child identified by `name`, if present +fn removeChildByName( + self: *LinuxWatcher, + parent: std.posix.fd_t, + name: []const u8, +) ?std.posix.fd_t { + if (self.children_fds.getEntry(parent)) |entry| { + for (0.., entry.value_ptr.items) |i, fd| { + if (self.watch_fds.get(fd)) |data| { + if (std.mem.eql(u8, data.name, name)) { + return entry.value_ptr.swapRemove(i); + } + } + } + } + return null; +} + +/// Start tracking directory tree and returns the watch descriptor for `root_dir_path` +/// **NOTE**: caller is expected to register the returned watch fd as a child +fn addTree( + watcher: *LinuxWatcher, + root_dir_path: []const u8, +) !std.posix.fd_t { + const gpa = watcher.gpa; + + var root_dir = try std.fs.openDirAbsolute(root_dir_path, .{ + .iterate = true, + }); + defer root_dir.close(); + + const parent_fd = try watcher.addDir(root_dir_path); + + // tracker for fds associated with dir paths + // helps to track children within a recursive walk + var lookup = std.StringHashMap(std.posix.fd_t).init(gpa); + defer lookup.deinit(); + + try lookup.put(root_dir_path, parent_fd); + + var it = try root_dir.walk(gpa); + while (try it.next()) |entry| switch (entry.kind) { + else => continue, + .directory => { + const dir_path = try std.fs.path.join(gpa, &.{ + root_dir_path, + entry.path, + }); + const dir_fd = try watcher.addDir(dir_path); + const p_dir = std.fs.path.dirname(dir_path).?; + const p_fd = lookup.get(p_dir).?; + + try watcher.addChild(p_fd, dir_fd); + try lookup.put(dir_path, dir_fd); + }, + }; + + return parent_fd; +} + +fn addDir( + watcher: *LinuxWatcher, + dir_path: []const u8, +) !std.posix.fd_t { + const gpa = watcher.gpa; + const mask = Mask.all(&.{ + .IN_ONLYDIR, .IN_CLOSE_WRITE, + .IN_MOVE, .IN_MOVE_SELF, + .IN_CREATE, .IN_DELETE, + .IN_EXCL_UNLINK, + }); + const watch_fd = try std.posix.inotify_add_watch( + watcher.notify_fd, + dir_path, + mask, + ); + const name_copy = try gpa.dupe(u8, std.fs.path.basename(dir_path)); + try watcher.watch_fds.put(gpa, watch_fd, .{ + .dir_path = dir_path, + .name = name_copy, + }); + log.debug("added {s} -> {}", .{ dir_path, watch_fd }); + return watch_fd; +} + +/// Explicitly stop watching a descriptor +/// **NOTE**: should only be called on an active `fd` +fn rmWatch( + watcher: *LinuxWatcher, + fd: std.posix.fd_t, +) void { + if (watcher.children_fds.getEntry(fd)) |entry| { + for (entry.value_ptr.items) |child_fd| { + watcher.rmWatch(child_fd); + } + watcher.children_fds.removeByPtr(entry.key_ptr); + } + std.posix.inotify_rm_watch(watcher.notify_fd, fd); +} + +/// Handle the start of the move process +/// Remove `name`-identified fd from children of `from_fd` +/// Register `cookie` for the moved fd for future identification +fn moveDirStart( + watcher: *LinuxWatcher, + from_fd: std.posix.fd_t, + cookie: u32, + name: []const u8, +) !void { + const moved_fd = watcher.removeChildByName(from_fd, name).?; + + try watcher.cookie_fds.put( + watcher.gpa, + cookie, + moved_fd, + ); +} + +/// Handle the end of the move process and returns the resulting moved fd +/// Register the moved fd as a child of `to_fd` +fn moveDirEnd( + watcher: *LinuxWatcher, + to_fd: std.posix.fd_t, + cookie: u32, + name: []const u8, +) !std.posix.fd_t { + const gpa = watcher.gpa; + const parent = watcher.watch_fds.get(to_fd).?; + + // known cookie - move within watched directories + if (watcher.cookie_fds.fetchRemove(cookie)) |entry| { + const moved_fd = entry.value; + + var watch_entry = watcher.watch_fds.getEntry(moved_fd).?.value_ptr; + gpa.free(watch_entry.name); + const name_copy = try gpa.dupe(u8, name); + watch_entry.name = name_copy; + + try watcher.updateDirPath(moved_fd, parent.dir_path); + try watcher.addChild(to_fd, moved_fd); + return moved_fd; + } else { // unknown cookie - move from the outside + const dir_path = try std.fs.path.join(gpa, &.{ parent.dir_path, name }); + const moved_fd = try watcher.addTree(dir_path); + try watcher.addChild(to_fd, moved_fd); + return moved_fd; + } +} + +/// Cascade path updates for `fd` and its children +fn updateDirPath( + watcher: *LinuxWatcher, + fd: std.posix.fd_t, + parent_dir: []const u8, +) !void { + const gpa = watcher.gpa; + var data = watcher.watch_fds.getEntry(fd).?.value_ptr; + gpa.free(data.dir_path); + const dir_path = try std.fs.path.join(gpa, &.{ parent_dir, data.name }); + data.dir_path = dir_path; + + if (watcher.children_fds.getEntry(fd)) |entry| { + for (entry.value_ptr.items) |child_fd| { + try watcher.updateDirPath(child_fd, dir_path); + } + } +} + +/// Handle the post-move event +/// Remove stale cookie waiting for the `moved_fd`, if present +fn moveDirComplete( + watcher: *LinuxWatcher, + moved_fd: std.posix.fd_t, +) !void { + var it = watcher.cookie_fds.iterator(); + while (it.next()) |entry| { + // cookie for fd exists - moved outside the watched directory + if (entry.value_ptr.* == moved_fd) { + watcher.rmWatch(moved_fd); + watcher.cookie_fds.removeByPtr(entry.key_ptr); + break; + } + } +} + +/// Clean up `fd`-related bookkeeping +/// **NOTE**: expects `fd` to be a no-longer-watched descriptor +fn dropWatch( + watcher: *LinuxWatcher, + fd: std.posix.fd_t, +) void { + const gpa = watcher.gpa; + if (watcher.watch_fds.fetchRemove(fd)) |entry| { + gpa.free(entry.value.dir_path); + gpa.free(entry.value.name); + } + + var it = watcher.children_fds.keyIterator(); + while (it.next()) |parent_fd| { + _ = watcher.removeChild(parent_fd.*, fd); + } + + if (watcher.children_fds.fetchRemove(fd)) |entry| { + log.warn("Stopping watch for {d} that has known children: {any}", .{ fd, entry.value }); + } +} + +pub fn start(watcher: *LinuxWatcher) !void { + const t = try std.Thread.spawn(.{}, LinuxWatcher.listen, .{watcher}); + t.detach(); +} + +pub fn listen(watcher: *LinuxWatcher) !void { + const gpa = watcher.gpa; + const Event = std.os.linux.inotify_event; + const event_size = @sizeOf(Event); + while (true) { + var buffer: [event_size * 10]u8 = undefined; + const len = try std.posix.read(watcher.notify_fd, &buffer); + if (len < 0) @panic("notify fd read error"); + + var event_data = buffer[0..len]; + while (event_data.len > 0) { + const event: *Event = @alignCast(@ptrCast(event_data[0..event_size])); + const parent = watcher.watch_fds.get(event.wd).?; + event_data = event_data[event_size + event.len ..]; + + // std.debug.print("flags: ", .{}); + // Mask.debugPrint(event.mask); + // std.debug.print("for {s}/{?s}\n", .{ parent.dir_path, event.getName() }); + + if (Mask.is(event.mask, .IN_IGNORED)) { + log.debug("IGNORE {s}", .{parent.dir_path}); + watcher.dropWatch(event.wd); + continue; + } else if (Mask.is(event.mask, .IN_MOVE_SELF)) { + if (event.getName() == null) { + try watcher.moveDirComplete(event.wd); + } + continue; + } + + if (Mask.is(event.mask, .IN_ISDIR)) { + if (Mask.is(event.mask, .IN_CREATE)) { + const dir_name = event.getName().?; + const dir_path = try std.fs.path.join(gpa, &.{ + parent.dir_path, + dir_name, + }); + + log.debug("ISDIR CREATE {s}", .{dir_path}); + + const new_fd = try watcher.addTree(dir_path); + try watcher.addChild(event.wd, new_fd); + const data = watcher.watch_fds.get(new_fd).?; + _ = data; + watcher.debouncer.newEvent(); + continue; + } else if (Mask.is(event.mask, .IN_MOVED_FROM)) { + log.debug("MOVING {s}/{s}", .{ parent.dir_path, event.getName().? }); + try watcher.moveDirStart(event.wd, event.cookie, event.getName().?); + continue; + } else if (Mask.is(event.mask, .IN_MOVED_TO)) { + log.debug("MOVED {s}/{s}", .{ parent.dir_path, event.getName().? }); + const moved_fd = try watcher.moveDirEnd(event.wd, event.cookie, event.getName().?); + const moved = watcher.watch_fds.get(moved_fd).?; + _ = moved; + watcher.debouncer.newEvent(); + continue; + } + } else { + if (Mask.is(event.mask, .IN_CLOSE_WRITE) or + Mask.is(event.mask, .IN_MOVED_TO)) + { + watcher.debouncer.newEvent(); + } + } + } + } +} + +const Mask = struct { + pub const IN_ACCESS = 0x00000001; + pub const IN_MODIFY = 0x00000002; + pub const IN_ATTRIB = 0x00000004; + pub const IN_CLOSE_WRITE = 0x00000008; + pub const IN_CLOSE_NOWRITE = 0x00000010; + pub const IN_CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE); + pub const IN_OPEN = 0x00000020; + pub const IN_MOVED_FROM = 0x00000040; + pub const IN_MOVED_TO = 0x00000080; + pub const IN_MOVE = (IN_MOVED_FROM | IN_MOVED_TO); + pub const IN_CREATE = 0x00000100; + pub const IN_DELETE = 0x00000200; + pub const IN_DELETE_SELF = 0x00000400; + pub const IN_MOVE_SELF = 0x00000800; + pub const IN_ALL_EVENTS = 0x00000fff; + + pub const IN_UNMOUNT = 0x00002000; + pub const IN_Q_OVERFLOW = 0x00004000; + pub const IN_IGNORED = 0x00008000; + + pub const IN_ONLYDIR = 0x01000000; + pub const IN_DONT_FOLLOW = 0x02000000; + pub const IN_EXCL_UNLINK = 0x04000000; + pub const IN_MASK_CREATE = 0x10000000; + pub const IN_MASK_ADD = 0x20000000; + + pub const IN_ISDIR = 0x40000000; + pub const IN_ONESHOT = 0x80000000; + + pub fn is(m: u32, comptime flag: std.meta.DeclEnum(Mask)) bool { + const f = @field(Mask, @tagName(flag)); + return (m & f) != 0; + } + + pub fn all(comptime flags: []const std.meta.DeclEnum(Mask)) u32 { + var result: u32 = 0; + inline for (flags) |f| result |= @field(Mask, @tagName(f)); + return result; + } + + pub fn debugPrint(m: u32) void { + const flags = .{ + .IN_ACCESS, + .IN_MODIFY, + .IN_ATTRIB, + .IN_CLOSE_WRITE, + .IN_CLOSE_NOWRITE, + .IN_CLOSE, + .IN_OPEN, + .IN_MOVED_FROM, + .IN_MOVED_TO, + .IN_MOVE, + .IN_CREATE, + .IN_DELETE, + .IN_DELETE_SELF, + .IN_MOVE_SELF, + .IN_ALL_EVENTS, + + .IN_UNMOUNT, + .IN_Q_OVERFLOW, + .IN_IGNORED, + + .IN_ONLYDIR, + .IN_DONT_FOLLOW, + .IN_EXCL_UNLINK, + .IN_MASK_CREATE, + .IN_MASK_ADD, + + .IN_ISDIR, + .IN_ONESHOT, + }; + inline for (flags) |f| { + if (is(m, f)) { + std.debug.print("{s} ", .{@tagName(f)}); + } + } + } +}; diff --git a/src/watcher/MacosWatcher.zig b/src/watcher/MacosWatcher.zig new file mode 100644 index 0000000..1a9f69b --- /dev/null +++ b/src/watcher/MacosWatcher.zig @@ -0,0 +1,114 @@ +const MacosWatcher = @This(); + +const std = @import("std"); +const fatal = @import("../../../fatal.zig"); +const Debouncer = @import("../../serve.zig").Debouncer; + +const c = @cImport({ + @cInclude("CoreServices/CoreServices.h"); +}); + +const log = std.log.scoped(.watcher); + +gpa: std.mem.Allocator, +debouncer: *Debouncer, +dir_paths: []const []const u8, + +pub fn init( + gpa: std.mem.Allocator, + debouncer: *Debouncer, + dir_paths: []const []const u8, +) MacosWatcher { + return .{ + .gpa = gpa, + .debouncer = debouncer, + .dir_paths = dir_paths, + }; +} + +pub fn start(watcher: *MacosWatcher) !void { + const t = try std.Thread.spawn(.{}, MacosWatcher.listen, .{watcher}); + t.detach(); +} + +pub fn listen(watcher: *MacosWatcher) void { + errdefer |err| switch (err) { + error.OutOfMemory => fatal.oom(), + }; + + const macos_paths = try watcher.gpa.alloc( + c.CFStringRef, + watcher.dir_paths.len, + ); + defer watcher.gpa.free(macos_paths); + + for (watcher.dir_paths, macos_paths) |str, *ref| { + ref.* = c.CFStringCreateWithCString( + null, + str.ptr, + c.kCFStringEncodingUTF8, + ); + } + + const paths_to_watch: c.CFArrayRef = c.CFArrayCreate( + null, + @ptrCast(macos_paths.ptr), + @intCast(macos_paths.len), + null, + ); + + var stream_context: c.FSEventStreamContext = .{ .info = watcher }; + const stream: c.FSEventStreamRef = c.FSEventStreamCreate( + null, + &macosCallback, + &stream_context, + paths_to_watch, + c.kFSEventStreamEventIdSinceNow, + 0.05, + c.kFSEventStreamCreateFlagFileEvents, + ); + + c.FSEventStreamScheduleWithRunLoop( + stream, + c.CFRunLoopGetCurrent(), + c.kCFRunLoopDefaultMode, + ); + + if (c.FSEventStreamStart(stream) == 0) { + fatal.msg("error: macos watcher FSEventStreamStart failed", .{}); + } + + c.CFRunLoopRun(); + + c.FSEventStreamStop(stream); + c.FSEventStreamInvalidate(stream); + c.FSEventStreamRelease(stream); + + c.CFRelease(paths_to_watch); +} + +pub fn macosCallback( + streamRef: c.ConstFSEventStreamRef, + clientCallBackInfo: ?*anyopaque, + numEvents: usize, + eventPaths: ?*anyopaque, + eventFlags: ?[*]const c.FSEventStreamEventFlags, + eventIds: ?[*]const c.FSEventStreamEventId, +) callconv(.c) void { + _ = eventIds; + _ = eventFlags; + _ = streamRef; + const watcher: *MacosWatcher = @alignCast(@ptrCast(clientCallBackInfo)); + + const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths)); + for (paths[0..numEvents]) |p| { + const path = std.mem.span(p); + log.debug("Changed: {s}\n", .{path}); + + // const basename = std.fs.path.basename(path); + // var base_path = path[0 .. path.len - basename.len]; + // if (std.mem.endsWith(u8, base_path, "/")) + // base_path = base_path[0 .. base_path.len - 1]; + watcher.debouncer.newEvent(); + } +} diff --git a/src/watcher/WindowsWatcher.zig b/src/watcher/WindowsWatcher.zig new file mode 100644 index 0000000..0a7fbd2 --- /dev/null +++ b/src/watcher/WindowsWatcher.zig @@ -0,0 +1,225 @@ +const WindowsWatcher = @This(); + +const std = @import("std"); +const windows = std.os.windows; +const fatal = @import("../../../fatal.zig"); +const Debouncer = @import("../../serve.zig").Debouncer; + +const log = std.log.scoped(.watcher); + +const notify_filter = windows.FileNotifyChangeFilter{ + .file_name = true, + .dir_name = true, + .attributes = false, + .size = false, + .last_write = true, + .last_access = false, + .creation = false, + .security = false, +}; + +const CompletionKey = usize; +/// Values should be a multiple of `ReadBufferEntrySize` +const ReadBufferIndex = u32; +const ReadBufferEntrySize = 1024; + +const WatchEntry = struct { + dir_path: [:0]const u8, + dir_handle: windows.HANDLE, + + overlap: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED), + buf_idx: ReadBufferIndex, +}; + +debouncer: *Debouncer, +iocp_port: windows.HANDLE, +entries: std.AutoHashMap(CompletionKey, WatchEntry), +read_buffer: []u8, + +pub fn init( + gpa: std.mem.Allocator, + debouncer: *Debouncer, + dir_paths: []const []const u8, +) WindowsWatcher { + errdefer |err| fatal.msg("error: unable to start the file watcher: {s}", .{ + @errorName(err), + }); + + var watcher = WindowsWatcher{ + .debouncer = debouncer, + .iocp_port = windows.INVALID_HANDLE_VALUE, + .entries = std.AutoHashMap(CompletionKey, WatchEntry).init(gpa), + .read_buffer = undefined, + }; + errdefer { + var iter = watcher.entries.valueIterator(); + while (iter.next()) |entry| { + windows.CloseHandle(entry.dir_handle); + gpa.free(entry.dir_path); + } + watcher.entries.deinit(); + } + + // Doubles as the number of WatchEntries + var comp_key: CompletionKey = 0; + + for (dir_paths) |path| { + const in_path = try gpa.dupeZ(u8, path); + try watcher.entries.putNoClobber( + comp_key, + try addPath(in_path, comp_key, &watcher.iocp_port), + ); + comp_key += 1; + } + + watcher.read_buffer = try gpa.alloc(u8, ReadBufferEntrySize * comp_key); + + // Here we need pointers to both the read_buffer and entry overlapped structs, + // which we can only do after setting up everything else. + watcher.entries.lockPointers(); + for (0..comp_key) |key| { + const entry = watcher.entries.getPtr(key).?; + if (windows.kernel32.ReadDirectoryChangesW( + entry.dir_handle, + @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])), + ReadBufferEntrySize, + @intFromBool(true), + notify_filter, + null, + &entry.overlap, + null, + ) == 0) { + log.err("ReadDirectoryChanges error: {s}", .{ + @tagName(windows.kernel32.GetLastError()), + }); + return error.QueueFailed; + } + } + return watcher; +} + +fn addPath( + path: [:0]const u8, + /// Assumed to increment by 1 after each invocation, starting at 0. + key: CompletionKey, + port: *windows.HANDLE, +) !WatchEntry { + const dir_handle = CreateFileA( + path, + windows.GENERIC_READ, // FILE_LIST_DIRECTORY, + windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, + null, + windows.OPEN_EXISTING, + windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED, + null, + ); + if (dir_handle == windows.INVALID_HANDLE_VALUE) { + log.err( + "Unable to open directory {s}: {s}", + .{ path, @tagName(windows.kernel32.GetLastError()) }, + ); + return error.InvalidHandle; + } + + if (port.* == windows.INVALID_HANDLE_VALUE) { + port.* = try windows.CreateIoCompletionPort(dir_handle, null, key, 0); + } else { + _ = try windows.CreateIoCompletionPort(dir_handle, port.*, key, 0); + } + + return .{ + .dir_path = path, + .dir_handle = dir_handle, + .buf_idx = @intCast(ReadBufferEntrySize * key), + }; +} + +pub fn start(watcher: *WindowsWatcher) !void { + const t = try std.Thread.spawn(.{}, WindowsWatcher.listen, .{watcher}); + t.detach(); +} + +pub fn listen(watcher: *WindowsWatcher) !void { + var dont_care: struct { + bytes_transferred: windows.DWORD = undefined, + overlap: ?*windows.OVERLAPPED = undefined, + } = .{}; + + var key: CompletionKey = undefined; + while (true) { + // Waits here until any of the directory handles associated with the iocp port + // have been updated. + const wait_result = windows.GetQueuedCompletionStatus( + watcher.iocp_port, + &dont_care.bytes_transferred, + &key, + &dont_care.overlap, + windows.INFINITE, + ); + if (wait_result != .Normal) { + log.err("GetQueuedCompletionStatus error: {s}", .{@tagName(wait_result)}); + return error.WaitFailed; + } + + const entry = watcher.entries.getPtr(key) orelse @panic("Invalid CompletionKey"); + + var info_iter = windows.FileInformationIterator(FILE_NOTIFY_INFORMATION){ + .buf = watcher.read_buffer[entry.buf_idx..][0..ReadBufferEntrySize], + }; + var path_buf: [windows.MAX_PATH]u8 = undefined; + while (info_iter.next()) |info| { + const filename: []const u8 = blk: { + const n = try std.unicode.utf16LeToUtf8( + &path_buf, + @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2], + ); + break :blk path_buf[0..n]; + }; + + const args = .{ entry.dir_path, filename }; + switch (info.Action) { + windows.FILE_ACTION_ADDED => log.debug("added {s}/{s}", args), + windows.FILE_ACTION_REMOVED => log.debug("removed {s}/{s}", args), + windows.FILE_ACTION_MODIFIED => log.debug("modified {s}/{s}", args), + windows.FILE_ACTION_RENAMED_OLD_NAME => log.debug("renamed_old_name {s}/{s}", args), + windows.FILE_ACTION_RENAMED_NEW_NAME => log.debug("renamed_new_name {s}/{s}", args), + else => log.debug("Unknown Action {s}/{s}", args), + } + + watcher.debouncer.newEvent(); + } + + // Re-queue the directory entry + if (windows.kernel32.ReadDirectoryChangesW( + entry.dir_handle, + @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])), + ReadBufferEntrySize, + @intFromBool(true), + notify_filter, + null, + &entry.overlap, + null, + ) == 0) { + log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())}); + return error.QueueFailed; + } + } +} + +const FILE_NOTIFY_INFORMATION = extern struct { + NextEntryOffset: windows.DWORD, + Action: windows.DWORD, + FileNameLength: windows.DWORD, + /// Flexible array member + FileName: windows.WCHAR, +}; + +extern "kernel32" fn CreateFileA( + lpFileName: windows.LPCSTR, + dwDesiredAccess: windows.DWORD, + dwShareMode: windows.DWORD, + lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, + dwCreationDisposition: windows.DWORD, + dwFlagsAndAttributes: windows.DWORD, + hTemplateFile: ?windows.HANDLE, +) callconv(.winapi) windows.HANDLE;