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