| ... | @@ -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 | } |
| 2497 | | 2501 | |
| ... | @@ -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 | |
| | 17724 | const 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 | |
| | 17891 | fn 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 | |
| | 17903 | fn 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 | |
| | 17915 | fn 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. |
| | 17927 | fn 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 | } |