refactor: improve file_watcher error handling

This commit is contained in:
CJ van den Berg 2026-02-20 17:23:34 +01:00
parent 54e047f63c
commit 5988ff69d4
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -19,7 +19,11 @@ pub const EventType = enum {
}; };
pub const Error = FileWatcherError; pub const Error = FileWatcherError;
pub const FileWatcherError = error{ FileWatcherFailed, ThespianSpawnFailed, OutOfMemory }; pub const FileWatcherError = error{
FileWatcherSendFailed,
ThespianSpawnFailed,
OutOfMemory,
};
const SpawnError = error{ OutOfMemory, ThespianSpawnFailed }; const SpawnError = error{ OutOfMemory, ThespianSpawnFailed };
/// Watch a path (file or directory) for changes. The caller will receive: /// Watch a path (file or directory) for changes. The caller will receive:
@ -51,7 +55,7 @@ fn get() SpawnError!Self {
} }
fn send(message: anytype) FileWatcherError!void { fn send(message: anytype) FileWatcherError!void {
return (try get()).pid.send(message) catch error.FileWatcherFailed; return (try get()).pid.send(message) catch error.FileWatcherSendFailed;
} }
fn create() SpawnError!Self { fn create() SpawnError!Self {
@ -83,10 +87,10 @@ const INotifyBackend = struct {
const in_flags: std.os.linux.O = .{ .NONBLOCK = true }; const in_flags: std.os.linux.O = .{ .NONBLOCK = true };
fn init() !@This() { fn init() error{ ProcessFdQuotaExceeded, SystemFdQuotaExceeded, SystemResources, Unexpected, ThespianFileDescriptorInitFailed }!@This() {
const ifd = std.posix.inotify_init1(@bitCast(in_flags)) catch return error.FileWatcherFailed; const ifd = try std.posix.inotify_init1(@bitCast(in_flags));
errdefer std.posix.close(ifd); errdefer std.posix.close(ifd);
const fwd = tp.file_descriptor.init(module_name, ifd) catch return error.FileWatcherFailed; const fwd = try tp.file_descriptor.init(module_name, ifd);
return .{ .inotify_fd = ifd, .fd_watcher = fwd, .watches = .empty }; return .{ .inotify_fd = ifd, .fd_watcher = fwd, .watches = .empty };
} }
@ -103,7 +107,7 @@ const INotifyBackend = struct {
try self.fd_watcher.wait_read(); try self.fd_watcher.wait_read();
} }
fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !void { fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) error{OutOfMemory}!void {
const path_z = try allocator.dupeZ(u8, path); const path_z = try allocator.dupeZ(u8, path);
defer allocator.free(path_z); defer allocator.free(path_z);
const wd = std.os.linux.inotify_add_watch(self.inotify_fd, path_z, watch_mask); const wd = std.os.linux.inotify_add_watch(self.inotify_fd, path_z, watch_mask);
@ -126,7 +130,7 @@ const INotifyBackend = struct {
} }
} }
fn drain(self: *@This(), allocator: std.mem.Allocator, parent: tp.pid_ref) !void { fn drain(self: *@This(), allocator: std.mem.Allocator, parent: tp.pid_ref) (std.posix.ReadError || error{ NoSpaceLeft, OutOfMemory, Exit })!void {
const InotifyEvent = extern struct { const InotifyEvent = extern struct {
wd: i32, wd: i32,
mask: u32, mask: u32,
@ -236,10 +240,10 @@ const KQueueBackend = struct {
const NOTE_ATTRIB: u32 = 0x00000008; const NOTE_ATTRIB: u32 = 0x00000008;
const NOTE_EXTEND: u32 = 0x00000004; const NOTE_EXTEND: u32 = 0x00000004;
fn init() !@This() { fn init() (std.posix.KQueueError || std.posix.KEventError)!@This() {
const kq = std.posix.kqueue() catch return error.FileWatcherFailed; const kq = try std.posix.kqueue();
errdefer std.posix.close(kq); errdefer std.posix.close(kq);
const pipe = std.posix.pipe() catch return error.FileWatcherFailed; const pipe = try std.posix.pipe();
errdefer { errdefer {
std.posix.close(pipe[0]); std.posix.close(pipe[0]);
std.posix.close(pipe[1]); std.posix.close(pipe[1]);
@ -254,7 +258,7 @@ const KQueueBackend = struct {
.data = 0, .data = 0,
.udata = 0, .udata = 0,
}; };
_ = std.posix.kevent(kq, &.{shutdown_kev}, &.{}, null) catch return error.FileWatcherFailed; _ = try std.posix.kevent(kq, &.{shutdown_kev}, &.{}, null);
return .{ .kq = kq, .shutdown_pipe = pipe, .thread = null, .watches = .empty }; return .{ .kq = kq, .shutdown_pipe = pipe, .thread = null, .watches = .empty };
} }
@ -295,9 +299,9 @@ const KQueueBackend = struct {
} }
} }
fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !void { fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) (std.posix.OpenError || std.posix.KEventError || error{OutOfMemory})!void {
if (self.watches.contains(path)) return; if (self.watches.contains(path)) return;
const path_fd = std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0) catch return error.FileWatcherFailed; const path_fd = try std.posix.open(path, .{ .ACCMODE = .RDONLY }, 0);
errdefer std.posix.close(path_fd); errdefer std.posix.close(path_fd);
const kev = std.posix.Kevent{ const kev = std.posix.Kevent{
.ident = @intCast(path_fd), .ident = @intCast(path_fd),
@ -307,7 +311,7 @@ const KQueueBackend = struct {
.data = 0, .data = 0,
.udata = 0, .udata = 0,
}; };
_ = std.posix.kevent(self.kq, &.{kev}, &.{}, null) catch return error.FileWatcherFailed; _ = try std.posix.kevent(self.kq, &.{kev}, &.{}, null);
const owned_path = try allocator.dupe(u8, path); const owned_path = try allocator.dupe(u8, path);
errdefer allocator.free(owned_path); errdefer allocator.free(owned_path);
try self.watches.put(allocator, owned_path, path_fd); try self.watches.put(allocator, owned_path, path_fd);
@ -320,7 +324,7 @@ const KQueueBackend = struct {
} }
} }
fn drain(self: *@This(), allocator: std.mem.Allocator, parent: tp.pid_ref) !void { fn drain(self: *@This(), allocator: std.mem.Allocator, parent: tp.pid_ref) tp.result {
_ = allocator; _ = allocator;
var events: [64]std.posix.Kevent = undefined; var events: [64]std.posix.Kevent = undefined;
const immediate: std.posix.timespec = .{ .sec = 0, .nsec = 0 }; const immediate: std.posix.timespec = .{ .sec = 0, .nsec = 0 };
@ -422,9 +426,8 @@ const WindowsBackend = struct {
0x00000010 | // FILE_NOTIFY_CHANGE_LAST_WRITE 0x00000010 | // FILE_NOTIFY_CHANGE_LAST_WRITE
0x00000040; // FILE_NOTIFY_CHANGE_CREATION 0x00000040; // FILE_NOTIFY_CHANGE_CREATION
fn init() !@This() { fn init() windows.CreateIoCompletionPortError!@This() {
const iocp = windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1) catch const iocp = try windows.CreateIoCompletionPort(windows.INVALID_HANDLE_VALUE, null, 0, 1);
return error.FileWatcherFailed;
return .{ .iocp = iocp, .thread = null, .watches = .empty }; return .{ .iocp = iocp, .thread = null, .watches = .empty };
} }
@ -461,7 +464,12 @@ const WindowsBackend = struct {
} }
} }
fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !void { fn add_watch(self: *@This(), allocator: std.mem.Allocator, path: []const u8) (windows.CreateIoCompletionPortError || error{
InvalidUtf8,
OutOfMemory,
FileWatcherInvalidHandle,
FileWatcherReadDirectoryChangesFailed,
})!void {
if (self.watches.contains(path)) return; if (self.watches.contains(path)) return;
const path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, path); const path_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, path);
defer allocator.free(path_w); defer allocator.free(path_w);
@ -474,16 +482,16 @@ const WindowsBackend = struct {
0x02000000 | 0x40000000, // FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED 0x02000000 | 0x40000000, // FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED
null, null,
); );
if (handle == windows.INVALID_HANDLE_VALUE) return error.FileWatcherFailed; if (handle == windows.INVALID_HANDLE_VALUE) return error.FileWatcherInvalidHandle;
errdefer _ = win32.CloseHandle(handle); errdefer _ = win32.CloseHandle(handle);
_ = windows.CreateIoCompletionPort(handle, self.iocp, @intFromPtr(handle), 0) catch return error.FileWatcherFailed; _ = try windows.CreateIoCompletionPort(handle, self.iocp, @intFromPtr(handle), 0);
const buf = try allocator.alignedAlloc(u8, .fromByteUnits(4), buf_size); const buf = try allocator.alignedAlloc(u8, .fromByteUnits(4), buf_size);
errdefer allocator.free(buf); errdefer allocator.free(buf);
const owned_path = try allocator.dupe(u8, path); const owned_path = try allocator.dupe(u8, path);
errdefer allocator.free(owned_path); errdefer allocator.free(owned_path);
var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED); var overlapped: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED);
if (win32.ReadDirectoryChangesW(handle, buf.ptr, buf_size, 1, notify_filter, null, &overlapped, null) == 0) if (win32.ReadDirectoryChangesW(handle, buf.ptr, buf_size, 1, notify_filter, null, &overlapped, null) == 0)
return error.FileWatcherFailed; return error.FileWatcherReadDirectoryChangesFailed;
try self.watches.put(allocator, owned_path, .{ try self.watches.put(allocator, owned_path, .{
.handle = handle, .handle = handle,
.buf = buf, .buf = buf,