From 99dec3f68965cb1bc3458918bd6bca25a0b73d75 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 29 Mar 2026 14:28:23 +0200 Subject: [PATCH] fix(inotify): report .closed events separately from .modified events --- src/backend/FSEvents.zig | 1 + src/backend/INotify.zig | 5 ++++- src/backend/KQueue.zig | 1 + src/backend/KQueueDir.zig | 1 + src/backend/Windows.zig | 1 + src/main.zig | 3 +++ src/nightwatch.zig | 1 + src/nightwatch_test.zig | 42 +++++++++++++++++++++++++++++++++++++++ src/types.zig | 5 +++++ 9 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/backend/FSEvents.zig b/src/backend/FSEvents.zig index 53dc69d..3726d97 100644 --- a/src/backend/FSEvents.zig +++ b/src/backend/FSEvents.zig @@ -6,6 +6,7 @@ const ObjectType = types.ObjectType; pub const watches_recursively = true; // FSEventStreamCreate watches the entire subtree pub const detects_file_modifications = true; +pub const emits_close_events = false; handler: *Handler, stream: ?*anyopaque, // FSEventStreamRef diff --git a/src/backend/INotify.zig b/src/backend/INotify.zig index eb71d23..67d7242 100644 --- a/src/backend/INotify.zig +++ b/src/backend/INotify.zig @@ -35,6 +35,7 @@ pub fn Create(comptime variant: InterfaceType) type { pub const watches_recursively = false; pub const detects_file_modifications = true; + pub const emits_close_events = true; pub const polling = variant == .polling; const WatchEntry = struct { path: []u8, is_dir: bool }; @@ -342,8 +343,10 @@ pub fn Create(comptime variant: InterfaceType) type { if (is_dir and self.has_watch_for_path(full_path)) continue; break :blk .deleted; - } else if (ev.mask & (IN.MODIFY | IN.CLOSE_WRITE) != 0) + } else if (ev.mask & IN.MODIFY != 0) .modified + else if (ev.mask & IN.CLOSE_WRITE != 0) + .closed else continue; try self.handler.change(full_path, event_type, object_type); diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index 6aed25f..1bf325d 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -6,6 +6,7 @@ const ObjectType = types.ObjectType; pub const watches_recursively = false; pub const detects_file_modifications = true; +pub const emits_close_events = false; handler: *Handler, kq: std.posix.fd_t, diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index a7574e7..fd27548 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -6,6 +6,7 @@ const ObjectType = types.ObjectType; pub const watches_recursively = false; pub const detects_file_modifications = false; +pub const emits_close_events = false; pub const WatchEntry = struct { fd: std.posix.fd_t, is_file: bool }; handler: *Handler, diff --git a/src/backend/Windows.zig b/src/backend/Windows.zig index ea4f046..ad477b9 100644 --- a/src/backend/Windows.zig +++ b/src/backend/Windows.zig @@ -6,6 +6,7 @@ const ObjectType = types.ObjectType; pub const watches_recursively = true; // ReadDirectoryChangesW with bWatchSubtree=1 pub const detects_file_modifications = true; +pub const emits_close_events = false; const windows = std.os.windows; diff --git a/src/main.zig b/src/main.zig index d322b14..9508587 100644 --- a/src/main.zig +++ b/src/main.zig @@ -50,12 +50,14 @@ const CliHandler = struct { const color: std.io.tty.Color = switch (event_type) { .created => .green, .modified => .blue, + .closed => .bright_black, .deleted => .red, .renamed => .magenta, }; const event_label = switch (event_type) { .created => "create ", .modified => "modify ", + .closed => "close ", .deleted => "delete ", .renamed => "rename ", }; @@ -160,6 +162,7 @@ fn usage(out: std.fs.File) !void { \\Events printed to stdout (columns: event type path): \\ create a file or directory was created \\ modify a file was modified + \\ close a file was closed after writing (Linux only) \\ delete a file or directory was deleted \\ rename a file or directory was renamed \\ diff --git a/src/nightwatch.zig b/src/nightwatch.zig index d0a20cc..4b891a8 100644 --- a/src/nightwatch.zig +++ b/src/nightwatch.zig @@ -114,6 +114,7 @@ pub fn Create(comptime variant: Variant) type { /// `watch()` do receive per-file `NOTE_WRITE` events and will report /// modifications. pub const detects_file_modifications = Backend.detects_file_modifications; + pub const emits_close_events = Backend.emits_close_events; /// Create a new watcher. /// diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index 86bf11d..323c56c 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -245,6 +245,42 @@ fn testModifyFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.testing.expect(th.hasChange(file_path, .modified, .file)); } +fn testCloseFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { + if (comptime !Watcher.emits_close_events) return error.SkipZigTest; + + const TH = MakeTestHandler(Watcher); + + const tmp = try makeTempDir(allocator); + defer { + removeTempDir(tmp); + allocator.free(tmp); + } + + const th = try TH.init(allocator); + defer th.deinit(); + + const file_path = try std.fs.path.join(allocator, &.{ tmp, "data.txt" }); + defer allocator.free(file_path); + { + const f = try std.fs.createFileAbsolute(file_path, .{}); + f.close(); + } + + var watcher = try Watcher.init(allocator, &th.handler); + defer watcher.deinit(); + try watcher.watch(tmp); + + { + const f = try std.fs.openFileAbsolute(file_path, .{ .mode = .write_only }); + defer f.close(); + try f.writeAll("hello nightwatch\n"); + } + + try drainEvents(Watcher, &watcher); + try std.testing.expect(th.hasChange(file_path, .modified, .file)); + try std.testing.expect(th.hasChange(file_path, .closed, .file)); +} + fn testDeleteFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); @@ -578,6 +614,12 @@ test "writing to a file emits a 'modified' event" { } } +test "closing a file after writing emits a 'closed' event (inotify only)" { + inline for (comptime std.enums.values(nw.Variant)) |variant| { + try testCloseFile(nw.Create(variant), std.testing.allocator); + } +} + test "deleting a file emits a 'deleted' event" { inline for (comptime std.enums.values(nw.Variant)) |variant| { try testDeleteFile(nw.Create(variant), std.testing.allocator); diff --git a/src/types.zig b/src/types.zig index 07b64fc..86d6dd6 100644 --- a/src/types.zig +++ b/src/types.zig @@ -7,6 +7,11 @@ pub const EventType = enum { created, /// A file's contents were modified. modified, + /// A file was closed. + /// + /// Only delivered by INotfiy (Linux) and only if the file was opened + /// for writing. + closed, /// A file or directory was deleted. deleted, /// A file or directory was renamed or moved.