Commit 3c8d4e04ea

Andrew Kelley <superjoe30@gmail.com>
2018-07-30 05:27:21
std: file system watching for linux
1 parent a870228
Changed files (6)
std/event/fs.zig
@@ -20,17 +20,18 @@ pub const Request = struct {
         PWriteV: PWriteV,
         PReadV: PReadV,
         OpenRead: OpenRead,
+        OpenRW: OpenRW,
         Close: Close,
         WriteFile: WriteFile,
         End, // special - means the fs thread should exit
 
         pub const PWriteV = struct {
             fd: os.FileHandle,
-            data: []const []const u8,
+            iov: []os.linux.iovec_const,
             offset: usize,
             result: Error!void,
 
-            pub const Error = error{};
+            pub const Error = os.File.WriteError;
         };
 
         pub const PReadV = struct {
@@ -50,6 +51,15 @@ pub const Request = struct {
             pub const Error = os.File.OpenError;
         };
 
+        pub const OpenRW = struct {
+            /// must be null terminated. TODO https://github.com/ziglang/zig/issues/265
+            path: []const u8,
+            result: Error!os.FileHandle,
+            mode: os.File.Mode,
+
+            pub const Error = os.File.OpenError;
+        };
+
         pub const WriteFile = struct {
             /// must be null terminated. TODO https://github.com/ziglang/zig/issues/265
             path: []const u8,
@@ -66,7 +76,7 @@ pub const Request = struct {
     };
 };
 
-/// data - both the outer and inner references - must live until pwritev promise completes.
+/// data - just the inner references - must live until pwritev promise completes.
 pub async fn pwritev(loop: *event.Loop, fd: os.FileHandle, offset: usize, data: []const []const u8) !void {
     //const data_dupe = try mem.dupe(loop.allocator, []const u8, data);
     //defer loop.allocator.free(data_dupe);
@@ -78,13 +88,23 @@ pub async fn pwritev(loop: *event.Loop, fd: os.FileHandle, offset: usize, data:
         resume p;
     }
 
+    const iovecs = try loop.allocator.alloc(os.linux.iovec_const, data.len);
+    defer loop.allocator.free(iovecs);
+
+    for (data) |buf, i| {
+        iovecs[i] = os.linux.iovec_const{
+            .iov_base = buf.ptr,
+            .iov_len = buf.len,
+        };
+    }
+
     var req_node = RequestNode{
         .next = undefined,
         .data = Request{
             .msg = Request.Msg{
                 .PWriteV = Request.Msg.PWriteV{
                     .fd = fd,
-                    .data = data,
+                    .iov = iovecs,
                     .offset = offset,
                     .result = undefined,
                 },
@@ -162,12 +182,15 @@ pub async fn openRead(loop: *event.Loop, path: []const u8) os.File.OpenError!os.
         resume p;
     }
 
+    const path_with_null = try std.cstr.addNullByte(loop.allocator, path);
+    defer loop.allocator.free(path_with_null);
+
     var req_node = RequestNode{
         .next = undefined,
         .data = Request{
             .msg = Request.Msg{
                 .OpenRead = Request.Msg.OpenRead{
-                    .path = path,
+                    .path = path_with_null[0..path.len],
                     .result = undefined,
                 },
             },
@@ -187,6 +210,48 @@ pub async fn openRead(loop: *event.Loop, path: []const u8) os.File.OpenError!os.
     return req_node.data.msg.OpenRead.result;
 }
 
+/// Creates if does not exist. Does not truncate.
+pub async fn openReadWrite(
+    loop: *event.Loop,
+    path: []const u8,
+    mode: os.File.Mode,
+) os.File.OpenError!os.FileHandle {
+    // workaround for https://github.com/ziglang/zig/issues/1194
+    var my_handle: promise = undefined;
+    suspend |p| {
+        my_handle = p;
+        resume p;
+    }
+
+    const path_with_null = try std.cstr.addNullByte(loop.allocator, path);
+    defer loop.allocator.free(path_with_null);
+
+    var req_node = RequestNode{
+        .next = undefined,
+        .data = Request{
+            .msg = Request.Msg{
+                .OpenRW = Request.Msg.OpenRW{
+                    .path = path_with_null[0..path.len],
+                    .mode = mode,
+                    .result = undefined,
+                },
+            },
+            .finish = Request.Finish{
+                .TickNode = event.Loop.NextTickNode{
+                    .next = undefined,
+                    .data = my_handle,
+                },
+            },
+        },
+    };
+
+    suspend |_| {
+        loop.linuxFsRequest(&req_node);
+    }
+
+    return req_node.data.msg.OpenRW.result;
+}
+
 /// This abstraction helps to close file handles in defer expressions
 /// without suspending. Start a CloseOperation before opening a file.
 pub const CloseOperation = struct {
@@ -302,6 +367,113 @@ pub async fn readFile(loop: *event.Loop, file_path: []const u8, max_size: usize)
     }
 }
 
+pub const Watch = struct {
+    channel: *event.Channel(Event),
+    putter: promise,
+
+    pub const Event = union(enum) {
+        CloseWrite,
+        Err: Error,
+    };
+
+    pub const Error = error{
+        UserResourceLimitReached,
+        SystemResources,
+    };
+
+    pub fn destroy(self: *Watch) void {
+        // TODO https://github.com/ziglang/zig/issues/1261
+        cancel self.putter;
+    }
+};
+
+pub fn watchFile(loop: *event.Loop, file_path: []const u8) !*Watch {
+    const path_with_null = try std.cstr.addNullByte(loop.allocator, file_path);
+    defer loop.allocator.free(path_with_null);
+
+    const inotify_fd = try os.linuxINotifyInit1(os.linux.IN_NONBLOCK | os.linux.IN_CLOEXEC);
+    errdefer os.close(inotify_fd);
+
+    const wd = try os.linuxINotifyAddWatchC(inotify_fd, path_with_null.ptr, os.linux.IN_CLOSE_WRITE);
+    errdefer os.close(wd);
+
+    const channel = try event.Channel(Watch.Event).create(loop, 0);
+    errdefer channel.destroy();
+
+    var result: *Watch = undefined;
+    _ = try async<loop.allocator> watchEventPutter(inotify_fd, wd, channel, &result);
+    return result;
+}
+
+async fn watchEventPutter(inotify_fd: i32, wd: i32, channel: *event.Channel(Watch.Event), out_watch: **Watch) void {
+    // TODO https://github.com/ziglang/zig/issues/1194
+    var my_handle: promise = undefined;
+    suspend |p| {
+        my_handle = p;
+        resume p;
+    }
+
+    var watch = Watch{
+        .putter = my_handle,
+        .channel = channel,
+    };
+    out_watch.* = &watch;
+
+    const loop = channel.loop;
+    loop.beginOneEvent();
+
+    defer {
+        channel.destroy();
+        os.close(wd);
+        os.close(inotify_fd);
+        loop.finishOneEvent();
+    }
+
+    var event_buf: [4096]u8 align(@alignOf(os.linux.inotify_event)) = undefined;
+
+    while (true) {
+        const rc = os.linux.read(inotify_fd, &event_buf, event_buf.len);
+        const errno = os.linux.getErrno(rc);
+        switch (errno) {
+            0 => {
+                // can't use @bytesToSlice because of the special variable length name field
+                var ptr = event_buf[0..].ptr;
+                const end_ptr = ptr + event_buf.len;
+                var ev: *os.linux.inotify_event = undefined;
+                while (@ptrToInt(ptr) < @ptrToInt(end_ptr)) : (ptr += @sizeOf(os.linux.inotify_event) + ev.len) {
+                    ev = @ptrCast(*os.linux.inotify_event, ptr);
+                    if (ev.mask & os.linux.IN_CLOSE_WRITE == os.linux.IN_CLOSE_WRITE) {
+                        await (async channel.put(Watch.Event.CloseWrite) catch unreachable);
+                    }
+                }
+            },
+            os.linux.EINTR => continue,
+            os.linux.EINVAL => unreachable,
+            os.linux.EFAULT => unreachable,
+            os.linux.EAGAIN => {
+                (await (async loop.linuxWaitFd(
+                    inotify_fd,
+                    os.linux.EPOLLET | os.linux.EPOLLIN,
+                ) catch unreachable)) catch |err| {
+                    const transformed_err = switch (err) {
+                        error.InvalidFileDescriptor => unreachable,
+                        error.FileDescriptorAlreadyPresentInSet => unreachable,
+                        error.InvalidSyscall => unreachable,
+                        error.OperationCausesCircularLoop => unreachable,
+                        error.FileDescriptorNotRegistered => unreachable,
+                        error.SystemResources => error.SystemResources,
+                        error.UserResourceLimitReached => error.UserResourceLimitReached,
+                        error.FileDescriptorIncompatibleWithEpoll => unreachable,
+                        error.Unexpected => unreachable,
+                    };
+                    await (async channel.put(Watch.Event{ .Err = transformed_err }) catch unreachable);
+                };
+            },
+            else => unreachable,
+        }
+    }
+}
+
 const test_tmp_dir = "std_event_fs_test";
 
 test "write a file, watch it, write it again" {
@@ -338,10 +510,39 @@ async fn testFsWatch(loop: *event.Loop) !void {
         \\line 1
         \\line 2
     ;
+    const line2_offset = 7;
 
     // first just write then read the file
     try await try async writeFile(loop, file_path, contents);
 
     const read_contents = try await try async readFile(loop, file_path, 1024 * 1024);
     assert(mem.eql(u8, read_contents, contents));
+
+    // now watch the file
+    var watch = try watchFile(loop, file_path);
+    defer watch.destroy();
+
+    const ev = try async watch.channel.get();
+    var ev_consumed = false;
+    defer if (!ev_consumed) cancel ev;
+
+    // overwrite line 2
+    const fd = try await try async openReadWrite(loop, file_path, os.File.default_mode);
+    {
+        defer os.close(fd);
+
+        try await try async pwritev(loop, fd, line2_offset, []const []const u8{"lorem ipsum"});
+    }
+
+    ev_consumed = true;
+    switch (await ev) {
+        Watch.Event.CloseWrite => {},
+        Watch.Event.Err => |err| return err,
+    }
+
+    const contents_updated = try await try async readFile(loop, file_path, 1024 * 1024);
+    assert(mem.eql(u8, contents_updated,
+        \\line 1
+        \\lorem ipsum
+    ));
 }
std/event/loop.zig
@@ -318,45 +318,46 @@ pub const Loop = struct {
     }
 
     /// resume_node must live longer than the promise that it holds a reference to.
-    pub fn addFd(self: *Loop, fd: i32, resume_node: *ResumeNode) !void {
-        _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst);
-        errdefer {
-            self.finishOneEvent();
-        }
-        try self.modFd(
+    /// flags must contain EPOLLET
+    pub fn linuxAddFd(self: *Loop, fd: i32, resume_node: *ResumeNode, flags: u32) !void {
+        assert(flags & posix.EPOLLET == posix.EPOLLET);
+        self.beginOneEvent();
+        errdefer self.finishOneEvent();
+        try self.linuxModFd(
             fd,
             posix.EPOLL_CTL_ADD,
-            os.linux.EPOLLIN | os.linux.EPOLLOUT | os.linux.EPOLLET,
+            flags,
             resume_node,
         );
     }
 
-    pub fn modFd(self: *Loop, fd: i32, op: u32, events: u32, resume_node: *ResumeNode) !void {
+    pub fn linuxModFd(self: *Loop, fd: i32, op: u32, flags: u32, resume_node: *ResumeNode) !void {
+        assert(flags & posix.EPOLLET == posix.EPOLLET);
         var ev = os.linux.epoll_event{
-            .events = events,
+            .events = flags,
             .data = os.linux.epoll_data{ .ptr = @ptrToInt(resume_node) },
         };
         try os.linuxEpollCtl(self.os_data.epollfd, op, fd, &ev);
     }
 
-    pub fn removeFd(self: *Loop, fd: i32) void {
-        self.removeFdNoCounter(fd);
+    pub fn linuxRemoveFd(self: *Loop, fd: i32) void {
+        self.linuxRemoveFdNoCounter(fd);
         self.finishOneEvent();
     }
 
-    fn removeFdNoCounter(self: *Loop, fd: i32) void {
+    fn linuxRemoveFdNoCounter(self: *Loop, fd: i32) void {
         os.linuxEpollCtl(self.os_data.epollfd, os.linux.EPOLL_CTL_DEL, fd, undefined) catch {};
     }
 
-    pub async fn waitFd(self: *Loop, fd: i32) !void {
-        defer self.removeFd(fd);
+    pub async fn linuxWaitFd(self: *Loop, fd: i32, flags: u32) !void {
+        defer self.linuxRemoveFd(fd);
         suspend |p| {
             // TODO explicitly put this memory in the coroutine frame #1194
             var resume_node = ResumeNode{
                 .id = ResumeNode.Id.Basic,
                 .handle = p,
             };
-            try self.addFd(fd, &resume_node);
+            try self.linuxAddFd(fd, &resume_node, flags);
         }
     }
 
@@ -382,7 +383,7 @@ pub const Loop = struct {
                     // the pending count is already accounted for
                     const epoll_events = posix.EPOLLONESHOT | os.linux.EPOLLIN | os.linux.EPOLLOUT |
                         os.linux.EPOLLET;
-                    self.modFd(
+                    self.linuxModFd(
                         eventfd_node.eventfd,
                         eventfd_node.epoll_op,
                         epoll_events,
@@ -416,7 +417,7 @@ pub const Loop = struct {
 
     /// Bring your own linked list node. This means it can't fail.
     pub fn onNextTick(self: *Loop, node: *NextTickNode) void {
-        _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst);
+        self.beginOneEvent(); // finished in dispatch()
         self.next_tick_queue.put(node);
         self.dispatch();
     }
@@ -470,8 +471,14 @@ pub const Loop = struct {
         }
     }
 
-    fn finishOneEvent(self: *Loop) void {
-        if (@atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst) == 1) {
+    /// call finishOneEvent when done
+    pub fn beginOneEvent(self: *Loop) void {
+        _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst);
+    }
+
+    pub fn finishOneEvent(self: *Loop) void {
+        const prev = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Sub, 1, AtomicOrder.SeqCst);
+        if (prev == 1) {
             // cause all the threads to stop
             switch (builtin.os) {
                 builtin.Os.linux => {
@@ -593,7 +600,7 @@ pub const Loop = struct {
     }
 
     fn linuxFsRequest(self: *Loop, request_node: *fs.RequestNode) void {
-        _ = @atomicRmw(usize, &self.pending_event_count, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst);
+        self.beginOneEvent(); // finished in linuxFsRun after processing the msg
         self.os_data.fs_queue.put(request_node);
         _ = @atomicRmw(i32, &self.os_data.fs_queue_len, AtomicRmwOp.Add, 1, AtomicOrder.SeqCst); // let this wrap
         const rc = os.linux.futex_wake(@ptrToInt(&self.os_data.fs_queue_len), os.linux.FUTEX_WAKE, 1);
@@ -610,14 +617,21 @@ pub const Loop = struct {
             while (self.os_data.fs_queue.get()) |node| {
                 processed_count +%= 1;
                 switch (node.data.msg) {
-                    @TagType(fs.Request.Msg).PWriteV => @panic("TODO"),
+                    @TagType(fs.Request.Msg).End => return,
+                    @TagType(fs.Request.Msg).PWriteV => |*msg| {
+                        msg.result = os.posix_pwritev(msg.fd, msg.iov.ptr, msg.iov.len, msg.offset);
+                    },
                     @TagType(fs.Request.Msg).PReadV => |*msg| {
                         msg.result = os.posix_preadv(msg.fd, msg.iov.ptr, msg.iov.len, msg.offset);
                     },
                     @TagType(fs.Request.Msg).OpenRead => |*msg| {
-                        const flags = posix.O_LARGEFILE | posix.O_RDONLY;
+                        const flags = posix.O_LARGEFILE | posix.O_RDONLY | posix.O_CLOEXEC;
                         msg.result = os.posixOpenC(msg.path.ptr, flags, 0);
                     },
+                    @TagType(fs.Request.Msg).OpenRW => |*msg| {
+                        const flags = posix.O_LARGEFILE | posix.O_RDWR | posix.O_CREAT | posix.O_CLOEXEC;
+                        msg.result = os.posixOpenC(msg.path.ptr, flags, msg.mode);
+                    },
                     @TagType(fs.Request.Msg).Close => |*msg| os.close(msg.fd),
                     @TagType(fs.Request.Msg).WriteFile => |*msg| blk: {
                         const flags = posix.O_LARGEFILE | posix.O_WRONLY | posix.O_CREAT |
@@ -629,7 +643,6 @@ pub const Loop = struct {
                         defer os.close(fd);
                         msg.result = os.posixWrite(fd, msg.contents);
                     },
-                    @TagType(fs.Request.Msg).End => return,
                 }
                 switch (node.data.finish) {
                     @TagType(fs.Request.Finish).TickNode => |*tick_node| self.onNextTick(tick_node),
std/event/tcp.zig
@@ -55,7 +55,7 @@ pub const Server = struct {
         errdefer cancel self.accept_coro.?;
 
         self.listen_resume_node.handle = self.accept_coro.?;
-        try self.loop.addFd(sockfd, &self.listen_resume_node);
+        try self.loop.linuxAddFd(sockfd, &self.listen_resume_node, posix.EPOLLIN | posix.EPOLLOUT | posix.EPOLLET);
         errdefer self.loop.removeFd(sockfd);
     }
 
@@ -116,7 +116,7 @@ pub async fn connect(loop: *Loop, _address: *const std.net.Address) !std.os.File
     errdefer std.os.close(sockfd);
 
     try std.os.posixConnectAsync(sockfd, &address.os_addr);
-    try await try async loop.waitFd(sockfd);
+    try await try async loop.linuxWaitFd(sockfd, posix.EPOLLIN | posix.EPOLLOUT);
     try std.os.posixGetSockOptConnectError(sockfd);
 
     return std.os.File.openHandle(sockfd);
@@ -181,4 +181,3 @@ async fn doAsyncTest(loop: *Loop, address: *const std.net.Address, server: *Serv
     assert(mem.eql(u8, msg, "hello from server\n"));
     server.close();
 }
-
std/os/linux/index.zig
@@ -567,6 +567,37 @@ pub const MNT_DETACH = 2;
 pub const MNT_EXPIRE = 4;
 pub const UMOUNT_NOFOLLOW = 8;
 
+pub const IN_CLOEXEC = O_CLOEXEC;
+pub const IN_NONBLOCK = O_NONBLOCK;
+
+pub const IN_ACCESS = 0x00000001;
+pub const IN_MODIFY = 0x00000002;
+pub const IN_ATTRIB = 0x00000004;
+pub const IN_CLOSE_WRITE = 0x00000008;
+pub const IN_CLOSE_NOWRITE = 0x00000010;
+pub const IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE;
+pub const IN_OPEN = 0x00000020;
+pub const IN_MOVED_FROM = 0x00000040;
+pub const IN_MOVED_TO = 0x00000080;
+pub const IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO;
+pub const IN_CREATE = 0x00000100;
+pub const IN_DELETE = 0x00000200;
+pub const IN_DELETE_SELF = 0x00000400;
+pub const IN_MOVE_SELF = 0x00000800;
+pub const IN_ALL_EVENTS = 0x00000fff;
+
+pub const IN_UNMOUNT = 0x00002000;
+pub const IN_Q_OVERFLOW = 0x00004000;
+pub const IN_IGNORED = 0x00008000;
+
+pub const IN_ONLYDIR = 0x01000000;
+pub const IN_DONT_FOLLOW = 0x02000000;
+pub const IN_EXCL_UNLINK = 0x04000000;
+pub const IN_MASK_ADD = 0x20000000;
+
+pub const IN_ISDIR = 0x40000000;
+pub const IN_ONESHOT = 0x80000000;
+
 pub const S_IFMT = 0o170000;
 
 pub const S_IFDIR = 0o040000;
@@ -704,6 +735,18 @@ pub fn getdents(fd: i32, dirp: [*]u8, count: usize) usize {
     return syscall3(SYS_getdents, @intCast(usize, fd), @ptrToInt(dirp), count);
 }
 
+pub fn inotify_init1(flags: u32) usize {
+    return syscall1(SYS_inotify_init1, flags);
+}
+
+pub fn inotify_add_watch(fd: i32, pathname: [*]const u8, mask: u32) usize {
+    return syscall3(SYS_inotify_add_watch, @intCast(usize, fd), @ptrToInt(pathname), mask);
+}
+
+pub fn inotify_rm_watch(fd: i32, wd: i32) usize {
+    return syscall2(SYS_inotify_rm_watch, @intCast(usize, fd), @intCast(usize, wd));
+}
+
 pub fn isatty(fd: i32) bool {
     var wsz: winsize = undefined;
     return syscall3(SYS_ioctl, @intCast(usize, fd), TIOCGWINSZ, @ptrToInt(&wsz)) == 0;
@@ -750,6 +793,10 @@ pub fn preadv(fd: i32, iov: [*]const iovec, count: usize, offset: u64) usize {
     return syscall4(SYS_preadv, @intCast(usize, fd), @ptrToInt(iov), count, offset);
 }
 
+pub fn pwritev(fd: i32, iov: [*]const iovec_const, count: usize, offset: u64) usize {
+    return syscall4(SYS_pwritev, @intCast(usize, fd), @ptrToInt(iov), count, offset);
+}
+
 // TODO https://github.com/ziglang/zig/issues/265
 pub fn rmdir(path: [*]const u8) usize {
     return syscall1(SYS_rmdir, @ptrToInt(path));
@@ -1068,6 +1115,11 @@ pub const iovec = extern struct {
     iov_len: usize,
 };
 
+pub const iovec_const = extern struct {
+    iov_base: [*]const u8,
+    iov_len: usize,
+};
+
 pub fn getsockname(fd: i32, noalias addr: *sockaddr, noalias len: *socklen_t) usize {
     return syscall3(SYS_getsockname, @intCast(usize, fd), @ptrToInt(addr), @ptrToInt(len));
 }
@@ -1376,6 +1428,14 @@ pub fn capset(hdrp: *cap_user_header_t, datap: *const cap_user_data_t) usize {
     return syscall2(SYS_capset, @ptrToInt(hdrp), @ptrToInt(datap));
 }
 
+pub const inotify_event = extern struct {
+    wd: i32,
+    mask: u32,
+    cookie: u32,
+    len: u32,
+    //name: [?]u8,
+};
+
 test "import" {
     if (builtin.os == builtin.Os.linux) {
         _ = @import("test.zig");
std/os/file.zig
@@ -29,7 +29,6 @@ pub const File = struct {
 
     /// `path` needs to be copied in memory to add a null terminating byte, hence the allocator.
     /// Call close to clean up.
-    /// TODO deprecated, just use open
     pub fn openRead(allocator: *mem.Allocator, path: []const u8) OpenError!File {
         if (is_posix) {
             const flags = posix.O_LARGEFILE | posix.O_RDONLY;
@@ -51,7 +50,6 @@ pub const File = struct {
     }
 
     /// Calls `openWriteMode` with os.File.default_mode for the mode.
-    /// TODO deprecated, just use open
     pub fn openWrite(allocator: *mem.Allocator, path: []const u8) OpenError!File {
         return openWriteMode(allocator, path, os.File.default_mode);
     }
@@ -60,7 +58,6 @@ pub const File = struct {
     /// If a file already exists in the destination it will be truncated.
     /// `path` needs to be copied in memory to add a null terminating byte, hence the allocator.
     /// Call close to clean up.
-    /// TODO deprecated, just use open
     pub fn openWriteMode(allocator: *mem.Allocator, path: []const u8, file_mode: Mode) OpenError!File {
         if (is_posix) {
             const flags = posix.O_LARGEFILE | posix.O_WRONLY | posix.O_CREAT | posix.O_CLOEXEC | posix.O_TRUNC;
@@ -85,7 +82,6 @@ pub const File = struct {
     /// If a file already exists in the destination this returns OpenError.PathAlreadyExists
     /// `path` needs to be copied in memory to add a null terminating byte, hence the allocator.
     /// Call close to clean up.
-    /// TODO deprecated, just use open
     pub fn openWriteNoClobber(allocator: *mem.Allocator, path: []const u8, file_mode: Mode) OpenError!File {
         if (is_posix) {
             const flags = posix.O_LARGEFILE | posix.O_WRONLY | posix.O_CREAT | posix.O_CLOEXEC | posix.O_EXCL;
std/os/index.zig
@@ -310,6 +310,29 @@ pub fn posixWrite(fd: i32, bytes: []const u8) !void {
     }
 }
 
+pub fn posix_pwritev(fd: i32, iov: [*]const posix.iovec_const, count: usize, offset: u64) PosixWriteError!void {
+    while (true) {
+        const rc = posix.pwritev(fd, iov, count, offset);
+        const err = posix.getErrno(rc);
+        switch (err) {
+            0 => return,
+            posix.EINTR => continue,
+            posix.EINVAL => unreachable,
+            posix.EFAULT => unreachable,
+            posix.EAGAIN => return PosixWriteError.WouldBlock,
+            posix.EBADF => return PosixWriteError.FileClosed,
+            posix.EDESTADDRREQ => return PosixWriteError.DestinationAddressRequired,
+            posix.EDQUOT => return PosixWriteError.DiskQuota,
+            posix.EFBIG => return PosixWriteError.FileTooBig,
+            posix.EIO => return PosixWriteError.InputOutput,
+            posix.ENOSPC => return PosixWriteError.NoSpaceLeft,
+            posix.EPERM => return PosixWriteError.AccessDenied,
+            posix.EPIPE => return PosixWriteError.BrokenPipe,
+            else => return unexpectedErrorPosix(err),
+        }
+    }
+}
+
 pub const PosixOpenError = error{
     OutOfMemory,
     AccessDenied,
@@ -2913,3 +2936,44 @@ pub fn bsdKEvent(
         }
     }
 }
+
+pub fn linuxINotifyInit1(flags: u32) !i32 {
+    const rc = linux.inotify_init1(flags);
+    const err = posix.getErrno(rc);
+    switch (err) {
+        0 => return @intCast(i32, rc),
+        posix.EINVAL => unreachable,
+        posix.EMFILE => return error.ProcessFdQuotaExceeded,
+        posix.ENFILE => return error.SystemFdQuotaExceeded,
+        posix.ENOMEM => return error.SystemResources,
+        else => return unexpectedErrorPosix(err),
+    }
+}
+
+pub fn linuxINotifyAddWatchC(inotify_fd: i32, pathname: [*]const u8, mask: u32) !i32 {
+    const rc = linux.inotify_add_watch(inotify_fd, pathname, mask);
+    const err = posix.getErrno(rc);
+    switch (err) {
+        0 => return @intCast(i32, rc),
+        posix.EACCES => return error.AccessDenied,
+        posix.EBADF => unreachable,
+        posix.EFAULT => unreachable,
+        posix.EINVAL => unreachable,
+        posix.ENAMETOOLONG => return error.NameTooLong,
+        posix.ENOENT => return error.FileNotFound,
+        posix.ENOMEM => return error.SystemResources,
+        posix.ENOSPC => return error.UserResourceLimitReached,
+        else => return unexpectedErrorPosix(err),
+    }
+}
+
+pub fn linuxINotifyRmWatch(inotify_fd: i32, wd: i32) !void {
+    const rc = linux.inotify_rm_watch(inotify_fd, wd);
+    const err = posix.getErrno(rc);
+    switch (err) {
+        0 => return rc,
+        posix.EBADF => unreachable,
+        posix.EINVAL => unreachable,
+        else => unreachable,
+    }
+}