From 1dd2a49dafbd89e34360b94771f64e1d33910c41 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 9 May 2023 10:08:23 -0700 Subject: [PATCH] add documentation to watch --- src/Watch.zig | 103 +++++++++++++++++++++++++++++++++----------------- src/main.zig | 2 +- 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/src/Watch.zig b/src/Watch.zig index c1a051e..42a9914 100644 --- a/src/Watch.zig +++ b/src/Watch.zig @@ -10,8 +10,7 @@ const Self = @This(); fileChanged: *const fn (usize) void, inotify_fd: ?std.os.fd_t = null, -// sizeof(std.os.pollfd) == 8, so this is 8k -// fds: [MAX_FDS]std.os.pollfd = [_]std.os.pollfd{.{ .fd = 0, .events = 0, .revents = 0 }} ** MAX_FDS, + nfds_t: usize = 0, wds: [MAX_FDS]i32 = [_]i32{0} ** MAX_FDS, modified: [MAX_FDS]bool = [_]bool{false} ** MAX_FDS, @@ -35,7 +34,15 @@ pub fn deinit(self: *Self) void { } } -pub fn watchFds(self: *Self) void { +/// starts the file watch. This function will not return, so it is best +/// to put this function in its own thread: +/// +/// const watcher_thread = try std.Thread.spawn(.{}, Watch.startWatch, .{&watcher}); +/// +/// Due to the nature of the poll(), behavior will almost definitely not work +/// well if files are added after the watch begins. A method for doing this +/// is intended later +pub fn startWatch(self: *Self) void { while (true) { if (self.nfds_t == 0) { std.time.sleep(250); @@ -53,6 +60,9 @@ pub fn watchFds(self: *Self) void { fds, -1, // Infinite timeout ) catch @panic("poll error")) > 0) { + + // fds[0] is inotify, so if we have data in that file descriptor, + // we can force the data into an inotify_event structure and act on it if (fds[0].revents & c.POLLIN == c.POLLIN) { // POLLIN means "there is data to read" var event_buf: [4096]u8 align(@alignOf(std.os.linux.inotify_event)) = undefined; // "borrowed" from https://ziglang.org/documentation/master/std/src/std/fs/watch.zig.html#L588 @@ -66,32 +76,7 @@ pub fn watchFds(self: *Self) void { @alignCast(@alignOf(*const std.os.linux.inotify_event), ptr), ); - if (ev.mask & std.os.linux.IN.MODIFY == std.os.linux.IN.MODIFY) { - for (self.wds, 0..) |wd, inx| { - if (ev.wd == wd) - self.modified[inx] = true; - } - } - // attrib added as build process moves in place and modifies attributes - // TODO: Also watch MOVED_TO, which is on the directory... - if (ev.mask & std.os.linux.IN.CLOSE_WRITE == std.os.linux.IN.CLOSE_WRITE or - ev.mask & std.os.linux.IN.ATTRIB == std.os.linux.IN.ATTRIB) - { - for (self.wds, 0..) |wd, inx| { - if (ev.wd == wd) - self.fileChanged(inx); - } - } - if (ev.mask & std.os.linux.IN.CLOSE_NOWRITE == std.os.linux.IN.CLOSE_NOWRITE) { - for (self.wds, 0..) |wd, inx| { - if (ev.wd == wd and self.modified[inx]) { - self.modified[inx] = false; - self.fileChanged(inx); - } - } - } - // see man 2 poll - self.handleFile(fds[0]); + // Read next event from inotify ptr = @alignCast( @alignOf(std.os.linux.inotify_event), ptr + @sizeOf(std.os.linux.inotify_event) + ev.len, @@ -102,13 +87,66 @@ pub fn watchFds(self: *Self) void { } } +/// This will determine whether the inotify event indicates an actionable +/// change to the file, and if so, will call self.fileChanged +fn processInotifyEvent(self: *Self, ev: *const std.os.linux.inotify_event) void { + // If the file was modified, it is good to know, but not + // actionable at this time. We can set a modification flag + // for later use. This flag and process may be unnecessary... + // how can we have a modify followed by CLOSE_NOWRITE? + // + // TODO: Delete the following + if (ev.mask & std.os.linux.IN.MODIFY == std.os.linux.IN.MODIFY) { + for (self.wds, 0..) |wd, inx| { + if (ev.wd == wd) + self.modified[inx] = true; + } + } + if (ev.mask & std.os.linux.IN.CLOSE_NOWRITE == std.os.linux.IN.CLOSE_NOWRITE) { + for (self.wds, 0..) |wd, inx| { + if (ev.wd == wd and self.modified[inx]) { + self.modified[inx] = false; + self.fileChanged(inx); + } + } + } + // There's a couple ways a file can be modified. The simplest + // way is to write(), then close(). For a variety of reasons + // due to safety, a lot of programs will write some temporary + // file, then copy or move it in place. This will fail to + // trigger IN_CLOSE_WRITE, so we need to detect it another + // way. The best is to watch for events on the parent directory + // to find move events. Note that using copy will trigger + // a IN_CLOSE_WRITE. Without building directory watching in, + // we can use IN_ATTRIB to satisfy the `zig build` use case, + // which modifies attributes after moving the file. + // + // THIS WILL NOT WORK in the generic sense, and ultimately + // we're going to have to watch the directory as well + // attrib added as build process moves in place and modifies attributes + // + // TODO: Also watch MOVED_TO, which is on the directory... + if (ev.mask & std.os.linux.IN.CLOSE_WRITE == std.os.linux.IN.CLOSE_WRITE or + ev.mask & std.os.linux.IN.ATTRIB == std.os.linux.IN.ATTRIB) + { + for (self.wds, 0..) |wd, inx| { + if (ev.wd == wd) + self.fileChanged(inx); + } + } +} + +/// adds a file to watch. The return will be a handle that will be returned +/// in the fileChanged event triffered from startWatch pub fn addFileWatch(self: *Self, path: [:0]const u8) !usize { self.inotify_fd = self.inotify_fd orelse try std.os.inotify_init1(std.os.linux.IN.NONBLOCK); errdefer { std.os.close(self.inotify_fd.?); self.inotify_fd = null; } - // open 20, close_norite 10, attrib 4 + // zig build modification pattern: open 20, close_nowrite 10, MOVED_TO (on the directory), attrib 4 + // unix cp: OPEN, MODIFY, CLOSE_WRITE, ATTRIB + // unix mv: MOVED_TO (on the directory) self.wds[self.nfds_t] = try std.os.inotify_add_watchZ( self.inotify_fd.?, path, @@ -119,8 +157,3 @@ pub fn addFileWatch(self: *Self, path: [:0]const u8) !usize { self.nfds_t += 1; return self.nfds_t - 1; } - -fn handleFile(self: Self, fd: std.os.pollfd) void { - _ = fd; - _ = self; -} diff --git a/src/main.zig b/src/main.zig index 8c33e33..9422810 100644 --- a/src/main.zig +++ b/src/main.zig @@ -135,7 +135,7 @@ pub fn main() !void { try stdout.print("Run `zig build test` to run the tests.\n", .{}); try bw.flush(); // don't forget to flush! - const watcher_thread = try std.Thread.spawn(.{}, Watch.watchFds, .{&watcher}); + const watcher_thread = try std.Thread.spawn(.{}, Watch.startWatch, .{&watcher}); while (true) { std.time.sleep(std.time.ns_per_s * 2);