From dd2bbb66ed38c6a09440951bd86d0c2a799240bb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Mon, 9 Mar 2026 11:14:47 +0100 Subject: [PATCH] refactor: use polling interface on linux in nightwatch cli --- src/main.zig | 34 +++++++++++++++++++++++++--------- src/nightwatch.zig | 35 +++++++++++++++++++++-------------- src/nightwatch_test.zig | 12 ++++++------ src/types.zig | 12 ++++++------ 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/main.zig b/src/main.zig index ed0456c..9abd0cc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,11 @@ const std = @import("std"); const builtin = @import("builtin"); const nightwatch = @import("nightwatch"); +const Watcher = switch (builtin.os.tag) { + .linux => nightwatch.Create(.polling), + else => nightwatch.Default, +}; + const is_posix = switch (builtin.os.tag) { .linux, .macos, .freebsd, .openbsd, .netbsd, .dragonfly => true, .windows => false, @@ -16,16 +21,23 @@ fn posix_sighandler(_: c_int) callconv(.c) void { } const CliHandler = struct { - handler: nightwatch.Handler, + handler: Watcher.Handler, out: std.fs.File, ignore: []const []const u8, - const vtable = nightwatch.Handler.VTable{ - .change = change_cb, - .rename = rename_cb, + const vtable: Watcher.Handler.VTable = switch (Watcher.interface_type) { + .polling => .{ + .change = change_cb, + .rename = rename_cb, + .wait_readable = wait_readable_cb, + }, + .threaded => .{ + .change = change_cb, + .rename = rename_cb, + }, }; - fn change_cb(h: *nightwatch.Handler, path: []const u8, event_type: nightwatch.EventType, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { + fn change_cb(h: *Watcher.Handler, path: []const u8, event_type: nightwatch.EventType, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { const self: *CliHandler = @fieldParentPtr("handler", h); for (self.ignore) |ignored| { if (std.mem.eql(u8, path, ignored)) return; @@ -47,7 +59,7 @@ const CliHandler = struct { 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, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { + fn rename_cb(h: *Watcher.Handler, src: []const u8, dst: []const u8, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { const self: *CliHandler = @fieldParentPtr("handler", h); for (self.ignore) |ignored| { if (std.mem.eql(u8, src, ignored) or std.mem.eql(u8, dst, ignored)) return; @@ -63,7 +75,7 @@ const CliHandler = struct { 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(_: *Watcher.Handler) error{HandlerFailed}!Watcher.Handler.ReadableStatus { return .will_notify; } }; @@ -215,7 +227,11 @@ pub fn main() !void { .ignore = ignore_list.items, }; - var watcher = try nightwatch.Default.init(allocator, &cli_handler.handler); + var watcher = switch (builtin.os.tag) { + .linux => try nightwatch.Create(.polling).init(allocator, &cli_handler.handler), + else => try nightwatch.Default.init(allocator, &cli_handler.handler), + }; + defer watcher.deinit(); for (watch_paths.items) |path| { @@ -226,7 +242,7 @@ pub fn main() !void { try stderr.interface.print("on watch: {s}\n", .{path}); } - if (nightwatch.linux_poll_mode) { + if (nightwatch.Default.interface_type == .polling) { try run_linux(&watcher); } else if (builtin.os.tag == .windows) { run_windows(); diff --git a/src/nightwatch.zig b/src/nightwatch.zig index 573ad62..167f246 100644 --- a/src/nightwatch.zig +++ b/src/nightwatch.zig @@ -6,10 +6,7 @@ const types = @import("types.zig"); pub const EventType = types.EventType; pub const ObjectType = types.ObjectType; pub const Error = types.Error; -pub const ReadableStatus = types.ReadableStatus; pub const InterfaceType = types.InterfaceType; -pub const Handler = types.Handler; -pub const PollingHandler = types.PollingHandler; pub const Variant = switch (builtin.os.tag) { .linux => InterfaceType, @@ -49,13 +46,21 @@ pub fn Create(comptime variant: Variant) type { }, else => @compileError("unsupported OS"), }; - pub const interfaceType: InterfaceType = switch (builtin.os.tag) { + pub const interface_type: InterfaceType = switch (builtin.os.tag) { .linux => variant, else => .threaded, }; + pub const Handler = switch (interface_type) { + .threaded => types.Handler, + .polling => types.PollingHandler, + }; + pub const InterceptorType = switch (interface_type) { + .threaded => Interceptor, + .polling => PollingInterceptor, + }; allocator: std.mem.Allocator, - interceptor: *Interceptor, + interceptor: *InterceptorType, /// True if the current backend detects file content modifications in real time. /// False only when kqueue_dir_only=true, where directory-level watches are used @@ -63,10 +68,10 @@ pub fn Create(comptime variant: Variant) type { pub const detects_file_modifications = Backend.detects_file_modifications; pub fn init(allocator: std.mem.Allocator, handler: *Handler) !@This() { - const ic = try allocator.create(Interceptor); + const ic = try allocator.create(InterceptorType); errdefer allocator.destroy(ic); ic.* = .{ - .handler = .{ .vtable = &Interceptor.vtable }, + .handler = .{ .vtable = &InterceptorType.vtable }, .user_handler = handler, .allocator = allocator, .backend = undefined, @@ -153,7 +158,7 @@ pub fn Create(comptime variant: Variant) type { 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}!Handler.ReadableStatus { const self: *Interceptor = @fieldParentPtr("handler", h); return self.user_handler.wait_readable(); } @@ -171,8 +176,10 @@ pub fn Create(comptime variant: Variant) type { .wait_readable = wait_readable_cb, }; - fn change_cb(h: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void { - const self: *Interceptor = @fieldParentPtr("handler", h); + const PollingHandler = types.PollingHandler; + + fn change_cb(h: *PollingHandler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void { + const self: *PollingInterceptor = @fieldParentPtr("handler", h); if (event_type == .created and object_type == .dir and !Backend.watches_recursively) { self.backend.add_watch(self.allocator, path) catch {}; recurse_watch(&self.backend, self.allocator, path); @@ -180,13 +187,13 @@ pub fn Create(comptime variant: Variant) type { return self.user_handler.change(path, event_type, object_type); } - fn rename_cb(h: *Handler, src: []const u8, dst: []const u8, object_type: ObjectType) error{HandlerFailed}!void { - const self: *Interceptor = @fieldParentPtr("handler", h); + fn rename_cb(h: *PollingHandler, src: []const u8, dst: []const u8, object_type: ObjectType) error{HandlerFailed}!void { + const self: *PollingInterceptor = @fieldParentPtr("handler", h); return self.user_handler.rename(src, dst, object_type); } - fn wait_readable_cb(h: *Handler) error{HandlerFailed}!ReadableStatus { - const self: *Interceptor = @fieldParentPtr("handler", h); + fn wait_readable_cb(h: *PollingHandler) error{HandlerFailed}!PollingHandler.ReadableStatus { + const self: *PollingInterceptor = @fieldParentPtr("handler", h); return self.user_handler.wait_readable(); } }; diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index 792a84d..2b6daeb 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -22,7 +22,7 @@ const RecordedEvent = union(enum) { }; const TestHandler = struct { - handler: nw.Handler, + handler: Watcher.Handler, allocator: std.mem.Allocator, events: std.ArrayListUnmanaged(RecordedEvent), @@ -46,12 +46,12 @@ const TestHandler = struct { // vtable // ----------------------------------------------------------------------- - const vtable = nw.Handler.VTable{ + const vtable = Watcher.Handler.VTable{ .change = change_cb, .rename = rename_cb, }; - fn change_cb(handler: *nw.Handler, path: []const u8, event_type: nw.EventType, object_type: nw.ObjectType) error{HandlerFailed}!void { + fn change_cb(handler: *Watcher.Handler, path: []const u8, event_type: nw.EventType, object_type: nw.ObjectType) error{HandlerFailed}!void { const self: *TestHandler = @fieldParentPtr("handler", handler); const owned = self.allocator.dupe(u8, path) catch return error.HandlerFailed; self.events.append(self.allocator, .{ @@ -62,7 +62,7 @@ const TestHandler = struct { }; } - fn rename_cb(handler: *nw.Handler, src: []const u8, dst: []const u8, _: nw.ObjectType) error{HandlerFailed}!void { + fn rename_cb(handler: *Watcher.Handler, src: []const u8, dst: []const u8, _: nw.ObjectType) error{HandlerFailed}!void { const self: *TestHandler = @fieldParentPtr("handler", handler); const owned_src = self.allocator.dupe(u8, src) catch return error.HandlerFailed; errdefer self.allocator.free(owned_src); @@ -78,7 +78,7 @@ const TestHandler = struct { // On Linux the inotify backend calls wait_readable() inside arm() and // after each read-drain. We return `will_notify` so it parks; the test // then calls handle_read_ready() explicitly to drive event delivery. - fn wait_readable_cb(handler: *nw.Handler) error{HandlerFailed}!nw.ReadableStatus { + fn wait_readable_cb(handler: *Watcher.Handler) error{HandlerFailed}!nw.ReadableStatus { _ = handler; return .will_notify; } @@ -180,7 +180,7 @@ fn removeTempDir(path: []const u8) void { /// - polling watchers: call handle_read_ready() so events are processed. /// - threaded watchers: the backend uses its own thread/callback; sleep briefly. fn drainEvents(watcher: *Watcher) !void { - switch (Watcher.interfaceType) { + switch (Watcher.interface_type) { .polling => try watcher.handle_read_ready(), .threaded => std.Thread.sleep(300 * std.time.ns_per_ms), } diff --git a/src/types.zig b/src/types.zig index be9b852..7ada3de 100644 --- a/src/types.zig +++ b/src/types.zig @@ -56,20 +56,20 @@ pub const PollingHandler = struct { }; pub const VTable = struct { - 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, object_type: ObjectType) error{HandlerFailed}!void, - wait_readable: *const fn (handler: *Handler) error{HandlerFailed}!ReadableStatus, + change: *const fn (handler: *PollingHandler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void, + rename: *const fn (handler: *PollingHandler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void, + wait_readable: *const fn (handler: *PollingHandler) error{HandlerFailed}!ReadableStatus, }; - pub fn change(handler: *Handler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void { + pub fn change(handler: *PollingHandler, path: []const u8, event_type: EventType, object_type: ObjectType) error{HandlerFailed}!void { return handler.vtable.change(handler, path, event_type, object_type); } - pub fn rename(handler: *Handler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void { + pub fn rename(handler: *PollingHandler, src_path: []const u8, dst_path: []const u8, object_type: ObjectType) error{HandlerFailed}!void { return handler.vtable.rename(handler, src_path, dst_path, object_type); } - pub fn wait_readable(handler: *Handler) error{HandlerFailed}!ReadableStatus { + pub fn wait_readable(handler: *PollingHandler) error{HandlerFailed}!ReadableStatus { return handler.vtable.wait_readable(handler); } };