feat: emit subtree creates on dir move-in; normalize FSEvents renames
This commit is contained in:
parent
bfd1125449
commit
ea72e0654d
8 changed files with 122 additions and 12 deletions
|
|
@ -7,8 +7,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;
|
||||
pub const emits_rename_for_files = false;
|
||||
pub const emits_rename_for_dirs = false;
|
||||
pub const emits_subtree_created_on_movein = false; // FSEvents emits per-path events only; no subtree synthesis
|
||||
|
||||
handler: *Handler,
|
||||
stream: ?*anyopaque, // FSEventStreamRef
|
||||
|
|
@ -235,7 +236,11 @@ fn callback(
|
|||
ctx.handler.change(path, .deleted, ot) catch {};
|
||||
}
|
||||
if (flags & kFSEventStreamEventFlagItemRenamed != 0) {
|
||||
ctx.handler.change(path, .renamed, ot) catch {};
|
||||
// FSEvents fires ItemRenamed for both sides of a rename unpaired.
|
||||
// Normalize to created/deleted based on whether the path still exists,
|
||||
// so move-in appears as created and move-out as deleted on all platforms.
|
||||
const exists = if (std.fs.accessAbsolute(path, .{})) |_| true else |_| false;
|
||||
ctx.handler.change(path, if (exists) .created else .deleted, ot) catch {};
|
||||
}
|
||||
if (flags & kFSEventStreamEventFlagItemModified != 0) {
|
||||
ctx.handler.change(path, .modified, ot) catch {};
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub fn Create(comptime variant: InterfaceType) type {
|
|||
pub const emits_close_events = true;
|
||||
pub const emits_rename_for_files = true;
|
||||
pub const emits_rename_for_dirs = true;
|
||||
pub const emits_subtree_created_on_movein = true;
|
||||
pub const polling = variant == .polling;
|
||||
|
||||
const WatchEntry = struct { path: []u8, is_dir: bool };
|
||||
|
|
@ -211,6 +212,26 @@ pub fn Create(comptime variant: InterfaceType) type {
|
|||
}
|
||||
}
|
||||
|
||||
// Walk dir_path recursively, emitting a 'created' event for every file and
|
||||
// subdirectory. Called after an unpaired IN_MOVED_TO for a directory so that
|
||||
// the caller sees individual 'created' events for the moved-in tree contents.
|
||||
fn emit_subtree_created(self: *@This(), dir_path: []const u8) error{HandlerFailed}!void {
|
||||
var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close();
|
||||
var iter = dir.iterate();
|
||||
while (iter.next() catch return) |entry| {
|
||||
const ot: ObjectType = switch (entry.kind) {
|
||||
.file => .file,
|
||||
.directory => .dir,
|
||||
else => continue,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -312,6 +333,7 @@ pub fn Create(comptime variant: InterfaceType) type {
|
|||
// No paired MOVED_FROM, file was moved in from outside the watched tree.
|
||||
const ot: ObjectType = if (ev.mask & IN.ISDIR != 0) .dir else .file;
|
||||
try self.handler.change(full_path, EventType.created, ot);
|
||||
if (ot == .dir) try self.emit_subtree_created(full_path);
|
||||
}
|
||||
} else if (ev.mask & IN.MOVE_SELF != 0) {
|
||||
// Suppress if the rename was already delivered as a paired
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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;
|
||||
pub const emits_subtree_created_on_movein = true;
|
||||
|
||||
handler: *Handler,
|
||||
kq: std.posix.fd_t,
|
||||
|
|
@ -282,8 +283,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8)
|
|||
// Emit all events outside the lock so handlers may safely call watch()/unwatch().
|
||||
// Emit created dirs, then deletions, then creations. Deletions first ensures that
|
||||
// a rename (old disappears, new appears) reports the source path before the dest.
|
||||
for (new_dirs.items) |full_path|
|
||||
for (new_dirs.items) |full_path| {
|
||||
try self.handler.change(full_path, EventType.created, .dir);
|
||||
try self.emit_subtree_created(allocator, full_path);
|
||||
}
|
||||
for (to_delete.items) |name| {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||
|
|
@ -299,6 +302,31 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8)
|
|||
// arena.deinit() frees current_files, current_dirs, new_dirs, and list metadata
|
||||
}
|
||||
|
||||
// Walk dir_path recursively, emitting 'created' events and registering per-file
|
||||
// vnode watches for modification tracking. Called after a new dir appears in
|
||||
// scan_dir (e.g. a directory moved into the watched tree) so callers see
|
||||
// individual 'created' events for all pre-existing contents.
|
||||
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();
|
||||
while (iter.next() catch return) |entry| {
|
||||
const ot: ObjectType = switch (entry.kind) {
|
||||
.file => .file,
|
||||
.directory => .dir,
|
||||
else => continue,
|
||||
};
|
||||
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 == .file) {
|
||||
self.register_file_watch(allocator, full_path);
|
||||
} else {
|
||||
try self.emit_subtree_created(allocator, full_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_file_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) void {
|
||||
self.file_watches_mutex.lock();
|
||||
const already = self.file_watches.contains(path);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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 emits_subtree_created_on_movein = true;
|
||||
pub const WatchEntry = struct { fd: std.posix.fd_t, is_file: bool };
|
||||
|
||||
handler: *Handler,
|
||||
|
|
@ -276,8 +277,10 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8)
|
|||
|
||||
// Emit all events outside the lock so handlers may safely call watch()/unwatch().
|
||||
// Order: new dirs, deletions (source before dest for renames), creations, modifications.
|
||||
for (new_dirs.items) |full_path|
|
||||
for (new_dirs.items) |full_path| {
|
||||
try self.handler.change(full_path, EventType.created, .dir);
|
||||
try self.emit_subtree_created(full_path);
|
||||
}
|
||||
for (to_delete.items) |name| {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||
|
|
@ -296,6 +299,26 @@ fn scan_dir(self: *@This(), allocator: std.mem.Allocator, dir_path: []const u8)
|
|||
// arena.deinit() frees current_files, current_dirs, new_dirs, and list metadata
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close();
|
||||
var iter = dir.iterate();
|
||||
while (iter.next() catch return) |entry| {
|
||||
const ot: ObjectType = switch (entry.kind) {
|
||||
.file => .file,
|
||||
.directory => .dir,
|
||||
else => continue,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{ WatchFailed, OutOfMemory }!void {
|
||||
self.watches_mutex.lock();
|
||||
const already = self.watches.contains(path);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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;
|
||||
pub const emits_subtree_created_on_movein = true;
|
||||
|
||||
const windows = std.os.windows;
|
||||
|
||||
|
|
@ -279,6 +280,13 @@ fn thread_fn(
|
|||
watches_mutex.lock();
|
||||
break;
|
||||
};
|
||||
if (event_type == .created and object_type == .dir) {
|
||||
emit_subtree_created(handler, full_path) catch |e| {
|
||||
std.log.err("nightwatch: handler returned {s}, stopping watch thread", .{@errorName(e)});
|
||||
watches_mutex.lock();
|
||||
break;
|
||||
};
|
||||
}
|
||||
watches_mutex.lock();
|
||||
if (next_entry_offset == 0) break;
|
||||
offset += next_entry_offset;
|
||||
|
|
@ -341,6 +349,27 @@ pub fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8)
|
|||
// Called from add_watch and from thread_fn on directory renames so that
|
||||
// FILE_ACTION_REMOVED events for children of a renamed directory can
|
||||
// resolve their ObjectType from cache.
|
||||
// Walk dir_path recursively, emitting 'created' events for all contents.
|
||||
// Called after a directory is created or moved into the watched tree so callers
|
||||
// see individual 'created' events for all pre-existing contents.
|
||||
// Must be called with watches_mutex NOT held (handler calls are outside the lock).
|
||||
fn emit_subtree_created(handler: *Handler, dir_path: []const u8) error{HandlerFailed}!void {
|
||||
var dir = std.fs.openDirAbsolute(dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close();
|
||||
var iter = dir.iterate();
|
||||
while (iter.next() catch return) |entry| {
|
||||
const ot: ObjectType = switch (entry.kind) {
|
||||
.file => .file,
|
||||
.directory => .dir,
|
||||
else => continue,
|
||||
};
|
||||
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 handler.change(full_path, EventType.created, ot);
|
||||
if (ot == .dir) try emit_subtree_created(handler, full_path);
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_path_types(self: *@This(), allocator: std.mem.Allocator, root: []const u8) void {
|
||||
scan_path_types_into(allocator, &self.path_types, root);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ pub fn Create(comptime variant: Variant) type {
|
|||
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;
|
||||
pub const emits_subtree_created_on_movein = Backend.emits_subtree_created_on_movein;
|
||||
|
||||
/// Create a new watcher.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -678,11 +678,8 @@ fn testMoveOutFile(comptime Watcher: type, allocator: std.mem.Allocator) !void {
|
|||
try std.fs.renameAbsolute(src_path, dst_path);
|
||||
try drainEvents(Watcher, &watcher);
|
||||
|
||||
// File moved out of the watched tree: appears as deleted (INotify, Windows)
|
||||
// or renamed (kqueue, which holds a vnode fd and sees NOTE_RENAME).
|
||||
const src_gone = th.hasChange(src_path, .deleted, .file) or
|
||||
th.hasChange(src_path, .renamed, .file);
|
||||
try std.testing.expect(src_gone);
|
||||
// File moved out of the watched tree: appears as deleted on all platforms.
|
||||
try std.testing.expect(th.hasChange(src_path, .deleted, .file));
|
||||
// No event for the destination - it is in an unwatched directory.
|
||||
try std.testing.expect(!th.hasChange(dst_path, .created, .file));
|
||||
}
|
||||
|
|
@ -767,6 +764,8 @@ fn testMoveInSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void
|
|||
try drainEvents(Watcher, &watcher);
|
||||
|
||||
try std.testing.expect(th.hasChange(dst_sub, .created, .dir));
|
||||
if (comptime Watcher.emits_subtree_created_on_movein)
|
||||
try std.testing.expect(th.hasChange(dst_file, .created, .file));
|
||||
|
||||
// Delete the file inside the moved-in subdir.
|
||||
try std.fs.deleteFileAbsolute(dst_file);
|
||||
|
|
|
|||
|
|
@ -38,8 +38,11 @@ pub const EventType = enum {
|
|||
/// watched directory are detected indirectly via directory-level
|
||||
/// `NOTE_WRITE` events and appear as `deleted` + `created`.
|
||||
///
|
||||
/// - **FSEvents**: each path involved in a rename receives its own
|
||||
/// `renamed` change event; the two sides are not paired.
|
||||
/// - **FSEvents**: renames are normalized to `deleted` (old path) and
|
||||
/// `created` (new path) via an existence check at event time. The two
|
||||
/// sides are not paired. Move-in and move-out therefore appear as
|
||||
/// `created` and `deleted` respectively, consistent with all other
|
||||
/// backends.
|
||||
renamed,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue