diff --git a/src/Kqueue.zig b/src/Kqueue.zig index 17b20f8..1fd3cea 100644 --- a/src/Kqueue.zig +++ b/src/Kqueue.zig @@ -358,6 +358,48 @@ fn prepTask(self: *Kqueue, task: *io.Task) !void { task.result = .{ .socket = error.Unexpected }; }, + .statx => |*req| { + self.synchronous_queue.push(task); + + if (posix.fstatat(posix.AT.FDCWD, req.path, 0)) |stat| { + req.result.* = .{ + .mask = 0, + .blksize = @intCast(stat.blksize), + .attributes = 0, + .nlink = @intCast(stat.nlink), + .uid = @intCast(stat.uid), + .gid = @intCast(stat.gid), + .mode = @intCast(stat.mode), + .__pad1 = 0, + .ino = @intCast(stat.ino), + .size = @intCast(stat.size), + .blocks = @intCast(stat.blocks), + .attributes_mask = 0, + .atime = .{ + .sec = @intCast(stat.atime().sec), + .nsec = @intCast(stat.atime().nsec), + }, + .btime = .{ + .sec = 0, + .nsec = 0, + }, + .ctime = .{ + .sec = @intCast(stat.ctime().sec), + .nsec = @intCast(stat.ctime().nsec), + }, + .mtime = .{ + .sec = @intCast(stat.mtime().sec), + .nsec = @intCast(stat.mtime().nsec), + }, + .rdev_major = major(@intCast(stat.rdev)), + .rdev_minor = minor(@intCast(stat.rdev)), + .dev_major = major(@intCast(stat.dev)), + .dev_minor = major(@intCast(stat.dev)), + .__pad2 = undefined, + }; + } else |_| task.result = .{ .statx = error.Unexpected }; + }, + .timer => { const now = std.time.milliTimestamp(); try self.addTimer(.{ .timeout = .{ .task = task, .added_ms = now } }); @@ -392,6 +434,7 @@ fn cancelTask(self: *Kqueue, task: *io.Task) !void { .msg_ring, .noop, .socket, + .statx, .userfd, .usermsg, .userptr, @@ -624,6 +667,7 @@ fn handleSynchronousCompletion( .msg_ring, .noop, .socket, + .statx, .userfd, .usermsg, .userptr, @@ -658,6 +702,7 @@ fn handleSynchronousCompletion( .poll => .{ .poll = error.Canceled }, .recv => .{ .recv = error.Canceled }, .socket => .{ .socket = error.Canceled }, + .statx => .{ .statx = error.Canceled }, .timer => .{ .timer = error.Canceled }, .userfd, .usermsg, .userptr => unreachable, .write => .{ .write = error.Canceled }, @@ -696,6 +741,7 @@ fn handleCompletion( .msg_ring, .noop, .socket, + .statx, .timer, .userfd, .usermsg, @@ -823,3 +869,21 @@ fn handleExpiredTimer(self: *Kqueue, rt: *io.Ring, t: Timer) !void { }, } } +fn major(dev: u64) u32 { + return switch (@import("builtin").target.os.tag) { + .macos, .visionos, .tvos, .ios, .watchos => @intCast((dev >> 24) & 0xff), + .freebsd, .openbsd, .netbsd, .dragonfly => @intCast((dev >> 8) & 0xff), + else => @compileError("unsupported OS for major()"), + }; +} + +fn minor(dev: u64) u32 { + return switch (@import("builtin").target.os.tag) { + .macos, .ios, .visionos, .tvos, .watchos => @intCast(dev & 0xffffff), + .openbsd => @intCast(dev & 0xff), + .freebsd => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + .dragonfly => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + .netbsd => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + else => @compileError("unsupported OS for minor()"), + }; +} diff --git a/src/Mock.zig b/src/Mock.zig index b7e3219..60a0248 100644 --- a/src/Mock.zig +++ b/src/Mock.zig @@ -21,6 +21,7 @@ noop_cb: ?*const fn (*io.Task) io.Result = null, poll_cb: ?*const fn (*io.Task) io.Result = null, recv_cb: ?*const fn (*io.Task) io.Result = null, socket_cb: ?*const fn (*io.Task) io.Result = null, +statx_cb: ?*const fn (*io.Task) io.Result = null, timer_cb: ?*const fn (*io.Task) io.Result = null, write_cb: ?*const fn (*io.Task) io.Result = null, writev_cb: ?*const fn (*io.Task) io.Result = null, @@ -70,6 +71,7 @@ pub fn submit(self: *Mock, queue: *Queue(io.Task, .in_flight)) !void { .poll => if (self.poll_cb) |cb| cb(task) else return error.NoMockCallback, .recv => if (self.recv_cb) |cb| cb(task) else return error.NoMockCallback, .socket => if (self.socket_cb) |cb| cb(task) else return error.NoMockCallback, + .statx => if (self.statx_cb) |cb| cb(task) else return error.NoMockCallback, .timer => if (self.timer_cb) |cb| cb(task) else return error.NoMockCallback, .userfd => if (self.userfd_cb) |cb| cb(task) else return error.NoMockCallback, .usermsg => if (self.usermsg_cb) |cb| cb(task) else return error.NoMockCallback, diff --git a/src/Uring.zig b/src/Uring.zig index 9f46cf5..33c678a 100644 --- a/src/Uring.zig +++ b/src/Uring.zig @@ -218,6 +218,13 @@ fn prepTask(self: *Uring, task: *io.Task) void { self.prepDeadline(task, sqe); }, + .statx => |*req| { + const sqe = self.getSqe(); + sqe.prep_statx(linux.AT.FDCWD, req.path, 0, linux.STATX_BASIC_STATS, @ptrCast(req.result)); + sqe.user_data = @intFromPtr(task); + self.prepDeadline(task, sqe); + }, + // user* is only sent internally between rings and higher level wrappers .userfd, .usermsg, .userptr => unreachable, } @@ -340,6 +347,13 @@ pub fn reapCompletions(self: *Uring, rt: *io.Ring) anyerror!void { else => |e| unexpectedError(e), } }, + .statx => |req| .{ .statx = switch (cqeToE(cqe.res)) { + .SUCCESS => req.result, + .INVAL => io.ResultError.Invalid, + .CANCELED => io.ResultError.Canceled, + else => |e| unexpectedError(e), + } }, + .usermsg => .{ .usermsg = @intCast(cqe.res) }, // userfd should never reach the runtime diff --git a/src/main.zig b/src/main.zig index 42e379b..88b235a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -395,6 +395,24 @@ pub const Ring = struct { return task; } + pub fn stat( + self: *Ring, + path: [:0]const u8, + result: *Statx, + ctx: Context, + ) Allocator.Error!*Task { + const task = try self.getTask(); + task.* = .{ + .userdata = ctx.ptr, + .msg = ctx.msg, + .callback = ctx.cb, + .req = .{ .statx = .{ .path = path, .result = result } }, + }; + + self.submission_q.push(task); + return task; + } + /// Spawns a thread with a Ring instance. The thread will be idle and waiting to receive work /// via msgRing when this function returns. Call kill on the returned thread to signal it to /// shutdown. @@ -474,6 +492,7 @@ pub const Op = enum { poll, socket, connect, + statx, /// userfd is meant to send file descriptors between Ring instances (using msgRing) userfd, @@ -523,6 +542,10 @@ pub const Request = union(Op) { addr: *posix.sockaddr, addr_len: posix.socklen_t, }, + statx: struct { + path: [:0]const u8, + result: *Statx, // this will be filled in by the op + }, userfd, usermsg, @@ -543,6 +566,7 @@ pub const Result = union(Op) { poll: ResultError!void, socket: ResultError!posix.fd_t, connect: ResultError!void, + statx: ResultError!*Statx, userfd: anyerror!posix.fd_t, usermsg: u16, @@ -591,6 +615,95 @@ const Foo = struct { } }; +/// Follows the ABI of linux statx. Not all platforms will contain all information, or may contain +/// more information than requested +pub const Statx = extern struct { + /// Mask of bits indicating filled fields + mask: u32, + + /// Block size for filesystem I/O + blksize: u32, + + /// Extra file attribute indicators + attributes: u64, + + /// Number of hard links + nlink: u32, + + /// User ID of owner + uid: posix.uid_t, + + /// Group ID of owner + gid: posix.gid_t, + + /// File type and mode + mode: u16, + __pad1: u16, + + /// Inode number + ino: u64, + + /// Total size in bytes + size: u64, + + /// Number of 512B blocks allocated + blocks: u64, + + /// Mask to show what's supported in `attributes`. + attributes_mask: u64, + + /// Last access file timestamp + atime: Timestamp, + + /// Creation file timestamp + btime: Timestamp, + + /// Last status change file timestamp + ctime: Timestamp, + + /// Last modification file timestamp + mtime: Timestamp, + + /// Major ID, if this file represents a device. + rdev_major: u32, + + /// Minor ID, if this file represents a device. + rdev_minor: u32, + + /// Major ID of the device containing the filesystem where this file resides. + dev_major: u32, + + /// Minor ID of the device containing the filesystem where this file resides. + dev_minor: u32, + + __pad2: [14]u64, + + pub const Timestamp = extern struct { + sec: i64, + nsec: u32, + __pad: u32 = 0, + }; + + pub fn major(dev: u64) u32 { + return switch (@import("builtin").target.os.tag) { + .macos, .visionos, .tvos, .ios, .watchos => @intCast((dev >> 24) & 0xff), + .freebsd, .openbsd, .netbsd, .dragonfly => @intCast((dev >> 8) & 0xff), + else => @compileError("unsupported OS for major()"), + }; + } + + pub fn minor(dev: u64) u32 { + return switch (@import("builtin").target.os.tag) { + .macos, .ios, .visionos, .tvos, .watchos => @intCast(dev & 0xffffff), + .openbsd => @intCast(dev & 0xff), + .freebsd => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + .dragonfly => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + .netbsd => @intCast((dev & 0xff) | ((dev >> 12) & 0xfff00)), + else => @compileError("unsupported OS for minor()"), + }; + } +}; + test "runtime: noop" { var rt: io.Ring = try .init(std.testing.allocator, 16); defer rt.deinit(); @@ -834,3 +947,18 @@ test "runtime: spawnThread" { try std.testing.expect(foo.did_work); try std.testing.expect(foo.kill); } + +test "runtime: stat" { + const gpa = std.testing.allocator; + var rt = try io.Ring.init(gpa, 16); + defer rt.deinit(); + + var foo: Foo = .{}; + const ctx: Context = .{ .ptr = &foo, .cb = Foo.callback }; + + var statx: Statx = undefined; + const task = try rt.stat("build.zig", &statx, ctx); + + try rt.run(.until_done); + try std.testing.expect(task.result != null); +}