1const Watch = @This();
2const builtin = @import("builtin");
3
4const std = @import("std");
5const Io = std.Io;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const fatal = std.process.fatal;
9const Configuration = std.Build.Configuration;
10
11const FsEvents = @import("Watch/FsEvents.zig");
12const Step = @import("Step.zig");
13const Maker = @import("../Maker.zig");
14
15os: Os,
16/// The number to show as the number of directories being watched.
17dir_count: usize,
18// These fields are common to most implementations so are kept here for simplicity.
19// They are `undefined` on implementations which do not utilize then.
20dir_table: DirTable,
21generation: Generation,
22maker: *Maker,
23
24pub const have_impl = Os != void;
25
26/// Key is the directory to watch which contains one or more files we are
27/// interested in noticing changes to.
28///
29/// Value is generation.
30const DirTable = std.array_hash_map.Custom(Cache.Path, void, Cache.Path.TableAdapter, false);
31
32/// Special key of "." means any changes in this directory trigger the steps.
33const ReactionSet = std.array_hash_map.String(StepSet);
34const StepSet = std.array_hash_map.Auto(Configuration.Step.Index, Generation);
35
36const Generation = u8;
37
38const Hash = std.hash.Wyhash;
39const Cache = std.Build.Cache;
40
41const Os = switch (builtin.os.tag) {
42 .linux => struct {
43 const posix = std.posix;
44
45 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
46 handle_table: HandleTable,
47 /// fanotify file descriptors are keyed by mount id since marks
48 /// are limited to a single filesystem.
49 poll_fds: std.array_hash_map.Auto(MountId, posix.pollfd),
50
51 const MountId = i32;
52 const HandleTable = std.array_hash_map.Custom(FileHandle, struct {
53 mount_id: MountId,
54 reaction_set: ReactionSet,
55 }, FileHandle.Adapter, false);
56
57 const fan_mask: std.os.linux.fanotify.MarkMask = .{
58 .CLOSE_WRITE = true,
59 .CREATE = true,
60 .DELETE = true,
61 .DELETE_SELF = true,
62 .EVENT_ON_CHILD = true,
63 .MOVED_FROM = true,
64 .MOVED_TO = true,
65 .MOVE_SELF = true,
66 .ONDIR = true,
67 };
68
69 const FileHandle = struct {
70 handle: *align(1) std.os.linux.file_handle,
71
72 fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
73 const bytes = lfh.slice();
74 const new_ptr = try gpa.alignedAlloc(
75 u8,
76 .of(std.os.linux.file_handle),
77 @sizeOf(std.os.linux.file_handle) + bytes.len,
78 );
79 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
80 new_header.* = lfh.handle.*;
81 const new: FileHandle = .{ .handle = new_header };
82 @memcpy(new.slice(), lfh.slice());
83 return new;
84 }
85
86 fn destroy(lfh: FileHandle, gpa: Allocator) void {
87 const ptr: [*]align(@alignOf(std.os.linux.file_handle)) u8 = @ptrCast(@alignCast(lfh.handle));
88 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
89 return gpa.free(allocated_slice);
90 }
91
92 fn slice(lfh: FileHandle) []u8 {
93 const ptr: [*]u8 = &lfh.handle.f_handle;
94 return ptr[0..lfh.handle.handle_bytes];
95 }
96
97 const Adapter = struct {
98 pub fn hash(self: Adapter, a: FileHandle) u32 {
99 _ = self;
100 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
101 return @truncate(Hash.hash(unsigned_type, a.slice()));
102 }
103 pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
104 _ = self;
105 _ = b_index;
106 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
107 }
108 };
109 };
110
111 fn init(maker: *Maker) !Watch {
112 return .{
113 .dir_table = .{},
114 .dir_count = 0,
115 .os = switch (builtin.os.tag) {
116 .linux => .{
117 .handle_table = .{},
118 .poll_fds = .{},
119 },
120 else => {},
121 },
122 .generation = 0,
123 .maker = maker,
124 };
125 }
126
127 fn deinit(w: *Watch) void {
128 const gpa = w.maker.gpa;
129
130 for (w.os.handle_table.keys(), w.os.handle_table.values()) |fh, *reaction| {
131 fh.destroy(gpa);
132 reaction.reaction_set.deinit(gpa);
133 }
134 w.os.handle_table.deinit(gpa);
135
136 for (w.os.poll_fds.values()) |pollfd| {
137 Io.Threaded.closeFd(pollfd.fd);
138 }
139 w.os.poll_fds.deinit(gpa);
140
141 w.dir_table.deinit(gpa);
142 w.* = undefined;
143 }
144
145 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
146 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
147 var buf: [std.fs.max_path_bytes]u8 = undefined;
148 const adjusted_path = if (path.sub_path.len == 0) "./" else std.mem.print(&buf, "{s}/", .{
149 path.sub_path,
150 }) catch return error.NameTooLong;
151 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
152 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
153 try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
154 const stack_lfh: FileHandle = .{ .handle = stack_ptr };
155 return stack_lfh.clone(gpa);
156 }
157
158 fn markDirtySteps(w: *Watch, fan_fd: posix.fd_t) !bool {
159 const maker = w.maker;
160 const fanotify = std.os.linux.fanotify;
161 const M = fanotify.event_metadata;
162 var events_buf: [256 + 4096]u8 = undefined;
163 var any_dirty = false;
164 while (true) {
165 var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
166 error.WouldBlock => return any_dirty,
167 else => |e| return e,
168 };
169 var meta: [*]align(1) M = @ptrCast(&events_buf);
170 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
171 len -= meta[0].event_len;
172 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
173 }) {
174 assert(meta[0].vers == M.VERSION);
175 if (meta[0].mask.Q_OVERFLOW) {
176 std.log.warn("file system watch queue overflowed; reconfiguring", .{});
177 return error.MustReconfigure;
178 }
179 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
180 switch (fid.hdr.info_type) {
181 .DFID_NAME => {
182 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
183 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
184 const file_name = std.mem.span(file_name_z);
185 const lfh: FileHandle = .{ .handle = file_handle };
186 if (w.os.handle_table.getPtr(lfh)) |value| {
187 if (value.reaction_set.getPtr(".")) |glob_set|
188 any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
189 if (value.reaction_set.getPtr(file_name)) |step_set|
190 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
191 }
192 },
193 else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}),
194 }
195 }
196 }
197 }
198
199 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
200 const maker = w.maker;
201 const gpa = maker.gpa;
202
203 // Add missing marks and note persisted ones.
204 for (steps) |step_index| {
205 const step = maker.stepByIndex(step_index);
206 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
207 const reaction_set = rs: {
208 const gop = try w.dir_table.getOrPut(gpa, path);
209 if (!gop.found_existing) {
210 var mount_id: MountId = undefined;
211 const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
212 error.FileNotFound => {
213 std.debug.assert(w.dir_table.swapRemove(path));
214 continue;
215 },
216 else => return err,
217 };
218 const fan_fd = blk: {
219 const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
220 if (!fd_gop.found_existing) {
221 const fan_fd = std.posix.fanotify_init(.{
222 .CLASS = .NOTIF,
223 .CLOEXEC = true,
224 .NONBLOCK = true,
225 .REPORT_NAME = true,
226 .REPORT_DIR_FID = true,
227 .REPORT_FID = true,
228 .REPORT_TARGET_FID = true,
229 }, 0) catch |err| switch (err) {
230 error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
231 else => |e| return e,
232 };
233 fd_gop.value_ptr.* = .{
234 .fd = fan_fd,
235 .events = std.posix.POLL.IN,
236 };
237 }
238 break :blk fd_gop.value_ptr.*.fd;
239 };
240 // `dir_handle` may already be present in the table in
241 // the case that we have multiple Cache.Path instances
242 // that compare inequal but ultimately point to the same
243 // directory on the file system.
244 // In such case, we must revert adding this directory, but keep
245 // the additions to the step set.
246 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
247 if (dh_gop.found_existing) {
248 _ = w.dir_table.pop();
249 } else {
250 assert(dh_gop.index == gop.index);
251 dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
252 posix.fanotify_mark(fan_fd, .{
253 .ADD = true,
254 .ONLYDIR = true,
255 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err|
256 fatal("unable to watch {f}: {t}", .{ path, err });
257 }
258 break :rs &dh_gop.value_ptr.reaction_set;
259 }
260 break :rs &w.os.handle_table.values()[gop.index].reaction_set;
261 };
262 for (files.items) |basename| {
263 const gop = try reaction_set.getOrPut(gpa, basename);
264 if (!gop.found_existing) gop.value_ptr.* = .{};
265 try gop.value_ptr.put(gpa, step_index, w.generation);
266 }
267 }
268 }
269
270 {
271 // Remove marks for files that are no longer inputs.
272 var i: usize = 0;
273 while (i < w.os.handle_table.entries.len) {
274 {
275 const reaction_set = &w.os.handle_table.values()[i].reaction_set;
276 var step_set_i: usize = 0;
277 while (step_set_i < reaction_set.entries.len) {
278 const step_set = &reaction_set.values()[step_set_i];
279 var dirent_i: usize = 0;
280 while (dirent_i < step_set.entries.len) {
281 const generations = step_set.values();
282 if (generations[dirent_i] == w.generation) {
283 dirent_i += 1;
284 continue;
285 }
286 step_set.swapRemoveAt(dirent_i);
287 }
288 if (step_set.entries.len > 0) {
289 step_set_i += 1;
290 continue;
291 }
292 reaction_set.swapRemoveAt(step_set_i);
293 }
294 if (reaction_set.entries.len > 0) {
295 i += 1;
296 continue;
297 }
298 }
299
300 const path = w.dir_table.keys()[i];
301
302 const mount_id = w.os.handle_table.values()[i].mount_id;
303 const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
304 posix.fanotify_mark(fan_fd, .{
305 .REMOVE = true,
306 .ONLYDIR = true,
307 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) {
308 error.FileNotFound => {}, // Expected, harmless.
309 else => |e| std.log.warn("unable to unwatch {f}: {t}", .{ path, e }),
310 };
311
312 w.dir_table.swapRemoveAt(i);
313 w.os.handle_table.swapRemoveAt(i);
314 }
315 w.generation +%= 1;
316 }
317 w.dir_count = w.dir_table.count();
318 }
319
320 fn wait(w: *Watch, timeout: Timeout) !WaitResult {
321 const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
322 if (events_len == 0)
323 return .timeout;
324 for (w.os.poll_fds.values()) |poll_fd| {
325 if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and
326 try markDirtySteps(w, poll_fd.fd))
327 {
328 return .dirty;
329 }
330 }
331 return .clean;
332 }
333 },
334 .windows => struct {
335 const windows = std.os.windows;
336
337 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
338 handle_table: std.array_hash_map.Custom(*Directory, void, Directory.TableAdapter, false),
339 ready_dirs: std.DoublyLinkedList,
340
341 const FileId = struct {
342 volumeSerialNumber: windows.ULONG,
343 indexNumber: windows.LARGE_INTEGER,
344 };
345
346 const Directory = struct {
347 reaction_set: ReactionSet,
348 id: FileId,
349 file: Io.File,
350 state: enum { idle, listening, ready },
351 iosb: windows.IO_STATUS_BLOCK,
352 // 64 KB is the packet size limit when monitoring over a network.
353 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
354 buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)),
355 ready_node: std.DoublyLinkedList.Node,
356
357 /// Start listening for events, buffer field will be overwritten eventually.
358 fn startListening(dir: *Directory, w: *Watch) !void {
359 assert(dir.file.flags.nonblocking);
360 assert(dir.state == .idle);
361 switch (windows.ntdll.NtNotifyChangeDirectoryFileEx(
362 dir.file.handle,
363 null,
364 &notifyApc,
365 w,
366 &dir.iosb,
367 &dir.buffer,
368 dir.buffer.len,
369 .{
370 .FILE_NAME = true,
371 .DIR_NAME = true,
372 .SIZE = true,
373 .LAST_WRITE = true,
374 .CREATION = true,
375 },
376 .FALSE,
377 .Notify,
378 )) {
379 .SUCCESS, .PENDING => dir.state = .listening,
380 .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported,
381 else => |status| return windows.unexpectedStatus(status),
382 }
383 }
384
385 fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(Io.Threaded.apc_align) callconv(.winapi) void {
386 const w: *Watch = @ptrCast(@alignCast(apc_context));
387 const dir: *Directory = @fieldParentPtr("iosb", iosb);
388 assert(iosb.u.Status != .PENDING);
389 assert(dir.state == .listening);
390 w.os.ready_dirs.append(&dir.ready_node);
391 dir.state = .ready;
392 }
393
394 fn init(gpa: Allocator, path: Cache.Path) !*Directory {
395 // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW)
396 // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
397 var dir_handle: windows.HANDLE = undefined;
398 const root_fd = path.root_dir.handle.handle;
399 const sub_path = path.subPathOrDot();
400 const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call
401 var iosb: windows.IO_STATUS_BLOCK = undefined;
402 switch (windows.ntdll.NtCreateFile(
403 &dir_handle,
404 .{
405 .SPECIFIC = .{ .FILE_DIRECTORY = .{
406 .LIST = true,
407 } },
408 .STANDARD = .{ .SYNCHRONIZE = true },
409 .GENERIC = .{ .READ = true },
410 },
411 &.{
412 .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd,
413 .ObjectName = @constCast(&sub_path_w.string()),
414 },
415 &iosb,
416 null,
417 .{},
418 .VALID_FLAGS,
419 .OPEN,
420 .{
421 .DIRECTORY_FILE = true,
422 .IO = .ASYNCHRONOUS,
423 .OPEN_FOR_BACKUP_INTENT = true,
424 },
425 null,
426 0,
427 )) {
428 .SUCCESS => {},
429 .OBJECT_NAME_INVALID => return error.BadPathName,
430 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
431 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
432 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
433 .NOT_A_DIRECTORY => return error.NotDir,
434 // This can happen if the directory has 'List folder contents' permission set to 'Deny'
435 .ACCESS_DENIED => return error.AccessDenied,
436 .INVALID_PARAMETER => unreachable,
437 else => |rc| return windows.unexpectedStatus(rc),
438 }
439 assert(dir_handle != windows.INVALID_HANDLE_VALUE);
440 errdefer windows.CloseHandle(dir_handle);
441
442 const dir_id = try getFileId(dir_handle);
443
444 const dir = try gpa.create(Directory);
445 dir.* = .{
446 .reaction_set = .empty,
447 .id = dir_id,
448 .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } },
449 .state = .idle,
450 .iosb = undefined,
451 .buffer = undefined,
452 .ready_node = undefined,
453 };
454 return dir;
455 }
456
457 fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void {
458 state: switch (dir.state) {
459 .idle => {},
460 .listening => {
461 var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
462 _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb);
463 while (switch (dir.state) {
464 .idle => unreachable,
465 .listening => true,
466 .ready => false,
467 }) Io.Threaded.waitForApcOrAlert();
468 continue :state .ready;
469 },
470 .ready => w.os.ready_dirs.remove(&dir.ready_node),
471 }
472 windows.CloseHandle(dir.file.handle);
473 gpa.destroy(dir);
474 }
475
476 /// Useful to make `*Directory` a key in `std.ArrayHashMap`.
477 const TableAdapter = struct {
478 pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 {
479 return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber)));
480 }
481 pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool {
482 _ = rhs_index;
483 return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and
484 lhs_dir.id.indexNumber == rhs_dir.id.indexNumber;
485 }
486 };
487 };
488
489 fn init(maker: *Maker) !Watch {
490 return .{
491 .dir_table = .{},
492 .dir_count = 0,
493 .os = switch (builtin.os.tag) {
494 .windows => .{
495 .handle_table = .empty,
496 .ready_dirs = .{},
497 },
498 else => {},
499 },
500 .generation = 0,
501 .maker = maker,
502 };
503 }
504
505 fn deinit(w: *Watch) void {
506 const gpa = w.maker.gpa;
507
508 for (w.os.handle_table.keys()) |dir| {
509 dir.deinit(gpa, w);
510 }
511 w.os.handle_table.deinit(gpa);
512
513 w.dir_table.deinit(gpa);
514 w.* = undefined;
515 }
516
517 fn getFileId(handle: windows.HANDLE) !FileId {
518 var file_id: FileId = undefined;
519 var io_status: windows.IO_STATUS_BLOCK = undefined;
520 var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined;
521 switch (windows.ntdll.NtQueryVolumeInformationFile(
522 handle,
523 &io_status,
524 &volume_info,
525 @sizeOf(windows.FILE.FS_VOLUME_INFORMATION),
526 .Volume,
527 )) {
528 .SUCCESS => {},
529 // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
530 // size provided. This is treated as success because the type of variable-length information that this would be relevant for
531 // (name, volume name, etc) we don't care about.
532 .BUFFER_OVERFLOW => {},
533 else => |rc| return windows.unexpectedStatus(rc),
534 }
535 file_id.volumeSerialNumber = volume_info.VolumeSerialNumber;
536 var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined;
537 switch (windows.ntdll.NtQueryInformationFile(
538 handle,
539 &io_status,
540 &internal_info,
541 @sizeOf(windows.FILE.INTERNAL_INFORMATION),
542 .Internal,
543 )) {
544 .SUCCESS => {},
545 else => |rc| return windows.unexpectedStatus(rc),
546 }
547 file_id.indexNumber = internal_info.IndexNumber;
548 return file_id;
549 }
550
551 fn markDirtySteps(w: *Watch, dir: *Directory) !bool {
552 const maker = w.maker;
553
554 var any_dirty = false;
555 const bytes_returned = dir.iosb.Information;
556 if (bytes_returned == 0) {
557 std.log.warn("file system watch queue overflowed; reconfiguring", .{});
558 return error.MustReconfigure;
559 }
560 var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
561 var offset: usize = 0;
562 while (true) {
563 const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
564 const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
565 if (dir.reaction_set.getPtr(".")) |glob_set|
566 any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
567 if (dir.reaction_set.getPtr(file_name)) |step_set|
568 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
569 if (notify.NextEntryOffset == 0)
570 break;
571
572 offset += notify.NextEntryOffset;
573 }
574
575 // We call this now since at this point we have finished reading dir.buffer.
576 try dir.startListening(w);
577 return any_dirty;
578 }
579
580 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
581 const maker = w.maker;
582 const gpa = maker.gpa;
583 // Add missing marks and note persisted ones.
584 for (steps) |step_index| {
585 const step = maker.stepByIndex(step_index);
586 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
587 const dir = dir: {
588 const gop = try w.dir_table.getOrPut(gpa, path);
589 if (!gop.found_existing) {
590 const dir: *Directory = try .init(gpa, path);
591 errdefer dir.deinit(gpa, w);
592 // `dir.id` may already be present in the table in
593 // the case that we have multiple Cache.Path instances
594 // that compare inequal but ultimately point to the same
595 // directory on the file system.
596 // In such case, we must revert adding this directory, but keep
597 // the additions to the step set.
598 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir);
599 if (dh_gop.found_existing) {
600 dir.deinit(gpa, w);
601 _ = w.dir_table.pop();
602 break :dir w.os.handle_table.keys()[dh_gop.index];
603 } else {
604 assert(dh_gop.index == gop.index);
605 try dir.startListening(w);
606 break :dir dir;
607 }
608 }
609 break :dir w.os.handle_table.keys()[gop.index];
610 };
611 for (files.items) |basename| {
612 const gop = try dir.reaction_set.getOrPut(gpa, basename);
613 if (!gop.found_existing) gop.value_ptr.* = .{};
614 try gop.value_ptr.put(gpa, step_index, w.generation);
615 }
616 }
617 }
618
619 {
620 // Remove marks for files that are no longer inputs.
621 var i: usize = 0;
622 while (i < w.os.handle_table.entries.len) {
623 const dir = w.os.handle_table.keys()[i];
624 {
625 var step_set_i: usize = 0;
626 while (step_set_i < dir.reaction_set.entries.len) {
627 const step_set = &dir.reaction_set.values()[step_set_i];
628 var dirent_i: usize = 0;
629 while (dirent_i < step_set.entries.len) {
630 const generations = step_set.values();
631 if (generations[dirent_i] == w.generation) {
632 dirent_i += 1;
633 continue;
634 }
635 step_set.swapRemoveAt(dirent_i);
636 }
637 if (step_set.entries.len > 0) {
638 step_set_i += 1;
639 continue;
640 }
641 dir.reaction_set.swapRemoveAt(step_set_i);
642 }
643 if (dir.reaction_set.entries.len > 0) {
644 i += 1;
645 continue;
646 }
647 }
648
649 w.dir_table.swapRemoveAt(i);
650 w.os.handle_table.swapRemoveAt(i);
651 dir.deinit(gpa, w);
652 }
653 w.generation +%= 1;
654 }
655 w.dir_count = w.dir_table.count();
656 }
657
658 fn wait(w: *Watch, timeout: Timeout) !WaitResult {
659 const maker = w.maker;
660 const io = maker.graph.io;
661
662 for (0..2) |attempt| {
663 while (w.os.ready_dirs.popFirst()) |ready_node| {
664 const dir: *Directory = @fieldParentPtr("ready_node", ready_node);
665 assert(dir.state == .ready);
666 dir.state = .idle;
667 switch (dir.iosb.u.Status) {
668 .SUCCESS => return if (try markDirtySteps(w, dir)) .dirty else .clean,
669 .PENDING => unreachable,
670 .CANCELLED => {},
671 else => |status| return windows.unexpectedStatus(status),
672 }
673 try dir.startListening(w);
674 }
675 try io.checkCancel();
676 if (attempt == 1) return .timeout;
677 const delay_interval: windows.LARGE_INTEGER = switch (timeout) {
678 .none => std.math.minInt(windows.LARGE_INTEGER),
679 .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100),
680 };
681 _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval);
682 } else unreachable;
683 }
684 },
685 .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct {
686 const posix = std.posix;
687
688 kq_fd: i32,
689 /// Indexes correspond 1:1 with `dir_table`.
690 handles: std.MultiArrayList(struct {
691 rs: ReactionSet,
692 /// If the corresponding dir_table Path has sub_path == "", then it
693 /// suffices as the open directory handle, and this value will be
694 /// -1. Otherwise, it needs to be opened in update(), and will be
695 /// stored here.
696 dir_fd: i32,
697 }),
698
699 const dir_open_flags: posix.O = f: {
700 var f: posix.O = .{
701 .ACCMODE = .RDONLY,
702 .NOFOLLOW = false,
703 .DIRECTORY = true,
704 .CLOEXEC = true,
705 };
706 if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
707 if (@hasField(posix.O, "PATH")) f.PATH = true;
708 break :f f;
709 };
710
711 const EV = std.c.EV;
712 const NOTE = std.c.NOTE;
713
714 fn init(maker: *Maker) !Watch {
715 return .{
716 .dir_table = .{},
717 .dir_count = 0,
718 .os = .{
719 .kq_fd = try Io.Kqueue.createFileDescriptor(),
720 .handles = .empty,
721 },
722 .generation = 0,
723 .maker = maker,
724 };
725 }
726
727 fn deinit(w: *Watch) void {
728 const gpa = w.maker.gpa;
729
730 for (w.os.handles.items(.rs), w.os.handles.items(.dir_fd)) |*rs, dir_fd| {
731 rs.deinit(gpa);
732 Io.Threaded.closeFd(dir_fd);
733 }
734 w.os.handles.deinit(gpa);
735
736 Io.Threaded.closeFd(w.os.kq_fd);
737
738 w.dir_table.deinit(gpa);
739 w.* = undefined;
740 }
741
742 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
743 const maker = w.maker;
744 const gpa = maker.gpa;
745 const handles = &w.os.handles;
746 for (steps) |step_index| {
747 const step = maker.stepByIndex(step_index);
748 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
749 const reaction_set = rs: {
750 const gop = try w.dir_table.getOrPut(gpa, path);
751 if (!gop.found_existing) {
752 const skip_open_dir = path.sub_path.len == 0;
753 const dir_fd = if (skip_open_dir)
754 path.root_dir.handle.handle
755 else
756 posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| {
757 fatal("failed to open directory {f}: {t}", .{ path, err });
758 };
759 // Empirically the dir has to stay open or else no events are triggered.
760 errdefer if (!skip_open_dir) Io.Threaded.closeFd(dir_fd);
761 const changes = [1]posix.Kevent{.{
762 .ident = @bitCast(@as(isize, dir_fd)),
763 .filter = std.c.EVFILT.VNODE,
764 .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
765 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
766 .data = 0,
767 .udata = gop.index,
768 }};
769 _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null);
770 assert(handles.len == gop.index);
771 try handles.append(gpa, .{
772 .rs = .{},
773 .dir_fd = if (skip_open_dir) -1 else dir_fd,
774 });
775 }
776
777 break :rs &handles.items(.rs)[gop.index];
778 };
779 for (files.items) |basename| {
780 const gop = try reaction_set.getOrPut(gpa, basename);
781 if (!gop.found_existing) gop.value_ptr.* = .{};
782 try gop.value_ptr.put(gpa, step_index, w.generation);
783 }
784 }
785 }
786
787 {
788 // Remove marks for files that are no longer inputs.
789 var i: usize = 0;
790 while (i < handles.len) {
791 {
792 const reaction_set = &handles.items(.rs)[i];
793 var step_set_i: usize = 0;
794 while (step_set_i < reaction_set.entries.len) {
795 const step_set = &reaction_set.values()[step_set_i];
796 var dirent_i: usize = 0;
797 while (dirent_i < step_set.entries.len) {
798 const generations = step_set.values();
799 if (generations[dirent_i] == w.generation) {
800 dirent_i += 1;
801 continue;
802 }
803 step_set.swapRemoveAt(dirent_i);
804 }
805 if (step_set.entries.len > 0) {
806 step_set_i += 1;
807 continue;
808 }
809 reaction_set.swapRemoveAt(step_set_i);
810 }
811 if (reaction_set.entries.len > 0) {
812 i += 1;
813 continue;
814 }
815 }
816
817 // If the sub_path == "" then this patch has already the
818 // dir fd that we need to use as the ident to remove the
819 // event. If it was opened above with openat() then we need
820 // to access that data via the dir_fd field.
821 const path = w.dir_table.keys()[i];
822 const dir_fd = if (path.sub_path.len == 0)
823 path.root_dir.handle.handle
824 else
825 handles.items(.dir_fd)[i];
826 assert(dir_fd != -1);
827
828 // The changelist also needs to update the udata field of the last
829 // event, since we are doing a swap remove, and we store the dir_table
830 // index in the udata field.
831 const last_dir_fd = fd: {
832 const last_path = w.dir_table.keys()[handles.len - 1];
833 const last_dir_fd = if (last_path.sub_path.len == 0)
834 last_path.root_dir.handle.handle
835 else
836 handles.items(.dir_fd)[handles.len - 1];
837 assert(last_dir_fd != -1);
838 break :fd last_dir_fd;
839 };
840 const changes = [_]posix.Kevent{
841 .{
842 .ident = @bitCast(@as(isize, dir_fd)),
843 .filter = std.c.EVFILT.VNODE,
844 .flags = EV.DELETE,
845 .fflags = 0,
846 .data = 0,
847 .udata = i,
848 },
849 .{
850 .ident = @bitCast(@as(isize, last_dir_fd)),
851 .filter = std.c.EVFILT.VNODE,
852 .flags = EV.ADD,
853 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
854 .data = 0,
855 .udata = i,
856 },
857 };
858 const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
859 _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
860 if (path.sub_path.len != 0) Io.Threaded.closeFd(dir_fd);
861
862 w.dir_table.swapRemoveAt(i);
863 handles.swapRemove(i);
864 }
865 w.generation +%= 1;
866 }
867 w.dir_count = w.dir_table.count();
868 }
869
870 fn wait(w: *Watch, timeout: Timeout) !WaitResult {
871 const maker = w.maker;
872 var timespec_buffer: posix.timespec = undefined;
873 var event_buffer: [100]posix.Kevent = undefined;
874 var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer));
875 if (n == 0) return .timeout;
876 const reaction_sets = w.os.handles.items(.rs);
877 var any_dirty = try markDirtySteps(maker, reaction_sets, event_buffer[0..n], false);
878 timespec_buffer = .{ .sec = 0, .nsec = 0 };
879 while (n == event_buffer.len) {
880 n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer);
881 if (n == 0) break;
882 any_dirty = try markDirtySteps(maker, reaction_sets, event_buffer[0..n], any_dirty);
883 }
884 return if (any_dirty) .dirty else .clean;
885 }
886
887 fn markDirtySteps(
888 maker: *Maker,
889 reaction_sets: []ReactionSet,
890 events: []const std.c.Kevent,
891 start_any_dirty: bool,
892 ) !bool {
893 var any_dirty = start_any_dirty;
894 for (events) |event| {
895 const index: usize = @intCast(event.udata);
896 const reaction_set = &reaction_sets[index];
897 // If we knew the basename of the changed file, here we would
898 // mark only the step set dirty, and possibly the glob set:
899 //if (reaction_set.getPtr(".")) |glob_set|
900 // any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
901 //if (reaction_set.getPtr(file_name)) |step_set|
902 // any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
903 // However we don't know the file name so just mark all the
904 // sets dirty for this directory.
905 for (reaction_set.values()) |*step_set| {
906 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
907 }
908 }
909 return any_dirty;
910 }
911 },
912 .macos => struct {
913 fse: FsEvents,
914
915 fn init(maker: *Maker) !Watch {
916 return .{
917 .os = .{ .fse = try .init(maker.graph.cache.cwd) },
918 .dir_count = 0,
919 .dir_table = undefined,
920 .generation = undefined,
921 .maker = maker,
922 };
923 }
924 fn deinit(w: *Watch) void {
925 const gpa = w.maker.gpa;
926 const io = w.maker.graph.io;
927 w.os.fse.deinit(gpa, io);
928 w.* = undefined;
929 }
930 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
931 try w.os.fse.setPaths(w.maker, steps);
932 w.dir_count = w.os.fse.watch_roots.len;
933 }
934 fn wait(w: *Watch, timeout: Timeout) !WaitResult {
935 return w.os.fse.wait(w.maker, switch (timeout) {
936 .none => null,
937 .ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
938 });
939 }
940 },
941 else => void,
942};
943
944pub fn init(maker: *Maker) !Watch {
945 return Os.init(maker);
946}
947
948pub const Match = struct {
949 /// Relative to the watched directory, the file path that triggers this
950 /// match.
951 basename: []const u8,
952 /// The step to re-run when file corresponding to `basename` is changed.
953 step_index: Configuration.Step.Index,
954
955 pub const Context = struct {
956 pub fn hash(self: Context, a: Match) u32 {
957 _ = self;
958 var hasher = Hash.init(@backingInt(a.step_index));
959 hasher.update(a.basename);
960 return @truncate(hasher.final());
961 }
962 pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
963 _ = self;
964 _ = b_index;
965 return a.step_index == b.step_index and std.mem.eql(u8, a.basename, b.basename);
966 }
967 };
968};
969
970fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) error{MustReconfigure}!bool {
971 var this_any_dirty = false;
972 for (step_set.keys()) |step_index| {
973 const step = maker.stepByIndex(step_index);
974 if (try maker.invalidateResult(step)) this_any_dirty = true;
975 }
976 return any_dirty or this_any_dirty;
977}
978
979pub fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
980 return Os.update(w, steps);
981}
982
983pub const Timeout = union(enum) {
984 none,
985 ms: u16,
986
987 pub fn to_i32_ms(t: Timeout) i32 {
988 return switch (t) {
989 .none => -1,
990 .ms => |ms| ms,
991 };
992 }
993
994 pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
995 return switch (t) {
996 .none => null,
997 .ms => |ms_u16| {
998 const ms: isize = ms_u16;
999 buf.* = .{
1000 .sec = @divTrunc(ms, std.time.ms_per_s),
1001 .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
1002 };
1003 return buf;
1004 },
1005 };
1006 }
1007};
1008
1009pub const WaitResult = enum {
1010 timeout,
1011 /// File system watching triggered on files that were marked as inputs to at least one Step.
1012 /// Relevant steps have been marked dirty.
1013 dirty,
1014 /// File system watching triggered but none of the events were relevant to
1015 /// what we are listening to. There is nothing to do.
1016 clean,
1017};
1018
1019/// May return `error.MustReconfigure`.
1020pub fn wait(w: *Watch, timeout: Timeout) !WaitResult {
1021 return Os.wait(w, timeout);
1022}
1023
1024pub fn deinit(w: *Watch) void {
1025 Os.deinit(w);
1026}