321 lines
8.5 KiB
Zig
321 lines
8.5 KiB
Zig
const std = @import("std");
|
|
const INotify = @import("inotify.zig");
|
|
const Io = std.Io;
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
|
|
const FileWatcher = @This();
|
|
|
|
root_dir: []u8,
|
|
inotify: INotify,
|
|
files: std.ArrayList(File),
|
|
watches: std.ArrayList(Watch),
|
|
|
|
const File = struct {
|
|
path: []u8,
|
|
debounce: Io.Duration,
|
|
last_event_at: ?Io.Timestamp,
|
|
|
|
pub fn deinit(self: File, gpa: Allocator) void {
|
|
gpa.free(self.path);
|
|
}
|
|
};
|
|
|
|
const Watch = struct {
|
|
wd: ?u32,
|
|
path: []u8,
|
|
|
|
pub fn deinit(self: Watch, gpa: Allocator) void {
|
|
gpa.free(self.path);
|
|
}
|
|
};
|
|
|
|
pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher {
|
|
if (!std.fs.path.isAbsolute(root_dir)) {
|
|
return error.PathIsNotAbsolute;
|
|
}
|
|
|
|
const inotify_buf = try gpa.alloc(u8, INotify.max_event_size * 10);
|
|
errdefer gpa.free(inotify_buf);
|
|
|
|
const root_dir_owned = try gpa.dupe(u8, root_dir);
|
|
errdefer gpa.free(root_dir_owned);
|
|
|
|
return FileWatcher{
|
|
.root_dir = root_dir_owned,
|
|
.inotify = try .init(inotify_buf),
|
|
.files = .empty,
|
|
.watches = .empty
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *FileWatcher, gpa: Allocator) void {
|
|
gpa.free(self.root_dir);
|
|
|
|
gpa.free(self.inotify.buf);
|
|
self.inotify.deinit();
|
|
|
|
for (self.files.items) |file| {
|
|
file.deinit(gpa);
|
|
}
|
|
self.files.deinit(gpa);
|
|
|
|
for (self.watches.items) |watch| {
|
|
watch.deinit(gpa);
|
|
}
|
|
self.watches.deinit(gpa);
|
|
}
|
|
|
|
fn findFileIndex(self: *FileWatcher, path: []const u8) ?usize {
|
|
for (0.., self.files.items) |i, file| {
|
|
if (std.mem.eql(u8, file.path, path)) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn findFile(self: *FileWatcher, path: []const u8) ?*File {
|
|
if (self.findFileIndex(path)) |file_index| {
|
|
return &self.files.items[file_index];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
fn findWatchByPath(self: *FileWatcher, path: []const u8) ?usize {
|
|
for (0.., self.watches.items) |i, watch| {
|
|
if (std.mem.eql(u8, watch.path, path)) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn findWatchByDescriptor(self: *FileWatcher, wd: u32) ?usize {
|
|
for (0.., self.watches.items) |i, watch| {
|
|
if (watch.wd == wd) {
|
|
return i;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn bufJoinPaths(path_buff: []u8, paths: []const []const u8) ![:0]u8 {
|
|
return try std.fmt.bufPrintZ(path_buff, "{f}", .{
|
|
std.fs.path.fmtJoin(paths)
|
|
});
|
|
}
|
|
|
|
fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void {
|
|
if (watch.wd != null) {
|
|
return;
|
|
}
|
|
|
|
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
|
const path = try bufJoinPaths(&path_buff, &.{ self.root_dir, watch.path });
|
|
|
|
watch.wd = self.inotify.add(path, INotify.Mask{
|
|
.create = true,
|
|
.modify = true,
|
|
.delete_self = true,
|
|
.move_self = true,
|
|
.moved_to = true,
|
|
.moved_from = true,
|
|
}) catch |err| switch (err) {
|
|
error.FileNotFound => null,
|
|
else => return err
|
|
};
|
|
}
|
|
|
|
pub fn toRelativePath(outer_path: []const u8, inner_path: []const u8) ?[]const u8 {
|
|
if (inner_path.len == outer_path.len) {
|
|
if (std.mem.eql(u8, inner_path, outer_path)) {
|
|
return "";
|
|
}
|
|
} else if (inner_path.len > outer_path.len) {
|
|
if (std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep) {
|
|
return inner_path[(outer_path.len+1)..];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn isInside(outer_path: []const u8, inner_path: []const u8) bool {
|
|
return toRelativePath(outer_path, inner_path) != null;
|
|
}
|
|
|
|
fn unregisterWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
|
for (self.watches.items) |*watch| {
|
|
const wd = watch.wd orelse continue;
|
|
|
|
if (isInside(dir, watch.path)) {
|
|
try self.inotify.remove(wd);
|
|
watch.wd = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn registerWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
|
for (self.watches.items) |*watch| {
|
|
if (watch.wd == null and isInside(dir, watch.path)) {
|
|
try self.attemptRegisteringWatch(watch);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn ensureWatchExists(self: *FileWatcher, gpa: Allocator, path: []const u8) !usize {
|
|
var watch_index: usize = undefined;
|
|
if (self.findWatchByPath(path)) |found_watch_index| {
|
|
watch_index = found_watch_index;
|
|
} else {
|
|
try self.watches.ensureUnusedCapacity(gpa, 1);
|
|
|
|
const path_owned = try gpa.dupe(u8, path);
|
|
errdefer gpa.free(path_owned);
|
|
|
|
watch_index = self.watches.items.len;
|
|
self.watches.appendAssumeCapacity(Watch{
|
|
.wd = null,
|
|
.path = path_owned
|
|
});
|
|
}
|
|
|
|
try self.attemptRegisteringWatch(&self.watches.items[watch_index]);
|
|
|
|
return watch_index;
|
|
}
|
|
|
|
fn removeWatch(self: *FileWatcher, gpa: Allocator, index: usize) void {
|
|
const watch = self.watches.swapRemove(index);
|
|
watch.deinit(gpa);
|
|
}
|
|
|
|
pub fn add(self: *FileWatcher, gpa: Allocator, path: []const u8, debounce: Io.Duration) !void {
|
|
if (std.fs.path.isAbsolute(path)) {
|
|
return error.PathIsAbsolute;
|
|
}
|
|
|
|
if (self.findFileIndex(path) != null) {
|
|
return;
|
|
}
|
|
|
|
var dir_iter: ?[]const u8 = path;
|
|
while (dir_iter) |current| {
|
|
_ = try self.ensureWatchExists(gpa, current);
|
|
|
|
dir_iter = std.fs.path.dirname(current);
|
|
}
|
|
|
|
_ = try self.ensureWatchExists(gpa, "");
|
|
|
|
try self.files.ensureUnusedCapacity(gpa, 1);
|
|
|
|
const path_owned = try gpa.dupe(u8, path);
|
|
errdefer gpa.free(path_owned);
|
|
|
|
self.files.appendAssumeCapacity(File{
|
|
.path = path_owned,
|
|
.debounce = debounce,
|
|
.last_event_at = null
|
|
});
|
|
}
|
|
|
|
pub fn remove(self: *FileWatcher, gpa: Allocator, path: []const u8) void {
|
|
const index = self.findFileIndex(path);
|
|
if (index == null) {
|
|
return;
|
|
}
|
|
|
|
const file = self.files.items[index];
|
|
self.files.swapRemove(index);
|
|
|
|
file.deinit(gpa);
|
|
}
|
|
|
|
fn nextEvents(self: *FileWatcher) !?*File {
|
|
while (try self.inotify.next()) |e| {
|
|
const watch_index = self.findWatchByDescriptor(e.wd) orelse continue;
|
|
var watch = &self.watches.items[watch_index];
|
|
|
|
if (e.mask.ignored) {
|
|
watch.wd = null;
|
|
|
|
} else if (e.mask.q_overflow) {
|
|
return error.QueueOverflow;
|
|
|
|
} else if (e.mask.delete_self or e.mask.modify) {
|
|
if (self.findFile(watch.path)) |file| {
|
|
return file;
|
|
}
|
|
|
|
} else if (e.mask.create) {
|
|
assert(e.name != null);
|
|
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
|
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
|
if (self.findWatchByPath(path)) |child_watch_index| {
|
|
try self.attemptRegisteringWatch(&self.watches.items[child_watch_index]);
|
|
}
|
|
|
|
if (self.findFile(path)) |file| {
|
|
return file;
|
|
}
|
|
|
|
} else if (e.mask.moved_from) {
|
|
assert(e.name != null);
|
|
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
|
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
|
try self.unregisterWatchesRecursively(path);
|
|
|
|
if (self.findFile(path)) |file| {
|
|
return file;
|
|
}
|
|
|
|
} else if (e.mask.moved_to) {
|
|
assert(e.name != null);
|
|
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
|
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
|
try self.registerWatchesRecursively(path);
|
|
|
|
if (self.findFile(path)) |file| {
|
|
return file;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
|
|
const now = Io.Clock.real.now(io);
|
|
|
|
var queue_overflow = false;
|
|
while (true) {
|
|
const file = (self.nextEvents() catch |err| switch (err) {
|
|
error.QueueOverflow => {
|
|
queue_overflow = true;
|
|
break;
|
|
},
|
|
else => return err
|
|
}) orelse break;
|
|
file.last_event_at = now;
|
|
}
|
|
|
|
if (queue_overflow) {
|
|
for (self.files.items) |*file| {
|
|
file.last_event_at = now;
|
|
}
|
|
}
|
|
|
|
for (self.files.items) |*file| {
|
|
const last_event_at = file.last_event_at orelse continue;
|
|
if (now.nanoseconds > last_event_at.addDuration(file.debounce).nanoseconds) {
|
|
file.last_event_at = null;
|
|
return file.path;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|