diff --git a/src/backend/INotify.zig b/src/backend/INotify.zig index eb71d23..db2864d 100644 --- a/src/backend/INotify.zig +++ b/src/backend/INotify.zig @@ -14,7 +14,7 @@ pub fn Create(comptime variant: InterfaceType) type { return struct { handler: *Handler, inotify_fd: std.posix.fd_t, - watches: std.AutoHashMapUnmanaged(i32, WatchEntry), // wd -> {owned path, is dir} + watches: std.AutoHashMapUnmanaged(i32, []u8), // wd -> owned path // Protects `watches` against concurrent access by the background thread // (handle_read_ready / has_watch_for_path) and the main thread // (add_watch / remove_watch). Void for the polling variant, which is @@ -37,8 +37,6 @@ pub fn Create(comptime variant: InterfaceType) type { pub const detects_file_modifications = true; pub const polling = variant == .polling; - const WatchEntry = struct { path: []u8, is_dir: bool }; - const Handler = switch (variant) { .threaded => types.Handler, .polling => types.PollingHandler, @@ -91,7 +89,7 @@ pub fn Create(comptime variant: InterfaceType) type { std.posix.close(self.stop_pipe[1]); } var it = self.watches.iterator(); - while (it.next()) |entry| allocator.free(entry.value_ptr.*.path); + while (it.next()) |entry| allocator.free(entry.value_ptr.*); self.watches.deinit(allocator); for (self.pending_renames.items) |r| allocator.free(r.path); self.pending_renames.deinit(allocator); @@ -145,18 +143,13 @@ pub fn Create(comptime variant: InterfaceType) type { return error.WatchFailed; }, } - const is_dir = blk: { - var d = std.fs.openDirAbsolute(path, .{}) catch break :blk false; - defer d.close(); - break :blk true; - }; const owned_path = try allocator.dupe(u8, path); errdefer allocator.free(owned_path); if (comptime variant == .threaded) self.watches_mutex.lock(); defer if (comptime variant == .threaded) self.watches_mutex.unlock(); const result = try self.watches.getOrPut(allocator, @intCast(wd)); - if (result.found_existing) allocator.free(result.value_ptr.*.path); - result.value_ptr.* = .{ .path = owned_path, .is_dir = is_dir }; + if (result.found_existing) allocator.free(result.value_ptr.*); + result.value_ptr.* = owned_path; } pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void { @@ -164,9 +157,9 @@ pub fn Create(comptime variant: InterfaceType) type { defer if (comptime variant == .threaded) self.watches_mutex.unlock(); var it = self.watches.iterator(); while (it.next()) |entry| { - if (!std.mem.eql(u8, entry.value_ptr.*.path, path)) continue; + if (!std.mem.eql(u8, entry.value_ptr.*, path)) continue; _ = std.os.linux.inotify_rm_watch(self.inotify_fd, entry.key_ptr.*); - allocator.free(entry.value_ptr.*.path); + allocator.free(entry.value_ptr.*); self.watches.removeByPtr(entry.key_ptr); return; } @@ -177,37 +170,11 @@ pub fn Create(comptime variant: InterfaceType) type { defer if (comptime variant == .threaded) self.watches_mutex.unlock(); var it = self.watches.iterator(); while (it.next()) |entry| { - if (std.mem.eql(u8, entry.value_ptr.*.path, path)) return true; + if (std.mem.eql(u8, entry.value_ptr.*, path)) return true; } return false; } - // Rewrite stored watch paths after a directory rename. - // Any entry equal to old_path is updated to new_path; any entry whose - // path begins with old_path + sep has its prefix replaced with new_path. - fn rename_watch_paths(self: *@This(), allocator: std.mem.Allocator, old_path: []const u8, new_path: []const u8) void { - if (comptime variant == .threaded) self.watches_mutex.lock(); - defer if (comptime variant == .threaded) self.watches_mutex.unlock(); - var it = self.watches.valueIterator(); - while (it.next()) |entry| { - if (std.mem.eql(u8, entry.path, old_path)) { - const owned = allocator.dupe(u8, new_path) catch continue; - allocator.free(entry.path); - entry.path = owned; - } else if (std.mem.startsWith(u8, entry.path, old_path) and - entry.path.len > old_path.len and - entry.path[old_path.len] == std.fs.path.sep) - { - const suffix = entry.path[old_path.len..]; // includes leading sep - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const new_sub = std.fmt.bufPrint(&path_buf, "{s}{s}", .{ new_path, suffix }) catch continue; - const owned = allocator.dupe(u8, new_sub) catch continue; - allocator.free(entry.path); - entry.path = owned; - } - } - } - pub fn handle_read_ready(self: *@This(), allocator: std.mem.Allocator) (std.posix.ReadError || error{ NoSpaceLeft, OutOfMemory, HandlerFailed })!void { const InotifyEvent = extern struct { wd: i32, @@ -217,13 +184,6 @@ pub fn Create(comptime variant: InterfaceType) type { }; var buf: [65536]u8 align(@alignOf(InotifyEvent)) = undefined; - // Src paths for which we already emitted a paired atomic rename this - // read, so IN_MOVE_SELF for the same inode can be suppressed. - var paired_src_paths: std.ArrayListUnmanaged([]u8) = .empty; - defer { - for (paired_src_paths.items) |p| allocator.free(p); - paired_src_paths.deinit(allocator); - } defer { // Any unpaired MOVED_FROM means the file was moved out of the watched tree. for (self.pending_renames.items) |r| { @@ -252,12 +212,10 @@ pub fn Create(comptime variant: InterfaceType) type { // cannot free the slice while we are still reading from it. var watched_buf: [std.fs.max_path_bytes]u8 = undefined; var watched_len: usize = 0; - var watched_is_dir = false; if (comptime variant == .threaded) self.watches_mutex.lock(); - if (self.watches.get(ev.wd)) |e| { - @memcpy(watched_buf[0..e.path.len], e.path); - watched_len = e.path.len; - watched_is_dir = e.is_dir; + if (self.watches.get(ev.wd)) |p| { + @memcpy(watched_buf[0..p.len], p); + watched_len = p.len; } if (comptime variant == .threaded) self.watches_mutex.unlock(); if (watched_len == 0) continue; @@ -294,16 +252,6 @@ pub fn Create(comptime variant: InterfaceType) type { // Complete rename pair: emit a single atomic rename message. const r = self.pending_renames.swapRemove(i); defer allocator.free(r.path); - // Rewrite any watch entries whose stored path starts with the - // old directory path so subsequent events use the new name. - if (r.object_type == .dir) - self.rename_watch_paths(allocator, r.path, full_path); - // Track the path MOVE_SELF will see so it can be suppressed. - // For directory renames the watch path has just been rewritten - // to full_path (new name); for file renames it stays r.path. - const move_self_path = if (r.object_type == .dir) full_path else r.path; - if (allocator.dupe(u8, move_self_path) catch null) |copy| - paired_src_paths.append(allocator, copy) catch allocator.free(copy); try self.handler.rename(r.path, full_path, r.object_type); } else { // No paired MOVED_FROM, file was moved in from outside the watched tree. @@ -311,31 +259,14 @@ pub fn Create(comptime variant: InterfaceType) type { try self.handler.change(full_path, EventType.created, ot); } } else if (ev.mask & IN.MOVE_SELF != 0) { - // Suppress if the rename was already delivered as a paired - // MOVED_FROM/MOVED_TO event, or if it is still in pending_renames - // as an unpaired MOVED_FROM (the defer above will emit 'deleted'). - // Only emit here when the watched root was moved and its parent - // directory is not itself watched (no MOVED_FROM was generated). - const already_handled = - (for (paired_src_paths.items) |p| { - if (std.mem.eql(u8, p, full_path)) break true; - } else false) or - (for (self.pending_renames.items) |r| { - if (std.mem.eql(u8, r.path, full_path)) break true; - } else false); - if (!already_handled) - try self.handler.change(full_path, EventType.deleted, .dir); - } else if (ev.mask & IN.DELETE_SELF != 0) { - // The watched path itself was deleted. IN_DELETE_SELF does not - // set IN_ISDIR, so use the is_dir recorded at watch registration. - const object_type: ObjectType = if (watched_is_dir) .dir else .file; - try self.handler.change(full_path, EventType.deleted, object_type); + // The watched directory itself was renamed/moved away. + try self.handler.change(full_path, EventType.deleted, .dir); } else { const is_dir = ev.mask & IN.ISDIR != 0; const object_type: ObjectType = if (is_dir) .dir else .file; const event_type: EventType = if (ev.mask & IN.CREATE != 0) .created - else if (ev.mask & IN.DELETE != 0) blk: { + else if (ev.mask & (IN.DELETE | IN.DELETE_SELF) != 0) blk: { // Suppress IN_DELETE|IN_ISDIR for subdirs that have their // own watch: IN_DELETE_SELF on that watch will fire the // same path without duplication. diff --git a/src/types.zig b/src/types.zig index 07b64fc..3108a80 100644 --- a/src/types.zig +++ b/src/types.zig @@ -11,22 +11,11 @@ pub const EventType = enum { deleted, /// A file or directory was renamed or moved. /// - /// Delivery varies by backend: - /// - /// - **INotify**: renames within the watched tree are delivered as a - /// single atomic `rename` callback with both source and destination - /// paths. A move out of the tree appears as `deleted`; a move into - /// the tree appears as `created`. - /// - /// - **kqueue / kqueuedir**: when a watched *directory* is itself - /// renamed, a `renamed` change event is emitted for the old directory - /// path (the new path is not known). Renames of *files inside* a - /// watched directory are detected indirectly via directory-level - /// `NOTE_WRITE` events and appear as a `deleted` event for the old - /// name followed by a `created` event for the new name. - /// - /// - **FSEvents / Windows**: each path involved in a rename receives - /// its own `renamed` change event; the two sides are not paired. + /// INotify delivers this as a single `rename` callback with both the + /// source and destination paths. All other backends (kqueue, FSEvents, + /// Windows) cannot pair the two sides atomically and emit a `deleted` + /// event for the old path followed by a `created` event for the new + /// path instead. renamed, }; diff --git a/test_manual.sh b/test_manual.sh index 12db2ac..d20691e 100755 --- a/test_manual.sh +++ b/test_manual.sh @@ -53,22 +53,6 @@ sleep 0.4 echo "[op] rmdir subdir" rmdir "$TESTDIR/subdir" -sleep 0.4 - -echo "[op] mkdir dirA" -mkdir "$TESTDIR/dirA" -sleep 0.4 - -echo "[op] touch dirA/file3.txt" -touch "$TESTDIR/dirA/file3.txt" -sleep 0.4 - -echo "[op] rename dirA -> dirB" -mv "$TESTDIR/dirA" "$TESTDIR/dirB" -sleep 0.4 - -echo "[op] rmdir dirB (and contents)" -rm -rf "$TESTDIR/dirB" sleep 0.5 echo ""