authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-30 22:30:19-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-30 22:30:19-08:00
log31994fd2d0b12116ecfab286699bd7a40f6fbb0c
tree19f45f2e3be8805eb12567eba9140c5a649fa408
parent13f13fe0a7e1dcc1a224efff0de89cdd36f11ca6

WIP


4 files changed, 301 insertions(+), 95 deletions(-)

lib/std/Io.zig+40-2
...@@ -257,6 +257,10 @@ pub const VTable = struct {...@@ -257,6 +257,10 @@ pub const VTable = struct {
257257
258pub const Operation = union(enum) {258pub const Operation = union(enum) {
259 file_read_streaming: FileReadStreaming,259 file_read_streaming: FileReadStreaming,
260 watch_init: WatchInit,
261 watch_deinit: WatchDeinit,
262 watch_mark_dir: WatchMarkDir,
263 watch_wait: WatchWait,
260264
261 pub const Tag = @typeInfo(Operation).@"union".tag_type.?;265 pub const Tag = @typeInfo(Operation).@"union".tag_type.?;
262266
...@@ -287,7 +291,41 @@ pub const Operation = union(enum) {...@@ -287,7 +291,41 @@ pub const Operation = union(enum) {
287 LockViolation,291 LockViolation,
288 } || Io.UnexpectedError;292 } || Io.UnexpectedError;
289293
290 pub const Result = usize;294 pub const Result = Error!usize;
295 };
296
297 pub const WatchInit = struct {
298 w: *File.Watch,
299
300 pub const Error = error{
301 OutOfMemory,
302 };
303
304 pub const Result = Error!void;
305 };
306
307 pub const WatchDeinit = struct {
308 w: *File.Watch,
309
310 pub const Result = void;
311 };
312
313 pub const WatchMarkDir = struct {
314 w: *File.Watch,
315 dir: Dir,
316 sub_path: []const u8,
317
318 pub const Error = error{NotDir};
319
320 pub const Result = Error!void;
321 };
322
323 pub const WatchWait = struct {
324 w: *File.Watch,
325
326 pub const Error = error{};
327
328 pub const Result = Error!void;
291 };329 };
292330
293 pub const Result = Result: {331 pub const Result = Result: {
...@@ -296,7 +334,7 @@ pub const Operation = union(enum) {...@@ -296,7 +334,7 @@ pub const Operation = union(enum) {
296 var field_types: [operation_fields.len]type = undefined;334 var field_types: [operation_fields.len]type = undefined;
297 for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| {335 for (operation_fields, &field_names, &field_types) |field, *field_name, *field_type| {
298 field_name.* = field.name;336 field_name.* = field.name;
299 field_type.* = field.type.Error!field.type.Result;337 field_type.* = field.type.Result;
300 }338 }
301 break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{}));339 break :Result @Union(.auto, Tag, &field_names, &field_types, &@splat(.{}));
302 };340 };
lib/std/Io/File.zig+44
...@@ -844,6 +844,50 @@ pub fn createMemoryMap(file: File, io: Io, options: MemoryMap.CreateOptions) Mem...@@ -844,6 +844,50 @@ pub fn createMemoryMap(file: File, io: Io, options: MemoryMap.CreateOptions) Mem
844 return .create(io, file, options);844 return .create(io, file, options);
845}845}
846846
847pub const Watch = struct {
848 implementation: ?*anyopaque,
849 events: Io.Queue(Event),
850
851 pub const Event = union(enum) {
852 /// File system watch queue overflowed; some events will be dropped.
853 overflow,
854 change: Change,
855
856 pub const Change = struct {
857 dir: Dir,
858 sub_path: []const u8,
859 };
860 };
861
862 pub const InitError = Io.Operation.WatchInit.Error;
863
864 pub fn init(w: *Watch, io: Io) InitError!void {
865 return (try io.operate(.{ .watch_init = .{ .w = w } })).watch_init;
866 }
867
868 pub fn deinit(w: *Watch, io: Io) void {
869 return (try io.operate(.{ .watch_deinit = .{ .w = w } })).watch_deinit;
870 }
871
872 pub const MarkError = Io.Operation.WatchMarkDir.Error;
873
874 pub fn markDir(w: *Watch, io: Io, dir: Dir, sub_path: []const u8) MarkError!void {
875 return (try io.operate(.{ .watch_mark_dir = .{
876 .w = w,
877 .dir = dir,
878 .sub_path = sub_path,
879 } })).watch_mark_dir;
880 }
881
882 pub const WaitError = error{};
883
884 /// Populates `events`, blocking until at least one event is added.
885 /// Blocking can be interrupted by closing the queue.
886 pub fn wait(w: *Watch, io: Io) WaitError!void {
887 return (try io.operate(.{ .watch_wait = .{ .w = w } })).watch_wait;
888 }
889};
890
847test {891test {
848 _ = Reader;892 _ = Reader;
849 _ = Writer;893 _ = Writer;
lib/std/Io/Threaded.zig+217
...@@ -2492,6 +2492,10 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper...@@ -2492,6 +2492,10 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper
2492 else => |e| e,2492 else => |e| e,
2493 },2493 },
2494 },2494 },
2495 .watch_init => |o| return .{ .watch_init = watchInit(t, o.w) },
2496 .watch_deinit => |o| return .{ .watch_deinit = watchDeinit(t, o.w) },
2497 .watch_mark_dir => |o| return .{ .watch_mark_dir = watchMarkDir(t, o.w, o.dir, o.sub_path) },
2498 .watch_wait => |o| return .{ .watch_wait = watchWait(t, o.w) },
2495 }2499 }
2496}2500}
24972501
...@@ -17716,3 +17720,216 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!...@@ -17716,3 +17720,216 @@ fn mmSyncWrite(file: File, memory: []u8, offset: u64) File.WritePositionalError!
17716 }17720 }
17717 }17721 }
17718}17722}
17723
17724const LinuxWatch = struct {
17725 /// Key is the directory to watch which contains one or more files we are
17726 /// interested in noticing changes to.
17727 dir_table: DirTable,
17728 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
17729 handle_table: HandleTable,
17730 /// fanotify file descriptors are keyed by mount id since marks
17731 /// are limited to a single filesystem.
17732 poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
17733
17734 const MountId = i32;
17735 const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, MountId, FileHandle.Adapter, false);
17736 const DirTable = std.ArrayHashMapUnmanaged(Path, void, Path.TableAdapter, false);
17737
17738 const Hash = std.hash.Wyhash;
17739
17740 const Path = struct {
17741 dir: Dir,
17742 sub_path: []const u8,
17743
17744 pub fn eql(self: Path, other: Path) bool {
17745 return self.dir.handle == other.dir.handle and std.mem.eql(u8, self.sub_path, other.sub_path);
17746 }
17747
17748 /// Useful to make `Path` a key in `std.ArrayHashMap`.
17749 pub const TableAdapter = struct {
17750 pub fn hash(self: TableAdapter, a: Path) u32 {
17751 _ = self;
17752 const seed: u32 = @bitCast(a.dir.handle);
17753 return @truncate(Hash.hash(seed, a.sub_path));
17754 }
17755 pub fn eql(self: TableAdapter, a: Path, b: Path, b_index: usize) bool {
17756 _ = self;
17757 _ = b_index;
17758 return a.eql(b);
17759 }
17760 };
17761 };
17762
17763 const fan_mask: std.os.linux.fanotify.MarkMask = .{
17764 .CLOSE_WRITE = true,
17765 .CREATE = true,
17766 .DELETE = true,
17767 .DELETE_SELF = true,
17768 .EVENT_ON_CHILD = true,
17769 .MOVED_FROM = true,
17770 .MOVED_TO = true,
17771 .MOVE_SELF = true,
17772 .ONDIR = true,
17773 };
17774
17775 const FileHandle = struct {
17776 handle: *align(1) std.os.linux.file_handle,
17777
17778 fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
17779 const bytes = lfh.slice();
17780 const new_ptr = try gpa.alignedAlloc(
17781 u8,
17782 .of(std.os.linux.file_handle),
17783 @sizeOf(std.os.linux.file_handle) + bytes.len,
17784 );
17785 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
17786 new_header.* = lfh.handle.*;
17787 const new: FileHandle = .{ .handle = new_header };
17788 @memcpy(new.slice(), lfh.slice());
17789 return new;
17790 }
17791
17792 const Adapter = struct {
17793 pub fn hash(self: Adapter, a: FileHandle) u32 {
17794 _ = self;
17795 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
17796 return @truncate(Hash.hash(unsigned_type, a.slice()));
17797 }
17798 pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
17799 _ = self;
17800 _ = b_index;
17801 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
17802 }
17803 };
17804 };
17805
17806 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
17807 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
17808 var buf: [Dir.max_path_bytes]u8 = undefined;
17809 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
17810 path.sub_path,
17811 }) catch return error.NameTooLong;
17812 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
17813 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
17814
17815 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))) {
17816 .SUCCESS => {},
17817 .FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space
17818 .INVAL => unreachable, // bad flags, or handle_bytes too big
17819 .NOENT => return error.FileNotFound,
17820 .NOTDIR => return error.NotDir,
17821 .OPNOTSUPP => return error.OperationUnsupported,
17822 .OVERFLOW => return error.NameTooLong,
17823 else => |err| return posix.unexpectedErrno(err),
17824 }
17825
17826 const stack_lfh: FileHandle = .{ .handle = stack_ptr };
17827 return stack_lfh.clone(gpa);
17828 }
17829
17830 fn markDir(lw: *LinuxWatch, t: *Threaded, path: Path) File.Watch.MarkError!void {
17831 const gpa = t.allocator;
17832 const gop = try lw.dir_table.getOrPut(gpa, path);
17833 if (!gop.found_existing) {
17834 var mount_id: MountId = undefined;
17835 const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
17836 error.FileNotFound => {
17837 assert(lw.dir_table.swapRemove(path));
17838 return;
17839 },
17840 else => return err,
17841 };
17842 const fan_fd = blk: {
17843 const fd_gop = try lw.poll_fds.getOrPut(gpa, mount_id);
17844 if (!fd_gop.found_existing) {
17845 const fan_fd = std.posix.fanotify_init(.{
17846 .CLASS = .NOTIF,
17847 .CLOEXEC = true,
17848 .NONBLOCK = true,
17849 .REPORT_NAME = true,
17850 .REPORT_DIR_FID = true,
17851 .REPORT_FID = true,
17852 .REPORT_TARGET_FID = true,
17853 }, 0) catch |err| switch (err) {
17854 error.UnsupportedFlags => return error.UnsupportedOperation,
17855 else => |e| return e,
17856 };
17857 fd_gop.value_ptr.* = .{
17858 .fd = fan_fd,
17859 .events = std.posix.POLL.IN,
17860 .revents = undefined,
17861 };
17862 }
17863 break :blk fd_gop.value_ptr.*.fd;
17864 };
17865 // `dir_handle` may already be present in the table in
17866 // the case that we have multiple Cache.Path instances
17867 // that compare inequal but ultimately point to the same
17868 // directory on the file system.
17869 // In such case, we must revert adding this directory, but keep
17870 // the additions to the step set.
17871 const dh_gop = try lw.handle_table.getOrPut(gpa, dir_handle);
17872 if (dh_gop.found_existing) {
17873 _ = lw.dir_table.pop();
17874 } else {
17875 assert(dh_gop.index == gop.index);
17876 dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
17877 posix.fanotify_mark(fan_fd, .{
17878 .ADD = true,
17879 .ONLYDIR = true,
17880 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| {
17881 fatal("unable to watch {f}: {s}", .{ path, @errorName(err) });
17882 };
17883 }
17884 break :rs &dh_gop.value_ptr.reaction_set;
17885 }
17886 break :rs &w.os.handle_table.values()[gop.index].reaction_set;
17887 @panic("TODO");
17888 }
17889};
17890
17891fn watchInit(t: *Threaded, w: *Io.Watch) File.Watch.InitError!void {
17892 switch (native_os) {
17893 .linux => {
17894 w.* = .{
17895 .queue = .empty,
17896 .implementation = try LinuxWatch.create(t, w),
17897 };
17898 },
17899 else => return error.OperationUnsupported,
17900 }
17901}
17902
17903fn watchDeinit(t: *Threaded, w: *Io.Watch) void {
17904 const gpa = t.allocator;
17905 switch (native_os) {
17906 .linux => {
17907 const ptr: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
17908 ptr.destroy(t);
17909 },
17910 else => unreachable,
17911 }
17912 w.* = undefined;
17913}
17914
17915fn watchMarkDir(t: *Threaded, w: *Io.Watch, dir: Dir, sub_path: []const u8) File.Watch.MarkError!void {
17916 switch (native_os) {
17917 .linux => {
17918 const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
17919 return lw.markDir(t, .{ .dir = dir, .sub_path = sub_path });
17920 },
17921 else => unreachable,
17922 }
17923}
17924
17925/// Populates `events`, blocking until at least one event is added.
17926/// Blocking can be interrupted by closing the queue.
17927fn watchWait(t: *Threaded, w: *Io.Watch, io: Io) File.Watch.WaitError!void {
17928 switch (native_os) {
17929 .linux => {
17930 const lw: *LinuxWatch = @alignCast(@ptrCast(w.implementation));
17931 return lw.watchWait(t);
17932 },
17933 else => unreachable,
17934 }
17935}
lib/std/posix.zig-93
...@@ -574,61 +574,6 @@ pub fn fanotify_init(flags: std.os.linux.fanotify.InitFlags, event_f_flags: u32)...@@ -574,61 +574,6 @@ pub fn fanotify_init(flags: std.os.linux.fanotify.InitFlags, event_f_flags: u32)
574 }574 }
575}575}
576576
577pub const FanotifyMarkError = error{
578 MarkAlreadyExists,
579 IsDir,
580 NotAssociatedWithFileSystem,
581 FileNotFound,
582 SystemResources,
583 UserMarkQuotaExceeded,
584 NotDir,
585 OperationUnsupported,
586 PermissionDenied,
587 CrossDevice,
588 NameTooLong,
589} || UnexpectedError;
590
591pub fn fanotify_mark(
592 fanotify_fd: fd_t,
593 flags: std.os.linux.fanotify.MarkFlags,
594 mask: std.os.linux.fanotify.MarkMask,
595 dirfd: fd_t,
596 pathname: ?[]const u8,
597) FanotifyMarkError!void {
598 if (pathname) |path| {
599 const path_c = try toPosixPath(path);
600 return fanotify_markZ(fanotify_fd, flags, mask, dirfd, &path_c);
601 } else {
602 return fanotify_markZ(fanotify_fd, flags, mask, dirfd, null);
603 }
604}
605
606pub fn fanotify_markZ(
607 fanotify_fd: fd_t,
608 flags: std.os.linux.fanotify.MarkFlags,
609 mask: std.os.linux.fanotify.MarkMask,
610 dirfd: fd_t,
611 pathname: ?[*:0]const u8,
612) FanotifyMarkError!void {
613 const rc = system.fanotify_mark(fanotify_fd, flags, mask, dirfd, pathname);
614 switch (errno(rc)) {
615 .SUCCESS => return,
616 .BADF => unreachable,
617 .EXIST => return error.MarkAlreadyExists,
618 .INVAL => unreachable,
619 .ISDIR => return error.IsDir,
620 .NODEV => return error.NotAssociatedWithFileSystem,
621 .NOENT => return error.FileNotFound,
622 .NOMEM => return error.SystemResources,
623 .NOSPC => return error.UserMarkQuotaExceeded,
624 .NOTDIR => return error.NotDir,
625 .OPNOTSUPP => return error.OperationUnsupported,
626 .PERM => return error.PermissionDenied,
627 .XDEV => return error.CrossDevice,
628 else => |err| return unexpectedErrno(err),
629 }
630}
631
632pub const MMapError = error{577pub const MMapError = error{
633 /// The underlying filesystem of the specified file does not support memory mapping.578 /// The underlying filesystem of the specified file does not support memory mapping.
634 MemoryMappingNotSupported,579 MemoryMappingNotSupported,
...@@ -1762,44 +1707,6 @@ pub fn ptrace(request: u32, pid: pid_t, addr: usize, data: usize) PtraceError!vo...@@ -1762,44 +1707,6 @@ pub fn ptrace(request: u32, pid: pid_t, addr: usize, data: usize) PtraceError!vo
1762 };1707 };
1763}1708}
17641709
1765pub const NameToFileHandleAtError = error{
1766 FileNotFound,
1767 NotDir,
1768 OperationUnsupported,
1769 NameTooLong,
1770 Unexpected,
1771};
1772
1773pub fn name_to_handle_at(
1774 dirfd: fd_t,
1775 pathname: []const u8,
1776 handle: *std.os.linux.file_handle,
1777 mount_id: *i32,
1778 flags: u32,
1779) NameToFileHandleAtError!void {
1780 const pathname_c = try toPosixPath(pathname);
1781 return name_to_handle_atZ(dirfd, &pathname_c, handle, mount_id, flags);
1782}
1783
1784pub fn name_to_handle_atZ(
1785 dirfd: fd_t,
1786 pathname_z: [*:0]const u8,
1787 handle: *std.os.linux.file_handle,
1788 mount_id: *i32,
1789 flags: u32,
1790) NameToFileHandleAtError!void {
1791 switch (errno(system.name_to_handle_at(dirfd, pathname_z, handle, mount_id, flags))) {
1792 .SUCCESS => {},
1793 .FAULT => unreachable, // pathname, mount_id, or handle outside accessible address space
1794 .INVAL => unreachable, // bad flags, or handle_bytes too big
1795 .NOENT => return error.FileNotFound,
1796 .NOTDIR => return error.NotDir,
1797 .OPNOTSUPP => return error.OperationUnsupported,
1798 .OVERFLOW => return error.NameTooLong,
1799 else => |err| return unexpectedErrno(err),
1800 }
1801}
1802
1803pub const IoCtl_SIOCGIFINDEX_Error = error{1710pub const IoCtl_SIOCGIFINDEX_Error = error{
1804 FileSystem,1711 FileSystem,
1805 InterfaceNotFound,1712 InterfaceNotFound,