@@ -605,7 +605,16 @@ const Os = switch (builtin.os.tag) {
605605
606606 kq_fd : i32 ,
607607 /// Indexes correspond 1:1 with `dir_table`.
608- reaction_sets : std .ArrayListUnmanaged (ReactionSet ),
608+ handles : std .MultiArrayList (struct {
609+ rs : ReactionSet ,
610+ /// If the corresponding dir_table Path has sub_path == "", then it
611+ /// suffices as the open directory handle, and this value will be
612+ /// -1. Otherwise, it needs to be opened in update(), and will be
613+ /// stored here.
614+ dir_fd : i32 ,
615+ /// Number of files being watched by this directory handle.
616+ ref_count : u32 ,
617+ }),
609618
610619 const dir_open_flags : posix.O = f : {
611620 var f : posix.O = .{
@@ -619,34 +628,39 @@ const Os = switch (builtin.os.tag) {
619628 break :f f ;
620629 };
621630
631+ const EV = std .c .EV ;
632+ const NOTE = std .c .NOTE ;
633+
622634 fn init () ! Watch {
623635 const kq_fd = try posix .kqueue ();
624636 errdefer posix .close (kq_fd );
625637 return .{
626638 .dir_table = .{},
627639 .os = .{
628640 .kq_fd = kq_fd ,
629- .reaction_sets = .{} ,
641+ .handles = .empty ,
630642 },
631643 .generation = 0 ,
632644 };
633645 }
634646
635647 fn update (w : * Watch , gpa : Allocator , steps : []const * Step ) ! void {
648+ const handles = & w .os .handles ;
636649 for (steps ) | step | {
637650 for (step .inputs .table .keys (), step .inputs .table .values ()) | path , * files | {
638651 const reaction_set = rs : {
639652 const gop = try w .dir_table .getOrPut (gpa , path );
640653 if (! gop .found_existing ) {
641- const dir_fd = if (path .sub_path .len == 0 )
654+ const skip_open_dir = path .sub_path .len == 0 ;
655+ const dir_fd = if (skip_open_dir )
642656 path .root_dir .handle .fd
643657 else
644658 posix .openat (path .root_dir .handle .fd , path .sub_path , dir_open_flags , 0 ) catch | err | {
645659 fatal ("failed to open directory {}: {s}" , .{ path , @errorName (err ) });
646660 };
647- const EV = std . c . EV ;
648- const NOTE = std . c . NOTE ;
649- var changes = [1 ]posix.Kevent {.{
661+ // Empirically the dir has to stay open or else no events are triggered.
662+ errdefer if ( ! skip_open_dir ) posix . close ( dir_fd ) ;
663+ const changes = [1 ]posix.Kevent {.{
650664 .ident = @bitCast (@as (isize , dir_fd )),
651665 .filter = std .c .EVFILT .VNODE ,
652666 .flags = EV .ADD | EV .ENABLE | EV .CLEAR ,
@@ -655,12 +669,16 @@ const Os = switch (builtin.os.tag) {
655669 .udata = gop .index ,
656670 }};
657671 _ = try posix .kevent (w .os .kq_fd , & changes , &.{}, null );
658- assert (w .os .reaction_sets .items .len == gop .index );
659- const reaction_set = try w .os .reaction_sets .addOne (gpa );
660- reaction_set .* = .{};
661- break :rs reaction_set ;
672+ assert (handles .len == gop .index );
673+ try handles .append (gpa , .{
674+ .rs = .{},
675+ .dir_fd = if (skip_open_dir ) -1 else dir_fd ,
676+ .ref_count = 1 ,
677+ });
678+ } else {
679+ handles .items (.ref_count )[gop .index ] += 1 ;
662680 }
663- break :rs & w . os . reaction_sets . items [gop .index ];
681+ break :rs & handles . items ( .rs ) [gop .index ];
664682 };
665683 for (files .items ) | basename | {
666684 const gop = try reaction_set .getOrPut (gpa , basename );
@@ -672,47 +690,86 @@ const Os = switch (builtin.os.tag) {
672690
673691 {
674692 // Remove marks for files that are no longer inputs.
675- //var i: usize = 0;
676- //while (i < w.os.handle_table.entries.len) {
677- // {
678- // const reaction_set = &w.os.handle_table.values()[i];
679- // var step_set_i: usize = 0;
680- // while (step_set_i < reaction_set.entries.len) {
681- // const step_set = &reaction_set.values()[step_set_i];
682- // var dirent_i: usize = 0;
683- // while (dirent_i < step_set.entries.len) {
684- // const generations = step_set.values();
685- // if (generations[dirent_i] == w.generation) {
686- // dirent_i += 1;
687- // continue;
688- // }
689- // step_set.swapRemoveAt(dirent_i);
690- // }
691- // if (step_set.entries.len > 0) {
692- // step_set_i += 1;
693- // continue;
694- // }
695- // reaction_set.swapRemoveAt(step_set_i);
696- // }
697- // if (reaction_set.entries.len > 0) {
698- // i += 1;
699- // continue;
700- // }
701- // }
702-
703- // const path = w.dir_table.keys()[i];
704-
705- // posix.fanotify_mark(fan_fd, .{
706- // .REMOVE = true,
707- // .ONLYDIR = true,
708- // }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| switch (err) {
709- // error.FileNotFound => {}, // Expected, harmless.
710- // else => |e| std.log.warn("unable to unwatch '{}': {s}", .{ path, @errorName(e) }),
711- // };
712-
713- // w.dir_table.swapRemoveAt(i);
714- // w.os.handle_table.swapRemoveAt(i);
715- //}
693+ var i : usize = 0 ;
694+ while (i < handles .len ) {
695+ {
696+ const reaction_set = & handles .items (.rs )[i ];
697+ var step_set_i : usize = 0 ;
698+ while (step_set_i < reaction_set .entries .len ) {
699+ const step_set = & reaction_set .values ()[step_set_i ];
700+ var dirent_i : usize = 0 ;
701+ while (dirent_i < step_set .entries .len ) {
702+ const generations = step_set .values ();
703+ if (generations [dirent_i ] == w .generation ) {
704+ dirent_i += 1 ;
705+ continue ;
706+ }
707+ step_set .swapRemoveAt (dirent_i );
708+ }
709+ if (step_set .entries .len > 0 ) {
710+ step_set_i += 1 ;
711+ continue ;
712+ }
713+ reaction_set .swapRemoveAt (step_set_i );
714+ }
715+ if (reaction_set .entries .len > 0 ) {
716+ i += 1 ;
717+ continue ;
718+ }
719+ }
720+
721+ const ref_count_ptr = & handles .items (.ref_count )[i ];
722+ ref_count_ptr .* -= 1 ;
723+ if (ref_count_ptr .* > 0 ) continue ;
724+
725+ // If the sub_path == "" then this patch has already the
726+ // dir fd that we need to use as the ident to remove the
727+ // event. If it was opened above with openat() then we need
728+ // to access that data via the dir_fd field.
729+ const path = w .dir_table .keys ()[i ];
730+ const dir_fd = if (path .sub_path .len == 0 )
731+ path .root_dir .handle .fd
732+ else
733+ handles .items (.dir_fd )[i ];
734+ assert (dir_fd != -1 );
735+
736+ // The changelist also needs to update the udata field of the last
737+ // event, since we are doing a swap remove, and we store the dir_table
738+ // index in the udata field.
739+ const last_dir_fd = fd : {
740+ const last_path = w .dir_table .keys ()[handles .len - 1 ];
741+ const last_dir_fd = if (last_path .sub_path .len != 0 )
742+ last_path .root_dir .handle .fd
743+ else
744+ handles .items (.dir_fd )[i ];
745+ assert (last_dir_fd != -1 );
746+ break :fd last_dir_fd ;
747+ };
748+ const changes = [_ ]posix.Kevent {
749+ .{
750+ .ident = @bitCast (@as (isize , dir_fd )),
751+ .filter = std .c .EVFILT .VNODE ,
752+ .flags = EV .DELETE ,
753+ .fflags = 0 ,
754+ .data = 0 ,
755+ .udata = i ,
756+ },
757+ .{
758+ .ident = @bitCast (@as (isize , last_dir_fd )),
759+ .filter = std .c .EVFILT .VNODE ,
760+ .flags = EV .ADD ,
761+ .fflags = NOTE .DELETE | NOTE .WRITE | NOTE .RENAME | NOTE .REVOKE ,
762+ .data = 0 ,
763+ .udata = i ,
764+ },
765+ };
766+ const filtered_changes = if (i == handles .len - 1 ) changes [0.. 1] else & changes ;
767+ _ = try posix .kevent (w .os .kq_fd , filtered_changes , &.{}, null );
768+ if (path .sub_path .len != 0 ) posix .close (dir_fd );
769+
770+ w .dir_table .swapRemoveAt (i );
771+ handles .swapRemove (i );
772+ }
716773 w .generation +%= 1 ;
717774 }
718775 }
@@ -722,7 +779,7 @@ const Os = switch (builtin.os.tag) {
722779 var event_buffer : [100 ]posix.Kevent = undefined ;
723780 var n = try posix .kevent (w .os .kq_fd , &.{}, & event_buffer , timeout .toTimespec (& timespec_buffer ));
724781 if (n == 0 ) return .timeout ;
725- const reaction_sets = w .os .reaction_sets .items ;
782+ const reaction_sets = w .os .handles .items ( .rs ) ;
726783 var any_dirty = markDirtySteps (gpa , reaction_sets , event_buffer [0.. n ], false );
727784 timespec_buffer = .{ .sec = 0 , .nsec = 0 };
728785 while (n == event_buffer .len ) {
0 commit comments