sokoban-v2/src/platform/file_watcher.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;
}