| ... | ... | @@ -681,6 +681,7 @@ pub const WatchEventError = error{ |
| 681 | 681 | UserResourceLimitReached, |
| 682 | 682 | SystemResources, |
| 683 | 683 | AccessDenied, |
| 684 | Unexpected, // TODO remove this possibility |
| 684 | 685 | }; |
| 685 | 686 | |
| 686 | 687 | pub fn Watch(comptime V: type) type { |
| ... | ... | @@ -699,27 +700,48 @@ pub fn Watch(comptime V: type) type { |
| 699 | 700 | value_ptr: *V, |
| 700 | 701 | }; |
| 701 | 702 | }, |
| 702 | | builtin.Os.linux => struct { |
| 703 | |
| 704 | builtin.Os.linux => LinuxOsData, |
| 705 | builtin.Os.windows => WindowsOsData, |
| 706 | |
| 707 | else => @compileError("Unsupported OS"), |
| 708 | }; |
| 709 | |
| 710 | const WindowsOsData = struct { |
| 711 | table_lock: event.Lock, |
| 712 | dir_table: DirTable, |
| 713 | all_putters: std.atomic.Queue(promise), |
| 714 | ref_count: std.atomic.Int(usize), |
| 715 | |
| 716 | const DirTable = std.AutoHashMap([]const u8, *Dir); |
| 717 | const FileTable = std.AutoHashMap([]const u16, V); |
| 718 | |
| 719 | const Dir = struct { |
| 703 | 720 | putter: promise, |
| 704 | | inotify_fd: i32, |
| 705 | | wd_table: WdTable, |
| 721 | file_table: FileTable, |
| 706 | 722 | table_lock: event.Lock, |
| 723 | }; |
| 724 | }; |
| 707 | 725 | |
| 708 | | const FileTable = std.AutoHashMap([]const u8, V); |
| 709 | | }, |
| 710 | | else => @compileError("Unsupported OS"), |
| 726 | const LinuxOsData = struct { |
| 727 | putter: promise, |
| 728 | inotify_fd: i32, |
| 729 | wd_table: WdTable, |
| 730 | table_lock: event.Lock, |
| 731 | |
| 732 | const WdTable = std.AutoHashMap(i32, Dir); |
| 733 | const FileTable = std.AutoHashMap([]const u8, V); |
| 734 | |
| 735 | const Dir = struct { |
| 736 | dirname: []const u8, |
| 737 | file_table: FileTable, |
| 738 | }; |
| 711 | 739 | }; |
| 712 | 740 | |
| 713 | | const WdTable = std.AutoHashMap(i32, Dir); |
| 714 | 741 | const FileToHandle = std.AutoHashMap([]const u8, promise); |
| 715 | 742 | |
| 716 | 743 | const Self = this; |
| 717 | 744 | |
| 718 | | const Dir = struct { |
| 719 | | dirname: []const u8, |
| 720 | | file_table: OsData.FileTable, |
| 721 | | }; |
| 722 | | |
| 723 | 745 | pub const Event = struct { |
| 724 | 746 | id: Id, |
| 725 | 747 | data: V, |
| ... | ... | @@ -741,6 +763,22 @@ pub fn Watch(comptime V: type) type { |
| 741 | 763 | _ = try async<loop.allocator> linuxEventPutter(inotify_fd, channel, &result); |
| 742 | 764 | return result; |
| 743 | 765 | }, |
| 766 | |
| 767 | builtin.Os.windows => { |
| 768 | const self = try loop.allocator.createOne(Self); |
| 769 | errdefer loop.allocator.destroy(self); |
| 770 | self.* = Self{ |
| 771 | .channel = channel, |
| 772 | .os_data = OsData{ |
| 773 | .table_lock = event.Lock.init(loop), |
| 774 | .dir_table = OsData.DirTable.init(loop.allocator), |
| 775 | .ref_count = std.atomic.Int(usize).init(1), |
| 776 | .all_putters = std.atomic.Queue(promise).init(), |
| 777 | }, |
| 778 | }; |
| 779 | return self; |
| 780 | }, |
| 781 | |
| 744 | 782 | builtin.Os.macosx => { |
| 745 | 783 | const self = try loop.allocator.createOne(Self); |
| 746 | 784 | errdefer loop.allocator.destroy(self); |
| ... | ... | @@ -758,9 +796,11 @@ pub fn Watch(comptime V: type) type { |
| 758 | 796 | } |
| 759 | 797 | } |
| 760 | 798 | |
| 799 | /// All addFile calls and removeFile calls must have completed. |
| 761 | 800 | pub fn destroy(self: *Self) void { |
| 762 | 801 | switch (builtin.os) { |
| 763 | 802 | builtin.Os.macosx => { |
| 803 | // TODO we need to cancel the coroutines before destroying the lock |
| 764 | 804 | self.os_data.table_lock.deinit(); |
| 765 | 805 | var it = self.os_data.file_table.iterator(); |
| 766 | 806 | while (it.next()) |entry| { |
| ... | ... | @@ -770,14 +810,41 @@ pub fn Watch(comptime V: type) type { |
| 770 | 810 | self.channel.destroy(); |
| 771 | 811 | }, |
| 772 | 812 | builtin.Os.linux => cancel self.os_data.putter, |
| 813 | builtin.Os.windows => { |
| 814 | while (self.os_data.all_putters.get()) |putter_node| { |
| 815 | cancel putter_node.data; |
| 816 | } |
| 817 | self.deref(); |
| 818 | }, |
| 773 | 819 | else => @compileError("Unsupported OS"), |
| 774 | 820 | } |
| 775 | 821 | } |
| 776 | 822 | |
| 823 | fn ref(self: *Self) void { |
| 824 | _ = self.os_data.ref_count.incr(); |
| 825 | } |
| 826 | |
| 827 | fn deref(self: *Self) void { |
| 828 | if (self.os_data.ref_count.decr() == 1) { |
| 829 | const allocator = self.channel.loop.allocator; |
| 830 | self.os_data.table_lock.deinit(); |
| 831 | var it = self.os_data.dir_table.iterator(); |
| 832 | while (it.next()) |entry| { |
| 833 | allocator.free(entry.key); |
| 834 | // TODO why does freeing this memory crash the test? |
| 835 | //allocator.destroy(entry.value); |
| 836 | } |
| 837 | self.os_data.dir_table.deinit(); |
| 838 | self.channel.destroy(); |
| 839 | allocator.destroy(self); |
| 840 | } |
| 841 | } |
| 842 | |
| 777 | 843 | pub async fn addFile(self: *Self, file_path: []const u8, value: V) !?V { |
| 778 | 844 | switch (builtin.os) { |
| 779 | 845 | builtin.Os.macosx => return await (async addFileMacosx(self, file_path, value) catch unreachable), |
| 780 | 846 | builtin.Os.linux => return await (async addFileLinux(self, file_path, value) catch unreachable), |
| 847 | builtin.Os.windows => return await (async addFileWindows(self, file_path, value) catch unreachable), |
| 781 | 848 | else => @compileError("Unsupported OS"), |
| 782 | 849 | } |
| 783 | 850 | } |
| ... | ... | @@ -874,6 +941,8 @@ pub fn Watch(comptime V: type) type { |
| 874 | 941 | } |
| 875 | 942 | |
| 876 | 943 | async fn addFileLinux(self: *Self, file_path: []const u8, value: V) !?V { |
| 944 | const value_copy = value; |
| 945 | |
| 877 | 946 | const dirname = os.path.dirname(file_path) orelse "."; |
| 878 | 947 | const dirname_with_null = try std.cstr.addNullByte(self.channel.loop.allocator, dirname); |
| 879 | 948 | var dirname_with_null_consumed = false; |
| ... | ... | @@ -896,7 +965,7 @@ pub fn Watch(comptime V: type) type { |
| 896 | 965 | |
| 897 | 966 | const gop = try self.os_data.wd_table.getOrPut(wd); |
| 898 | 967 | if (!gop.found_existing) { |
| 899 | | gop.kv.value = Dir{ |
| 968 | gop.kv.value = OsData.Dir{ |
| 900 | 969 | .dirname = dirname_with_null, |
| 901 | 970 | .file_table = OsData.FileTable.init(self.channel.loop.allocator), |
| 902 | 971 | }; |
| ... | ... | @@ -907,15 +976,201 @@ pub fn Watch(comptime V: type) type { |
| 907 | 976 | const file_table_gop = try dir.file_table.getOrPut(basename_with_null); |
| 908 | 977 | if (file_table_gop.found_existing) { |
| 909 | 978 | const prev_value = file_table_gop.kv.value; |
| 910 | | file_table_gop.kv.value = value; |
| 979 | file_table_gop.kv.value = value_copy; |
| 911 | 980 | return prev_value; |
| 912 | 981 | } else { |
| 913 | | file_table_gop.kv.value = value; |
| 982 | file_table_gop.kv.value = value_copy; |
| 914 | 983 | basename_with_null_consumed = true; |
| 915 | 984 | return null; |
| 916 | 985 | } |
| 917 | 986 | } |
| 918 | 987 | |
| 988 | async fn addFileWindows(self: *Self, file_path: []const u8, value: V) !?V { |
| 989 | const value_copy = value; |
| 990 | // TODO we might need to convert dirname and basename to canonical file paths ("short"?) |
| 991 | |
| 992 | const dirname = try std.mem.dupe(self.channel.loop.allocator, u8, os.path.dirname(file_path) orelse "."); |
| 993 | var dirname_consumed = false; |
| 994 | defer if (!dirname_consumed) self.channel.loop.allocator.free(dirname); |
| 995 | |
| 996 | const dirname_utf16le = try std.unicode.utf8ToUtf16LeWithNull(self.channel.loop.allocator, dirname); |
| 997 | defer self.channel.loop.allocator.free(dirname_utf16le); |
| 998 | |
| 999 | // TODO https://github.com/ziglang/zig/issues/265 |
| 1000 | const basename = os.path.basename(file_path); |
| 1001 | const basename_utf16le_null = try std.unicode.utf8ToUtf16LeWithNull(self.channel.loop.allocator, basename); |
| 1002 | var basename_utf16le_null_consumed = false; |
| 1003 | defer if (!basename_utf16le_null_consumed) self.channel.loop.allocator.free(basename_utf16le_null); |
| 1004 | const basename_utf16le_no_null = basename_utf16le_null[0..basename_utf16le_null.len-1]; |
| 1005 | |
| 1006 | const dir_handle = windows.CreateFileW( |
| 1007 | dirname_utf16le.ptr, |
| 1008 | windows.FILE_LIST_DIRECTORY, |
| 1009 | windows.FILE_SHARE_READ | windows.FILE_SHARE_DELETE | windows.FILE_SHARE_WRITE, |
| 1010 | null, |
| 1011 | windows.OPEN_EXISTING, |
| 1012 | windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED, |
| 1013 | null, |
| 1014 | ); |
| 1015 | if (dir_handle == windows.INVALID_HANDLE_VALUE) { |
| 1016 | const err = windows.GetLastError(); |
| 1017 | switch (err) { |
| 1018 | windows.ERROR.FILE_NOT_FOUND, |
| 1019 | windows.ERROR.PATH_NOT_FOUND, |
| 1020 | => return error.PathNotFound, |
| 1021 | else => return os.unexpectedErrorWindows(err), |
| 1022 | } |
| 1023 | } |
| 1024 | var dir_handle_consumed = false; |
| 1025 | defer if (!dir_handle_consumed) os.close(dir_handle); |
| 1026 | |
| 1027 | const held = await (async self.os_data.table_lock.acquire() catch unreachable); |
| 1028 | defer held.release(); |
| 1029 | |
| 1030 | const gop = try self.os_data.dir_table.getOrPut(dirname); |
| 1031 | if (gop.found_existing) { |
| 1032 | const dir = gop.kv.value; |
| 1033 | const held_dir_lock = await (async dir.table_lock.acquire() catch unreachable); |
| 1034 | defer held_dir_lock.release(); |
| 1035 | |
| 1036 | const file_gop = try dir.file_table.getOrPut(basename_utf16le_no_null); |
| 1037 | if (file_gop.found_existing) { |
| 1038 | const prev_value = file_gop.kv.value; |
| 1039 | file_gop.kv.value = value_copy; |
| 1040 | return prev_value; |
| 1041 | } else { |
| 1042 | file_gop.kv.value = value_copy; |
| 1043 | basename_utf16le_null_consumed = true; |
| 1044 | return null; |
| 1045 | } |
| 1046 | } else { |
| 1047 | errdefer _ = self.os_data.dir_table.remove(dirname); |
| 1048 | const dir = try self.channel.loop.allocator.createOne(OsData.Dir); |
| 1049 | errdefer self.channel.loop.allocator.destroy(dir); |
| 1050 | |
| 1051 | dir.* = OsData.Dir{ |
| 1052 | .file_table = OsData.FileTable.init(self.channel.loop.allocator), |
| 1053 | .table_lock = event.Lock.init(self.channel.loop), |
| 1054 | .putter = undefined, |
| 1055 | }; |
| 1056 | assert((try dir.file_table.put(basename_utf16le_no_null, value_copy)) == null); |
| 1057 | basename_utf16le_null_consumed = true; |
| 1058 | |
| 1059 | dir.putter = try async self.windowsDirReader(dir_handle, dir); |
| 1060 | dir_handle_consumed = true; |
| 1061 | |
| 1062 | dirname_consumed = true; |
| 1063 | |
| 1064 | return null; |
| 1065 | } |
| 1066 | } |
| 1067 | |
| 1068 | async fn windowsDirReader(self: *Self, dir_handle: windows.HANDLE, dir: *OsData.Dir) void { |
| 1069 | // TODO https://github.com/ziglang/zig/issues/1194 |
| 1070 | suspend { |
| 1071 | resume @handle(); |
| 1072 | } |
| 1073 | |
| 1074 | self.ref(); |
| 1075 | defer self.deref(); |
| 1076 | |
| 1077 | defer os.close(dir_handle); |
| 1078 | |
| 1079 | var putter_node = std.atomic.Queue(promise).Node{ |
| 1080 | .data = @handle(), |
| 1081 | .prev = null, |
| 1082 | .next = null, |
| 1083 | }; |
| 1084 | self.os_data.all_putters.put(&putter_node); |
| 1085 | defer _ = self.os_data.all_putters.remove(&putter_node); |
| 1086 | |
| 1087 | var resume_node = Loop.ResumeNode.Basic{ |
| 1088 | .base = Loop.ResumeNode{ |
| 1089 | .id = Loop.ResumeNode.Id.Basic, |
| 1090 | .handle = @handle(), |
| 1091 | }, |
| 1092 | }; |
| 1093 | const completion_key = @ptrToInt(&resume_node.base); |
| 1094 | var overlapped = windows.OVERLAPPED{ |
| 1095 | .Internal = 0, |
| 1096 | .InternalHigh = 0, |
| 1097 | .Offset = 0, |
| 1098 | .OffsetHigh = 0, |
| 1099 | .hEvent = null, |
| 1100 | }; |
| 1101 | var event_buf: [4096]u8 align(@alignOf(windows.FILE_NOTIFY_INFORMATION)) = undefined; |
| 1102 | |
| 1103 | while (true) { |
| 1104 | _ = os.windowsCreateIoCompletionPort( |
| 1105 | dir_handle, self.channel.loop.os_data.io_port, completion_key, undefined, |
| 1106 | ) catch |err| { |
| 1107 | await (async self.channel.put(err) catch unreachable); |
| 1108 | return; |
| 1109 | }; |
| 1110 | { |
| 1111 | // TODO only 1 beginOneEvent for the whole coroutine |
| 1112 | self.channel.loop.beginOneEvent(); |
| 1113 | errdefer self.channel.loop.finishOneEvent(); |
| 1114 | suspend { |
| 1115 | _ = windows.ReadDirectoryChangesW( |
| 1116 | dir_handle, |
| 1117 | &event_buf, |
| 1118 | @intCast(windows.DWORD, event_buf.len), |
| 1119 | windows.FALSE, // watch subtree |
| 1120 | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME | |
| 1121 | windows.FILE_NOTIFY_CHANGE_ATTRIBUTES | windows.FILE_NOTIFY_CHANGE_SIZE | |
| 1122 | windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_LAST_ACCESS | |
| 1123 | windows.FILE_NOTIFY_CHANGE_CREATION | windows.FILE_NOTIFY_CHANGE_SECURITY, |
| 1124 | null, // number of bytes transferred (unused for async) |
| 1125 | &overlapped, |
| 1126 | null, // completion routine - unused because we use IOCP |
| 1127 | ); |
| 1128 | } |
| 1129 | } |
| 1130 | var bytes_transferred: windows.DWORD = undefined; |
| 1131 | if (windows.GetOverlappedResult(dir_handle, &overlapped, &bytes_transferred, windows.FALSE) == 0) { |
| 1132 | const errno = windows.GetLastError(); |
| 1133 | const err = switch (errno) { |
| 1134 | else => os.unexpectedErrorWindows(errno), |
| 1135 | }; |
| 1136 | await (async self.channel.put(err) catch unreachable); |
| 1137 | } else { |
| 1138 | // can't use @bytesToSlice because of the special variable length name field |
| 1139 | var ptr = event_buf[0..].ptr; |
| 1140 | const end_ptr = ptr + bytes_transferred; |
| 1141 | var ev: *windows.FILE_NOTIFY_INFORMATION = undefined; |
| 1142 | while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) : (ptr += ev.NextEntryOffset) { |
| 1143 | ev = @ptrCast(*windows.FILE_NOTIFY_INFORMATION, ptr); |
| 1144 | const emit = switch (ev.Action) { |
| 1145 | windows.FILE_ACTION_REMOVED => WatchEventId.Delete, |
| 1146 | windows.FILE_ACTION_MODIFIED => WatchEventId.CloseWrite, |
| 1147 | else => null, |
| 1148 | }; |
| 1149 | if (emit) |id| { |
| 1150 | const basename_utf16le = ([*]u16)(&ev.FileName)[0..ev.FileNameLength/2]; |
| 1151 | const user_value = blk: { |
| 1152 | const held = await (async dir.table_lock.acquire() catch unreachable); |
| 1153 | defer held.release(); |
| 1154 | |
| 1155 | if (dir.file_table.get(basename_utf16le)) |entry| { |
| 1156 | break :blk entry.value; |
| 1157 | } else { |
| 1158 | break :blk null; |
| 1159 | } |
| 1160 | }; |
| 1161 | if (user_value) |v| { |
| 1162 | await (async self.channel.put(Event{ |
| 1163 | .id = id, |
| 1164 | .data = v, |
| 1165 | }) catch unreachable); |
| 1166 | } |
| 1167 | } |
| 1168 | if (ev.NextEntryOffset == 0) break; |
| 1169 | } |
| 1170 | } |
| 1171 | } |
| 1172 | } |
| 1173 | |
| 919 | 1174 | pub async fn removeFile(self: *Self, file_path: []const u8) ?V { |
| 920 | 1175 | @panic("TODO"); |
| 921 | 1176 | } |
| ... | ... | @@ -933,7 +1188,7 @@ pub fn Watch(comptime V: type) type { |
| 933 | 1188 | .os_data = OsData{ |
| 934 | 1189 | .putter = @handle(), |
| 935 | 1190 | .inotify_fd = inotify_fd, |
| 936 | | .wd_table = WdTable.init(loop.allocator), |
| 1191 | .wd_table = OsData.WdTable.init(loop.allocator), |
| 937 | 1192 | .table_lock = event.Lock.init(loop), |
| 938 | 1193 | }, |
| 939 | 1194 | }; |
| ... | ... | @@ -1065,15 +1320,15 @@ async fn testFsWatch(loop: *Loop) !void { |
| 1065 | 1320 | const read_contents = try await try async readFile(loop, file_path, 1024 * 1024); |
| 1066 | 1321 | assert(mem.eql(u8, read_contents, contents)); |
| 1067 | 1322 | |
| 1068 | | //// now watch the file |
| 1069 | | //var watch = try Watch(void).create(loop, 0); |
| 1070 | | //defer watch.destroy(); |
| 1323 | // now watch the file |
| 1324 | var watch = try Watch(void).create(loop, 0); |
| 1325 | defer watch.destroy(); |
| 1071 | 1326 | |
| 1072 | | //assert((try await try async watch.addFile(file_path, {})) == null); |
| 1327 | assert((try await try async watch.addFile(file_path, {})) == null); |
| 1073 | 1328 | |
| 1074 | | //const ev = try async watch.channel.get(); |
| 1075 | | //var ev_consumed = false; |
| 1076 | | //defer if (!ev_consumed) cancel ev; |
| 1329 | const ev = try async watch.channel.get(); |
| 1330 | var ev_consumed = false; |
| 1331 | defer if (!ev_consumed) cancel ev; |
| 1077 | 1332 | |
| 1078 | 1333 | // overwrite line 2 |
| 1079 | 1334 | const fd = try await try async openReadWrite(loop, file_path, os.File.default_mode); |
| ... | ... | @@ -1083,11 +1338,11 @@ async fn testFsWatch(loop: *Loop) !void { |
| 1083 | 1338 | try await try async pwritev(loop, fd, []const []const u8{"lorem ipsum"}, line2_offset); |
| 1084 | 1339 | } |
| 1085 | 1340 | |
| 1086 | | //ev_consumed = true; |
| 1087 | | //switch ((try await ev).id) { |
| 1088 | | // WatchEventId.CloseWrite => {}, |
| 1089 | | // WatchEventId.Delete => @panic("wrong event"), |
| 1090 | | //} |
| 1341 | ev_consumed = true; |
| 1342 | switch ((try await ev).id) { |
| 1343 | WatchEventId.CloseWrite => {}, |
| 1344 | WatchEventId.Delete => @panic("wrong event"), |
| 1345 | } |
| 1091 | 1346 | |
| 1092 | 1347 | const contents_updated = try await try async readFile(loop, file_path, 1024 * 1024); |
| 1093 | 1348 | assert(mem.eql(u8, contents_updated, |