| ... | ... | @@ -10,6 +10,8 @@ dir_table: DirTable, |
| 10 | 10 | os: Os, |
| 11 | 11 | generation: Generation, |
| 12 | 12 | |
| 13 | pub const have_impl = Os != void; |
| 14 | |
| 13 | 15 | /// Key is the directory to watch which contains one or more files we are |
| 14 | 16 | /// interested in noticing changes to. |
| 15 | 17 | /// |
| ... | ... | @@ -236,6 +238,16 @@ const Os = switch (builtin.os.tag) { |
| 236 | 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 | 252 | .windows => struct { |
| 241 | 253 | const windows = std.os.windows; |
| ... | ... | @@ -509,6 +521,182 @@ const Os = switch (builtin.os.tag) { |
| 509 | 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 | 701 | else => void, |
| 514 | 702 | }; |
| ... | ... | @@ -560,6 +748,20 @@ pub fn init() !Watch { |
| 560 | 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 | 765 | else => @panic("unimplemented"), |
| 564 | 766 | } |
| 565 | 767 | } |
| ... | ... | @@ -609,10 +811,7 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool { |
| 609 | 811 | } |
| 610 | 812 | |
| 611 | 813 | pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { |
| 612 | | switch (builtin.os.tag) { |
| 613 | | .linux, .windows => return Os.update(w, gpa, steps), |
| 614 | | else => @compileError("unimplemented"), |
| 615 | | } |
| 814 | return Os.update(w, gpa, steps); |
| 616 | 815 | } |
| 617 | 816 | |
| 618 | 817 | pub const Timeout = union(enum) { |
| ... | ... | @@ -625,6 +824,20 @@ pub const Timeout = union(enum) { |
| 625 | 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 | }; |
| 629 | 842 | |
| 630 | 843 | pub const WaitResult = enum { |
| ... | ... | @@ -638,46 +851,5 @@ pub const WaitResult = enum { |
| 638 | 851 | }; |
| 639 | 852 | |
| 640 | 853 | pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult { |
| 641 | | switch (builtin.os.tag) { |
| 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 | | } |
| 854 | return Os.wait(w, gpa, timeout); |
| 683 | 855 | } |