//! Thin typed wrappers over the Linux syscalls att_wm needs. //! //! Zig 0.16 moved most of `std.posix` behind the new `std.Io` interface, which //! is the wrong shape for a window manager: everything here is a raw fd driven //! by a single `poll()` loop, with no allocator and no async runtime. Going //! straight to `std.os.linux` is both simpler and closer to what the code //! actually does. const std = @import("std"); const linux = std.os.linux; /// Must be the linux decoder, not `std.posix.errno`: with libc linked the /// latter expects a libc-style -1 return and reports every raw syscall error /// as success, which then overflows the casts below. const errno = linux.errno; pub const fd_t = linux.fd_t; pub const pid_t = linux.pid_t; pub const E = linux.E; pub const Error = error{ Again, Interrupted, ConnectionReset, AddressInUse, NotFound, PermissionDenied, ConnectionRefused, BrokenPipe, NameTooLong, OutOfMemory, Unexpected, }; fn check(rc: usize) Error!usize { return switch (errno(rc)) { .SUCCESS => rc, .AGAIN => error.Again, .INTR => error.Interrupted, .ADDRINUSE => error.AddressInUse, .NOENT => error.NotFound, .ACCES, .PERM => error.PermissionDenied, .CONNREFUSED => error.ConnectionRefused, .CONNRESET => error.ConnectionReset, .PIPE => error.BrokenPipe, .NAMETOOLONG => error.NameTooLong, .NOMEM => error.OutOfMemory, else => error.Unexpected, }; } pub fn read(fd: fd_t, buf: []u8) Error!usize { if (buf.len == 0) return 0; while (true) { return check(linux.read(fd, buf.ptr, buf.len)) catch |err| switch (err) { error.Interrupted => continue, else => err, }; } } pub fn write(fd: fd_t, bytes: []const u8) Error!usize { if (bytes.len == 0) return 0; return check(linux.write(fd, bytes.ptr, bytes.len)); } /// Write everything, retrying short writes. Best effort: errors are swallowed /// because every caller is emitting diagnostics or usage text. pub fn writeAllBestEffort(fd: fd_t, bytes: []const u8) void { var off: usize = 0; while (off < bytes.len) { off += write(fd, bytes[off..]) catch return; } } pub fn close(fd: fd_t) void { _ = linux.close(fd); } pub fn socket(domain: u32, socket_type: u32, protocol: u32) Error!fd_t { return @intCast(try check(linux.socket(domain, socket_type, protocol))); } pub fn bind(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void { _ = try check(linux.bind(fd, addr, len)); } pub fn listen(fd: fd_t, backlog: u31) Error!void { _ = try check(linux.listen(fd, backlog)); } pub fn accept4(fd: fd_t, flags: u32) Error!fd_t { return @intCast(try check(linux.accept4(fd, null, null, flags))); } pub fn connect(fd: fd_t, addr: *const linux.sockaddr, len: linux.socklen_t) Error!void { _ = try check(linux.connect(fd, addr, len)); } pub fn ftruncate(fd: fd_t, length: u64) Error!void { _ = try check(linux.ftruncate(fd, @intCast(length))); } /// Make a memfd immutable, so a compositor mapping it cannot have the bytes /// changed underneath it. pub fn addSeals(fd: fd_t, seals: usize) Error!void { _ = try check(linux.fcntl(fd, linux.F.ADD_SEALS, seals)); } pub fn timerfdCreate(flags: linux.TFD) Error!fd_t { return @intCast(try check(linux.timerfd_create(.MONOTONIC, flags))); } pub fn timerfdSetTime(fd: fd_t, spec: *const linux.itimerspec) Error!void { _ = try check(linux.timerfd_settime(fd, .{}, spec, null)); } pub fn fork() Error!pid_t { return @intCast(try check(linux.fork())); } pub fn setsid() void { _ = linux.setsid(); } pub fn exit(code: u8) noreturn { linux.exit(code); } pub fn waitpid(pid: pid_t) void { var status: u32 = undefined; while (true) { const rc = linux.wait4(pid, &status, 0, null); switch (errno(rc)) { .INTR => continue, else => return, } } } /// Remove a path. Best effort: the only caller is clearing a stale socket. pub fn unlink(path: []const u8) void { var buf: [std.fs.max_path_bytes]u8 = undefined; if (path.len >= buf.len) return; @memcpy(buf[0..path.len], path); buf[path.len] = 0; _ = linux.unlink(@ptrCast(&buf)); } /// Build a unix socket address. Paths must fit in sun_path with room for the /// terminating NUL. pub fn sockaddrUn(path: []const u8) Error!linux.sockaddr.un { var addr: linux.sockaddr.un = .{ .family = linux.AF.UNIX, .path = undefined }; if (path.len >= addr.path.len) return error.NameTooLong; @memset(&addr.path, 0); @memcpy(addr.path[0..path.len], path); return addr; } pub fn sockaddrUnLen(addr: *const linux.sockaddr.un) linux.socklen_t { _ = addr; return @sizeOf(linux.sockaddr.un); } /// execvp: run `argv[0]`, searching PATH when it contains no slash. /// /// Only ever called between fork() and exec in the child, so it must not /// allocate; the candidate path is assembled in a stack buffer. pub fn execvpe( argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8, path_env: ?[]const u8, ) Error { const file = std.mem.span(argv[0].?); if (std.mem.indexOfScalar(u8, file, '/') != null) { return execErr(linux.execve(argv[0].?, argv, envp)); } const search = path_env orelse "/usr/local/bin:/usr/bin:/bin"; var buf: [std.fs.max_path_bytes]u8 = undefined; var last: Error = error.NotFound; var it = std.mem.tokenizeScalar(u8, search, ':'); while (it.next()) |dir| { if (dir.len + 1 + file.len + 1 > buf.len) continue; @memcpy(buf[0..dir.len], dir); buf[dir.len] = '/'; @memcpy(buf[dir.len + 1 ..][0..file.len], file); buf[dir.len + 1 + file.len] = 0; const candidate: [*:0]const u8 = @ptrCast(&buf); last = execErr(linux.execve(candidate, argv, envp)); // ENOENT just means "not in this directory"; keep looking. if (last != error.NotFound) return last; } return last; } /// execve only returns on failure, so its result is always an error. fn execErr(rc: usize) Error { _ = check(rc) catch |err| return err; return error.Unexpected; }