diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-26 13:27:46 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-27 09:47:39 -0300 |
| commit | 11f380f6d7222f2cad93c2cdf13701ea1f903d47 (patch) | |
| tree | 803194ee5853a6b4cda93f90a95e28d1f02e69ae /src/nested.zig | |
| parent | fbc194068687e49a8490c85c9f1257a2f2bb9079 (diff) | |
| download | pardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.tar.gz pardes-11f380f6d7222f2cad93c2cdf13701ea1f903d47.zip | |
One core behind N frontends, the board's own runner moved in, and every board cap on one screen
## The wire is the effect stream, not a new protocol
`pardes --detach` leaves a core running with no terminal; `pardes --attach` is a frontend that owns
a terminal and a socket and nothing else. N frontends on one core all look at the same screen —
`screen -x`, not N sessions.
The codec (`src/detached/wire.zig`) carries exactly one `Event` or one `Host.VTable` call per
message. That is not a coincidence and it is why there is no third vocabulary to keep in step: the
core's IO seam was already a struct of function pointers with plain-data arguments, so a socket is
a legal implementation of it. `nested.zig`'s socket could not be reused — it carries a builtin
command line, and a command line cannot carry a frame.
ARCHITECTURE-NEUTRAL on purpose, not as decoration. The frontend on the far end may be
riscv32-freestanding on the ESP32-P4 while the core is x86_64 Linux, so every field is an explicit
little-endian fixed width and no message is a blit of a native struct. A protocol that only works
between two builds of the same compiler would have thrown away the one frontend that motivated it.
## The board comes in; its toolchain stays out
`src/p4.zig` becomes `src/esp32p4.zig`, and the pardes half of `../05-zig-p4` — the vaxis-over-
serial runner, the UART editor terminal, the keystroke rescue ring, the on-die test suite — moves
into `src/esp32p4/`. `build.zig.zon` gains `.zig_p4 = .{ .path = "../05-zig-p4" }`, so
`zig build -Dplatform=esp32p4 -Desp32p4-firmware` builds, flashes, monitors and self-tests the
board from this repo's `build.zig`.
The DIVISION is the point. What moved is what only pardes wants: the runner that drives a pardes
core over a serial line. What stayed is everything a second project would also want — the HAL, the
register/radio/oracle layers, the linker script, `_start`. `zig_p4` declares no dependencies of its
own and its `build()` early-returns when it is not the root package, so this costs the package
graph exactly zero packages and the editor's own builds nothing at all.
## limits.zig: nine forgettable places become one budget
Nine `platform == .esp32p4` capacity tests lived in nine files. They were never nine decisions —
they are ONE decision, how much memory this build may spend, taken nine times where no reader could
see the total. `src/limits.zig` puts the whole budget on one screen with every cap named against
what it is measured against, derived from two booleans.
The payoff is testability on a machine that is not the board: the caps are ordinary comptime values,
so a host build can be compiled against the board's numbers and the parking, eviction and clamping
paths a 240 KiB core takes get exercised by the normal test suite instead of only over a UART.
## A bare `zig build`
`zig build` with no arguments now builds the tty and GUI binaries and installs them into
`~/.local/bin`, and says so once on stdout with the flag that overrides it. The old default built
one binary into `zig-out` — a path nothing on a `PATH` ever looks at, which made "build it" and
"use it" two different commands for no reason.
Diffstat (limited to 'src/nested.zig')
| -rw-r--r-- | src/nested.zig | 61 |
1 files changed, 41 insertions, 20 deletions
diff --git a/src/nested.zig b/src/nested.zig index eb01b2e0..887d0703 100644 --- a/src/nested.zig +++ b/src/nested.zig @@ -36,21 +36,28 @@ const libc = std.c; 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; -const darwin = switch (builtin.os.tag) { +/// 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. -const supported = builtin.os.tag == .linux or darwin; +/// 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. -const sun_path_len = @typeInfo(@FieldType(libc.sockaddr.un, "path")).array.len; +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 @@ -72,10 +79,15 @@ extern "c" fn proc_pidpath(pid: c_int, buffer: *anyopaque, buffersize: u32) c_in 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 both callers -/// are past that: `listen` runs before the first pane exists, and `acceptLine` -/// runs on a thread of its own long after spawning has settled. -fn setCloexec(fd: c_int) void { +/// 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 forks nothing at all (its ptys live in its frontends). +/// +/// CLOEXEC still matters for both: a `--detach` session is long-lived, and an +/// inherited listener would keep its socket bound long after it 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); } @@ -87,10 +99,12 @@ 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. Asked by the client -/// (to derive the path), by the listener (to create and vet it) and by the -/// sweeper (to scan it), so it is written once. -fn socketDir(buf: *[sun_path_len:0]u8) ?[:0]const u8 { +/// 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; @@ -311,14 +325,19 @@ pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool { return true; } -/// The three things ensureSocketDir has to know about a path, from whichever +/// 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 three fields. Both -/// spellings refuse to follow a symlink, which is the point of asking. -const DirFacts = struct { mode: u32, uid: libc.uid_t }; +/// 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 }; -fn statNoFollow(path: [:0]const u8) ?DirFacts { +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; @@ -334,9 +353,11 @@ fn statNoFollow(path: [:0]const u8) ?DirFacts { /// 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, and $XDG_RUNTIME_DIR -/// passes this untouched (the login session already makes it 0700). -fn ensureSocketDir(dir: [:0]const u8) bool { +/// 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 |
