| ... | ... | @@ -25,9 +25,7 @@ const WatchEventId = enum { |
| 25 | 25 | }; |
| 26 | 26 | |
| 27 | 27 | fn eqlString(a: []const u16, b: []const u16) bool { |
| 28 | | if (a.len != b.len) return false; |
| 29 | | if (a.ptr == b.ptr) return true; |
| 30 | | return mem.compare(u16, a, b) == .Equal; |
| 28 | return mem.eql(u16, a, b); |
| 31 | 29 | } |
| 32 | 30 | |
| 33 | 31 | fn hashString(s: []const u16) u32 { |
| ... | ... | @@ -43,7 +41,7 @@ const WatchEventError = error{ |
| 43 | 41 | |
| 44 | 42 | pub fn Watch(comptime V: type) type { |
| 45 | 43 | return struct { |
| 46 | | channel: *event.Channel(Event.Error!Event), |
| 44 | channel: event.Channel(Event.Error!Event), |
| 47 | 45 | os_data: OsData, |
| 48 | 46 | allocator: *Allocator, |
| 49 | 47 | |
| ... | ... | @@ -110,19 +108,14 @@ pub fn Watch(comptime V: type) type { |
| 110 | 108 | pub const Event = struct { |
| 111 | 109 | id: Id, |
| 112 | 110 | data: V, |
| 111 | dirname: []const u8, |
| 112 | basename: []const u8, |
| 113 | 113 | |
| 114 | 114 | pub const Id = WatchEventId; |
| 115 | 115 | pub const Error = WatchEventError; |
| 116 | 116 | }; |
| 117 | 117 | |
| 118 | 118 | pub fn init(allocator: *Allocator, event_buf_count: usize) !*Self { |
| 119 | | const channel = try allocator.create(event.Channel(Event.Error!Event)); |
| 120 | | errdefer allocator.destroy(channel); |
| 121 | | var buf = try allocator.alloc(Event.Error!Event, event_buf_count); |
| 122 | | errdefer allocator.free(buf); |
| 123 | | channel.init(buf); |
| 124 | | errdefer channel.deinit(); |
| 125 | | |
| 126 | 119 | const self = try allocator.create(Self); |
| 127 | 120 | errdefer allocator.destroy(self); |
| 128 | 121 | |
| ... | ... | @@ -133,15 +126,17 @@ pub fn Watch(comptime V: type) type { |
| 133 | 126 | |
| 134 | 127 | self.* = Self{ |
| 135 | 128 | .allocator = allocator, |
| 136 | | .channel = channel, |
| 129 | .channel = undefined, |
| 137 | 130 | .os_data = OsData{ |
| 138 | 131 | .putter_frame = undefined, |
| 139 | 132 | .inotify_fd = inotify_fd, |
| 140 | 133 | .wd_table = OsData.WdTable.init(allocator), |
| 141 | | .table_lock = event.Lock.init(), |
| 134 | .table_lock = event.Lock{}, |
| 142 | 135 | }, |
| 143 | 136 | }; |
| 144 | 137 | |
| 138 | var buf = try allocator.alloc(Event.Error!Event, event_buf_count); |
| 139 | self.channel.init(buf); |
| 145 | 140 | self.os_data.putter_frame = async self.linuxEventPutter(); |
| 146 | 141 | return self; |
| 147 | 142 | }, |
| ... | ... | @@ -149,14 +144,16 @@ pub fn Watch(comptime V: type) type { |
| 149 | 144 | .windows => { |
| 150 | 145 | self.* = Self{ |
| 151 | 146 | .allocator = allocator, |
| 152 | | .channel = channel, |
| 147 | .channel = undefined, |
| 153 | 148 | .os_data = OsData{ |
| 154 | | .table_lock = event.Lock.init(), |
| 149 | .table_lock = event.Lock{}, |
| 155 | 150 | .dir_table = OsData.DirTable.init(allocator), |
| 156 | 151 | .ref_count = std.atomic.Int(usize).init(1), |
| 157 | | .all_putters = std.atomic.Queue(anyframe).init(), |
| 152 | .all_putters = std.atomic.Queue(WindowsOsData.Put).init(), |
| 158 | 153 | }, |
| 159 | 154 | }; |
| 155 | var buf = try allocator.alloc(Event.Error!Event, event_buf_count); |
| 156 | self.channel.init(buf); |
| 160 | 157 | return self; |
| 161 | 158 | }, |
| 162 | 159 | |
| ... | ... | @@ -194,6 +191,17 @@ pub fn Watch(comptime V: type) type { |
| 194 | 191 | }, |
| 195 | 192 | .linux => { |
| 196 | 193 | self.os_data.cancelled = true; |
| 194 | { |
| 195 | // Remove all directory watches linuxEventPutter will take care of |
| 196 | // cleaning up the memory and closing the inotify fd. |
| 197 | var dir_it = self.os_data.wd_table.iterator(); |
| 198 | while (dir_it.next()) |wd_entry| { |
| 199 | const rc = os.linux.inotify_rm_watch(self.os_data.inotify_fd, wd_entry.key); |
| 200 | // Errno can only be EBADF, EINVAL if either the inotify fs or the wd are invalid |
| 201 | std.debug.assert(rc == 0); |
| 202 | } |
| 203 | } |
| 204 | |
| 197 | 205 | await self.os_data.putter_frame; |
| 198 | 206 | self.allocator.destroy(self); |
| 199 | 207 | }, |
| ... | ... | @@ -322,19 +330,12 @@ pub fn Watch(comptime V: type) type { |
| 322 | 330 | |
| 323 | 331 | fn addFileLinux(self: *Self, file_path: []const u8, value: V) !?V { |
| 324 | 332 | const dirname = std.fs.path.dirname(file_path) orelse "."; |
| 325 | | const dirname_with_null = try std.cstr.addNullByte(self.allocator, dirname); |
| 326 | | var dirname_with_null_consumed = false; |
| 327 | | defer if (!dirname_with_null_consumed) self.channel.free(dirname_with_null); |
| 328 | | |
| 329 | 333 | const basename = std.fs.path.basename(file_path); |
| 330 | | const basename_with_null = try std.cstr.addNullByte(self.allocator, basename); |
| 331 | | var basename_with_null_consumed = false; |
| 332 | | defer if (!basename_with_null_consumed) self.allocator.free(basename_with_null); |
| 333 | 334 | |
| 334 | | const wd = try os.inotify_add_watchZ( |
| 335 | const wd = try os.inotify_add_watch( |
| 335 | 336 | self.os_data.inotify_fd, |
| 336 | | dirname_with_null.ptr, |
| 337 | | os.linux.IN_CLOSE_WRITE | os.linux.IN_ONLYDIR | os.linux.IN_EXCL_UNLINK, |
| 337 | dirname, |
| 338 | os.linux.IN_CLOSE_WRITE | os.linux.IN_ONLYDIR | os.linux.IN_DELETE | os.linux.IN_EXCL_UNLINK, |
| 338 | 339 | ); |
| 339 | 340 | // wd is either a newly created watch or an existing one. |
| 340 | 341 | |
| ... | ... | @@ -343,22 +344,21 @@ pub fn Watch(comptime V: type) type { |
| 343 | 344 | |
| 344 | 345 | const gop = try self.os_data.wd_table.getOrPut(wd); |
| 345 | 346 | if (!gop.found_existing) { |
| 346 | | gop.kv.value = OsData.Dir{ |
| 347 | | .dirname = dirname_with_null, |
| 347 | gop.entry.value = OsData.Dir{ |
| 348 | .dirname = try self.allocator.dupe(u8, dirname), |
| 348 | 349 | .file_table = OsData.FileTable.init(self.allocator), |
| 349 | 350 | }; |
| 350 | | dirname_with_null_consumed = true; |
| 351 | 351 | } |
| 352 | | const dir = &gop.kv.value; |
| 353 | 352 | |
| 354 | | const file_table_gop = try dir.file_table.getOrPut(basename_with_null); |
| 353 | const dir = &gop.entry.value; |
| 354 | const file_table_gop = try dir.file_table.getOrPut(basename); |
| 355 | 355 | if (file_table_gop.found_existing) { |
| 356 | | const prev_value = file_table_gop.kv.value; |
| 357 | | file_table_gop.kv.value = value; |
| 356 | const prev_value = file_table_gop.entry.value; |
| 357 | file_table_gop.entry.value = value; |
| 358 | 358 | return prev_value; |
| 359 | 359 | } else { |
| 360 | | file_table_gop.kv.value = value; |
| 361 | | basename_with_null_consumed = true; |
| 360 | file_table_gop.entry.key = try self.allocator.dupe(u8, basename); |
| 361 | file_table_gop.entry.value = value; |
| 362 | 362 | return null; |
| 363 | 363 | } |
| 364 | 364 | } |
| ... | ... | @@ -539,76 +539,96 @@ pub fn Watch(comptime V: type) type { |
| 539 | 539 | } |
| 540 | 540 | |
| 541 | 541 | pub fn removeFile(self: *Self, file_path: []const u8) ?V { |
| 542 | | @panic("TODO"); |
| 542 | switch (builtin.os.tag) { |
| 543 | .linux => { |
| 544 | const dirname = std.fs.path.dirname(file_path) orelse "."; |
| 545 | const basename = std.fs.path.basename(file_path); |
| 546 | |
| 547 | const held = self.os_data.table_lock.acquire(); |
| 548 | defer held.release(); |
| 549 | |
| 550 | const dir = self.os_data.wd_table.get(dirname) orelse return null; |
| 551 | if (dir.file_table.remove(basename)) |file_entry| { |
| 552 | self.allocator.free(file_entry.key); |
| 553 | return file_entry.value; |
| 554 | } |
| 555 | return null; |
| 556 | }, |
| 557 | .macos, .freebsd, .netbsd, .dragonfly, .openbsd => @panic("TODO"), |
| 558 | .windows => return @panic("TODO"), |
| 559 | else => @compileError("Unsupported OS"), |
| 560 | } |
| 543 | 561 | } |
| 544 | 562 | |
| 545 | 563 | fn linuxEventPutter(self: *Self) void { |
| 546 | 564 | global_event_loop.beginOneEvent(); |
| 547 | 565 | |
| 548 | 566 | defer { |
| 549 | | self.os_data.table_lock.deinit(); |
| 550 | | var wd_it = self.os_data.wd_table.iterator(); |
| 551 | | while (wd_it.next()) |wd_entry| { |
| 552 | | var file_it = wd_entry.value.file_table.iterator(); |
| 553 | | while (file_it.next()) |file_entry| { |
| 554 | | self.allocator.free(file_entry.key); |
| 555 | | } |
| 556 | | self.allocator.free(wd_entry.value.dirname); |
| 557 | | wd_entry.value.file_table.deinit(); |
| 558 | | } |
| 567 | std.debug.assert(self.os_data.wd_table.count() == 0); |
| 559 | 568 | self.os_data.wd_table.deinit(); |
| 560 | | global_event_loop.finishOneEvent(); |
| 561 | 569 | os.close(self.os_data.inotify_fd); |
| 562 | | self.channel.deinit(); |
| 563 | 570 | self.allocator.free(self.channel.buffer_nodes); |
| 571 | self.channel.deinit(); |
| 572 | global_event_loop.finishOneEvent(); |
| 564 | 573 | } |
| 565 | 574 | |
| 566 | 575 | var event_buf: [4096]u8 align(@alignOf(os.linux.inotify_event)) = undefined; |
| 567 | 576 | |
| 568 | 577 | while (!self.os_data.cancelled) { |
| 569 | | const rc = os.linux.read(self.os_data.inotify_fd, &event_buf, event_buf.len); |
| 570 | | const errno = os.linux.getErrno(rc); |
| 571 | | switch (errno) { |
| 572 | | 0 => { |
| 573 | | // can't use @bytesToSlice because of the special variable length name field |
| 574 | | var ptr = event_buf[0..].ptr; |
| 575 | | const end_ptr = ptr + event_buf.len; |
| 576 | | var ev: *os.linux.inotify_event = undefined; |
| 577 | | while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) { |
| 578 | | ev = @ptrCast(*os.linux.inotify_event, ptr); |
| 579 | | if (ev.mask & os.linux.IN_CLOSE_WRITE == os.linux.IN_CLOSE_WRITE) { |
| 580 | | const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); |
| 581 | | // `ev.len` counts all bytes in `ev.name` including terminating null byte. |
| 582 | | const basename_with_null = basename_ptr[0..ev.len]; |
| 583 | | const user_value = blk: { |
| 584 | | const held = self.os_data.table_lock.acquire(); |
| 585 | | defer held.release(); |
| 586 | | |
| 587 | | const dir = &self.os_data.wd_table.get(ev.wd).?.value; |
| 588 | | if (dir.file_table.get(basename_with_null)) |entry| { |
| 589 | | break :blk entry.value; |
| 590 | | } else { |
| 591 | | break :blk null; |
| 592 | | } |
| 593 | | }; |
| 594 | | if (user_value) |v| { |
| 595 | | self.channel.put(Event{ |
| 596 | | .id = WatchEventId.CloseWrite, |
| 597 | | .data = v, |
| 598 | | }); |
| 599 | | } |
| 578 | const bytes_read = global_event_loop.read(self.os_data.inotify_fd, &event_buf, false) catch unreachable; |
| 579 | |
| 580 | var ptr: [*]u8 = &event_buf; |
| 581 | const end_ptr = ptr + bytes_read; |
| 582 | while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) { |
| 583 | const ev = @ptrCast(*const os.linux.inotify_event, ptr); |
| 584 | if (ev.mask & os.linux.IN_CLOSE_WRITE == os.linux.IN_CLOSE_WRITE) { |
| 585 | const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); |
| 586 | const basename = std.mem.span(@ptrCast([*:0]u8, basename_ptr)); |
| 587 | |
| 588 | const held = self.os_data.table_lock.acquire(); |
| 589 | defer held.release(); |
| 590 | |
| 591 | const dir = &self.os_data.wd_table.get(ev.wd).?; |
| 592 | if (dir.file_table.getEntry(basename)) |file_value| { |
| 593 | self.channel.put(Event{ |
| 594 | .id = .CloseWrite, |
| 595 | .data = file_value.value, |
| 596 | .dirname = dir.dirname, |
| 597 | .basename = file_value.key, |
| 598 | }); |
| 599 | } |
| 600 | } else if (ev.mask & os.linux.IN_IGNORED == os.linux.IN_IGNORED) { |
| 601 | // Directory watch was removed |
| 602 | const held = self.os_data.table_lock.acquire(); |
| 603 | defer held.release(); |
| 604 | if (self.os_data.wd_table.remove(ev.wd)) |*wd_entry| { |
| 605 | var file_it = wd_entry.value.file_table.iterator(); |
| 606 | while (file_it.next()) |file_entry| { |
| 607 | self.allocator.free(file_entry.key); |
| 600 | 608 | } |
| 601 | | |
| 602 | | ptr = @alignCast(@alignOf(os.linux.inotify_event), ptr + @sizeOf(os.linux.inotify_event) + ev.len); |
| 609 | self.allocator.free(wd_entry.value.dirname); |
| 610 | wd_entry.value.file_table.deinit(); |
| 603 | 611 | } |
| 604 | | }, |
| 605 | | os.linux.EINTR => continue, |
| 606 | | os.linux.EINVAL => unreachable, |
| 607 | | os.linux.EFAULT => unreachable, |
| 608 | | os.linux.EAGAIN => { |
| 609 | | global_event_loop.linuxWaitFd(self.os_data.inotify_fd, os.linux.EPOLLET | os.linux.EPOLLIN | os.EPOLLONESHOT); |
| 610 | | }, |
| 611 | | else => unreachable, |
| 612 | } else if (ev.mask & os.linux.IN_DELETE == os.linux.IN_DELETE) { |
| 613 | // File or directory was removed or deleted |
| 614 | const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); |
| 615 | const basename = std.mem.span(@ptrCast([*:0]u8, basename_ptr)); |
| 616 | |
| 617 | const held = self.os_data.table_lock.acquire(); |
| 618 | defer held.release(); |
| 619 | const dir = &self.os_data.wd_table.get(ev.wd).?; |
| 620 | |
| 621 | if (dir.file_table.getEntry(basename)) |file_value| { |
| 622 | self.channel.put(Event{ |
| 623 | .id = .Delete, |
| 624 | .data = file_value.value, |
| 625 | .dirname = dir.dirname, |
| 626 | .basename = file_value.key, |
| 627 | }); |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | ptr = @alignCast(@alignOf(os.linux.inotify_event), ptr + @sizeOf(os.linux.inotify_event) + ev.len); |
| 612 | 632 | } |
| 613 | 633 | } |
| 614 | 634 | } |
| ... | ... | @@ -617,19 +637,19 @@ pub fn Watch(comptime V: type) type { |
| 617 | 637 | |
| 618 | 638 | const test_tmp_dir = "std_event_fs_test"; |
| 619 | 639 | |
| 620 | | test "write a file, watch it, write it again" { |
| 621 | | // TODO re-enable this test |
| 622 | | if (true) return error.SkipZigTest; |
| 640 | test "write a file, watch it, write it again, delete it" { |
| 641 | if (!std.io.is_async) return error.SkipZigTest; |
| 642 | // TODO https://github.com/ziglang/zig/issues/1908 |
| 643 | if (builtin.single_threaded) return error.SkipZigTest; |
| 623 | 644 | |
| 624 | | try fs.cwd().makePath(test_tmp_dir); |
| 625 | | defer fs.cwd().deleteTree(test_tmp_dir) catch {}; |
| 645 | try std.fs.cwd().makePath(test_tmp_dir); |
| 646 | defer std.fs.cwd().deleteTree(test_tmp_dir) catch {}; |
| 626 | 647 | |
| 627 | | const allocator = std.heap.page_allocator; |
| 628 | | return testFsWatch(&allocator); |
| 648 | return testWriteWatchWriteDelete(std.testing.allocator); |
| 629 | 649 | } |
| 630 | 650 | |
| 631 | | fn testFsWatch(allocator: *Allocator) !void { |
| 632 | | const file_path = try std.fs.path.join(allocator, [_][]const u8{ test_tmp_dir, "file.txt" }); |
| 651 | fn testWriteWatchWriteDelete(allocator: *Allocator) !void { |
| 652 | const file_path = try std.fs.path.join(allocator, &[_][]const u8{ test_tmp_dir, "file.txt" }); |
| 633 | 653 | defer allocator.free(file_path); |
| 634 | 654 | |
| 635 | 655 | const contents = |
| ... | ... | @@ -639,9 +659,10 @@ fn testFsWatch(allocator: *Allocator) !void { |
| 639 | 659 | const line2_offset = 7; |
| 640 | 660 | |
| 641 | 661 | // first just write then read the file |
| 642 | | try writeFile(allocator, file_path, contents); |
| 662 | try std.fs.cwd().writeFile(file_path, contents); |
| 643 | 663 | |
| 644 | | const read_contents = try readFile(allocator, file_path, 1024 * 1024); |
| 664 | const read_contents = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); |
| 665 | defer allocator.free(read_contents); |
| 645 | 666 | testing.expectEqualSlices(u8, contents, read_contents); |
| 646 | 667 | |
| 647 | 668 | // now watch the file |
| ... | ... | @@ -650,28 +671,47 @@ fn testFsWatch(allocator: *Allocator) !void { |
| 650 | 671 | |
| 651 | 672 | testing.expect((try watch.addFile(file_path, {})) == null); |
| 652 | 673 | |
| 653 | | const ev = watch.channel.get(); |
| 674 | var ev = async watch.channel.get(); |
| 654 | 675 | var ev_consumed = false; |
| 655 | | defer if (!ev_consumed) await ev; |
| 676 | defer if (!ev_consumed) { |
| 677 | _ = await ev; |
| 678 | }; |
| 656 | 679 | |
| 657 | 680 | // overwrite line 2 |
| 658 | | const fd = try await openReadWrite(file_path, File.default_mode); |
| 681 | const file = try std.fs.cwd().openFile(file_path, .{ .read = true, .write = true }); |
| 659 | 682 | { |
| 660 | | defer os.close(fd); |
| 661 | | |
| 662 | | try pwritev(allocator, fd, []const []const u8{"lorem ipsum"}, line2_offset); |
| 683 | defer file.close(); |
| 684 | const write_contents = "lorem ipsum"; |
| 685 | var iovec = [_]os.iovec_const{.{ |
| 686 | .iov_base = write_contents, |
| 687 | .iov_len = write_contents.len, |
| 688 | }}; |
| 689 | _ = try file.pwritevAll(&iovec, line2_offset); |
| 663 | 690 | } |
| 664 | 691 | |
| 665 | | ev_consumed = true; |
| 666 | 692 | switch ((try await ev).id) { |
| 667 | | WatchEventId.CloseWrite => {}, |
| 668 | | WatchEventId.Delete => @panic("wrong event"), |
| 693 | .CloseWrite => { |
| 694 | ev_consumed = true; |
| 695 | }, |
| 696 | .Delete => @panic("wrong event"), |
| 669 | 697 | } |
| 670 | | const contents_updated = try readFile(allocator, file_path, 1024 * 1024); |
| 698 | |
| 699 | const contents_updated = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); |
| 700 | defer allocator.free(contents_updated); |
| 701 | |
| 671 | 702 | testing.expectEqualSlices(u8, |
| 672 | 703 | \\line 1 |
| 673 | 704 | \\lorem ipsum |
| 674 | 705 | , contents_updated); |
| 675 | 706 | |
| 676 | | // TODO test deleting the file and then re-adding it. we should get events for both |
| 707 | ev = async watch.channel.get(); |
| 708 | ev_consumed = false; |
| 709 | |
| 710 | try std.fs.cwd().deleteFile(file_path); |
| 711 | switch ((try await ev).id) { |
| 712 | .Delete => { |
| 713 | ev_consumed = true; |
| 714 | }, |
| 715 | .CloseWrite => @panic("wrong event"), |
| 716 | } |
| 677 | 717 | } |