Compare commits
4 commits
95c7580a87
...
ecdb311c89
| Author | SHA1 | Date | |
|---|---|---|---|
| ecdb311c89 | |||
| 4e5ff53d0d | |||
| f6158c8240 | |||
| 99dec3f689 |
10 changed files with 174 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
\\
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue