From 7171ad1905fc492a106617e2c917ce4e79fa9baa Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 30 Mar 2026 21:46:08 +0200 Subject: [PATCH] fix(kqueue): fix handling of move-in subdirs --- src/backend/KQueue.zig | 10 ++++++++++ src/backend/KQueueDir.zig | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index 542031c..8f3e717 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -171,6 +171,9 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); return; }; + // Clean up snapshot so that a new dir at the same path is not + // skipped by scan_dir's snapshots.contains() check. + self.remove_watch(allocator, dir_path); } else if (ev.fflags & NOTE_RENAME != 0) { self.handler.change(dir_path, EventType.renamed, .dir) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); @@ -285,6 +288,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // a rename (old disappears, new appears) reports the source path before the dest. for (new_dirs.items) |full_path| { try self.handler.change(full_path, EventType.created, .dir); + // Start watching the moved-in dir so future changes inside it are detected + // and so its deletion fires NOTE_DELETE rather than being silently missed. + self.add_watch(allocator, full_path) catch |e| + std.log.err("nightwatch: add_watch on moved-in dir failed: {s}", .{@errorName(e)}); try self.emit_subtree_created(allocator, full_path); } for (to_delete.items) |name| { @@ -322,6 +329,9 @@ fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: if (ot == .file) { self.register_file_watch(allocator, full_path); } else { + // Watch nested subdirs so changes inside them are detected after move-in. + self.add_watch(allocator, full_path) catch |e| + std.log.err("nightwatch: add_watch on moved-in subdir failed: {s}", .{@errorName(e)}); try self.emit_subtree_created(allocator, full_path); } } diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index be082e7..ca95d63 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -152,6 +152,9 @@ fn thread_fn(self: *@This(), allocator: std.mem.Allocator) void { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); return; }; + // Clean up snapshot so that a new dir at the same path is not + // skipped by scan_dir's snapshots.contains() check. + self.remove_watch(allocator, watch_path); } else if (ev.fflags & NOTE_RENAME != 0) { self.handler.change(watch_path, EventType.renamed, .dir) catch |e| { std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)}); @@ -279,7 +282,11 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // Order: new dirs, deletions (source before dest for renames), creations, modifications. for (new_dirs.items) |full_path| { try self.handler.change(full_path, EventType.created, .dir); - try self.emit_subtree_created(full_path); + // Start watching the moved-in dir so future changes inside it are detected + // and so its deletion fires NOTE_DELETE rather than being silently missed. + self.add_watch(allocator, full_path) catch |e| + std.log.err("nightwatch: add_watch on moved-in dir failed: {s}", .{@errorName(e)}); + try self.emit_subtree_created(allocator, full_path); } for (to_delete.items) |name| { var path_buf: [std.fs.max_path_bytes]u8 = undefined; @@ -302,7 +309,7 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) // Walk dir_path recursively, emitting 'created' events for all contents. // Called after a new dir appears in scan_dir so callers see individual // 'created' events for the pre-existing tree of a moved-in directory. -fn emit_subtree_created(self: *@This(), dir_path: []const u8) error{HandlerFailed}!void { +fn emit_subtree_created(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8) !void { var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return; defer dir.close(); var iter = dir.iterate(); @@ -315,7 +322,12 @@ fn emit_subtree_created(self: *@This(), dir_path: []const u8) error{HandlerFaile var path_buf: [std.fs.max_path_bytes]u8 = undefined; const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, entry.name }) catch continue; try self.handler.change(full_path, EventType.created, ot); - if (ot == .dir) try self.emit_subtree_created(full_path); + if (ot == .dir) { + // Watch nested subdirs so changes inside them are detected after move-in. + self.add_watch(allocator, full_path) catch |e| + std.log.err("nightwatch: add_watch on moved-in subdir failed: {s}", .{@errorName(e)}); + try self.emit_subtree_created(allocator, full_path); + } } }