Compare commits

...

4 commits

10 changed files with 174 additions and 8 deletions

View file

@ -6,6 +6,9 @@ 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;
pub const emits_rename_for_files = true;
pub const emits_rename_for_dirs = true;
handler: *Handler,
stream: ?*anyopaque, // FSEventStreamRef

View file

@ -35,6 +35,9 @@ 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 emits_rename_for_files = true;
pub const emits_rename_for_dirs = true;
pub const polling = variant == .polling;
const WatchEntry = struct { path: []u8, is_dir: bool };
@ -342,8 +345,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);

View file

@ -6,6 +6,9 @@ const ObjectType = types.ObjectType;
pub const watches_recursively = false;
pub const detects_file_modifications = true;
pub const emits_close_events = false;
pub const emits_rename_for_files = false;
pub const emits_rename_for_dirs = true;
handler: *Handler,
kq: std.posix.fd_t,

View file

@ -6,6 +6,9 @@ const ObjectType = types.ObjectType;
pub const watches_recursively = false;
pub const detects_file_modifications = false;
pub const emits_close_events = false;
pub const emits_rename_for_files = false;
pub const emits_rename_for_dirs = true;
pub const WatchEntry = struct { fd: std.posix.fd_t, is_file: bool };
handler: *Handler,

View file

@ -6,6 +6,9 @@ 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;
pub const emits_rename_for_files = true;
pub const emits_rename_for_dirs = true;
const windows = std.os.windows;
@ -165,8 +168,11 @@ fn thread_fn(
continue;
};
// Determine object_type: try GetFileAttributesW; cache result.
const object_type: ObjectType = if (event_type == .deleted) blk: {
// Path no longer exists; use cached type if available.
// For deleted paths and the old name in a rename, the path no
// longer exists at event time so GetFileAttributesW would fail;
// use the cached type instead.
const object_type: ObjectType = if (event_type == .deleted or
info.Action == FILE_ACTION_RENAMED_OLD_NAME) blk: {
const cached = path_types.fetchRemove(full_path);
break :blk if (cached) |kv| blk2: {
allocator.free(kv.key);
@ -248,6 +254,36 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8)
if (win32.ReadDirectoryChangesW(handle, buf.ptr, buf_size, 1, notify_filter, null, &w.overlapped, null) == 0)
return error.WatchFailed;
try self.watches.put(allocator, owned_path, w);
// Seed path_types with pre-existing entries so delete/rename events for
// paths that existed before this watch started can resolve their ObjectType.
self.scan_path_types(allocator, owned_path);
}
// Walk root recursively and seed path_types with the type of every entry.
// Called from add_watch (mutex already held) so pre-existing paths are
// known before any FILE_ACTION_REMOVED or FILE_ACTION_RENAMED_OLD_NAME fires.
fn scan_path_types(self: *@This(), allocator: std.mem.Allocator, root: []const u8) void {
var dir = std.fs.openDirAbsolute(root, .{ .iterate = true }) catch return;
defer dir.close();
var iter = dir.iterate();
while (iter.next() catch return) |entry| {
const ot: ObjectType = switch (entry.kind) {
.directory => .dir,
.file => .file,
else => continue,
};
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const full_path = std.fmt.bufPrint(&path_buf, "{s}\\{s}", .{ root, entry.name }) catch continue;
const gop = self.path_types.getOrPut(allocator, full_path) catch continue;
if (!gop.found_existing) {
gop.key_ptr.* = allocator.dupe(u8, full_path) catch {
_ = self.path_types.remove(full_path);
continue;
};
}
gop.value_ptr.* = ot;
if (ot == .dir) self.scan_path_types(allocator, gop.key_ptr.*);
}
}
pub fn remove_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void {

View file

@ -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
\\

View file

@ -114,6 +114,9 @@ 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;
pub const emits_rename_for_files = Backend.emits_rename_for_files;
pub const emits_rename_for_dirs = Backend.emits_rename_for_dirs;
/// Create a new watcher.
///

View file

@ -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);
@ -358,15 +394,56 @@ fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void {
try std.fs.renameAbsolute(src_path, dst_path);
try drainEvents(Watcher, &watcher);
if (builtin.os.tag == .linux) {
try std.testing.expect(th.hasRename(src_path, dst_path));
if (comptime Watcher.emits_rename_for_files) {
// INotify delivers a paired atomic rename callback; FSEvents/Windows
// deliver individual .renamed change events per path.
const has_rename = th.hasRename(src_path, dst_path) or
th.hasChange(src_path, .renamed, .file);
try std.testing.expect(has_rename);
} else {
const has_old = th.hasChange(src_path, .renamed, .file) or th.hasChange(src_path, .deleted, .file);
const has_new = th.hasChange(dst_path, .renamed, .file) or th.hasChange(dst_path, .created, .file);
try std.testing.expect(has_old or has_new);
// KQueue/KQueueDir: file rename appears as delete + create.
try std.testing.expect(th.hasChange(src_path, .deleted, .file));
try std.testing.expect(th.hasChange(dst_path, .created, .file));
}
}
fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void {
if (comptime !Watcher.emits_rename_for_dirs) 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 src_path = try std.fs.path.join(allocator, &.{ tmp, "before" });
defer allocator.free(src_path);
const dst_path = try std.fs.path.join(allocator, &.{ tmp, "after" });
defer allocator.free(dst_path);
try std.fs.makeDirAbsolute(src_path);
var watcher = try Watcher.init(allocator, &th.handler);
defer watcher.deinit();
try watcher.watch(tmp);
try std.fs.renameAbsolute(src_path, dst_path);
try drainEvents(Watcher, &watcher);
// All backends with emits_rename_for_dirs=true deliver at least a rename
// event for the source path. INotify delivers a paired rename callback;
// KQueue/KQueueDir deliver change(.renamed, .dir) for the old path only;
// FSEvents/Windows deliver change(.renamed, .dir) for both paths.
const has_rename = th.hasRename(src_path, dst_path) or
th.hasChange(src_path, .renamed, .dir);
try std.testing.expect(has_rename);
}
fn testUnwatchedDir(comptime Watcher: type, allocator: std.mem.Allocator) !void {
const TH = MakeTestHandler(Watcher);
@ -578,6 +655,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);
@ -602,6 +685,12 @@ test "renaming a file is reported correctly per-platform" {
}
}
test "renaming a directory emits a rename event" {
inline for (comptime std.enums.values(nw.Variant)) |variant| {
try testRenameDir(nw.Create(variant), std.testing.allocator);
}
}
test "an unwatched directory produces no events" {
inline for (comptime std.enums.values(nw.Variant)) |variant| {
try testUnwatchedDir(nw.Create(variant), std.testing.allocator);

View file

@ -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.

View file

@ -55,6 +55,22 @@ Start-Sleep -Milliseconds 400
Write-Host "[op] rmdir subdir"
Remove-Item -Path "$TESTDIR\subdir"
Start-Sleep -Milliseconds 400
Write-Host "[op] mkdir dirA"
New-Item -ItemType Directory -Path "$TESTDIR\dirA" | Out-Null
Start-Sleep -Milliseconds 400
Write-Host "[op] touch dirA\file3.txt"
New-Item -ItemType File -Path "$TESTDIR\dirA\file3.txt" | Out-Null
Start-Sleep -Milliseconds 400
Write-Host "[op] rename dirA -> dirB"
Rename-Item -Path "$TESTDIR\dirA" -NewName "dirB"
Start-Sleep -Milliseconds 400
Write-Host "[op] rmdir dirB (and contents)"
Remove-Item -Recurse -Force -Path "$TESTDIR\dirB"
Start-Sleep -Milliseconds 500
Write-Host ""