| ... | ... | @@ -2492,6 +2492,10 @@ fn operate(userdata: ?*anyopaque, operation: Io.Operation) Io.Cancelable!Io.Oper |
| 2492 | 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 | 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 | } |