diff --git a/example_inotify.zig b/example_inotify.zig new file mode 100644 index 0000000..7600d9a --- /dev/null +++ b/example_inotify.zig @@ -0,0 +1,86 @@ +const std = @import("std"); +const pike = @import("pike.zig"); + +const os = std.os; +const mem = std.mem; +const log = std.log; + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; + defer _ = gpa.deinit(); + + try pike.init(); + defer pike.deinit(); + + const notifier = try pike.Notifier.init(); + defer notifier.deinit(); + + var watchers = std.ArrayList(i32).init(&gpa.allocator); + defer watchers.deinit(); + + var watch = try pike.Inotify.init(); + defer watch.deinit(&watchers); + + try watchers.append(try watch.add("src", .{ .create = true, .modify = true, .delete = true, .access = true })); + try watchers.append(try watch.add("build.zig", .{ .create = true, .modify = true, .delete = true, .access = true })); + + try watch.registerTo(¬ifier); + var note = async inotify_handler(&watch); + + var stopped = false; + + var frame = async run(¬ifier, &stopped); + while(!stopped) { + try notifier.poll(10_000); + } + + log.info("Exited gracefully.", .{}); + + try nosuspend await frame; + try nosuspend await note; +} + +fn run(notifier: *const pike.Notifier, stopped: *bool) !void { + var event = try pike.Event.init(); + defer event.deinit(); + + try event.registerTo(notifier); + + var signal = try pike.Signal.init(.{ .interrupt = true }); + defer signal.deinit(); + + defer { + stopped.* = true; + event.post() catch unreachable; + } + + log.info("Press Ctrl+C.", .{}); + + try signal.wait(); +} + +pub fn inotify_handler(inotify: *pike.Inotify) !void { + var reader = inotify.reader(); + + const event_size = @sizeOf(pike.InotifyEvent); + var buf: [event_size + std.fs.MAX_PATH_BYTES + 1]u8 = undefined; + while(true) { + const num_bytes = try reader.read(&buf); + if (num_bytes == 0) return; + const event = std.mem.bytesToValue(pike.InotifyEvent, buf[0..event_size]); + const file = mem.trim(u8, buf[0..num_bytes], " \t\r\n"); + const evtype = pike.InotifyEventTypes; + + if(event.mask & @enumToInt(evtype.access) != 0) { + log.info("File: {s} Action: {}", .{file, evtype.access}); + } else if(event.mask & @enumToInt(evtype.modify) != 0) { + log.info("File: {s} Action: {}", .{file, evtype.modify}); + } else if(event.mask & @enumToInt(evtype.delete) != 0) { + log.info("File: {s} Action: {}", .{file, evtype.delete}); + } else if(event.mask & @enumToInt(evtype.create) != 0) { + log.info("File: {s} Action: {}", .{file, evtype.create}); + } else { + log.info("File: {s} Action: {}", .{file, event.mask}); + } + } +} \ No newline at end of file diff --git a/inotify_posix.zig b/inotify_posix.zig new file mode 100644 index 0000000..b64a864 --- /dev/null +++ b/inotify_posix.zig @@ -0,0 +1,168 @@ +const std = @import("std"); +const pike = @import("pike.zig"); +const posix = @import("os/posix.zig"); +const Waker = @import("waker.zig").Waker; +const os = std.os; +const io = std.io; +const system = os.system; +const log = std.log.scoped(.inotify); + +const mem = std.mem; +const meta = std.meta; +const builtin = @import("builtin"); + +pub const InotifyMask = struct { + access: bool = false, + modify: bool = false, + attrib: bool = false, + close_write: bool = false, + close_nowrite: bool = false, + open: bool = false, + moved_from: bool = false, + moved_to: bool = false, + create: bool = false, + delete: bool = false, + delete_self: bool = false, + move_self: bool = false, + + fn toInt(self: InotifyMask) u32 { + var mask: u32 = 0; + if(self.access) mask |= @enumToInt(InotifyEventTypes.access); + if(self.modify) mask |= @enumToInt(InotifyEventTypes.modify); + if(self.attrib) mask |= @enumToInt(InotifyEventTypes.attrib); + if(self.close_write) mask |= @enumToInt(InotifyEventTypes.close_write); + if(self.close_nowrite) mask |= @enumToInt(InotifyEventTypes.close_nowrite); + if(self.open) mask |= @enumToInt(InotifyEventTypes.open); + if(self.moved_from) mask |= @enumToInt(InotifyEventTypes.moved_from); + if(self.moved_to) mask |= @enumToInt(InotifyEventTypes.moved_to); + if(self.create) mask |= @enumToInt(InotifyEventTypes.create); + if(self.delete) mask |= @enumToInt(InotifyEventTypes.delete); + if(self.delete_self) mask |= @enumToInt(InotifyEventTypes.delete_self); + if(self.move_self) mask |= @enumToInt(InotifyEventTypes.move_self); + return mask; + } +}; + +pub const InotifyEventTypes = enum(u32) { + access = 1 << 0, + modify = 1 << 1, + attrib = 1 << 2, + close_write = 1 << 3, + close_nowrite = 1 << 4, + open = 1 << 5, + moved_from = 1 << 6, + moved_to = 1 << 7, + create = 1 << 8, + delete = 1 << 9, + delete_self = 1 << 10, + move_self = 1 << 11, + base_events = 0x00000fff, + umount = 0x0002000, + overflow = 0x0004000, + ignored = 0x0008000, + only_dir = 0x01000000, + dont_follow = 0x02000000, + add_mask = 0x20000000, + directory = 0x40000000, + one_shot = 0x80000000, +}; + +pub const InotifyEvent = extern struct { + wd: os.fd_t, + mask: u32, + cookie: u32, + len: u32, +}; + +pub const Inotify = struct { + + pub const Reader = io.Reader(*Self, anyerror, read); + + handle: pike.Handle, + mask: InotifyMask = .{}, + readers: Waker = .{}, + + const Self = @This(); + var lock: std.Thread.Mutex = .{}; + var waker: Waker(pike.Task, InotifyMask) = .{}; + + pub fn init() !Self { + return Self{ + .handle = .{ + .inner = try os.inotify_init1(os.linux.IN.NONBLOCK | os.linux.IN.CLOEXEC), + .wake_fn = wake, + }, + }; + } + + fn wake(handle: *pike.Handle, batch: *pike.Batch, opts: pike.WakeOptions) void { + const self = @fieldParentPtr(Self, "handle", handle); + + if (opts.read_ready) { + if (self.readers.notify()) |task| { + batch.push(task); + } + } + if (opts.shutdown) { + if (self.readers.shutdown()) |task| batch.push(task); + } + } + + fn ErrorUnionOf(comptime func: anytype) std.builtin.TypeInfo.ErrorUnion { + return @typeInfo(@typeInfo(@TypeOf(func)).Fn.return_type.?).ErrorUnion; + } + + fn call(self: *Self, comptime function: anytype, args: anytype, comptime opts: pike.CallOptions) !ErrorUnionOf(function).payload { + while (true) { + const result = @call(.{ .modifier = .always_inline }, function, args) catch |err| switch (err) { + error.WouldBlock => { + if (comptime opts.write) { + try self.writers.wait(.{ .use_lifo = true }); + } else if (comptime opts.read) { + try self.readers.wait(.{}); + } + continue; + }, + else => return err, + }; + + return result; + } + } + + pub fn read(self: *Self, buf: []u8) !usize { + const num_bytes = self.call(posix.read_, .{ self.handle.inner, buf }, .{ .read = true }) catch |err| switch (err) { + error.NotOpenForReading, + error.ConnectionResetByPeer, + error.OperationCancelled, + => return 0, + else => return err, + }; + + return num_bytes; + } + + pub inline fn reader(self: *Self) Reader { + return Reader{ .context = self }; + } + + pub fn deinit(self: *Self, watchers: *std.ArrayList(i32)) void { + if (self.readers.shutdown()) |task| pike.dispatch(task, .{}); + + for (watchers.items) |watcher| os.inotify_rm_watch(self.handle.inner, watcher); + os.close(self.handle.inner); + } + + pub fn add(self: *const Self, path: []const u8, mask: InotifyMask) std.os.INotifyAddWatchError!i32 { + var watcher = try os.inotify_add_watch(self.handle.inner, path, mask.toInt()); + return watcher; + } + + pub fn remove(self: *const Self, watch: os.fd_t) void { + os.inotify_rm_watch(self.handle.inner, watch); + } + + pub fn registerTo(self: *const Self, notifier: *const pike.Notifier) !void { + try notifier.register(&self.handle, .{ .read = true, .write = true }); + } +}; \ No newline at end of file diff --git a/pike.zig b/pike.zig index 08b3b2c..934bc36 100644 --- a/pike.zig +++ b/pike.zig @@ -135,6 +135,11 @@ else if (builtin.os.tag == .windows) else @compileError("pike: unable to figure out a 'Event' implementation to use for the build target"); +pub usingnamespace if (has_epoll) + @import("inotify_posix.zig") +else + @compileLog("pike: unable to figure out a 'Inotify' implementation to use for the build target"); + // Export 'Waker' and 'PackedWaker'. pub usingnamespace @import("waker.zig");