summaryrefslogtreecommitdiff
path: root/src/nested.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/nested.zig')
-rw-r--r--src/nested.zig743
1 files changed, 0 insertions, 743 deletions
diff --git a/src/nested.zig b/src/nested.zig
deleted file mode 100644
index 1997d8e4..00000000
--- a/src/nested.zig
+++ /dev/null
@@ -1,743 +0,0 @@
-//! A pardes launched inside a pardes hands its file to the outer one.
-//!
-//! Every top-level instance listens on `<dir>/pardes-<pid>.sock`, where `<dir>`
-//! is `$XDG_RUNTIME_DIR` or, when the session has none, `~/.local/state/pardes`
-//! created 0700. NOT /tmp: this socket takes a command line and runs it, and a
-//! world-writable directory means both that somebody else can plant a listener
-//! at a pid we are about to guess and that a file they planted under the sticky
-//! bit cannot be unlinked, so bind fails and the feature goes quietly off.
-//!
-//! An instance that finds an ancestor process running the same executable
-//! resolves its positional argument, writes ONE line — `Look /abs/path` — to
-//! that ancestor's socket and exits silently; the outer pardes runs the line
-//! through executeBuiltinLine and opens a pane for it. The wire format is a
-//! builtin command line because that is a language pardes already speaks: no
-//! serialization, nothing to version. The receive side still filters it down
-//! to `Look `, because executeBuiltinLine dispatches ANY builtin and this
-//! socket sits at a path anyone can derive from a pid — `Exec …` arriving here
-//! is not something this protocol is allowed to say.
-//!
-//! Linux and darwin. The two differ in every primitive this needs and in none
-//! of the design: /proc against libproc for the ancestor walk, SOCK_CLOEXEC
-//! and accept4 against a plain socket plus an fcntl, and a `sun_path` of 108
-//! bytes against one of 104 — which is why no buffer below spells a number,
-//! they are all sized from the field itself. Anywhere else the walk returns
-//! null and a pardes inside a pardes opens a second session, as before.
-//!
-//! macOS also has a third executable in the family: the app bundle. Its binary
-//! is named `pardes`, like the tty frontend, wherever the bundle is installed.
-//! Executable identity therefore comes from the family name rather than its
-//! path — see samePardesExecutable.
-const std = @import("std");
-const builtin = @import("builtin");
-const libc = std.c;
-
-// std.c has getenv but neither setter; the tests below need both
-extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
-extern "c" fn unsetenv(name: [*:0]const u8) c_int;
-
-/// THE SOCKET CONVENTIONS BELOW ARE SHARED, and the ones marked `pub` are
-/// shared with src/detached/server.zig — a second unix socket in the same
-/// per-user directory, under a different name (`pardes-detached-<name>.sock`
-/// rather than `pardes-<pid>.sock`). They were copied into that file when it
-/// landed; one directory vetted by two different predicates is exactly the
-/// divergence the reasoning here is meant to prevent, so there is one of each.
-pub const darwin = switch (builtin.os.tag) {
- .macos, .ios, .tvos, .watchos, .visionos => true,
- else => false,
-};
-
-/// This module is only as portable as its two ingredients: a way to name the
-/// executable and parent of an arbitrary pid, and unix sockets. The detached
-/// transport needs the second alone, and the same answer.
-pub const supported = builtin.os.tag == .linux or darwin;
-
-/// `sun_path` is 108 bytes on linux and 104 on darwin, and it is the hard
-/// limit on this whole feature: a path that does not fit is not a socket
-/// address, it is a truncated one pointing somewhere else. Taken from the
-/// struct so that the buffers, the fit checks and the memcpy below cannot
-/// disagree with the kernel or with each other.
-pub const sun_path_len = @typeInfo(@FieldType(libc.sockaddr.un, "path")).array.len;
-
-/// libproc, darwin's answer to /proc. `proc_pidpath` is readlink of
-/// `/proc/<pid>/exe`; `PROC_PIDTBSDINFO` carries the parent pid that linux
-/// spells `PPid:`. Both are same-uid readable, which is the only permission
-/// an ancestor walk through one's own processes needs.
-const PROC_PIDTBSDINFO: c_int = 3;
-const proc_bsdinfo = extern struct {
- flags: u32,
- status: u32,
- xstatus: u32,
- pid: u32,
- ppid: u32,
- /// uids, gids, comm, name, the tty and the start time: filled by the
- /// kernel and unread here, but the call fails unless the buffer is the
- /// whole 136-byte record.
- rest: [116]u8,
-};
-extern "c" fn proc_pidpath(pid: c_int, buffer: *anyopaque, buffersize: u32) c_int;
-extern "c" fn proc_pidinfo(pid: c_int, flavor: c_int, arg: u64, buffer: *anyopaque, buffersize: c_int) c_int;
-
-/// Linux opens sockets CLOEXEC in one call; darwin has to set it afterwards.
-/// The gap is a race only against a fork on another thread, and every caller
-/// is past that: `listen` runs before the first pane exists, `acceptLine` runs
-/// on a thread of its own long after spawning has settled, and the detached
-/// session — which forks EVERY pane shell in the session, because the daemon
-/// owns them now (`host_io.zig`) — has no other thread to race with, since it
-/// services its pane ptys from the same `poll(2)` that accepts its frontends.
-///
-/// CLOEXEC matters MORE for that last one than it did when a frontend forked
-/// the shells: a pane shell is long-lived and arbitrary, and an inherited
-/// listener would keep the session's socket bound long after the session
-/// ended — the same shape as the inherited lock fd that once held a flock
-/// forever.
-pub fn setCloexec(fd: c_int) void {
- const FD_CLOEXEC: c_int = 1;
- _ = libc.fcntl(fd, libc.F.SETFD, FD_CLOEXEC);
-}
-
-/// The longest command line this protocol carries or accepts. `Look ` plus a
-/// PATH_MAX path fits with room over; anything longer cannot have come from
-/// the client and is dropped rather than truncated into a different command.
-pub const max_line = 4200;
-
-/// Where the sockets live. `$XDG_RUNTIME_DIR` first — a per-user 0700 tmpfs
-/// the login session already cleans up — else `~/.local/state/pardes`, which
-/// is per-user for the same reason a home directory is. NEVER /tmp: these
-/// sockets take a command line, or keystrokes into a live editor. Asked by the
-/// client (to derive the path), by the listener (to create and vet it), by the
-/// sweeper (to scan it) and by the detached transport (all three, for its own
-/// name), so it is written once.
-pub fn socketDir(buf: *[sun_path_len:0]u8) ?[:0]const u8 {
- if (libc.getenv("XDG_RUNTIME_DIR")) |x|
- return std.fmt.bufPrintSentinel(buf, "{s}", .{std.mem.span(x)}, 0) catch null;
- const home = libc.getenv("HOME") orelse return null;
- return std.fmt.bufPrintSentinel(buf, "{s}/.local/state/pardes", .{std.mem.span(home)}, 0) catch null;
-}
-
-/// `<dir>/pardes-<pid>.sock`. `<pid>` is the LISTENING instance's own pid, so
-/// two pardes never collide and a nested child derives the exact path from the
-/// ancestor pid its tree walk found. The buffer is sun_path-sized: a longer
-/// path is not a socket address at all.
-pub fn socketPath(buf: *[sun_path_len]u8, pid: libc.pid_t) ?[:0]const u8 {
- var dir_buf: [sun_path_len:0]u8 = undefined;
- const dir = socketDir(&dir_buf) orelse return null;
- // unsigned: {d} prints a leading '+' for a positive SIGNED int
- return std.fmt.bufPrintSentinel(buf, "{s}/pardes-{d}.sock", .{ dir, @as(u32, @intCast(pid)) }, 0) catch null;
-}
-
-/// Normalize the kernel suffix left on a running executable after its file is
-/// replaced. `zig build` does this routinely while an outer session is live.
-fn stripDeleted(link: []const u8) []const u8 {
- const suffix = " (deleted)";
- return if (std.mem.endsWith(u8, link, suffix)) link[0 .. link.len - suffix.len] else link;
-}
-
-/// The tty, SDL and macOS builds are sibling frontends of the same program.
-/// Their installed names differ only by `-gui` (and, for cross builds, share
-/// the same `-os-arch` tail), or not at all when one of them is the app bundle
-/// — so any of them must recognise any other as an outer pardes. Paths are
-/// deliberately ignored: the GUI may be installed system-wide while the tty
-/// frontend is installed in the user's bin directory.
-fn samePardesExecutable(a_raw: []const u8, b_raw: []const u8) bool {
- const a = stripDeleted(a_raw);
- const b = stripDeleted(b_raw);
- return sameFamily(std.fs.path.basename(a), std.fs.path.basename(b));
-}
-
-/// What is left of a family name after the frontend part: `` for `pardes` and
-/// `pardes-gui`, `-linux-aarch64` for the cross-built spellings of both. Null
-/// when the name is not in the family at all — `not-pardes`, `pardesfoo`, and
-/// helper binaries such as `pardes-snap` are other programs.
-fn familyTail(name: []const u8) ?[]const u8 {
- const rest = if (std.mem.startsWith(u8, name, "pardes-gui"))
- name["pardes-gui".len..]
- else if (std.mem.startsWith(u8, name, "pardes"))
- name["pardes".len..]
- else
- return null;
- if (rest.len == 0) return rest;
- // Build names have exactly `-os-arch` after the frontend. Validating both
- // fields keeps sibling installs flexible without mistaking pardes-snap,
- // pardes-perf, and the other helper executables for editor frontends.
- if (rest[0] != '-') return null;
- var fields = std.mem.splitScalar(u8, rest[1..], '-');
- const os = fields.next() orelse return null;
- const arch = fields.next() orelse return null;
- if (fields.next() != null) return null;
- if (std.meta.stringToEnum(std.Target.Os.Tag, os) == null) return null;
- if (std.meta.stringToEnum(std.Target.Cpu.Arch, arch) == null) return null;
- return rest;
-}
-
-/// Two executable names in the same family. The tails have to agree — a linux
-/// binary and an x86_64 one are two builds — unless one of them has no tail at
-/// all, which is the untagged name the default build and, unavoidably, the app
-/// bundle both produce: CFBundleExecutable is a fixed string, so the bundled
-/// copy of `pardes-macos-aarch64` is called `pardes` and nothing in the name
-/// records what it was. A foreign-arch ancestor cannot be running here.
-fn sameFamily(a: []const u8, b: []const u8) bool {
- const a_tail = familyTail(a) orelse return false;
- const b_tail = familyTail(b) orelse return false;
- if (std.mem.eql(u8, a, b)) return true;
- return a_tail.len == 0 or b_tail.len == 0 or std.mem.eql(u8, a_tail, b_tail);
-}
-
-/// The `PPid:` field of a /proc/<pid>/status blob. Deliberately NOT field 4 of
-/// /proc/<pid>/stat: that field is positional after `comm`, and a comm may
-/// contain spaces and parentheses — a process named `sh (a b)` shifts every
-/// field after it and the parse silently reads the wrong number.
-fn parsePPid(status: []const u8) ?libc.pid_t {
- var lines = std.mem.splitScalar(u8, status, '\n');
- while (lines.next()) |line| {
- if (!std.mem.startsWith(u8, line, "PPid:")) continue;
- return std.fmt.parseInt(libc.pid_t, std.mem.trim(u8, line["PPid:".len..], " \t\r"), 10) catch null;
- }
- return null;
-}
-
-/// The pid in a `pardes-<pid>.sock` filename, for the startup sweep. Strictly
-/// digits: parseInt alone would take `pardes-+7.sock` and `pardes--7.sock`,
-/// and the sweep unlinks what this answers about.
-fn sweepPid(name: []const u8) ?libc.pid_t {
- if (!std.mem.startsWith(u8, name, "pardes-") or !std.mem.endsWith(u8, name, ".sock")) return null;
- const digits = name["pardes-".len .. name.len - ".sock".len];
- if (digits.len == 0) return null;
- for (digits) |ch| if (!std.ascii.isDigit(ch)) return null;
- return std.fmt.parseInt(libc.pid_t, digits, 10) catch null;
-}
-
-/// Name the executable behind a pid, the way this OS spells it.
-fn exeOf(pid: libc.pid_t, buf: *[4096]u8) ?[]const u8 {
- switch (builtin.os.tag) {
- .linux => {
- var name: [64:0]u8 = undefined;
- const link = std.fmt.bufPrintSentinel(&name, "/proc/{d}/exe", .{@as(u32, @intCast(pid))}, 0) catch return null;
- const n = libc.readlink(link, buf, buf.len);
- if (n <= 0) return null;
- return buf[0..@intCast(n)];
- },
- else => {
- if (comptime !darwin) return null;
- // Documented to want a PROC_PIDPATHINFO_MAXSIZE buffer, which is
- // exactly this one, and to return the length it wrote.
- const n = proc_pidpath(pid, buf, @intCast(buf.len));
- if (n <= 0) return null;
- return buf[0..@intCast(n)];
- },
- }
-}
-
-/// ...and its parent.
-fn parentOf(pid: libc.pid_t) ?libc.pid_t {
- switch (builtin.os.tag) {
- .linux => {
- var name: [64:0]u8 = undefined;
- var buf: [4096]u8 = undefined;
- const status = std.fmt.bufPrintSentinel(&name, "/proc/{d}/status", .{@as(u32, @intCast(pid))}, 0) catch return null;
- const fd = libc.open(status, .{ .ACCMODE = .RDONLY });
- if (fd < 0) return null;
- const got = libc.read(fd, &buf, buf.len);
- _ = libc.close(fd);
- if (got <= 0) return null;
- return parsePPid(buf[0..@intCast(got)]);
- },
- else => {
- if (comptime !darwin) return null;
- var info: proc_bsdinfo = undefined;
- const n = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, @sizeOf(proc_bsdinfo));
- // A short answer means the record this was compiled against is not
- // the one the kernel filled, and `ppid` is then some other field.
- if (n < @as(c_int, @sizeOf(proc_bsdinfo))) return null;
- return @intCast(info.ppid);
- },
- }
-}
-
-/// The pid of the nearest ancestor running a pardes executable, or null.
-/// Identity is that ancestor's executable path against our own; the tty, SDL
-/// and app-bundle siblings also match when they were installed together. A
-/// name alone would call every unrelated `pardes` ancestor an outer instance.
-/// The hop cap is not for the process tree, which cannot loop, but because the
-/// walk is driven by numbers read out of the kernel and should not be able to
-/// spin on a surprising one.
-pub fn outer() ?libc.pid_t {
- if (comptime !supported) return null;
- var self_buf: [4096]u8 = undefined;
- const self_exe = exeOf(libc.getpid(), &self_buf) orelse return null;
- // A process harness may deliberately launch a fresh top-level pardes from
- // inside another one. Its pid is a process-tree boundary, not an opt-out
- // for the new session itself: pane shells below the child still detect it.
- // This is what lets the snapshot harness exercise nested launches while
- // the harness happens to be running in a real pardes pane.
- const boundary = if (libc.getenv("PARDES_NESTED_BOUNDARY_PID")) |raw|
- std.fmt.parseInt(libc.pid_t, std.mem.span(raw), 10) catch 0
- else
- 0;
- var pid = libc.getppid();
- var hops: usize = 0;
- while (pid > 1 and hops < 64) : (hops += 1) {
- if (pid == boundary) return null;
- var buf: [4096]u8 = undefined;
- if (exeOf(pid, &buf)) |exe| if (samePardesExecutable(exe, self_exe)) return pid;
- pid = parentOf(pid) orelse return null;
- }
- return null;
-}
-
-/// Hand `Look <path>[:<line>]` to the pardes listening as `pid` and say
-/// whether it landed. False for every failure — no socket file, nobody
-/// accepting, a path that does not fit — because an outer instance that
-/// cannot be reached (an older build, a stale path) must never cost the
-/// caller its own launch. Writes and returns: the answer is a pane appearing
-/// on someone else's screen, and there is nothing to wait for.
-pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool {
- if (comptime !supported) return false;
- // The protocol is one line, so a path with a line break IN it says
- // something else entirely: `we\nird.txt` arrived as `Look .../we` and the
- // outer instance opened a different file that happened to exist. \r goes
- // too — the receive side trims a trailing one. Unsendable, not escaped:
- // the caller falls through and opens the file in its own session.
- if (std.mem.indexOfAny(u8, path, "\r\n") != null) return false;
- var cmd_buf: [max_line]u8 = undefined;
- const cmd = (if (line > 0)
- std.fmt.bufPrint(&cmd_buf, "Look {s}:{d}\n", .{ path, line })
- else
- std.fmt.bufPrint(&cmd_buf, "Look {s}\n", .{path})) catch return false;
-
- // sun_path-sized by construction, so `sock` cannot be longer than the
- // field it is about to be copied into — socketPath returns null instead.
- var path_buf: [sun_path_len]u8 = undefined;
- const sock = socketPath(&path_buf, pid) orelse return false;
- var addr: libc.sockaddr.un = .{ .path = @splat(0) };
- @memcpy(addr.path[0 .. sock.len + 1], sock[0 .. sock.len + 1]);
- const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
- if (fd < 0) return false;
- setCloexec(fd);
- defer _ = libc.close(fd);
- if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) return false;
- var off: usize = 0;
- while (off < cmd.len) {
- const n = libc.write(fd, cmd.ptr + off, cmd.len - off);
- if (n < 0) {
- if (libc.errno(n) == .INTR) continue;
- return false;
- }
- if (n == 0) return false;
- off += @intCast(n);
- }
- return true;
-}
-
-/// The two things `ensureSocketDir` has to know about a path, from whichever
-/// call the platform actually offers. Darwin has fstatat and no statx; on
-/// linux std.c.fstatat is `void` — glibc hides it behind a versioned symbol
-/// std cannot name — so linux asks statx for the same fields. Both spellings
-/// refuse to follow a symlink, which is the point of asking.
-///
-/// `pub` for the detached transport, which vets the same directory and also
-/// vets the SOCKET FILE with it (src/detached/server.zig `vetted`): `mode`
-/// carries the type bits, so one call answers "is this a socket, ours, and
-/// private" as well as it answers it for a directory.
-pub const DirFacts = struct { mode: u32, uid: libc.uid_t };
-
-pub fn statNoFollow(path: [:0]const u8) ?DirFacts {
- if (comptime darwin) {
- var st: libc.Stat = undefined;
- if (libc.fstatat(libc.AT.FDCWD, path, &st, libc.AT.SYMLINK_NOFOLLOW) != 0) return null;
- return .{ .mode = st.mode, .uid = st.uid };
- } else {
- const linux = std.os.linux;
- var stx: linux.Statx = undefined;
- const want: linux.STATX = .{ .TYPE = true, .MODE = true, .UID = true };
- if (libc.statx(linux.AT.FDCWD, path, linux.AT.SYMLINK_NOFOLLOW, want, &stx) != 0) return null;
- return .{ .mode = stx.mode, .uid = stx.uid };
- }
-}
-
-/// Create the socket directory if it is missing and refuse it unless it is a
-/// directory WE own with nothing granted to group or other. A planted path is
-/// the whole attack on a socket that runs commands — or, for the detached
-/// transport that shares this, on one that carries keystrokes into a live
-/// editor — and $XDG_RUNTIME_DIR passes this untouched (the login session
-/// already makes it 0700).
-pub fn ensureSocketDir(dir: [:0]const u8) bool {
- // mkdir -p, because the HOME branch is three levels deep and a machine
- // without ~/.local/state would otherwise switch the feature off in
- // silence. Under $XDG_RUNTIME_DIR every prefix already exists and simply
- // EEXISTs, which is the ordinary case for the leaf too.
- var partial: [sun_path_len:0]u8 = undefined;
- @memcpy(partial[0 .. dir.len + 1], dir[0 .. dir.len + 1]);
- for (1..dir.len) |i| {
- if (dir[i] != '/') continue;
- partial[i] = 0;
- _ = libc.mkdir(partial[0..i :0], 0o700);
- partial[i] = '/';
- }
- _ = libc.mkdir(dir, 0o700);
- // A symlink where the directory should be is exactly the plant this
- // guards against, so the stat above it does not follow one.
- const st = statNoFollow(dir) orelse return false;
- const IFMT: u32 = 0o170000;
- const IFDIR: u32 = 0o040000;
- if (st.mode & IFMT != IFDIR) return false;
- if (st.uid != libc.getuid()) return false;
- return st.mode & 0o077 == 0;
-}
-
-/// Unlink the socket files of pardes processes that are gone. A pardes killed
-/// rather than quit runs no defer, so its file outlives it; harmless by
-/// construction (bind unlinks first, a client's connect is refused) but it is
-/// our own litter and the snapshot suite alone leaves ~90 behind per run.
-/// Bounded: one readdir of a directory only we write to, one kill(0) each.
-fn sweep(dir: [:0]const u8) void {
- const d = libc.opendir(dir) orelse return;
- defer _ = libc.closedir(d);
- const me = libc.getpid();
- while (libc.readdir(d)) |ent| {
- const pid = sweepPid(std.mem.sliceTo(&ent.name, 0)) orelse continue;
- if (pid == me) continue;
- // 0 = alive; EPERM = alive and someone else's. Only ESRCH is a corpse.
- const rc = libc.kill(pid, @enumFromInt(0));
- if (rc == 0 or libc.errno(rc) != .SRCH) continue;
- var pbuf: [sun_path_len]u8 = undefined;
- _ = libc.unlink(socketPath(&pbuf, pid) orelse continue);
- }
-}
-
-/// Bind and listen so nested instances can find us; -1 if anything fails, and
-/// a pardes without a socket is simply one whose children open their own UI.
-/// The path is always this process's own, so nobody outside holds a buffer of
-/// it — the shells each kept one and passed it back to be unlinked, which is a
-/// way for the two spellings to go out of step and for no other reason.
-///
-/// CLOEXEC matters more here than on any other fd in the program: pane shells
-/// are forked with forkpty and inherit everything open, and an orphaned bash
-/// holding this one would keep the socket bound long after we exit — the same
-/// shape as the inherited lock fd that once held a flock forever.
-pub fn listen() c_int {
- if (comptime !supported) return -1;
- var dir_buf: [sun_path_len:0]u8 = undefined;
- const dir = socketDir(&dir_buf) orelse return -1;
- if (!ensureSocketDir(dir)) return -1;
- sweep(dir);
- // Fits by construction: socketPath writes into a sun_path-sized buffer and
- // returns null rather than a truncated address.
- var path_buf: [sun_path_len]u8 = undefined;
- const path = socketPath(&path_buf, libc.getpid()) orelse return -1;
- var addr: libc.sockaddr.un = .{ .path = @splat(0) };
- @memcpy(addr.path[0 .. path.len + 1], path[0 .. path.len + 1]);
- const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
- if (fd < 0) return -1;
- setCloexec(fd);
- _ = libc.unlink(path); // pid reuse: a dead pardes' file would EADDRINUSE forever
- if (libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) {
- _ = libc.close(fd);
- return -1;
- }
- // Owner-only, and BEFORE listen(2), which is the moment anyone could
- // connect: the directory is already private, this is the second wall.
- _ = libc.chmod(path, 0o600);
- if (libc.listen(fd, 8) != 0) {
- _ = libc.close(fd);
- return -1;
- }
- return fd;
-}
-
-/// Close the listener and take its file away. Guarded on the fd rather than on
-/// the path, so a bind that FAILED cannot unlink a path this process never
-/// created; anything else is a no-op, which is what --nested and every
-/// unsupported build hand it.
-pub fn unlisten(fd: c_int) void {
- if (fd < 0) return;
- _ = libc.close(fd);
- var path_buf: [sun_path_len]u8 = undefined;
- if (socketPath(&path_buf, libc.getpid())) |path| _ = libc.unlink(path);
-}
-
-/// Block until a nested instance sends a `Look` line, and return it inside
-/// `buf`. Null only when the listening fd itself is gone — teardown closed it,
-/// or it was never a socket — because anything else (EMFILE, ECONNABORTED)
-/// would otherwise kill the listener thread for the life of the process while
-/// the socket stayed bound, and every later launch would exit 0 having done
-/// nothing. Every accepted connection is CLOEXEC for the reason the listener
-/// is.
-pub fn acceptLine(fd: c_int, buf: []u8) ?[]const u8 {
- if (comptime !supported) return null;
- while (true) {
- const conn = libc.accept(fd, null, null);
- if (conn < 0) {
- switch (libc.errno(conn)) {
- .INTR => continue,
- // the fd went away or never was one: nothing will ever arrive
- .BADF, .INVAL, .NOTSOCK => return null,
- // transient. Sleep first: EMFILE persists until some other fd
- // is freed, and a bare `continue` would spin a core on it.
- else => {
- var ts: libc.timespec = .{ .sec = 0, .nsec = 100 * std.time.ns_per_ms };
- _ = libc.nanosleep(&ts, null);
- continue;
- },
- }
- }
- defer _ = libc.close(conn);
- setCloexec(conn);
- // A peer that connects and says nothing must not hold the listener:
- // this is a serial accept loop, and one silent connection used to
- // block every later launch until it let go. The client writes its one
- // short line immediately, so a second is already generous.
- const tv: libc.timeval = .{ .sec = 1, .usec = 0 };
- _ = libc.setsockopt(conn, libc.SOL.SOCKET, libc.SO.RCVTIMEO, &tv, @sizeOf(libc.timeval));
- var len: usize = 0;
- while (len < buf.len) {
- const n = libc.read(conn, buf.ptr + len, buf.len - len);
- if (n < 0 and libc.errno(n) == .INTR) continue;
- if (n <= 0) break; // EOF, or the receive timeout expired
- len += @intCast(n);
- if (std.mem.indexOfScalar(u8, buf[0..len], '\n') != null) break;
- }
- const end = std.mem.indexOfScalar(u8, buf[0..len], '\n') orelse len;
- // a full buffer with no newline is an overlong line: drop it whole
- // rather than run its truncation as some other command
- if (end == buf.len) continue;
- const line = std.mem.trimEnd(u8, buf[0..end], "\r");
- // one verb (see the file header): this socket may open things, and
- // that is all it may do
- if (!std.mem.startsWith(u8, line, "Look ")) continue;
- return line;
- }
-}
-
-test "socket path: XDG first, then a private dir under HOME, never /tmp" {
- var buf: [sun_path_len]u8 = undefined;
- // The environment is process-wide and every test in this binary shares it.
- // The last case below reaches the "no directory at all" branch by blanking
- // both variables, and without this every later test ran without a HOME.
- var xdg_buf: [4096:0]u8 = undefined;
- var home_buf: [4096:0]u8 = undefined;
- const xdg0 = if (libc.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&xdg_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
- const home0 = if (libc.getenv("HOME")) |v| std.fmt.bufPrintSentinel(&home_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
- defer {
- if (xdg0) |v| {
- _ = setenv("XDG_RUNTIME_DIR", v, 1);
- } else _ = unsetenv("XDG_RUNTIME_DIR");
- if (home0) |v| {
- _ = setenv("HOME", v, 1);
- } else _ = unsetenv("HOME");
- }
- _ = setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1);
- try std.testing.expectEqualStrings("/run/user/1000/pardes-4242.sock", socketPath(&buf, 4242).?);
- _ = unsetenv("XDG_RUNTIME_DIR");
- _ = setenv("HOME", "/home/who", 1);
- try std.testing.expectEqualStrings("/home/who/.local/state/pardes/pardes-4242.sock", socketPath(&buf, 4242).?);
- // sun_path holds the NUL, so a directory that fills it has no socket
- // address at all — say so instead of binding a truncated one. Sized from
- // the field: the limit is 108 on linux and 104 on darwin, and a literal
- // here would test nothing on whichever platform it was not written for.
- _ = setenv("XDG_RUNTIME_DIR", "/" ++ ("x" ** (sun_path_len - 8)), 1);
- try std.testing.expect(socketPath(&buf, 4242) == null);
- _ = unsetenv("XDG_RUNTIME_DIR");
- _ = unsetenv("HOME");
- try std.testing.expect(socketPath(&buf, 4242) == null);
-}
-
-test "a rebuilt binary still matches its own running instance" {
- // `zig build` under a live pardes: the outer's exe link gains the suffix,
- // the new process's does not, and before this the two stopped comparing
- // equal — every nested launch opened a second UI.
- try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes (deleted)"));
- try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes"));
- try std.testing.expectEqualStrings("", stripDeleted(" (deleted)"));
- // only a SUFFIX, and only the whole one
- try std.testing.expectEqualStrings("/x (deleted) y", stripDeleted("/x (deleted) y"));
- try std.testing.expectEqualStrings("/x (delete)", stripDeleted("/x (delete)"));
-}
-
-test "tty and GUI sibling executables recognise each other" {
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/bin/pardes",
- "/work/zig-out/bin/pardes-gui",
- ));
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/bin/pardes-linux-aarch64",
- "/work/zig-out/bin/pardes-gui-linux-aarch64 (deleted)",
- ));
- // Installation paths do not define the family. This is the ordinary
- // system-GUI/user-TTY pairing and the reason this comparison uses names.
- try std.testing.expect(samePardesExecutable(
- "/home/who/.local/bin/pardes",
- "/usr/bin/pardes-gui",
- ));
- try std.testing.expect(!samePardesExecutable(
- "/work/zig-out/bin/pardes-linux-aarch64",
- "/work/zig-out/bin/pardes-gui-linux-x86_64",
- ));
- try std.testing.expect(!samePardesExecutable(
- "/work/zig-out/bin/not-pardes",
- "/work/zig-out/bin/not-pardes-gui",
- ));
- try std.testing.expect(!samePardesExecutable(
- "/one/bin/not-pardes",
- "/two/bin/not-pardes",
- ));
- try std.testing.expect(!samePardesExecutable(
- "/work/zig-out/bin/pardes-snap",
- "/usr/bin/pardes",
- ));
-}
-
-test "the app bundle is in the same executable family" {
- // What `pardes foo.zig` typed into the bundle's own shell has to resolve:
- // the ancestor is zig-out/pardes.app/..., this process is zig-out/bin/...,
- // and nothing below zig-out is shared.
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/pardes.app/Contents/MacOS/pardes",
- "/work/zig-out/bin/pardes",
- ));
- // ...and the SDL sibling, which reaches it by the name rule instead.
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/pardes.app/Contents/MacOS/pardes",
- "/work/zig-out/bin/pardes-gui",
- ));
- // The case this machine actually produces: `zig build` installs the tty
- // binary under its os-arch tail, and the bundle carries the same build
- // under the one name CFBundleExecutable can spell.
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/pardes.app/Contents/MacOS/pardes",
- "/work/zig-out/bin/pardes-macos-aarch64",
- ));
- // Installation location does not matter here either.
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/pardes.app/Contents/MacOS/pardes",
- "/opt/zig-out/bin/pardes",
- ));
- try std.testing.expect(samePardesExecutable(
- "/work/zig-out/pardes/Contents/MacOS/pardes",
- "/work/zig-out/bin/pardes",
- ));
- // Nothing here may loosen the rule for two unrelated programs that merely
- // sit in a bin and a bundle of the same tree.
- try std.testing.expect(!samePardesExecutable(
- "/work/zig-out/other.app/Contents/MacOS/other",
- "/work/zig-out/bin/pardes",
- ));
-}
-
-test "the ancestor walk reads this process's own parent" {
- // The one thing a hand-written `struct proc_bsdinfo` gets wrong silently:
- // a field ordering that puts something else where ppid should be still
- // returns a plausible number. getppid knows the answer, so compare.
- //
- // Also the only check that libproc answers us at all — every caller of
- // outer() treats a failure as "no outer instance", which is exactly what a
- // permission problem would look like.
- if (comptime !supported) return error.SkipZigTest;
- try std.testing.expectEqual(libc.getppid(), parentOf(libc.getpid()).?);
- // ...and that the walk terminates rather than spinning on pid 1's parent.
- try std.testing.expect(parentOf(1) == null or parentOf(1).? <= 1);
-
- var buf: [4096]u8 = undefined;
- const exe = exeOf(libc.getpid(), &buf).?;
- try std.testing.expect(exe.len > 0);
- try std.testing.expect(exe[0] == '/');
- // The test binary is not a pardes, so the walk must come back empty rather
- // than matching some ancestor by accident.
- try std.testing.expect(outer() == null);
-}
-
-extern "c" fn mkdtemp(template: [*:0]u8) ?[*:0]u8;
-extern "c" fn rmdir(path: [*:0]const u8) c_int;
-
-test "a Look line survives the socket round trip" {
- // Everything the protocol actually does, against a real kernel: bind,
- // chmod, connect, write, accept, read, and the one-verb filter. The pure
- // functions above cannot see any of it, and every primitive here is
- // spelled differently on the two platforms this now supports.
- if (comptime !supported) return error.SkipZigTest;
-
- // A private directory of our own. Not the developer's real state dir: this
- // binds a socket named after a pid that is the TEST's, and sweep() unlinks
- // what it finds beside it.
- var tmpl: [64:0]u8 = undefined;
- _ = std.fmt.bufPrintSentinel(&tmpl, "/tmp/pardes-nested-XXXXXX", .{}, 0) catch unreachable;
- if (mkdtemp(&tmpl) == null) return error.SkipZigTest;
- const dir = std.mem.sliceTo(&tmpl, 0);
- defer _ = rmdir(tmpl[0..dir.len :0]);
-
- var xdg_buf: [4096:0]u8 = undefined;
- const xdg0 = if (libc.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&xdg_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
- defer {
- if (xdg0) |v| {
- _ = setenv("XDG_RUNTIME_DIR", v, 1);
- } else _ = unsetenv("XDG_RUNTIME_DIR");
- }
- _ = setenv("XDG_RUNTIME_DIR", tmpl[0..dir.len :0], 1);
-
- const fd = listen();
- try std.testing.expect(fd >= 0);
- defer unlisten(fd);
-
- // Sent to our own pid, which is the pid listen() named the socket after.
- // The client closes as it returns, and the line is already queued, so the
- // single-threaded accept below finds a complete connection waiting — no
- // thread and no timeout needed to prove the protocol.
- try std.testing.expect(sendLook(libc.getpid(), "/etc/hosts", 42));
- var buf: [max_line]u8 = undefined;
- try std.testing.expectEqualStrings("Look /etc/hosts:42", acceptLine(fd, &buf).?);
-
- // ...and without a line number, which is the directory and image case.
- try std.testing.expect(sendLook(libc.getpid(), "/etc", 0));
- try std.testing.expectEqualStrings("Look /etc", acceptLine(fd, &buf).?);
-
- // The socket takes one verb. Anything else is dropped rather than run, so
- // the next Look is what comes back — proving the filter skipped it without
- // dropping the connection after it.
- try std.testing.expect(writeLine(libc.getpid(), "Exec rm -rf /\n"));
- try std.testing.expect(sendLook(libc.getpid(), "/etc/passwd", 0));
- try std.testing.expectEqualStrings("Look /etc/passwd", acceptLine(fd, &buf).?);
-
- // A path that cannot be one line is not escaped, it is refused.
- try std.testing.expect(!sendLook(libc.getpid(), "/etc/ho\nsts", 0));
-}
-
-/// sendLook with the framing bypassed, so a test can put something on the wire
-/// that the client would never send.
-fn writeLine(pid: libc.pid_t, line: []const u8) bool {
- var path_buf: [sun_path_len]u8 = undefined;
- const sock = socketPath(&path_buf, pid) orelse return false;
- var addr: libc.sockaddr.un = .{ .path = @splat(0) };
- @memcpy(addr.path[0 .. sock.len + 1], sock[0 .. sock.len + 1]);
- const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0);
- if (fd < 0) return false;
- defer _ = libc.close(fd);
- if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) return false;
- return libc.write(fd, line.ptr, line.len) == @as(isize, @intCast(line.len));
-}
-
-test "the sweep only recognises its own socket names" {
- try std.testing.expectEqual(@as(libc.pid_t, 7), sweepPid("pardes-7.sock").?);
- try std.testing.expectEqual(@as(libc.pid_t, 4194304), sweepPid("pardes-4194304.sock").?);
- try std.testing.expect(sweepPid("pardes-.sock") == null);
- try std.testing.expect(sweepPid("pardes-7.sockx") == null);
- try std.testing.expect(sweepPid("pardes-7") == null);
- try std.testing.expect(sweepPid("bus") == null);
- try std.testing.expect(sweepPid("pardes-osc133.bash") == null);
- // parseInt alone would take these, and the sweep UNLINKS what it answers
- try std.testing.expect(sweepPid("pardes-+7.sock") == null);
- try std.testing.expect(sweepPid("pardes--7.sock") == null);
- try std.testing.expect(sweepPid("pardes- 7.sock") == null);
-}
-
-test "PPid comes off the status field, not a comm-shifted stat line" {
- // the comm here contains a space AND parentheses — the exact shape that
- // breaks `field 4 of /proc/<pid>/stat`
- const status = "Name:\tsh (a b)\nUmask:\t0022\nState:\tS (sleeping)\n" ++
- "Tgid:\t1234\nNgid:\t0\nPid:\t1234\nPPid:\t991\nTracerPid:\t0\n";
- try std.testing.expectEqual(@as(libc.pid_t, 991), parsePPid(status).?);
- try std.testing.expectEqual(@as(libc.pid_t, 0), parsePPid("PPid:\t0\n").?);
- try std.testing.expect(parsePPid("Name:\tinit\nTracerPid:\t0\n") == null);
- try std.testing.expect(parsePPid("PPid:\tnotanumber\n") == null);
- // a truncated read must not answer from a half line
- try std.testing.expect(parsePPid("Name:\tsh\nPPi") == null);
-}