| ... | @@ -10,6 +10,8 @@ dir_table: DirTable, | ... | @@ -10,6 +10,8 @@ dir_table: DirTable, |
| 10 | os: Os, | 10 | os: Os, |
| 11 | generation: Generation, | 11 | generation: Generation, |
| 12 | | 12 | |
| | 13 | pub const have_impl = Os != void; |
| | 14 | |
| 13 | /// Key is the directory to watch which contains one or more files we are | 15 | /// 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 | } |
| 610 | | 812 | |
| 611 | pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { | 813 | pub 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 | } |
| 617 | | 816 | |
| 618 | pub const Timeout = union(enum) { | 817 | pub 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 | }; |
| 629 | | 842 | |
| 630 | pub const WaitResult = enum { | 843 | pub const WaitResult = enum { |
| ... | @@ -638,46 +851,5 @@ pub const WaitResult = enum { | ... | @@ -638,46 +851,5 @@ pub const WaitResult = enum { |
| 638 | }; | 851 | }; |
| 639 | | 852 | |
| 640 | pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult { | 853 | pub 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 | } |