From 31994fd2d0b12116ecfab286699bd7a40f6fbb0c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 30 Jan 2026 22:30:19 -0800 Subject: [PATCH] WIP --- lib/std/Io.zig | 42 +++++++- lib/std/Io/File.zig | 44 ++++++++ lib/std/Io/Threaded.zig | 217 ++++++++++++++++++++++++++++++++++++++++ lib/std/posix.zig | 93 ----------------- 4 files changed, 301 insertions(+), 95 deletions(-) diff --git a/lib/std/Io.zig b/lib/std/Io.zig index c3ba3575e4..e09f6f20df 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -257,6 +257,10 @@ pub const VTable = struct { pub const Operation = union(enum) { file_read_streaming: FileReadStreaming, + watch_init: WatchInit, + watch_deinit: WatchDeinit, + watch_mark_dir: WatchMarkDir, + watch_wait: WatchWait, pub const Tag = @typeInfo(Operation).@"union".tag_type.?; @@ -287,7 +291,41 @@ pub const Operation = union(enum) { LockViolation, } || Io.UnexpectedError; - pub const Result = usize; + pub const Result = Error!usize; + }; + + pub const WatchInit = struct { + w: *File.Watch, + + pub const Error = error{ + OutOfMemory, + }; + + pub const Result = Error!void; + }; + + pub const WatchDeinit = struct { + w: *File.Watch, + + pub const Result = void; + }; + + pub const WatchMarkDir = struct { + w: *File.Watch, + dir: Dir, + sub_path: []const u8, + + pub const Error = error{NotDir}; + + pub const Result = Error!void; + }; + + pub const WatchWait = struct { + w: *File.Watch, + + pub const Error = error{}; + + pub const Result = Error!void; }; pub const Result = Result: { @@ -296,7 +334,7 @@ pub const Operation = union(enum) { var field_types: [operation_fields.len]type = undefined; for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| { field_name.* = field.name; - field_type.* = field.type.Error!field.type.Result; + field_type.* = field.type.Result; } break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{})); }; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index e0297e0573..dbde605a50 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -844,6 +844,50 @@ pub fn createMemoryMap(file: File, io: Io, options: MemoryMap.CreateOptions) Mem return .create(io, file, options); } +pub const Watch = struct { + implementation: ?*anyopaque, + events: Io.Queue(Event), + + pub const Event = union(enum) { + /// File system watch queue overflowed; some events will be dropped. + overflow, + change: Change, + + pub const Change = struct { + dir: Dir, + sub_path: []const u8, + }; + }; + + pub const InitError = Io.Operation.WatchInit.Error; + + pub fn init(w: *Watch, io: Io) InitError!void { + return (try io.operate(.{ .watch_init = .{ .w = w } })).watch_init; + } + + pub fn deinit(w: *Watch, io: Io) void { + return (try io.operate(.{ .watch_deinit = .{ .w = w } })).watch_deinit; + } + + pub const MarkError = Io.Operation.WatchMarkDir.Error; + + pub fn markDir(w: *Watch, io: Io, dir: Dir, sub_path: []const u8) MarkError!void { + return (try io.operate(.{ .watch_mark_dir = .{ + .w = w, + .dir = dir, + .sub_path = sub_path, + } })).watch_mark_dir; + } + + pub const WaitError = error{}; + + /// Populates `events`, blocking until at least one event is added. + /// Blocking can be interrupted by closing the queue. + pub fn wait(w: *Watch, io: Io) WaitError!void { + return (try io.operate(.{ .watch_wait = .{ .w = w } })).watch_wait; + } +}; + test { _ = Reader; _ = Writer; diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index f9002567a2..adfa6dfd2c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -2492,6 +2492,10 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper else => |e| e, }, }, + .watch_init => |o| return .{ .watch_init = watchInit(t, o.w) }, + .watch_deinit => |o| return .{ .watch_deinit = watchDeinit(t, o.w) }, + .watch_mark_dir => |o| return .{ .watch_mark_dir = watchMarkDir(t, o.w, o.dir, o.sub_path) }, + .watch_wait => |o| return .{ .watch_wait = watchWait(t, o.w) }, } } @@ -17716,3 +17720,216 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError! } } } + +const LinuxWatch = struct { + /// Key is the directory to watch which contains one or more files we are + /// interested in noticing changes to. + dir_table: DirTable, + /// Keyed differently but indexes correspond 1:1 with `dir_table`. + handle_table: HandleTable, + /// fanotify file descriptors are keyed by mount id since marks + /// are limited to a single filesystem. + poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), + + const MountId = i32; + const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, MountId, FileHandle.Adapter, false); + const DirTable = std.ArrayHashMapUnmanaged(Path, void, Path.TableAdapter, false); + + const Hash = std.hash.Wyhash; + + const Path = struct { + dir: Dir, + sub_path: []const u8, + + pub fn eql(self: Path, other: Path) bool { + return self.dir.handle == other.dir.handle and std.mem.eql(u8, self.sub_path, other.sub_path); + } + + /// Useful to make `Path` a key in `std.ArrayHashMap`. + pub const TableAdapter = struct { + pub fn hash(self: TableAdapter, a: Path) u32 { + _ = self; + const seed: u32 = @bitCast(a.dir.handle); + return @truncate(Hash.hash(seed, a.sub_path)); + } + pub fn eql(self: TableAdapter, a: Path, b: Path, b_index: usize) bool { + _ = self; + _ = b_index; + return a.eql(b); + } + }; + }; + + const fan_mask: std.os.linux.fanotify.MarkMask = .{ + .CLOSE_WRITE = true, + .CREATE = true, + .DELETE = true, + .DELETE_SELF = true, + .EVENT_ON_CHILD = true, + .MOVED_FROM = true, + .MOVED_TO = true, + .MOVE_SELF = true, + .ONDIR = true, + }; + + const FileHandle = struct { + handle: *align(1) std.os.linux.file_handle, + + fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { + const bytes = lfh.slice(); + const new_ptr = try gpa.alignedAlloc( + u8, + .of(std.os.linux.file_handle), + @sizeOf(std.os.linux.file_handle) + bytes.len, + ); + const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); + new_header.* = lfh.handle.*; + const new: FileHandle = .{ .handle = new_header }; + @memcpy(new.slice(), lfh.slice()); + return new; + } + + const Adapter = struct { + pub fn hash(self: Adapter, a: FileHandle) u32 { + _ = self; + const unsigned_type: u32 = @bitCast(a.handle.handle_type); + return @truncate(Hash.hash(unsigned_type, a.slice())); + } + pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool { + _ = self; + _ = b_index; + return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); + } + }; + }; + + fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { + var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; + var buf: [Dir.max_path_bytes]u8 = undefined; + const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ + path.sub_path, + }) catch return error.NameTooLong; + const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); + stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); + + switch (posix.errno(posix.system.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID))) { + .SUCCESS => {}, + .FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space + .INVAL => unreachable, // bad flags, or handle_bytes too big + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .OPNOTSUPP => return error.OperationUnsupported, + .OVERFLOW => return error.NameTooLong, + else => |err| return posix.unexpectedErrno(err), + } + + const stack_lfh: FileHandle = .{ .handle = stack_ptr }; + return stack_lfh.clone(gpa); + } + + fn markDir(lw: *LinuxWatch, t: *Threaded, path: Path) File.Watch.MarkError!void { + const gpa = t.allocator; + const gop = try lw.dir_table.getOrPut(gpa, path); + if (!gop.found_existing) { + var mount_id: MountId = undefined; + const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) { + error.FileNotFound => { + assert(lw.dir_table.swapRemove(path)); + return; + }, + else => return err, + }; + const fan_fd = blk: { + const fd_gop = try lw.poll_fds.getOrPut(gpa, mount_id); + if (!fd_gop.found_existing) { + const fan_fd = std.posix.fanotify_init(.{ + .CLASS = .NOTIF, + .CLOEXEC = true, + .NONBLOCK = true, + .REPORT_NAME = true, + .REPORT_DIR_FID = true, + .REPORT_FID = true, + .REPORT_TARGET_FID = true, + }, 0) catch |err| switch (err) { + error.UnsupportedFlags => return error.UnsupportedOperation, + else => |e| return e, + }; + fd_gop.value_ptr.* = .{ + .fd = fan_fd, + .events = std.posix.POLL.IN, + .revents = undefined, + }; + } + break :blk fd_gop.value_ptr.*.fd; + }; + // `dir_handle` may already be present in the table in + // the case that we have multiple Cache.Path instances + // that compare inequal but ultimately point to the same + // directory on the file system. + // In such case, we must revert adding this directory, but keep + // the additions to the step set. + const dh_gop = try lw.handle_table.getOrPut(gpa, dir_handle); + if (dh_gop.found_existing) { + _ = lw.dir_table.pop(); + } else { + assert(dh_gop.index == gop.index); + dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; + posix.fanotify_mark(fan_fd, .{ + .ADD = true, + .ONLYDIR = true, + }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| { + fatal("unable to watch {f}: {s}", .{ path, @errorName(err) }); + }; + } + break :rs &dh_gop.value_ptr.reaction_set; + } + break :rs &w.os.handle_table.values()[gop.index].reaction_set; + @panic("TODO"); + } +}; + +fn watchInit(t: *Threaded, w: *Io.Watch) File.Watch.InitError!void { + switch (native_os) { + .linux => { + w.* = .{ + .queue = .empty, + .implementation = try LinuxWatch.create(t, w), + }; + }, + else => return error.OperationUnsupported, + } +} + +fn watchDeinit(t: *Threaded, w: *Io.Watch) void { + const gpa = t.allocator; + switch (native_os) { + .linux => { + const ptr: *LinuxWatch = @alignCast(@ptrCast(w.implementation)); + ptr.destroy(t); + }, + else => unreachable, + } + w.* = undefined; +} + +fn watchMarkDir(t: *Threaded, w: *Io.Watch, dir: Dir, sub_path: []const u8) File.Watch.MarkError!void { + switch (native_os) { + .linux => { + const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation)); + return lw.markDir(t, .{ .dir = dir, .sub_path = sub_path }); + }, + else => unreachable, + } +} + +/// Populates `events`, blocking until at least one event is added. +/// Blocking can be interrupted by closing the queue. +fn watchWait(t: *Threaded, w: *Io.Watch, io: Io) File.Watch.WaitError!void { + switch (native_os) { + .linux => { + const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation)); + return lw.watchWait(t); + }, + else => unreachable, + } +} diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 5e2cde9aa5..50f08af99e 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -574,61 +574,6 @@ pub fn fanotify_init(flags: std.os.linux.fanotify.InitFlags, event_f_flags: u32) } } -pub const FanotifyMarkError = error{ - MarkAlreadyExists, - IsDir, - NotAssociatedWithFileSystem, - FileNotFound, - SystemResources, - UserMarkQuotaExceeded, - NotDir, - OperationUnsupported, - PermissionDenied, - CrossDevice, - NameTooLong, -} || UnexpectedError; - -pub fn fanotify_mark( - fanotify_fd: fd_t, - flags: std.os.linux.fanotify.MarkFlags, - mask: std.os.linux.fanotify.MarkMask, - dirfd: fd_t, - pathname: ?[]const u8, -) FanotifyMarkError!void { - if (pathname) |path| { - const path_c = try toPosixPath(path); - return fanotify_markZ(fanotify_fd, flags, mask, dirfd, &path_c); - } else { - return fanotify_markZ(fanotify_fd, flags, mask, dirfd, null); - } -} - -pub fn fanotify_markZ( - fanotify_fd: fd_t, - flags: std.os.linux.fanotify.MarkFlags, - mask: std.os.linux.fanotify.MarkMask, - dirfd: fd_t, - pathname: ?[*:0]const u8, -) FanotifyMarkError!void { - const rc = system.fanotify_mark(fanotify_fd, flags, mask, dirfd, pathname); - switch (errno(rc)) { - .SUCCESS => return, - .BADF => unreachable, - .EXIST => return error.MarkAlreadyExists, - .INVAL => unreachable, - .ISDIR => return error.IsDir, - .NODEV => return error.NotAssociatedWithFileSystem, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOSPC => return error.UserMarkQuotaExceeded, - .NOTDIR => return error.NotDir, - .OPNOTSUPP => return error.OperationUnsupported, - .PERM => return error.PermissionDenied, - .XDEV => return error.CrossDevice, - else => |err| return unexpectedErrno(err), - } -} - pub const MMapError = error{ /// The underlying filesystem of the specified file does not support memory mapping. MemoryMappingNotSupported, @@ -1762,44 +1707,6 @@ pub fn ptrace(request: u32, pid: pid_t, addr: usize, data: usize) PtraceError!vo }; } -pub const NameToFileHandleAtError = error{ - FileNotFound, - NotDir, - OperationUnsupported, - NameTooLong, - Unexpected, -}; - -pub fn name_to_handle_at( - dirfd: fd_t, - pathname: []const u8, - handle: *std.os.linux.file_handle, - mount_id: *i32, - flags: u32, -) NameToFileHandleAtError!void { - const pathname_c = try toPosixPath(pathname); - return name_to_handle_atZ(dirfd, &pathname_c, handle, mount_id, flags); -} - -pub fn name_to_handle_atZ( - dirfd: fd_t, - pathname_z: [*:0]const u8, - handle: *std.os.linux.file_handle, - mount_id: *i32, - flags: u32, -) NameToFileHandleAtError!void { - switch (errno(system.name_to_handle_at(dirfd, pathname_z, handle, mount_id, flags))) { - .SUCCESS => {}, - .FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space - .INVAL => unreachable, // bad flags, or handle_bytes too big - .NOENT => return error.FileNotFound, - .NOTDIR => return error.NotDir, - .OPNOTSUPP => return error.OperationUnsupported, - .OVERFLOW => return error.NameTooLong, - else => |err| return unexpectedErrno(err), - } -} - pub const IoCtl_SIOCGIFINDEX_Error = error{ FileSystem, InterfaceNotFound,