feat: add object type to APIs and CLI output
This commit is contained in:
parent
a2523a5b53
commit
0dcd30acb4
3 changed files with 111 additions and 81 deletions
28
src/main.zig
28
src/main.zig
|
|
@ -24,27 +24,36 @@ const CliHandler = struct {
|
||||||
.wait_readable = if (nightwatch.linux_poll_mode) wait_readable_cb else {},
|
.wait_readable = if (nightwatch.linux_poll_mode) wait_readable_cb else {},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn change_cb(h: *nightwatch.Handler, path: []const u8, event_type: nightwatch.EventType) error{HandlerFailed}!void {
|
fn change_cb(h: *nightwatch.Handler, path: []const u8, event_type: nightwatch.EventType, object_type: nightwatch.ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *CliHandler = @fieldParentPtr("handler", h);
|
const self: *CliHandler = @fieldParentPtr("handler", h);
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var stdout = self.out.writer(&buf);
|
var stdout = self.out.writer(&buf);
|
||||||
defer stdout.interface.flush() catch {};
|
defer stdout.interface.flush() catch {};
|
||||||
const label = switch (event_type) {
|
const event_label = switch (event_type) {
|
||||||
.created => "create ",
|
.created => "create ",
|
||||||
.modified => "modify ",
|
.modified => "modify ",
|
||||||
.deleted => "delete ",
|
.deleted => "delete ",
|
||||||
.dir_created => "mkdir ",
|
|
||||||
.renamed => "rename ",
|
.renamed => "rename ",
|
||||||
};
|
};
|
||||||
stdout.interface.print("{s} {s}\n", .{ label, path }) catch return error.HandlerFailed;
|
const type_label = switch (object_type) {
|
||||||
|
.file => "file",
|
||||||
|
.dir => "dir ",
|
||||||
|
.unknown => "? ",
|
||||||
|
};
|
||||||
|
stdout.interface.print("{s} {s} {s}\n", .{ event_label, type_label, path }) catch return error.HandlerFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_cb(h: *nightwatch.Handler, src: []const u8, dst: []const u8) error{HandlerFailed}!void {
|
fn rename_cb(h: *nightwatch.Handler, src: []const u8, dst: []const u8, object_type: nightwatch.ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *CliHandler = @fieldParentPtr("handler", h);
|
const self: *CliHandler = @fieldParentPtr("handler", h);
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
var stdout = self.out.writer(&buf);
|
var stdout = self.out.writer(&buf);
|
||||||
defer stdout.interface.flush() catch {};
|
defer stdout.interface.flush() catch {};
|
||||||
stdout.interface.print("rename {s} -> {s}\n", .{ src, dst }) catch return error.HandlerFailed;
|
const type_label = switch (object_type) {
|
||||||
|
.file => "file",
|
||||||
|
.dir => "dir ",
|
||||||
|
.unknown => "? ",
|
||||||
|
};
|
||||||
|
stdout.interface.print("rename {s} {s} -> {s}\n", .{ type_label, src, dst }) catch return error.HandlerFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_readable_cb(_: *nightwatch.Handler) error{HandlerFailed}!nightwatch.ReadableStatus {
|
fn wait_readable_cb(_: *nightwatch.Handler) error{HandlerFailed}!nightwatch.ReadableStatus {
|
||||||
|
|
@ -101,13 +110,14 @@ fn usage(out: std.fs.File) !void {
|
||||||
\\
|
\\
|
||||||
\\The Watch never sleeps.
|
\\The Watch never sleeps.
|
||||||
\\
|
\\
|
||||||
\\Events printed to stdout:
|
\\Events printed to stdout (columns: event type path):
|
||||||
\\ create a file was created
|
\\ create a file or directory was created
|
||||||
\\ modify a file was modified
|
\\ modify a file was modified
|
||||||
\\ delete a file or directory was deleted
|
\\ delete a file or directory was deleted
|
||||||
\\ mkdir a directory was created
|
|
||||||
\\ rename a file or directory was renamed
|
\\ rename a file or directory was renamed
|
||||||
\\
|
\\
|
||||||
|
\\Type column: file, dir, or ? (unknown)
|
||||||
|
\\
|
||||||
\\Stand down with Ctrl-C.
|
\\Stand down with Ctrl-C.
|
||||||
\\
|
\\
|
||||||
, .{});
|
, .{});
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,19 @@ pub const EventType = enum {
|
||||||
created,
|
created,
|
||||||
modified,
|
modified,
|
||||||
deleted,
|
deleted,
|
||||||
/// A new directory was created inside a watched directory.
|
|
||||||
/// The library automatically begins watching it; no action is required.
|
|
||||||
dir_created,
|
|
||||||
/// Only produced on macOS and Windows where the OS gives no pairing info.
|
/// Only produced on macOS and Windows where the OS gives no pairing info.
|
||||||
/// On Linux, paired renames are emitted as a { "FW", "rename", from, to } message instead.
|
/// On Linux, paired renames are emitted as a rename event with both paths instead.
|
||||||
renamed,
|
renamed,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const ObjectType = enum {
|
||||||
|
file,
|
||||||
|
dir,
|
||||||
|
/// The object type could not be determined (e.g. a deleted file on Windows
|
||||||
|
/// where the path no longer exists to query).
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
HandlerFailed,
|
HandlerFailed,
|
||||||
OutOfMemory,
|
OutOfMemory,
|
||||||
|
|
@ -29,18 +34,18 @@ pub const Handler = struct {
|
||||||
vtable: *const VTable,
|
vtable: *const VTable,
|
||||||
|
|
||||||
pub const VTable = struct {
|
pub const VTable = struct {
|
||||||
change: *const fn (handler: *Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void,
|
change: *const fn (handler: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
rename: *const fn (handler: *Handler, src_path: []const u8, dst_path: []const u8) error{HandlerFailed}!void,
|
rename: *const fn (handler: *Handler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void,
|
||||||
/// Only present in Linux poll mode (linux_poll_mode == true).
|
/// Only present in Linux poll mode (linux_poll_mode == true).
|
||||||
wait_readable: if (linux_poll_mode) *const fn (handler: *Handler) error{HandlerFailed}!ReadableStatus else void,
|
wait_readable: if (linux_poll_mode) *const fn (handler: *Handler) error{HandlerFailed}!ReadableStatus else void,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn change(handler: *Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void {
|
fn change(handler: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void {
|
||||||
return handler.vtable.change(handler, path, event_type);
|
return handler.vtable.change(handler, path, event_type, object_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename(handler: *Handler, src_path: []const u8, dst_path: []const u8) error{HandlerFailed}!void {
|
fn rename(handler: *Handler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void {
|
||||||
return handler.vtable.rename(handler, src_path, dst_path);
|
return handler.vtable.rename(handler, src_path, dst_path, object_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_readable(handler: *Handler) error{HandlerFailed}!ReadableStatus {
|
fn wait_readable(handler: *Handler) error{HandlerFailed}!ReadableStatus {
|
||||||
|
|
@ -138,18 +143,18 @@ const Interceptor = struct {
|
||||||
.wait_readable = if (linux_poll_mode) wait_readable_cb else {},
|
.wait_readable = if (linux_poll_mode) wait_readable_cb else {},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn change_cb(h: *Handler, path: []const u8, event_type: EventType) error{HandlerFailed}!void {
|
fn change_cb(h: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *Interceptor = @fieldParentPtr("handler", h);
|
const self: *Interceptor = @fieldParentPtr("handler", h);
|
||||||
if (event_type == .dir_created and !Backend.watches_recursively) {
|
if (event_type == .created and object_type == .dir and !Backend.watches_recursively) {
|
||||||
self.backend.add_watch(self.allocator, path) catch {};
|
self.backend.add_watch(self.allocator, path) catch {};
|
||||||
recurse_watch(&self.backend, self.allocator, path);
|
recurse_watch(&self.backend, self.allocator, path);
|
||||||
}
|
}
|
||||||
return self.user_handler.change(path, event_type);
|
return self.user_handler.change(path, event_type, object_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_cb(h: *Handler, src: []const u8, dst: []const u8) error{HandlerFailed}!void {
|
fn rename_cb(h: *Handler, src: []const u8, dst: []const u8, object_type: ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *Interceptor = @fieldParentPtr("handler", h);
|
const self: *Interceptor = @fieldParentPtr("handler", h);
|
||||||
return self.user_handler.rename(src, dst);
|
return self.user_handler.rename(src, dst, object_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_readable_cb(h: *Handler) error{HandlerFailed}!ReadableStatus {
|
fn wait_readable_cb(h: *Handler) error{HandlerFailed}!ReadableStatus {
|
||||||
|
|
@ -311,6 +316,7 @@ const INotifyBackend = struct {
|
||||||
const PendingRename = struct {
|
const PendingRename = struct {
|
||||||
cookie: u32,
|
cookie: u32,
|
||||||
path: []u8, // owned by drain's allocator
|
path: []u8, // owned by drain's allocator
|
||||||
|
object_type: ObjectType,
|
||||||
};
|
};
|
||||||
|
|
||||||
var buf: [4096]u8 align(@alignOf(InotifyEvent)) = undefined;
|
var buf: [4096]u8 align(@alignOf(InotifyEvent)) = undefined;
|
||||||
|
|
@ -318,7 +324,7 @@ const INotifyBackend = struct {
|
||||||
defer {
|
defer {
|
||||||
// Any unpaired MOVED_FROM means the file was moved out of the watched tree.
|
// Any unpaired MOVED_FROM means the file was moved out of the watched tree.
|
||||||
for (pending_renames.items) |r| {
|
for (pending_renames.items) |r| {
|
||||||
self.handler.change(r.path, EventType.deleted) catch {};
|
self.handler.change(r.path, EventType.deleted, r.object_type) catch {};
|
||||||
allocator.free(r.path);
|
allocator.free(r.path);
|
||||||
}
|
}
|
||||||
pending_renames.deinit(allocator);
|
pending_renames.deinit(allocator);
|
||||||
|
|
@ -355,6 +361,7 @@ const INotifyBackend = struct {
|
||||||
try pending_renames.append(allocator, .{
|
try pending_renames.append(allocator, .{
|
||||||
.cookie = ev.cookie,
|
.cookie = ev.cookie,
|
||||||
.path = try allocator.dupe(u8, full_path),
|
.path = try allocator.dupe(u8, full_path),
|
||||||
|
.object_type = if (ev.mask & IN.ISDIR != 0) .dir else .file,
|
||||||
});
|
});
|
||||||
} else if (ev.mask & IN.MOVED_TO != 0) {
|
} else if (ev.mask & IN.MOVED_TO != 0) {
|
||||||
// Look for a paired MOVED_FROM.
|
// Look for a paired MOVED_FROM.
|
||||||
|
|
@ -369,29 +376,32 @@ const INotifyBackend = struct {
|
||||||
// Complete rename pair: emit a single atomic rename message.
|
// Complete rename pair: emit a single atomic rename message.
|
||||||
const r = pending_renames.swapRemove(i);
|
const r = pending_renames.swapRemove(i);
|
||||||
defer allocator.free(r.path);
|
defer allocator.free(r.path);
|
||||||
try self.handler.rename(r.path, full_path);
|
try self.handler.rename(r.path, full_path, r.object_type);
|
||||||
} else {
|
} else {
|
||||||
// No paired MOVED_FROM, file was moved in from outside the watched tree.
|
// No paired MOVED_FROM, file was moved in from outside the watched tree.
|
||||||
try self.handler.change(full_path, EventType.created);
|
const ot: ObjectType = if (ev.mask & IN.ISDIR != 0) .dir else .file;
|
||||||
|
try self.handler.change(full_path, EventType.created, ot);
|
||||||
}
|
}
|
||||||
} else if (ev.mask & IN.MOVE_SELF != 0) {
|
} else if (ev.mask & IN.MOVE_SELF != 0) {
|
||||||
// The watched directory itself was renamed/moved away.
|
// The watched directory itself was renamed/moved away.
|
||||||
try self.handler.change(full_path, EventType.deleted);
|
try self.handler.change(full_path, EventType.deleted, .dir);
|
||||||
} else {
|
} 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)
|
const event_type: EventType = if (ev.mask & IN.CREATE != 0)
|
||||||
if (ev.mask & IN.ISDIR != 0) .dir_created else .created
|
.created
|
||||||
else if (ev.mask & (IN.DELETE | IN.DELETE_SELF) != 0) blk: {
|
else if (ev.mask & (IN.DELETE | IN.DELETE_SELF) != 0) blk: {
|
||||||
// Suppress IN_DELETE|IN_ISDIR for subdirs that have their
|
// Suppress IN_DELETE|IN_ISDIR for subdirs that have their
|
||||||
// own watch: IN_DELETE_SELF on that watch will fire the
|
// own watch: IN_DELETE_SELF on that watch will fire the
|
||||||
// same path without duplication.
|
// same path without duplication.
|
||||||
if (ev.mask & IN.ISDIR != 0 and self.has_watch_for_path(full_path))
|
if (is_dir and self.has_watch_for_path(full_path))
|
||||||
continue;
|
continue;
|
||||||
break :blk .deleted;
|
break :blk .deleted;
|
||||||
} else if (ev.mask & (IN.MODIFY | IN.CLOSE_WRITE) != 0)
|
} else if (ev.mask & (IN.MODIFY | IN.CLOSE_WRITE) != 0)
|
||||||
.modified
|
.modified
|
||||||
else
|
else
|
||||||
continue;
|
continue;
|
||||||
try self.handler.change(full_path, event_type);
|
try self.handler.change(full_path, event_type, object_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -599,19 +609,18 @@ const FSEventsBackend = struct {
|
||||||
// FSEvents coalesces operations, so multiple flags may be set on
|
// FSEvents coalesces operations, so multiple flags may be set on
|
||||||
// a single event. Emit one change call per applicable flag so
|
// a single event. Emit one change call per applicable flag so
|
||||||
// callers see all relevant event types (e.g. created + modified).
|
// callers see all relevant event types (e.g. created + modified).
|
||||||
const is_dir = flags & kFSEventStreamEventFlagItemIsDir != 0;
|
const ot: ObjectType = if (flags & kFSEventStreamEventFlagItemIsDir != 0) .dir else .file;
|
||||||
if (flags & kFSEventStreamEventFlagItemCreated != 0) {
|
if (flags & kFSEventStreamEventFlagItemCreated != 0) {
|
||||||
const et: EventType = if (is_dir) .dir_created else .created;
|
ctx.handler.change(path, .created, ot) catch {};
|
||||||
ctx.handler.change(path, et) catch {};
|
|
||||||
}
|
}
|
||||||
if (flags & kFSEventStreamEventFlagItemRemoved != 0) {
|
if (flags & kFSEventStreamEventFlagItemRemoved != 0) {
|
||||||
ctx.handler.change(path, .deleted) catch {};
|
ctx.handler.change(path, .deleted, ot) catch {};
|
||||||
}
|
}
|
||||||
if (flags & kFSEventStreamEventFlagItemRenamed != 0) {
|
if (flags & kFSEventStreamEventFlagItemRenamed != 0) {
|
||||||
ctx.handler.change(path, .renamed) catch {};
|
ctx.handler.change(path, .renamed, ot) catch {};
|
||||||
}
|
}
|
||||||
if (flags & kFSEventStreamEventFlagItemModified != 0) {
|
if (flags & kFSEventStreamEventFlagItemModified != 0) {
|
||||||
ctx.handler.change(path, .modified) catch {};
|
ctx.handler.change(path, .modified, ot) catch {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -755,7 +764,7 @@ const KQueueBackend = struct {
|
||||||
self.file_watches_mutex.unlock();
|
self.file_watches_mutex.unlock();
|
||||||
if (file_path) |fp| {
|
if (file_path) |fp| {
|
||||||
if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND) != 0)
|
if (ev.fflags & (NOTE_WRITE | NOTE_EXTEND) != 0)
|
||||||
self.handler.change(fp, EventType.modified) catch return;
|
self.handler.change(fp, EventType.modified, .file) catch return;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -768,9 +777,9 @@ const KQueueBackend = struct {
|
||||||
self.watches_mutex.unlock();
|
self.watches_mutex.unlock();
|
||||||
if (dir_path == null) continue;
|
if (dir_path == null) continue;
|
||||||
if (ev.fflags & NOTE_DELETE != 0) {
|
if (ev.fflags & NOTE_DELETE != 0) {
|
||||||
self.handler.change(dir_path.?, EventType.deleted) catch return;
|
self.handler.change(dir_path.?, EventType.deleted, .dir) catch return;
|
||||||
} else if (ev.fflags & NOTE_RENAME != 0) {
|
} else if (ev.fflags & NOTE_RENAME != 0) {
|
||||||
self.handler.change(dir_path.?, EventType.renamed) catch return;
|
self.handler.change(dir_path.?, EventType.renamed, .dir) catch return;
|
||||||
} else if (ev.fflags & NOTE_WRITE != 0) {
|
} else if (ev.fflags & NOTE_WRITE != 0) {
|
||||||
self.scan_dir(allocator, dir_path.?) catch {};
|
self.scan_dir(allocator, dir_path.?) catch {};
|
||||||
}
|
}
|
||||||
|
|
@ -867,10 +876,10 @@ const KQueueBackend = struct {
|
||||||
self.snapshots_mutex.unlock();
|
self.snapshots_mutex.unlock();
|
||||||
|
|
||||||
// Emit all events outside the lock so handlers may safely call watch()/unwatch().
|
// Emit all events outside the lock so handlers may safely call watch()/unwatch().
|
||||||
// Emit dir_created, then deletions, then creations. Deletions first ensures that
|
// Emit created dirs, then deletions, then creations. Deletions first ensures that
|
||||||
// a rename (old disappears, new appears) reports the source path before the dest.
|
// 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.dir_created);
|
try self.handler.change(full_path, EventType.created, .dir);
|
||||||
for (to_delete.items) |name| {
|
for (to_delete.items) |name| {
|
||||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch {
|
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch {
|
||||||
|
|
@ -878,14 +887,14 @@ const KQueueBackend = struct {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
self.deregister_file_watch(allocator, full_path);
|
self.deregister_file_watch(allocator, full_path);
|
||||||
try self.handler.change(full_path, EventType.deleted);
|
try self.handler.change(full_path, EventType.deleted, .file);
|
||||||
allocator.free(name);
|
allocator.free(name);
|
||||||
}
|
}
|
||||||
for (to_create.items) |name| {
|
for (to_create.items) |name| {
|
||||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
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;
|
const full_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, name }) catch continue;
|
||||||
self.register_file_watch(allocator, full_path);
|
self.register_file_watch(allocator, full_path);
|
||||||
try self.handler.change(full_path, EventType.created);
|
try self.handler.change(full_path, EventType.created, .file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1113,6 +1122,7 @@ const WindowsBackend = struct {
|
||||||
thread: ?std.Thread,
|
thread: ?std.Thread,
|
||||||
watches: std.StringHashMapUnmanaged(Watch),
|
watches: std.StringHashMapUnmanaged(Watch),
|
||||||
watches_mutex: std.Thread.Mutex,
|
watches_mutex: std.Thread.Mutex,
|
||||||
|
path_types: std.StringHashMapUnmanaged(ObjectType),
|
||||||
|
|
||||||
// A completion key of zero is used to signal the background thread to exit.
|
// A completion key of zero is used to signal the background thread to exit.
|
||||||
const SHUTDOWN_KEY: windows.ULONG_PTR = 0;
|
const SHUTDOWN_KEY: windows.ULONG_PTR = 0;
|
||||||
|
|
@ -1149,7 +1159,7 @@ const WindowsBackend = struct {
|
||||||
|
|
||||||
fn init(handler: *Handler) windows.CreateIoCompletionPortError!@This() {
|
fn init(handler: *Handler) windows.CreateIoCompletionPortError!@This() {
|
||||||
const iocp = try windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1);
|
const iocp = try windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1);
|
||||||
return .{ .handler = handler, .iocp = iocp, .thread = null, .watches = .empty, .watches_mutex = .{} };
|
return .{ .handler = handler, .iocp = iocp, .thread = null, .watches = .empty, .watches_mutex = .{}, .path_types = .empty };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
|
fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
|
||||||
|
|
@ -1163,19 +1173,23 @@ const WindowsBackend = struct {
|
||||||
allocator.free(entry.value_ptr.*.buf);
|
allocator.free(entry.value_ptr.*.buf);
|
||||||
}
|
}
|
||||||
self.watches.deinit(allocator);
|
self.watches.deinit(allocator);
|
||||||
|
var pt_it = self.path_types.iterator();
|
||||||
|
while (pt_it.next()) |entry| std.heap.page_allocator.free(entry.key_ptr.*);
|
||||||
|
self.path_types.deinit(std.heap.page_allocator);
|
||||||
_ = win32.CloseHandle(self.iocp);
|
_ = win32.CloseHandle(self.iocp);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arm(self: *@This(), allocator: std.mem.Allocator) (error{AlreadyArmed} || std.Thread.SpawnError)!void {
|
fn arm(self: *@This(), allocator: std.mem.Allocator) (error{AlreadyArmed} || std.Thread.SpawnError)!void {
|
||||||
_ = allocator;
|
_ = allocator;
|
||||||
if (self.thread != null) return error.AlreadyArmed;
|
if (self.thread != null) return error.AlreadyArmed;
|
||||||
self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self.iocp, &self.watches, &self.watches_mutex, self.handler });
|
self.thread = try std.Thread.spawn(.{}, thread_fn, .{ self.iocp, &self.watches, &self.watches_mutex, &self.path_types, self.handler });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn thread_fn(
|
fn thread_fn(
|
||||||
iocp: windows.HANDLE,
|
iocp: windows.HANDLE,
|
||||||
watches: *std.StringHashMapUnmanaged(Watch),
|
watches: *std.StringHashMapUnmanaged(Watch),
|
||||||
watches_mutex: *std.Thread.Mutex,
|
watches_mutex: *std.Thread.Mutex,
|
||||||
|
path_types: *std.StringHashMapUnmanaged(ObjectType),
|
||||||
handler: *Handler,
|
handler: *Handler,
|
||||||
) void {
|
) void {
|
||||||
var bytes: windows.DWORD = 0;
|
var bytes: windows.DWORD = 0;
|
||||||
|
|
@ -1215,29 +1229,32 @@ const WindowsBackend = struct {
|
||||||
offset += info.NextEntryOffset;
|
offset += info.NextEntryOffset;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// Distinguish files from directories.
|
// Determine object_type: try GetFileAttributesW; cache result.
|
||||||
const is_dir = blk: {
|
const object_type: ObjectType = if (event_type == .deleted) blk: {
|
||||||
|
// Path no longer exists; use cached type if available.
|
||||||
|
const cached = path_types.fetchRemove(full_path);
|
||||||
|
break :blk if (cached) |kv| kv.value else .unknown;
|
||||||
|
} else blk: {
|
||||||
var full_path_w: [std.fs.max_path_bytes]windows.WCHAR = undefined;
|
var full_path_w: [std.fs.max_path_bytes]windows.WCHAR = undefined;
|
||||||
const len = std.unicode.utf8ToUtf16Le(&full_path_w, full_path) catch break :blk false;
|
const len = std.unicode.utf8ToUtf16Le(&full_path_w, full_path) catch break :blk .unknown;
|
||||||
full_path_w[len] = 0;
|
full_path_w[len] = 0;
|
||||||
const attrs = win32.GetFileAttributesW(full_path_w[0..len :0]);
|
const attrs = win32.GetFileAttributesW(full_path_w[0..len :0]);
|
||||||
const INVALID: windows.DWORD = 0xFFFFFFFF;
|
const INVALID: windows.DWORD = 0xFFFFFFFF;
|
||||||
const FILE_ATTRIBUTE_DIRECTORY: windows.DWORD = 0x10;
|
const FILE_ATTRIBUTE_DIRECTORY: windows.DWORD = 0x10;
|
||||||
break :blk attrs != INVALID and (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
const ot: ObjectType = if (attrs == INVALID) .unknown else if (attrs & FILE_ATTRIBUTE_DIRECTORY != 0) .dir else .file;
|
||||||
|
// Cache the determined type.
|
||||||
|
if (ot != .unknown) {
|
||||||
|
const owned_key = std.heap.page_allocator.dupe(u8, full_path) catch break :blk ot;
|
||||||
|
path_types.put(std.heap.page_allocator, owned_key, ot) catch std.heap.page_allocator.free(owned_key);
|
||||||
|
}
|
||||||
|
break :blk ot;
|
||||||
};
|
};
|
||||||
const adjusted_event_type: EventType = if (is_dir and event_type == .created)
|
|
||||||
.dir_created
|
|
||||||
else if (is_dir) { // Other directory events (modified, deleted, renamed), skip.
|
|
||||||
if (info.NextEntryOffset == 0) break;
|
|
||||||
offset += info.NextEntryOffset;
|
|
||||||
continue;
|
|
||||||
} else event_type;
|
|
||||||
// Capture next_entry_offset before releasing the mutex: after unlock,
|
// Capture next_entry_offset before releasing the mutex: after unlock,
|
||||||
// the main thread may call remove_watch() which frees w.buf, making
|
// the main thread may call remove_watch() which frees w.buf, making
|
||||||
// the `info` pointer (which points into w.buf) a dangling reference.
|
// the `info` pointer (which points into w.buf) a dangling reference.
|
||||||
const next_entry_offset = info.NextEntryOffset;
|
const next_entry_offset = info.NextEntryOffset;
|
||||||
watches_mutex.unlock();
|
watches_mutex.unlock();
|
||||||
handler.change(full_path, adjusted_event_type) catch {
|
handler.change(full_path, event_type, object_type) catch {
|
||||||
watches_mutex.lock();
|
watches_mutex.lock();
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const nw = @import("nightwatch");
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const RecordedEvent = union(enum) {
|
const RecordedEvent = union(enum) {
|
||||||
change: struct { path: []u8, event_type: nw.EventType },
|
change: struct { path: []u8, event_type: nw.EventType, object_type: nw.ObjectType },
|
||||||
rename: struct { src: []u8, dst: []u8 },
|
rename: struct { src: []u8, dst: []u8 },
|
||||||
|
|
||||||
fn deinit(self: RecordedEvent, allocator: std.mem.Allocator) void {
|
fn deinit(self: RecordedEvent, allocator: std.mem.Allocator) void {
|
||||||
|
|
@ -52,18 +52,18 @@ const TestHandler = struct {
|
||||||
.wait_readable = if (nw.linux_poll_mode) wait_readable_cb else {},
|
.wait_readable = if (nw.linux_poll_mode) wait_readable_cb else {},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn change_cb(handler: *nw.Handler, path: []const u8, event_type: nw.EventType) error{HandlerFailed}!void {
|
fn change_cb(handler: *nw.Handler, path: []const u8, event_type: nw.EventType, object_type: nw.ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *TestHandler = @fieldParentPtr("handler", handler);
|
const self: *TestHandler = @fieldParentPtr("handler", handler);
|
||||||
const owned = self.allocator.dupe(u8, path) catch return error.HandlerFailed;
|
const owned = self.allocator.dupe(u8, path) catch return error.HandlerFailed;
|
||||||
self.events.append(self.allocator, .{
|
self.events.append(self.allocator, .{
|
||||||
.change = .{ .path = owned, .event_type = event_type },
|
.change = .{ .path = owned, .event_type = event_type, .object_type = object_type },
|
||||||
}) catch {
|
}) catch {
|
||||||
self.allocator.free(owned);
|
self.allocator.free(owned);
|
||||||
return error.HandlerFailed;
|
return error.HandlerFailed;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename_cb(handler: *nw.Handler, src: []const u8, dst: []const u8) error{HandlerFailed}!void {
|
fn rename_cb(handler: *nw.Handler, src: []const u8, dst: []const u8, _: nw.ObjectType) error{HandlerFailed}!void {
|
||||||
const self: *TestHandler = @fieldParentPtr("handler", handler);
|
const self: *TestHandler = @fieldParentPtr("handler", handler);
|
||||||
const owned_src = self.allocator.dupe(u8, src) catch return error.HandlerFailed;
|
const owned_src = self.allocator.dupe(u8, src) catch return error.HandlerFailed;
|
||||||
errdefer self.allocator.free(owned_src);
|
errdefer self.allocator.free(owned_src);
|
||||||
|
|
@ -88,8 +88,8 @@ const TestHandler = struct {
|
||||||
// Query helpers
|
// Query helpers
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
fn hasChange(self: *const TestHandler, path: []const u8, event_type: nw.EventType) bool {
|
fn hasChange(self: *const TestHandler, path: []const u8, event_type: nw.EventType, object_type: nw.ObjectType) bool {
|
||||||
return self.indexOfChange(path, event_type) != null;
|
return self.indexOfChange(path, event_type, object_type) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hasRename(self: *const TestHandler, src: []const u8, dst: []const u8) bool {
|
fn hasRename(self: *const TestHandler, src: []const u8, dst: []const u8) bool {
|
||||||
|
|
@ -97,10 +97,11 @@ const TestHandler = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list index of the first matching change event, or null.
|
/// Returns the list index of the first matching change event, or null.
|
||||||
fn indexOfChange(self: *const TestHandler, path: []const u8, event_type: nw.EventType) ?usize {
|
fn indexOfChange(self: *const TestHandler, path: []const u8, event_type: nw.EventType, object_type: nw.ObjectType) ?usize {
|
||||||
for (self.events.items, 0..) |e, i| {
|
for (self.events.items, 0..) |e, i| {
|
||||||
if (e == .change and
|
if (e == .change and
|
||||||
e.change.event_type == event_type and
|
e.change.event_type == event_type and
|
||||||
|
e.change.object_type == object_type and
|
||||||
std.mem.eql(u8, e.change.path, path)) return i;
|
std.mem.eql(u8, e.change.path, path)) return i;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -216,7 +217,7 @@ test "creating a file emits a 'created' event" {
|
||||||
|
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
|
|
||||||
try std.testing.expect(th.hasChange(file_path, .created));
|
try std.testing.expect(th.hasChange(file_path, .created, .file));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "writing to a file emits a 'modified' event" {
|
test "writing to a file emits a 'modified' event" {
|
||||||
|
|
@ -251,7 +252,7 @@ test "writing to a file emits a 'modified' event" {
|
||||||
|
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
|
|
||||||
try std.testing.expect(th.hasChange(file_path, .modified));
|
try std.testing.expect(th.hasChange(file_path, .modified, .file));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "deleting a file emits a 'deleted' event" {
|
test "deleting a file emits a 'deleted' event" {
|
||||||
|
|
@ -268,23 +269,25 @@ test "deleting a file emits a 'deleted' event" {
|
||||||
|
|
||||||
const file_path = try std.fs.path.join(allocator, &.{ tmp, "gone.txt" });
|
const file_path = try std.fs.path.join(allocator, &.{ tmp, "gone.txt" });
|
||||||
defer allocator.free(file_path);
|
defer allocator.free(file_path);
|
||||||
{
|
|
||||||
const f = try std.fs.createFileAbsolute(file_path, .{});
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
var watcher = try Watcher.init(allocator, &th.handler);
|
var watcher = try Watcher.init(allocator, &th.handler);
|
||||||
defer watcher.deinit();
|
defer watcher.deinit();
|
||||||
try watcher.watch(tmp);
|
try watcher.watch(tmp);
|
||||||
|
|
||||||
|
// Create the file after the watcher is active so the backend can cache its type.
|
||||||
|
{
|
||||||
|
const f = try std.fs.createFileAbsolute(file_path, .{});
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
try std.fs.deleteFileAbsolute(file_path);
|
try std.fs.deleteFileAbsolute(file_path);
|
||||||
|
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
|
|
||||||
try std.testing.expect(th.hasChange(file_path, .deleted));
|
try std.testing.expect(th.hasChange(file_path, .deleted, .file));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "creating a sub-directory emits a 'dir_created' event" {
|
test "creating a sub-directory emits a 'created' event with object_type dir" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
const tmp = try makeTempDir(allocator);
|
const tmp = try makeTempDir(allocator);
|
||||||
|
|
@ -306,7 +309,7 @@ test "creating a sub-directory emits a 'dir_created' event" {
|
||||||
|
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
|
|
||||||
try std.testing.expect(th.hasChange(dir_path, .dir_created));
|
try std.testing.expect(th.hasChange(dir_path, .created, .dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "renaming a file is reported correctly per-platform" {
|
test "renaming a file is reported correctly per-platform" {
|
||||||
|
|
@ -344,8 +347,8 @@ test "renaming a file is reported correctly per-platform" {
|
||||||
try std.testing.expect(th.hasRename(src_path, dst_path));
|
try std.testing.expect(th.hasRename(src_path, dst_path));
|
||||||
} else {
|
} else {
|
||||||
// macOS/Windows emit individual .renamed change events per path.
|
// macOS/Windows emit individual .renamed change events per path.
|
||||||
const has_old = th.hasChange(src_path, .renamed) or th.hasChange(src_path, .deleted);
|
const has_old = th.hasChange(src_path, .renamed, .file) or th.hasChange(src_path, .deleted, .file);
|
||||||
const has_new = th.hasChange(dst_path, .renamed) or th.hasChange(dst_path, .created);
|
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);
|
try std.testing.expect(has_old or has_new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -407,7 +410,7 @@ test "unwatch stops delivering events for that directory" {
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
try std.testing.expect(th.hasChange(file1, .created));
|
try std.testing.expect(th.hasChange(file1, .created, .file));
|
||||||
|
|
||||||
// Stop watching, then create another file - must NOT appear.
|
// Stop watching, then create another file - must NOT appear.
|
||||||
watcher.unwatch(tmp);
|
watcher.unwatch(tmp);
|
||||||
|
|
@ -454,7 +457,7 @@ test "multiple files created sequentially all appear in the event list" {
|
||||||
try drainEvents(&watcher);
|
try drainEvents(&watcher);
|
||||||
|
|
||||||
for (paths) |p| {
|
for (paths) |p| {
|
||||||
try std.testing.expect(th.hasChange(p, .created));
|
try std.testing.expect(th.hasChange(p, .created, .file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -495,8 +498,8 @@ test "rename: old-name event precedes new-name event" {
|
||||||
// Both paths must have produced some event.
|
// Both paths must have produced some event.
|
||||||
const src_idx = th.indexOfAnyPath(src_path) orelse
|
const src_idx = th.indexOfAnyPath(src_path) orelse
|
||||||
return error.MissingSrcEvent;
|
return error.MissingSrcEvent;
|
||||||
const dst_idx = th.indexOfChange(dst_path, .renamed) orelse
|
const dst_idx = th.indexOfChange(dst_path, .renamed, .file) orelse
|
||||||
th.indexOfChange(dst_path, .created) orelse
|
th.indexOfChange(dst_path, .created, .file) orelse
|
||||||
return error.MissingDstEvent;
|
return error.MissingDstEvent;
|
||||||
|
|
||||||
// The source (old name) event must precede the destination (new name) event.
|
// The source (old name) event must precede the destination (new name) event.
|
||||||
|
|
@ -552,7 +555,7 @@ test "rename-then-modify: rename event precedes the subsequent modify event" {
|
||||||
th.indexOfAnyPath(src_path) orelse return error.MissingSrcEvent;
|
th.indexOfAnyPath(src_path) orelse return error.MissingSrcEvent;
|
||||||
|
|
||||||
// The modify event on the new name must come strictly after the rename.
|
// The modify event on the new name must come strictly after the rename.
|
||||||
const modify_idx = th.indexOfChange(dst_path, .modified) orelse
|
const modify_idx = th.indexOfChange(dst_path, .modified, .file) orelse
|
||||||
return error.MissingModifyEvent;
|
return error.MissingModifyEvent;
|
||||||
|
|
||||||
try std.testing.expect(rename_idx < modify_idx);
|
try std.testing.expect(rename_idx < modify_idx);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue