1const std = @import("std"); 2const builtin = @import("builtin"); 3const event = std.event; 4const assert = std.debug.assert; 5const testing = std.testing; 6const os = std.os; 7const mem = std.mem; 8const windows = os.windows; 9const Loop = event.Loop; 10const fd_t = os.fd_t; 11const File = std.fs.File; 12const Allocator = mem.Allocator; 13 14const global_event_loop = Loop.instance orelse 15 @compileError("std.fs.Watch currently only works with event-based I/O"); 16 17const WatchEventId = enum { 18 CloseWrite, 19 Delete, 20}; 21 22const WatchEventError = error{ 23 UserResourceLimitReached, 24 SystemResources, 25 AccessDenied, 26 Unexpected, // TODO remove this possibility 27}; 28 29pub fn Watch(comptime V: type) type { 30 return struct { 31 channel: event.Channel(Event.Error!Event), 32 os_data: OsData, 33 allocator: Allocator, 34 35 const OsData = switch (builtin.os.tag) { 36 // TODO https://github.com/ziglang/zig/issues/3778 37 .macos, .freebsd, .netbsd, .dragonfly, .openbsd => KqOsData, 38 .linux => LinuxOsData, 39 .windows => WindowsOsData, 40 41 else => @compileError("Unsupported OS"), 42 }; 43 44 const KqOsData = struct { 45 table_lock: event.Lock, 46 file_table: FileTable, 47 48 const FileTable = std.StringHashMapUnmanaged(*Put); 49 const Put = struct { 50 putter_frame: @Frame(kqPutEvents), 51 cancelled: bool = false, 52 value: V, 53 }; 54 }; 55 56 const WindowsOsData = struct { 57 table_lock: event.Lock, 58 dir_table: DirTable, 59 cancelled: bool = false, 60 61 const DirTable = std.StringHashMapUnmanaged(*Dir); 62 const FileTable = std.StringHashMapUnmanaged(V); 63 64 const Dir = struct { 65 putter_frame: @Frame(windowsDirReader), 66 file_table: FileTable, 67 dir_handle: os.windows.HANDLE, 68 }; 69 }; 70 71 const LinuxOsData = struct { 72 putter_frame: @Frame(linuxEventPutter), 73 inotify_fd: i32, 74 wd_table: WdTable, 75 table_lock: event.Lock, 76 cancelled: bool = false, 77 78 const WdTable = std.AutoHashMapUnmanaged(i32, Dir); 79 const FileTable = std.StringHashMapUnmanaged(V); 80 81 const Dir = struct { 82 dirname: []const u8, 83 file_table: FileTable, 84 }; 85 }; 86 87 const Self = @This(); 88 89 pub const Event = struct { 90 id: Id, 91 data: V, 92 dirname: []const u8, 93 basename: []const u8, 94 95 pub const Id = WatchEventId; 96 pub const Error = WatchEventError; 97 }; 98 99 pub fn init(allocator: Allocator, event_buf_count: usize) !*Self { 100 const self = try allocator.create(Self); 101 errdefer allocator.destroy(self); 102 103 switch (builtin.os.tag) { 104 .linux => { 105 const inotify_fd = try os.inotify_init1(os.linux.IN_NONBLOCK | os.linux.IN_CLOEXEC); 106 errdefer os.close(inotify_fd); 107 108 self.* = Self{ 109 .allocator = allocator, 110 .channel = undefined, 111 .os_data = OsData{ 112 .putter_frame = undefined, 113 .inotify_fd = inotify_fd, 114 .wd_table = OsData.WdTable.init(allocator), 115 .table_lock = event.Lock{}, 116 }, 117 }; 118 119 var buf = try allocator.alloc(Event.Error!Event, event_buf_count); 120 self.channel.init(buf); 121 self.os_data.putter_frame = async self.linuxEventPutter(); 122 return self; 123 }, 124 125 .windows => { 126 self.* = Self{ 127 .allocator = allocator, 128 .channel = undefined, 129 .os_data = OsData{ 130 .table_lock = event.Lock{}, 131 .dir_table = OsData.DirTable.init(allocator), 132 }, 133 }; 134 135 var buf = try allocator.alloc(Event.Error!Event, event_buf_count); 136 self.channel.init(buf); 137 return self; 138 }, 139 140 .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { 141 self.* = Self{ 142 .allocator = allocator, 143 .channel = undefined, 144 .os_data = OsData{ 145 .table_lock = event.Lock{}, 146 .file_table = OsData.FileTable.init(allocator), 147 }, 148 }; 149 150 var buf = try allocator.alloc(Event.Error!Event, event_buf_count); 151 self.channel.init(buf); 152 return self; 153 }, 154 else => @compileError("Unsupported OS"), 155 } 156 } 157 158 pub fn deinit(self: *Self) void { 159 switch (builtin.os.tag) { 160 .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { 161 var it = self.os_data.file_table.iterator(); 162 while (it.next()) |entry| { 163 const key = entry.key_ptr.*; 164 const value = entry.value_ptr.*; 165 value.cancelled = true; 166 // @TODO Close the fd here? 167 await value.putter_frame; 168 self.allocator.free(key); 169 self.allocator.destroy(value); 170 } 171 }, 172 .linux => { 173 self.os_data.cancelled = true; 174 { 175 // Remove all directory watches linuxEventPutter will take care of 176 // cleaning up the memory and closing the inotify fd. 177 var dir_it = self.os_data.wd_table.keyIterator(); 178 while (dir_it.next()) |wd_key| { 179 const rc = os.linux.inotify_rm_watch(self.os_data.inotify_fd, wd_key.*); 180 // Errno can only be EBADF, EINVAL if either the inotify fs or the wd are invalid 181 std.debug.assert(rc == 0); 182 } 183 } 184 await self.os_data.putter_frame; 185 }, 186 .windows => { 187 self.os_data.cancelled = true; 188 var dir_it = self.os_data.dir_table.iterator(); 189 while (dir_it.next()) |dir_entry| { 190 if (windows.kernel32.CancelIoEx(dir_entry.value.dir_handle, null) != 0) { 191 // We canceled the pending ReadDirectoryChangesW operation, but our 192 // frame is still suspending, now waiting indefinitely. 193 // Thus, it is safe to resume it ourslves 194 resume dir_entry.value.putter_frame; 195 } else { 196 std.debug.assert(windows.kernel32.GetLastError() == .NOT_FOUND); 197 // We are at another suspend point, we can await safely for the 198 // function to exit the loop 199 await dir_entry.value.putter_frame; 200 } 201 202 self.allocator.free(dir_entry.key_ptr.*); 203 var file_it = dir_entry.value.file_table.keyIterator(); 204 while (file_it.next()) |file_entry| { 205 self.allocator.free(file_entry.*); 206 } 207 dir_entry.value.file_table.deinit(self.allocator); 208 self.allocator.destroy(dir_entry.value_ptr.*); 209 } 210 self.os_data.dir_table.deinit(self.allocator); 211 }, 212 else => @compileError("Unsupported OS"), 213 } 214 self.allocator.free(self.channel.buffer_nodes); 215 self.channel.deinit(); 216 self.allocator.destroy(self); 217 } 218 219 pub fn addFile(self: *Self, file_path: []const u8, value: V) !?V { 220 switch (builtin.os.tag) { 221 .macos, .freebsd, .netbsd, .dragonfly, .openbsd => return addFileKEvent(self, file_path, value), 222 .linux => return addFileLinux(self, file_path, value), 223 .windows => return addFileWindows(self, file_path, value), 224 else => @compileError("Unsupported OS"), 225 } 226 } 227 228 fn addFileKEvent(self: *Self, file_path: []const u8, value: V) !?V { 229 var realpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 230 const realpath = try os.realpath(file_path, &realpath_buf); 231 232 const held = self.os_data.table_lock.acquire(); 233 defer held.release(); 234 235 const gop = try self.os_data.file_table.getOrPut(self.allocator, realpath); 236 errdefer assert(self.os_data.file_table.remove(realpath)); 237 if (gop.found_existing) { 238 const prev_value = gop.value_ptr.value; 239 gop.value_ptr.value = value; 240 return prev_value; 241 } 242 243 gop.key_ptr.* = try self.allocator.dupe(u8, realpath); 244 errdefer self.allocator.free(gop.key_ptr.*); 245 gop.value_ptr.* = try self.allocator.create(OsData.Put); 246 errdefer self.allocator.destroy(gop.value_ptr.*); 247 gop.value_ptr.* = .{ 248 .putter_frame = undefined, 249 .value = value, 250 }; 251 252 // @TODO Can I close this fd and get an error from bsdWaitKev? 253 const flags = if (comptime builtin.target.isDarwin()) os.O.SYMLINK | os.O.EVTONLY else 0; 254 const fd = try os.open(realpath, flags, 0); 255 gop.value_ptr.putter_frame = async self.kqPutEvents(fd, gop.key_ptr.*, gop.value_ptr.*); 256 return null; 257 } 258 259 fn kqPutEvents(self: *Self, fd: os.fd_t, file_path: []const u8, put: *OsData.Put) void { 260 global_event_loop.beginOneEvent(); 261 defer { 262 global_event_loop.finishOneEvent(); 263 // @TODO: Remove this if we force close otherwise 264 os.close(fd); 265 } 266 267 // We need to manually do a bsdWaitKev to access the fflags. 268 var resume_node = event.Loop.ResumeNode.Basic{ 269 .base = .{ 270 .id = .Basic, 271 .handle = @frame(), 272 .overlapped = event.Loop.ResumeNode.overlapped_init, 273 }, 274 .kev = undefined, 275 }; 276 277 var kevs = [1]os.Kevent{undefined}; 278 const kev = &kevs[0]; 279 280 while (!put.cancelled) { 281 kev.* = os.Kevent{ 282 .ident = @intCast(usize, fd), 283 .filter = os.EVFILT_VNODE, 284 .flags = os.EV_ADD | os.EV_ENABLE | os.EV_CLEAR | os.EV_ONESHOT | 285 os.NOTE_WRITE | os.NOTE_DELETE | os.NOTE_REVOKE, 286 .fflags = 0, 287 .data = 0, 288 .udata = @ptrToInt(&resume_node.base), 289 }; 290 suspend { 291 global_event_loop.beginOneEvent(); 292 errdefer global_event_loop.finishOneEvent(); 293 294 const empty_kevs = &[0]os.Kevent{}; 295 _ = os.kevent(global_event_loop.os_data.kqfd, &kevs, empty_kevs, null) catch |err| switch (err) { 296 error.EventNotFound, 297 error.ProcessNotFound, 298 error.Overflow, 299 => unreachable, 300 error.AccessDenied, error.SystemResources => |e| { 301 self.channel.put(e); 302 continue; 303 }, 304 }; 305 } 306 307 if (kev.flags & os.EV_ERROR != 0) { 308 self.channel.put(os.unexpectedErrno(os.errno(kev.data))); 309 continue; 310 } 311 312 if (kev.fflags & os.NOTE_DELETE != 0 or kev.fflags & os.NOTE_REVOKE != 0) { 313 self.channel.put(Self.Event{ 314 .id = .Delete, 315 .data = put.value, 316 .dirname = std.fs.path.dirname(file_path) orelse "/", 317 .basename = std.fs.path.basename(file_path), 318 }); 319 } else if (kev.fflags & os.NOTE_WRITE != 0) { 320 self.channel.put(Self.Event{ 321 .id = .CloseWrite, 322 .data = put.value, 323 .dirname = std.fs.path.dirname(file_path) orelse "/", 324 .basename = std.fs.path.basename(file_path), 325 }); 326 } 327 } 328 } 329 330 fn addFileLinux(self: *Self, file_path: []const u8, value: V) !?V { 331 const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; 332 const basename = std.fs.path.basename(file_path); 333 334 const wd = try os.inotify_add_watch( 335 self.os_data.inotify_fd, 336 dirname, 337 os.linux.IN_CLOSE_WRITE | os.linux.IN_ONLYDIR | os.linux.IN_DELETE | os.linux.IN_EXCL_UNLINK, 338 ); 339 // wd is either a newly created watch or an existing one. 340 341 const held = self.os_data.table_lock.acquire(); 342 defer held.release(); 343 344 const gop = try self.os_data.wd_table.getOrPut(self.allocator, wd); 345 errdefer assert(self.os_data.wd_table.remove(wd)); 346 if (!gop.found_existing) { 347 gop.value_ptr.* = OsData.Dir{ 348 .dirname = try self.allocator.dupe(u8, dirname), 349 .file_table = OsData.FileTable.init(self.allocator), 350 }; 351 } 352 353 const dir = gop.value_ptr; 354 const file_table_gop = try dir.file_table.getOrPut(self.allocator, basename); 355 errdefer assert(dir.file_table.remove(basename)); 356 if (file_table_gop.found_existing) { 357 const prev_value = file_table_gop.value_ptr.*; 358 file_table_gop.value_ptr.* = value; 359 return prev_value; 360 } else { 361 file_table_gop.key_ptr.* = try self.allocator.dupe(u8, basename); 362 file_table_gop.value_ptr.* = value; 363 return null; 364 } 365 } 366 367 fn addFileWindows(self: *Self, file_path: []const u8, value: V) !?V { 368 // TODO we might need to convert dirname and basename to canonical file paths ("short"?) 369 const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; 370 var dirname_path_space: windows.PathSpace = undefined; 371 dirname_path_space.len = try std.unicode.utf8ToUtf16Le(&dirname_path_space.data, dirname); 372 dirname_path_space.data[dirname_path_space.len] = 0; 373 374 const basename = std.fs.path.basename(file_path); 375 var basename_path_space: windows.PathSpace = undefined; 376 basename_path_space.len = try std.unicode.utf8ToUtf16Le(&basename_path_space.data, basename); 377 basename_path_space.data[basename_path_space.len] = 0; 378 379 const held = self.os_data.table_lock.acquire(); 380 defer held.release(); 381 382 const gop = try self.os_data.dir_table.getOrPut(self.allocator, dirname); 383 errdefer assert(self.os_data.dir_table.remove(dirname)); 384 if (gop.found_existing) { 385 const dir = gop.value_ptr.*; 386 387 const file_gop = try dir.file_table.getOrPut(self.allocator, basename); 388 errdefer assert(dir.file_table.remove(basename)); 389 if (file_gop.found_existing) { 390 const prev_value = file_gop.value_ptr.*; 391 file_gop.value_ptr.* = value; 392 return prev_value; 393 } else { 394 file_gop.value_ptr.* = value; 395 file_gop.key_ptr.* = try self.allocator.dupe(u8, basename); 396 return null; 397 } 398 } else { 399 const dir_handle = try windows.OpenFile(dirname_path_space.span(), .{ 400 .dir = std.fs.cwd().fd, 401 .access_mask = windows.FILE_LIST_DIRECTORY, 402 .creation = windows.FILE_OPEN, 403 .io_mode = .evented, 404 .open_dir = true, 405 }); 406 errdefer windows.CloseHandle(dir_handle); 407 408 const dir = try self.allocator.create(OsData.Dir); 409 errdefer self.allocator.destroy(dir); 410 411 gop.key_ptr.* = try self.allocator.dupe(u8, dirname); 412 errdefer self.allocator.free(gop.key_ptr.*); 413 414 dir.* = OsData.Dir{ 415 .file_table = OsData.FileTable.init(self.allocator), 416 .putter_frame = undefined, 417 .dir_handle = dir_handle, 418 }; 419 gop.value_ptr.* = dir; 420 try dir.file_table.put(self.allocator, try self.allocator.dupe(u8, basename), value); 421 dir.putter_frame = async self.windowsDirReader(dir, gop.key_ptr.*); 422 return null; 423 } 424 } 425 426 fn windowsDirReader(self: *Self, dir: *OsData.Dir, dirname: []const u8) void { 427 defer os.close(dir.dir_handle); 428 var resume_node = Loop.ResumeNode.Basic{ 429 .base = Loop.ResumeNode{ 430 .id = .Basic, 431 .handle = @frame(), 432 .overlapped = windows.OVERLAPPED{ 433 .Internal = 0, 434 .InternalHigh = 0, 435 .DUMMYUNIONNAME = .{ 436 .DUMMYSTRUCTNAME = .{ 437 .Offset = 0, 438 .OffsetHigh = 0, 439 }, 440 }, 441 .hEvent = null, 442 }, 443 }, 444 }; 445 446 var event_buf: [4096]u8 align(@alignOf(windows.FILE_NOTIFY_INFORMATION)) = undefined; 447 448 global_event_loop.beginOneEvent(); 449 defer global_event_loop.finishOneEvent(); 450 451 while (!self.os_data.cancelled) main_loop: { 452 suspend { 453 _ = windows.kernel32.ReadDirectoryChangesW( 454 dir.dir_handle, 455 &event_buf, 456 event_buf.len, 457 windows.FALSE, // watch subtree 458 windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME | 459 windows.FILE_NOTIFY_CHANGE_ATTRIBUTES | windows.FILE_NOTIFY_CHANGE_SIZE | 460 windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_LAST_ACCESS | 461 windows.FILE_NOTIFY_CHANGE_CREATION | windows.FILE_NOTIFY_CHANGE_SECURITY, 462 null, // number of bytes transferred (unused for async) 463 &resume_node.base.overlapped, 464 null, // completion routine - unused because we use IOCP 465 ); 466 } 467 468 var bytes_transferred: windows.DWORD = undefined; 469 if (windows.kernel32.GetOverlappedResult( 470 dir.dir_handle, 471 &resume_node.base.overlapped, 472 &bytes_transferred, 473 windows.FALSE, 474 ) == 0) { 475 const potential_error = windows.kernel32.GetLastError(); 476 const err = switch (potential_error) { 477 .OPERATION_ABORTED, .IO_INCOMPLETE => err_blk: { 478 if (self.os_data.cancelled) 479 break :main_loop 480 else 481 break :err_blk windows.unexpectedError(potential_error); 482 }, 483 else => |err| windows.unexpectedError(err), 484 }; 485 self.channel.put(err); 486 } else { 487 var ptr: [*]u8 = &event_buf; 488 const end_ptr = ptr + bytes_transferred; 489 while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) { 490 const ev = @ptrCast(*const windows.FILE_NOTIFY_INFORMATION, ptr); 491 const emit = switch (ev.Action) { 492 windows.FILE_ACTION_REMOVED => WatchEventId.Delete, 493 windows.FILE_ACTION_MODIFIED => .CloseWrite, 494 else => null, 495 }; 496 if (emit) |id| { 497 const basename_ptr = @ptrCast([*]u16, ptr + @sizeOf(windows.FILE_NOTIFY_INFORMATION)); 498 const basename_utf16le = basename_ptr[0 .. ev.FileNameLength / 2]; 499 var basename_data: [std.fs.MAX_PATH_BYTES]u8 = undefined; 500 const basename = basename_data[0 .. std.unicode.utf16leToUtf8(&basename_data, basename_utf16le) catch unreachable]; 501 502 if (dir.file_table.getEntry(basename)) |entry| { 503 self.channel.put(Event{ 504 .id = id, 505 .data = entry.value_ptr.*, 506 .dirname = dirname, 507 .basename = entry.key_ptr.*, 508 }); 509 } 510 } 511 512 if (ev.NextEntryOffset == 0) break; 513 ptr = @alignCast(@alignOf(windows.FILE_NOTIFY_INFORMATION), ptr + ev.NextEntryOffset); 514 } 515 } 516 } 517 } 518 519 pub fn removeFile(self: *Self, file_path: []const u8) !?V { 520 switch (builtin.os.tag) { 521 .linux => { 522 const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; 523 const basename = std.fs.path.basename(file_path); 524 525 const held = self.os_data.table_lock.acquire(); 526 defer held.release(); 527 528 const dir = self.os_data.wd_table.get(dirname) orelse return null; 529 if (dir.file_table.fetchRemove(basename)) |file_entry| { 530 self.allocator.free(file_entry.key); 531 return file_entry.value; 532 } 533 return null; 534 }, 535 .windows => { 536 const dirname = std.fs.path.dirname(file_path) orelse if (file_path[0] == '/') "/" else "."; 537 const basename = std.fs.path.basename(file_path); 538 539 const held = self.os_data.table_lock.acquire(); 540 defer held.release(); 541 542 const dir = self.os_data.dir_table.get(dirname) orelse return null; 543 if (dir.file_table.fetchRemove(basename)) |file_entry| { 544 self.allocator.free(file_entry.key); 545 return file_entry.value; 546 } 547 return null; 548 }, 549 .macos, .freebsd, .netbsd, .dragonfly, .openbsd => { 550 var realpath_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; 551 const realpath = try os.realpath(file_path, &realpath_buf); 552 553 const held = self.os_data.table_lock.acquire(); 554 defer held.release(); 555 556 const entry = self.os_data.file_table.getEntry(realpath) orelse return null; 557 entry.value_ptr.cancelled = true; 558 // @TODO Close the fd here? 559 await entry.value_ptr.putter_frame; 560 self.allocator.free(entry.key_ptr.*); 561 self.allocator.destroy(entry.value_ptr.*); 562 563 assert(self.os_data.file_table.remove(realpath)); 564 }, 565 else => @compileError("Unsupported OS"), 566 } 567 } 568 569 fn linuxEventPutter(self: *Self) void { 570 global_event_loop.beginOneEvent(); 571 572 defer { 573 std.debug.assert(self.os_data.wd_table.count() == 0); 574 self.os_data.wd_table.deinit(self.allocator); 575 os.close(self.os_data.inotify_fd); 576 self.allocator.free(self.channel.buffer_nodes); 577 self.channel.deinit(); 578 global_event_loop.finishOneEvent(); 579 } 580 581 var event_buf: [4096]u8 align(@alignOf(os.linux.inotify_event)) = undefined; 582 583 while (!self.os_data.cancelled) { 584 const bytes_read = global_event_loop.read(self.os_data.inotify_fd, &event_buf, false) catch unreachable; 585 586 var ptr: [*]u8 = &event_buf; 587 const end_ptr = ptr + bytes_read; 588 while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) { 589 const ev = @ptrCast(*const os.linux.inotify_event, ptr); 590 if (ev.mask & os.linux.IN_CLOSE_WRITE == os.linux.IN_CLOSE_WRITE) { 591 const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); 592 const basename = std.mem.span(@ptrCast([*:0]u8, basename_ptr)); 593 594 const dir = &self.os_data.wd_table.get(ev.wd).?; 595 if (dir.file_table.getEntry(basename)) |file_value| { 596 self.channel.put(Event{ 597 .id = .CloseWrite, 598 .data = file_value.value_ptr.*, 599 .dirname = dir.dirname, 600 .basename = file_value.key_ptr.*, 601 }); 602 } 603 } else if (ev.mask & os.linux.IN_IGNORED == os.linux.IN_IGNORED) { 604 // Directory watch was removed 605 const held = self.os_data.table_lock.acquire(); 606 defer held.release(); 607 if (self.os_data.wd_table.fetchRemove(ev.wd)) |wd_entry| { 608 var file_it = wd_entry.value.file_table.keyIterator(); 609 while (file_it.next()) |file_entry| { 610 self.allocator.free(file_entry.*); 611 } 612 self.allocator.free(wd_entry.value.dirname); 613 wd_entry.value.file_table.deinit(self.allocator); 614 } 615 } else if (ev.mask & os.linux.IN_DELETE == os.linux.IN_DELETE) { 616 // File or directory was removed or deleted 617 const basename_ptr = ptr + @sizeOf(os.linux.inotify_event); 618 const basename = std.mem.span(@ptrCast([*:0]u8, basename_ptr)); 619 620 const dir = &self.os_data.wd_table.get(ev.wd).?; 621 if (dir.file_table.getEntry(basename)) |file_value| { 622 self.channel.put(Event{ 623 .id = .Delete, 624 .data = file_value.value_ptr.*, 625 .dirname = dir.dirname, 626 .basename = file_value.key_ptr.*, 627 }); 628 } 629 } 630 631 ptr = @alignCast(@alignOf(os.linux.inotify_event), ptr + @sizeOf(os.linux.inotify_event) + ev.len); 632 } 633 } 634 } 635 }; 636} 637 638const test_tmp_dir = "std_event_fs_test"; 639 640test "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; 644 645 try std.fs.cwd().makePath(test_tmp_dir); 646 defer std.fs.cwd().deleteTree(test_tmp_dir) catch {}; 647 648 return testWriteWatchWriteDelete(std.testing.allocator); 649} 650 651fn testWriteWatchWriteDelete(allocator: Allocator) !void { 652 const file_path = try std.fs.path.join(allocator, &[_][]const u8{ test_tmp_dir, "file.txt" }); 653 defer allocator.free(file_path); 654 655 const contents = 656 \\line 1 657 \\line 2 658 ; 659 const line2_offset = 7; 660 661 // first just write then read the file 662 try std.fs.cwd().writeFile(file_path, contents); 663 664 const read_contents = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); 665 defer allocator.free(read_contents); 666 try testing.expectEqualSlices(u8, contents, read_contents); 667 668 // now watch the file 669 var watch = try Watch(void).init(allocator, 0); 670 defer watch.deinit(); 671 672 try testing.expect((try watch.addFile(file_path, {})) == null); 673 674 var ev = async watch.channel.get(); 675 var ev_consumed = false; 676 defer if (!ev_consumed) { 677 _ = await ev; 678 }; 679 680 // overwrite line 2 681 const file = try std.fs.cwd().openFile(file_path, .{ .read = true, .write = true }); 682 { 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); 690 } 691 692 switch ((try await ev).id) { 693 .CloseWrite => { 694 ev_consumed = true; 695 }, 696 .Delete => @panic("wrong event"), 697 } 698 699 const contents_updated = try std.fs.cwd().readFileAlloc(allocator, file_path, 1024 * 1024); 700 defer allocator.free(contents_updated); 701 702 try testing.expectEqualSlices(u8, 703 \\line 1 704 \\lorem ipsum 705 , contents_updated); 706 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 } 717} 718 719// TODO Test: Add another file watch, remove the old file watch, get an event in the new 720