| ... | ... | @@ -1,41 +1,21 @@ |
| 1 | const builtin = @import("builtin"); |
| 1 | 2 | const std = @import("../std.zig"); |
| 2 | 3 | const Watch = @This(); |
| 3 | 4 | const Step = std.Build.Step; |
| 4 | 5 | const Allocator = std.mem.Allocator; |
| 5 | 6 | const assert = std.debug.assert; |
| 7 | const fatal = std.zig.fatal; |
| 6 | 8 | |
| 7 | 9 | dir_table: DirTable, |
| 8 | | /// Keyed differently but indexes correspond 1:1 with `dir_table`. |
| 9 | | handle_table: HandleTable, |
| 10 | | fan_fd: std.posix.fd_t, |
| 10 | os: Os, |
| 11 | 11 | generation: Generation, |
| 12 | 12 | |
| 13 | | pub const fan_mask: std.os.linux.fanotify.MarkMask = .{ |
| 14 | | .CLOSE_WRITE = true, |
| 15 | | .CREATE = true, |
| 16 | | .DELETE = true, |
| 17 | | .DELETE_SELF = true, |
| 18 | | .EVENT_ON_CHILD = true, |
| 19 | | .MOVED_FROM = true, |
| 20 | | .MOVED_TO = true, |
| 21 | | .MOVE_SELF = true, |
| 22 | | .ONDIR = true, |
| 23 | | }; |
| 24 | | |
| 25 | | pub const init: Watch = .{ |
| 26 | | .dir_table = .{}, |
| 27 | | .handle_table = .{}, |
| 28 | | .fan_fd = -1, |
| 29 | | .generation = 0, |
| 30 | | }; |
| 31 | | |
| 32 | 13 | /// Key is the directory to watch which contains one or more files we are |
| 33 | 14 | /// interested in noticing changes to. |
| 34 | 15 | /// |
| 35 | 16 | /// Value is generation. |
| 36 | 17 | const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); |
| 37 | 18 | |
| 38 | | const HandleTable = std.ArrayHashMapUnmanaged(LinuxFileHandle, ReactionSet, LinuxFileHandle.Adapter, false); |
| 39 | 19 | /// Special key of "." means any changes in this directory trigger the steps. |
| 40 | 20 | const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); |
| 41 | 21 | const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation); |
| ... | ... | @@ -45,6 +25,255 @@ const Generation = u8; |
| 45 | 25 | const Hash = std.hash.Wyhash; |
| 46 | 26 | const Cache = std.Build.Cache; |
| 47 | 27 | |
| 28 | const Os = switch (builtin.os.tag) { |
| 29 | .linux => struct { |
| 30 | const posix = std.posix; |
| 31 | |
| 32 | /// Keyed differently but indexes correspond 1:1 with `dir_table`. |
| 33 | handle_table: HandleTable, |
| 34 | poll_fds: [1]posix.pollfd, |
| 35 | |
| 36 | const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, ReactionSet, FileHandle.Adapter, false); |
| 37 | |
| 38 | const fan_mask: std.os.linux.fanotify.MarkMask = .{ |
| 39 | .CLOSE_WRITE = true, |
| 40 | .CREATE = true, |
| 41 | .DELETE = true, |
| 42 | .DELETE_SELF = true, |
| 43 | .EVENT_ON_CHILD = true, |
| 44 | .MOVED_FROM = true, |
| 45 | .MOVED_TO = true, |
| 46 | .MOVE_SELF = true, |
| 47 | .ONDIR = true, |
| 48 | }; |
| 49 | |
| 50 | const FileHandle = struct { |
| 51 | handle: *align(1) std.os.linux.file_handle, |
| 52 | |
| 53 | fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { |
| 54 | const bytes = lfh.slice(); |
| 55 | const new_ptr = try gpa.alignedAlloc( |
| 56 | u8, |
| 57 | @alignOf(std.os.linux.file_handle), |
| 58 | @sizeOf(std.os.linux.file_handle) + bytes.len, |
| 59 | ); |
| 60 | const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); |
| 61 | new_header.* = lfh.handle.*; |
| 62 | const new: FileHandle = .{ .handle = new_header }; |
| 63 | @memcpy(new.slice(), lfh.slice()); |
| 64 | return new; |
| 65 | } |
| 66 | |
| 67 | fn destroy(lfh: FileHandle, gpa: Allocator) void { |
| 68 | const ptr: [*]u8 = @ptrCast(lfh.handle); |
| 69 | const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes]; |
| 70 | return gpa.free(allocated_slice); |
| 71 | } |
| 72 | |
| 73 | fn slice(lfh: FileHandle) []u8 { |
| 74 | const ptr: [*]u8 = &lfh.handle.f_handle; |
| 75 | return ptr[0..lfh.handle.handle_bytes]; |
| 76 | } |
| 77 | |
| 78 | const Adapter = struct { |
| 79 | pub fn hash(self: Adapter, a: FileHandle) u32 { |
| 80 | _ = self; |
| 81 | const unsigned_type: u32 = @bitCast(a.handle.handle_type); |
| 82 | return @truncate(Hash.hash(unsigned_type, a.slice())); |
| 83 | } |
| 84 | pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool { |
| 85 | _ = self; |
| 86 | _ = b_index; |
| 87 | return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); |
| 88 | } |
| 89 | }; |
| 90 | }; |
| 91 | |
| 92 | fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !FileHandle { |
| 93 | var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; |
| 94 | var mount_id: i32 = undefined; |
| 95 | var buf: [std.fs.max_path_bytes]u8 = undefined; |
| 96 | const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ |
| 97 | path.sub_path, |
| 98 | }) catch return error.NameTooLong; |
| 99 | const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); |
| 100 | stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); |
| 101 | try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID); |
| 102 | const stack_lfh: FileHandle = .{ .handle = stack_ptr }; |
| 103 | return stack_lfh.clone(gpa); |
| 104 | } |
| 105 | |
| 106 | fn markDirtySteps(w: *Watch, gpa: Allocator) !bool { |
| 107 | const fan_fd = w.os.getFanFd(); |
| 108 | const fanotify = std.os.linux.fanotify; |
| 109 | const M = fanotify.event_metadata; |
| 110 | var events_buf: [256 + 4096]u8 = undefined; |
| 111 | var any_dirty = false; |
| 112 | while (true) { |
| 113 | var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) { |
| 114 | error.WouldBlock => return any_dirty, |
| 115 | else => |e| return e, |
| 116 | }; |
| 117 | var meta: [*]align(1) M = @ptrCast(&events_buf); |
| 118 | while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ |
| 119 | len -= meta[0].event_len; |
| 120 | meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); |
| 121 | }) { |
| 122 | assert(meta[0].vers == M.VERSION); |
| 123 | if (meta[0].mask.Q_OVERFLOW) { |
| 124 | any_dirty = true; |
| 125 | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); |
| 126 | markAllFilesDirty(w, gpa); |
| 127 | return true; |
| 128 | } |
| 129 | const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); |
| 130 | switch (fid.hdr.info_type) { |
| 131 | .DFID_NAME => { |
| 132 | const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); |
| 133 | const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); |
| 134 | const file_name = std.mem.span(file_name_z); |
| 135 | const lfh: FileHandle = .{ .handle = file_handle }; |
| 136 | if (w.os.handle_table.getPtr(lfh)) |reaction_set| { |
| 137 | if (reaction_set.getPtr(".")) |glob_set| |
| 138 | any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); |
| 139 | if (reaction_set.getPtr(file_name)) |step_set| |
| 140 | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); |
| 141 | } |
| 142 | }, |
| 143 | else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}), |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | fn getFanFd(os: *const @This()) posix.fd_t { |
| 150 | return os.poll_fds[0].fd; |
| 151 | } |
| 152 | |
| 153 | fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { |
| 154 | const fan_fd = w.os.getFanFd(); |
| 155 | // Add missing marks and note persisted ones. |
| 156 | for (steps) |step| { |
| 157 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { |
| 158 | const reaction_set = rs: { |
| 159 | const gop = try w.dir_table.getOrPut(gpa, path); |
| 160 | if (!gop.found_existing) { |
| 161 | const dir_handle = try Os.getDirHandle(gpa, path); |
| 162 | // `dir_handle` may already be present in the table in |
| 163 | // the case that we have multiple Cache.Path instances |
| 164 | // that compare inequal but ultimately point to the same |
| 165 | // directory on the file system. |
| 166 | // In such case, we must revert adding this directory, but keep |
| 167 | // the additions to the step set. |
| 168 | const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle); |
| 169 | if (dh_gop.found_existing) { |
| 170 | _ = w.dir_table.pop(); |
| 171 | } else { |
| 172 | assert(dh_gop.index == gop.index); |
| 173 | dh_gop.value_ptr.* = .{}; |
| 174 | posix.fanotify_mark(fan_fd, .{ |
| 175 | .ADD = true, |
| 176 | .ONLYDIR = true, |
| 177 | }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| { |
| 178 | fatal("unable to watch {}: {s}", .{ path, @errorName(err) }); |
| 179 | }; |
| 180 | } |
| 181 | break :rs dh_gop.value_ptr; |
| 182 | } |
| 183 | break :rs &w.os.handle_table.values()[gop.index]; |
| 184 | }; |
| 185 | for (files.items) |basename| { |
| 186 | const gop = try reaction_set.getOrPut(gpa, basename); |
| 187 | if (!gop.found_existing) gop.value_ptr.* = .{}; |
| 188 | try gop.value_ptr.put(gpa, step, w.generation); |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | { |
| 194 | // Remove marks for files that are no longer inputs. |
| 195 | var i: usize = 0; |
| 196 | while (i < w.os.handle_table.entries.len) { |
| 197 | { |
| 198 | const reaction_set = &w.os.handle_table.values()[i]; |
| 199 | var step_set_i: usize = 0; |
| 200 | while (step_set_i < reaction_set.entries.len) { |
| 201 | const step_set = &reaction_set.values()[step_set_i]; |
| 202 | var dirent_i: usize = 0; |
| 203 | while (dirent_i < step_set.entries.len) { |
| 204 | const generations = step_set.values(); |
| 205 | if (generations[dirent_i] == w.generation) { |
| 206 | dirent_i += 1; |
| 207 | continue; |
| 208 | } |
| 209 | step_set.swapRemoveAt(dirent_i); |
| 210 | } |
| 211 | if (step_set.entries.len > 0) { |
| 212 | step_set_i += 1; |
| 213 | continue; |
| 214 | } |
| 215 | reaction_set.swapRemoveAt(step_set_i); |
| 216 | } |
| 217 | if (reaction_set.entries.len > 0) { |
| 218 | i += 1; |
| 219 | continue; |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | const path = w.dir_table.keys()[i]; |
| 224 | |
| 225 | posix.fanotify_mark(fan_fd, .{ |
| 226 | .REMOVE = true, |
| 227 | .ONLYDIR = true, |
| 228 | }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| switch (err) { |
| 229 | error.FileNotFound => {}, // Expected, harmless. |
| 230 | else => |e| std.log.warn("unable to unwatch '{}': {s}", .{ path, @errorName(e) }), |
| 231 | }; |
| 232 | |
| 233 | w.dir_table.swapRemoveAt(i); |
| 234 | w.os.handle_table.swapRemoveAt(i); |
| 235 | } |
| 236 | w.generation +%= 1; |
| 237 | } |
| 238 | } |
| 239 | }, |
| 240 | else => void, |
| 241 | }; |
| 242 | |
| 243 | pub fn init() !Watch { |
| 244 | switch (builtin.os.tag) { |
| 245 | .linux => { |
| 246 | const fan_fd = try std.posix.fanotify_init(.{ |
| 247 | .CLASS = .NOTIF, |
| 248 | .CLOEXEC = true, |
| 249 | .NONBLOCK = true, |
| 250 | .REPORT_NAME = true, |
| 251 | .REPORT_DIR_FID = true, |
| 252 | .REPORT_FID = true, |
| 253 | .REPORT_TARGET_FID = true, |
| 254 | }, 0); |
| 255 | return .{ |
| 256 | .dir_table = .{}, |
| 257 | .os = switch (builtin.os.tag) { |
| 258 | .linux => .{ |
| 259 | .handle_table = .{}, |
| 260 | .poll_fds = .{ |
| 261 | .{ |
| 262 | .fd = fan_fd, |
| 263 | .events = std.posix.POLL.IN, |
| 264 | .revents = undefined, |
| 265 | }, |
| 266 | }, |
| 267 | }, |
| 268 | else => {}, |
| 269 | }, |
| 270 | .generation = 0, |
| 271 | }; |
| 272 | }, |
| 273 | else => @panic("unimplemented"), |
| 274 | } |
| 275 | } |
| 276 | |
| 48 | 277 | pub const Match = struct { |
| 49 | 278 | /// Relative to the watched directory, the file path that triggers this |
| 50 | 279 | /// match. |
| ... | ... | @@ -68,119 +297,8 @@ pub const Match = struct { |
| 68 | 297 | }; |
| 69 | 298 | }; |
| 70 | 299 | |
| 71 | | pub const LinuxFileHandle = struct { |
| 72 | | handle: *align(1) std.os.linux.file_handle, |
| 73 | | |
| 74 | | pub fn clone(lfh: LinuxFileHandle, gpa: Allocator) Allocator.Error!LinuxFileHandle { |
| 75 | | const bytes = lfh.slice(); |
| 76 | | const new_ptr = try gpa.alignedAlloc( |
| 77 | | u8, |
| 78 | | @alignOf(std.os.linux.file_handle), |
| 79 | | @sizeOf(std.os.linux.file_handle) + bytes.len, |
| 80 | | ); |
| 81 | | const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); |
| 82 | | new_header.* = lfh.handle.*; |
| 83 | | const new: LinuxFileHandle = .{ .handle = new_header }; |
| 84 | | @memcpy(new.slice(), lfh.slice()); |
| 85 | | return new; |
| 86 | | } |
| 87 | | |
| 88 | | pub fn destroy(lfh: LinuxFileHandle, gpa: Allocator) void { |
| 89 | | const ptr: [*]u8 = @ptrCast(lfh.handle); |
| 90 | | const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes]; |
| 91 | | return gpa.free(allocated_slice); |
| 92 | | } |
| 93 | | |
| 94 | | pub fn slice(lfh: LinuxFileHandle) []u8 { |
| 95 | | const ptr: [*]u8 = &lfh.handle.f_handle; |
| 96 | | return ptr[0..lfh.handle.handle_bytes]; |
| 97 | | } |
| 98 | | |
| 99 | | pub const Adapter = struct { |
| 100 | | pub fn hash(self: Adapter, a: LinuxFileHandle) u32 { |
| 101 | | _ = self; |
| 102 | | const unsigned_type: u32 = @bitCast(a.handle.handle_type); |
| 103 | | return @truncate(Hash.hash(unsigned_type, a.slice())); |
| 104 | | } |
| 105 | | pub fn eql(self: Adapter, a: LinuxFileHandle, b: LinuxFileHandle, b_index: usize) bool { |
| 106 | | _ = self; |
| 107 | | _ = b_index; |
| 108 | | return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); |
| 109 | | } |
| 110 | | }; |
| 111 | | }; |
| 112 | | |
| 113 | | pub fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !LinuxFileHandle { |
| 114 | | var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; |
| 115 | | var mount_id: i32 = undefined; |
| 116 | | var buf: [std.fs.max_path_bytes]u8 = undefined; |
| 117 | | const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ |
| 118 | | path.sub_path, |
| 119 | | }) catch return error.NameTooLong; |
| 120 | | const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); |
| 121 | | stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); |
| 122 | | try std.posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID); |
| 123 | | const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr }; |
| 124 | | return stack_lfh.clone(gpa); |
| 125 | | } |
| 126 | | |
| 127 | | pub fn markDirtySteps(w: *Watch, gpa: Allocator) !bool { |
| 128 | | const fanotify = std.os.linux.fanotify; |
| 129 | | const M = fanotify.event_metadata; |
| 130 | | var events_buf: [256 + 4096]u8 = undefined; |
| 131 | | var any_dirty = false; |
| 132 | | while (true) { |
| 133 | | var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) { |
| 134 | | error.WouldBlock => return any_dirty, |
| 135 | | else => |e| return e, |
| 136 | | }; |
| 137 | | var meta: [*]align(1) M = @ptrCast(&events_buf); |
| 138 | | while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ |
| 139 | | len -= meta[0].event_len; |
| 140 | | meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); |
| 141 | | }) { |
| 142 | | assert(meta[0].vers == M.VERSION); |
| 143 | | if (meta[0].mask.Q_OVERFLOW) { |
| 144 | | any_dirty = true; |
| 145 | | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); |
| 146 | | markAllFilesDirty(w, gpa); |
| 147 | | return true; |
| 148 | | } |
| 149 | | const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); |
| 150 | | switch (fid.hdr.info_type) { |
| 151 | | .DFID_NAME => { |
| 152 | | const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); |
| 153 | | const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); |
| 154 | | const file_name = std.mem.span(file_name_z); |
| 155 | | const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle }; |
| 156 | | if (w.handle_table.getPtr(lfh)) |reaction_set| { |
| 157 | | if (reaction_set.getPtr(".")) |glob_set| |
| 158 | | any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); |
| 159 | | if (reaction_set.getPtr(file_name)) |step_set| |
| 160 | | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); |
| 161 | | } |
| 162 | | }, |
| 163 | | else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}), |
| 164 | | } |
| 165 | | } |
| 166 | | } |
| 167 | | } |
| 168 | | |
| 169 | | pub fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void { |
| 170 | | for (all_steps) |step| switch (step.state) { |
| 171 | | .dependency_failure, .failure, .skipped => step.recursiveReset(gpa), |
| 172 | | else => continue, |
| 173 | | }; |
| 174 | | // Now that all dirty steps have been found, the remaining steps that |
| 175 | | // succeeded from last run shall be marked "cached". |
| 176 | | for (all_steps) |step| switch (step.state) { |
| 177 | | .success => step.result_cached = true, |
| 178 | | else => continue, |
| 179 | | }; |
| 180 | | } |
| 181 | | |
| 182 | 300 | fn markAllFilesDirty(w: *Watch, gpa: Allocator) void { |
| 183 | | for (w.handle_table.values()) |reaction_set| { |
| 301 | for (w.os.handle_table.values()) |reaction_set| { |
| 184 | 302 | for (reaction_set.values()) |step_set| { |
| 185 | 303 | for (step_set.keys()) |step| { |
| 186 | 304 | step.recursiveReset(gpa); |
| ... | ... | @@ -199,3 +317,47 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool { |
| 199 | 317 | } |
| 200 | 318 | return any_dirty or this_any_dirty; |
| 201 | 319 | } |
| 320 | |
| 321 | pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { |
| 322 | switch (builtin.os.tag) { |
| 323 | .linux => return Os.update(w, gpa, steps), |
| 324 | else => @compileError("unimplemented"), |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | pub const Timeout = union(enum) { |
| 329 | none, |
| 330 | ms: u16, |
| 331 | |
| 332 | pub fn to_i32_ms(t: Timeout) i32 { |
| 333 | return switch (t) { |
| 334 | .none => -1, |
| 335 | .ms => |ms| ms, |
| 336 | }; |
| 337 | } |
| 338 | }; |
| 339 | |
| 340 | pub const WaitResult = enum { |
| 341 | timeout, |
| 342 | /// File system watching triggered on files that were marked as inputs to at least one Step. |
| 343 | /// Relevant steps have been marked dirty. |
| 344 | dirty, |
| 345 | /// File system watching triggered but none of the events were relevant to |
| 346 | /// what we are listening to. There is nothing to do. |
| 347 | clean, |
| 348 | }; |
| 349 | |
| 350 | pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult { |
| 351 | switch (builtin.os.tag) { |
| 352 | .linux => { |
| 353 | const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms()); |
| 354 | return if (events_len == 0) |
| 355 | .timeout |
| 356 | else if (try Os.markDirtySteps(w, gpa)) |
| 357 | .dirty |
| 358 | else |
| 359 | .clean; |
| 360 | }, |
| 361 | else => @compileError("unimplemented"), |
| 362 | } |
| 363 | } |