authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-10-24 16:24:56-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-10-24 16:24:56-07:00
log7a46ba73ceb9d5356ac0e1e0afc869b7abfa490c
tree7554bfe1ffc030d06c159bfce5b1929aab814491
parent56996a2809421a7dfbb74f7533d40faf6c1482e3

implement --watch for kqueue

it doesn't detect and remove no longer watched things yet it also isn't aware of any file names reported by kqueue. I'm unsure if that functionality exists.

2 files changed, 219 insertions(+), 50 deletions(-)

lib/compiler/build_runner.zig+1-4
...@@ -447,10 +447,7 @@ pub fn main() !void {...@@ -447,10 +447,7 @@ pub fn main() !void {
447447
448 if (!watch) return cleanExit();448 if (!watch) return cleanExit();
449449
450 switch (builtin.os.tag) {450 if (!Watch.have_impl) fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)});
451 .linux, .windows => {},
452 else => fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
453 }
454451
455 try w.update(gpa, run.step_stack.keys());452 try w.update(gpa, run.step_stack.keys());
456453
lib/std/Build/Watch.zig+218-46
...@@ -10,6 +10,8 @@ dir_table: DirTable,...@@ -10,6 +10,8 @@ dir_table: DirTable,
10os: Os,10os: Os,
11generation: Generation,11generation: Generation,
1212
13pub const have_impl = Os != void;
14
13/// Key is the directory to watch which contains one or more files we are15/// Key is the directory to watch which contains one or more files we are
14/// interested in noticing changes to.16/// interested in noticing changes to.
15///17///
...@@ -236,6 +238,16 @@ const Os = switch (builtin.os.tag) {...@@ -236,6 +238,16 @@ const Os = switch (builtin.os.tag) {
236 w.generation +%= 1;238 w.generation +%= 1;
237 }239 }
238 }240 }
241
242 fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
243 const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms());
244 return if (events_len == 0)
245 .timeout
246 else if (try Os.markDirtySteps(w, gpa))
247 .dirty
248 else
249 .clean;
250 }
239 },251 },
240 .windows => struct {252 .windows => struct {
241 const windows = std.os.windows;253 const windows = std.os.windows;
...@@ -509,6 +521,182 @@ const Os = switch (builtin.os.tag) {...@@ -509,6 +521,182 @@ const Os = switch (builtin.os.tag) {
509 w.generation +%= 1;521 w.generation +%= 1;
510 }522 }
511 }523 }
524
525 fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
526 var bytes_transferred: std.os.windows.DWORD = undefined;
527 var key: usize = undefined;
528 var overlapped_ptr: ?*std.os.windows.OVERLAPPED = undefined;
529 return while (true) switch (std.os.windows.GetQueuedCompletionStatus(
530 w.os.io_cp.?,
531 &bytes_transferred,
532 &key,
533 &overlapped_ptr,
534 @bitCast(timeout.to_i32_ms()),
535 )) {
536 .Normal => {
537 if (bytes_transferred == 0)
538 break error.Unexpected;
539
540 // This 'orelse' detects a race condition that happens when we receive a
541 // completion notification for a directory that no longer exists in our list.
542 const dir = w.os.dir_list.get(key) orelse break .clean;
543
544 break if (try Os.markDirtySteps(w, gpa, dir))
545 .dirty
546 else
547 .clean;
548 },
549 .Timeout => break .timeout,
550 // This status is issued because CancelIo was called, skip and try again.
551 .Cancelled => continue,
552 else => break error.Unexpected,
553 };
554 }
555 },
556 .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .macos, .tvos, .visionos, .watchos, .haiku => struct {
557 const posix = std.posix;
558
559 kq_fd: i32,
560 /// Indexes correspond 1:1 with `dir_table`.
561 reaction_sets: std.ArrayListUnmanaged(ReactionSet),
562
563 const dir_open_flags: posix.O = f: {
564 var f: posix.O = .{
565 .ACCMODE = .RDONLY,
566 .NOFOLLOW = false,
567 .DIRECTORY = true,
568 .CLOEXEC = true,
569 };
570 if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
571 if (@hasField(posix.O, "PATH")) f.PATH = true;
572 break :f f;
573 };
574
575 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
576 for (steps) |step| {
577 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
578 const reaction_set = rs: {
579 const gop = try w.dir_table.getOrPut(gpa, path);
580 if (!gop.found_existing) {
581 const dir_fd = if (path.sub_path.len == 0)
582 path.root_dir.handle.fd
583 else
584 posix.openat(path.root_dir.handle.fd, path.sub_path, dir_open_flags, 0) catch |err| {
585 fatal("failed to open directory {}: {s}", .{ path, @errorName(err) });
586 };
587 const EV = std.c.EV;
588 const NOTE = std.c.NOTE;
589 var changes = [1]posix.Kevent{.{
590 .ident = @bitCast(@as(isize, dir_fd)),
591 .filter = std.c.EVFILT.VNODE,
592 .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
593 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
594 .data = 0,
595 .udata = gop.index,
596 }};
597 _ = try posix.kevent(w.os.kq_fd, &changes, &.{}, null);
598 assert(w.os.reaction_sets.items.len == gop.index);
599 const reaction_set = try w.os.reaction_sets.addOne(gpa);
600 reaction_set.* = .{};
601 break :rs reaction_set;
602 }
603 break :rs &w.os.reaction_sets.items[gop.index];
604 };
605 for (files.items) |basename| {
606 const gop = try reaction_set.getOrPut(gpa, basename);
607 if (!gop.found_existing) gop.value_ptr.* = .{};
608 try gop.value_ptr.put(gpa, step, w.generation);
609 }
610 }
611 }
612
613 {
614 // Remove marks for files that are no longer inputs.
615 //var i: usize = 0;
616 //while (i < w.os.handle_table.entries.len) {
617 // {
618 // const reaction_set = &w.os.handle_table.values()[i];
619 // var step_set_i: usize = 0;
620 // while (step_set_i < reaction_set.entries.len) {
621 // const step_set = &reaction_set.values()[step_set_i];
622 // var dirent_i: usize = 0;
623 // while (dirent_i < step_set.entries.len) {
624 // const generations = step_set.values();
625 // if (generations[dirent_i] == w.generation) {
626 // dirent_i += 1;
627 // continue;
628 // }
629 // step_set.swapRemoveAt(dirent_i);
630 // }
631 // if (step_set.entries.len > 0) {
632 // step_set_i += 1;
633 // continue;
634 // }
635 // reaction_set.swapRemoveAt(step_set_i);
636 // }
637 // if (reaction_set.entries.len > 0) {
638 // i += 1;
639 // continue;
640 // }
641 // }
642
643 // const path = w.dir_table.keys()[i];
644
645 // posix.fanotify_mark(fan_fd, .{
646 // .REMOVE = true,
647 // .ONLYDIR = true,
648 // }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| switch (err) {
649 // error.FileNotFound => {}, // Expected, harmless.
650 // else => |e| std.log.warn("unable to unwatch '{}': {s}", .{ path, @errorName(e) }),
651 // };
652
653 // w.dir_table.swapRemoveAt(i);
654 // w.os.handle_table.swapRemoveAt(i);
655 //}
656 w.generation +%= 1;
657 }
658 }
659
660 fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
661 var timespec_buffer: posix.timespec = undefined;
662 var event_buffer: [100]posix.Kevent = undefined;
663 var n = try posix.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer));
664 if (n == 0) return .timeout;
665 const reaction_sets = w.os.reaction_sets.items;
666 var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false);
667 timespec_buffer = .{ .sec = 0, .nsec = 0 };
668 while (n == event_buffer.len) {
669 n = try posix.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer);
670 if (n == 0) break;
671 any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty);
672 }
673 return if (any_dirty) .dirty else .clean;
674 }
675
676 fn markDirtySteps(
677 gpa: Allocator,
678 reaction_sets: []ReactionSet,
679 events: []const std.c.Kevent,
680 start_any_dirty: bool,
681 ) bool {
682 var any_dirty = start_any_dirty;
683 for (events) |event| {
684 const index: usize = @intCast(event.udata);
685 const reaction_set = &reaction_sets[index];
686 // If we knew the basename of the changed file, here we would
687 // mark only the step set dirty, and possibly the glob set:
688 //if (reaction_set.getPtr(".")) |glob_set|
689 // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
690 //if (reaction_set.getPtr(file_name)) |step_set|
691 // any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
692 // However we don't know the file name so just mark all the
693 // sets dirty for this directory.
694 for (reaction_set.values()) |*step_set| {
695 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
696 }
697 }
698 return any_dirty;
699 }
512 },700 },
513 else => void,701 else => void,
514};702};
...@@ -560,6 +748,20 @@ pub fn init() !Watch {...@@ -560,6 +748,20 @@ pub fn init() !Watch {
560 .generation = 0,748 .generation = 0,
561 };749 };
562 },750 },
751 .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .macos, .tvos, .visionos, .watchos => {
752 const posix = std.posix;
753
754 const kq_fd = try posix.kqueue();
755 errdefer posix.close(kq_fd);
756 return .{
757 .dir_table = .{},
758 .os = .{
759 .kq_fd = kq_fd,
760 .reaction_sets = .{},
761 },
762 .generation = 0,
763 };
764 },
563 else => @panic("unimplemented"),765 else => @panic("unimplemented"),
564 }766 }
565}767}
...@@ -609,10 +811,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {...@@ -609,10 +811,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
609}811}
610812
611pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {813pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
612 switch (builtin.os.tag) {814 return Os.update(w, gpa, steps);
613 .linux, .windows => return Os.update(w, gpa, steps),
614 else => @compileError("unimplemented"),
615 }
616}815}
617816
618pub const Timeout = union(enum) {817pub const Timeout = union(enum) {
...@@ -625,6 +824,20 @@ pub const Timeout = union(enum) {...@@ -625,6 +824,20 @@ pub const Timeout = union(enum) {
625 .ms => |ms| ms,824 .ms => |ms| ms,
626 };825 };
627 }826 }
827
828 pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
829 return switch (t) {
830 .none => null,
831 .ms => |ms_u16| {
832 const ms: isize = ms_u16;
833 buf.* = .{
834 .sec = @divTrunc(ms, std.time.ms_per_s),
835 .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
836 };
837 return buf;
838 },
839 };
840 }
628};841};
629842
630pub const WaitResult = enum {843pub const WaitResult = enum {
...@@ -638,46 +851,5 @@ pub const WaitResult = enum {...@@ -638,46 +851,5 @@ pub const WaitResult = enum {
638};851};
639852
640pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {853pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
641 switch (builtin.os.tag) {854 return Os.wait(w, gpa, timeout);
642 .linux => {
643 const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms());
644 return if (events_len == 0)
645 .timeout
646 else if (try Os.markDirtySteps(w, gpa))
647 .dirty
648 else
649 .clean;
650 },
651 .windows => {
652 var bytes_transferred: std.os.windows.DWORD = undefined;
653 var key: usize = undefined;
654 var overlapped_ptr: ?*std.os.windows.OVERLAPPED = undefined;
655 return while (true) switch (std.os.windows.GetQueuedCompletionStatus(
656 w.os.io_cp.?,
657 &bytes_transferred,
658 &key,
659 &overlapped_ptr,
660 @bitCast(timeout.to_i32_ms()),
661 )) {
662 .Normal => {
663 if (bytes_transferred == 0)
664 break error.Unexpected;
665
666 // This 'orelse' detects a race condition that happens when we receive a
667 // completion notification for a directory that no longer exists in our list.
668 const dir = w.os.dir_list.get(key) orelse break .clean;
669
670 break if (try Os.markDirtySteps(w, gpa, dir))
671 .dirty
672 else
673 .clean;
674 },
675 .Timeout => break .timeout,
676 // This status is issued because CancelIo was called, skip and try again.
677 .Cancelled => continue,
678 else => break error.Unexpected,
679 };
680 },
681 else => @compileError("unimplemented"),
682 }
683}855}